master
  1package gitdiff
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"os"
  7	"strings"
  8)
  9
 10// File describes changes to a single file. It can be either a text file or a
 11// binary file.
 12type File struct {
 13	OldName string
 14	NewName string
 15
 16	IsNew    bool
 17	IsDelete bool
 18	IsCopy   bool
 19	IsRename bool
 20
 21	OldMode os.FileMode
 22	NewMode os.FileMode
 23
 24	OldOIDPrefix string
 25	NewOIDPrefix string
 26	Score        int
 27
 28	// TextFragments contains the fragments describing changes to a text file. It
 29	// may be empty if the file is empty or if only the mode changes.
 30	TextFragments []*TextFragment
 31
 32	// IsBinary is true if the file is a binary file. If the patch includes
 33	// binary data, BinaryFragment will be non-nil and describe the changes to
 34	// the data. If the patch is reversible, ReverseBinaryFragment will also be
 35	// non-nil and describe the changes needed to restore the original file
 36	// after applying the changes in BinaryFragment.
 37	IsBinary              bool
 38	BinaryFragment        *BinaryFragment
 39	ReverseBinaryFragment *BinaryFragment
 40}
 41
 42// String returns a git diff representation of this file. The value can be
 43// parsed by this library to obtain the same File, but may not be the same as
 44// the original input.
 45func (f *File) String() string {
 46	var diff strings.Builder
 47	newFormatter(&diff).FormatFile(f)
 48	return diff.String()
 49}
 50
 51// TextFragment describes changed lines starting at a specific line in a text file.
 52type TextFragment struct {
 53	Comment string
 54
 55	OldPosition int64
 56	OldLines    int64
 57
 58	NewPosition int64
 59	NewLines    int64
 60
 61	LinesAdded   int64
 62	LinesDeleted int64
 63
 64	LeadingContext  int64
 65	TrailingContext int64
 66
 67	Lines []Line
 68}
 69
 70// String returns a git diff format of this fragment. See [File.String] for
 71// more details on this format.
 72func (f *TextFragment) String() string {
 73	var diff strings.Builder
 74	newFormatter(&diff).FormatTextFragment(f)
 75	return diff.String()
 76}
 77
 78// Header returns a git diff header of this fragment. See [File.String] for
 79// more details on this format.
 80func (f *TextFragment) Header() string {
 81	var hdr strings.Builder
 82	newFormatter(&hdr).FormatTextFragmentHeader(f)
 83	return hdr.String()
 84}
 85
 86// Validate checks that the fragment is self-consistent and appliable. Validate
 87// returns an error if and only if the fragment is invalid.
 88func (f *TextFragment) Validate() error {
 89	if f == nil {
 90		return errors.New("nil fragment")
 91	}
 92
 93	var (
 94		oldLines, newLines                     int64
 95		leadingContext, trailingContext        int64
 96		contextLines, addedLines, deletedLines int64
 97	)
 98
 99	// count the types of lines in the fragment content
100	for i, line := range f.Lines {
101		switch line.Op {
102		case OpContext:
103			oldLines++
104			newLines++
105			contextLines++
106			if addedLines == 0 && deletedLines == 0 {
107				leadingContext++
108			} else {
109				trailingContext++
110			}
111		case OpAdd:
112			newLines++
113			addedLines++
114			trailingContext = 0
115		case OpDelete:
116			oldLines++
117			deletedLines++
118			trailingContext = 0
119		default:
120			return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1)
121		}
122	}
123
124	// check the actual counts against the reported counts
125	if oldLines != f.OldLines {
126		return lineCountErr("old", oldLines, f.OldLines)
127	}
128	if newLines != f.NewLines {
129		return lineCountErr("new", newLines, f.NewLines)
130	}
131	if leadingContext != f.LeadingContext {
132		return lineCountErr("leading context", leadingContext, f.LeadingContext)
133	}
134	if trailingContext != f.TrailingContext {
135		return lineCountErr("trailing context", trailingContext, f.TrailingContext)
136	}
137	if addedLines != f.LinesAdded {
138		return lineCountErr("added", addedLines, f.LinesAdded)
139	}
140	if deletedLines != f.LinesDeleted {
141		return lineCountErr("deleted", deletedLines, f.LinesDeleted)
142	}
143
144	// if a file is being created, it can only contain additions
145	if f.OldPosition == 0 && f.OldLines != 0 {
146		return errors.New("file creation fragment contains context or deletion lines")
147	}
148
149	return nil
150}
151
152func lineCountErr(kind string, actual, reported int64) error {
153	return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported)
154}
155
156// Line is a line in a text fragment.
157type Line struct {
158	Op   LineOp
159	Line string
160}
161
162func (fl Line) String() string {
163	return fl.Op.String() + fl.Line
164}
165
166// Old returns true if the line appears in the old content of the fragment.
167func (fl Line) Old() bool {
168	return fl.Op == OpContext || fl.Op == OpDelete
169}
170
171// New returns true if the line appears in the new content of the fragment.
172func (fl Line) New() bool {
173	return fl.Op == OpContext || fl.Op == OpAdd
174}
175
176// NoEOL returns true if the line is missing a trailing newline character.
177func (fl Line) NoEOL() bool {
178	return len(fl.Line) == 0 || fl.Line[len(fl.Line)-1] != '\n'
179}
180
181// LineOp describes the type of a text fragment line: context, added, or removed.
182type LineOp int
183
184const (
185	// OpContext indicates a context line
186	OpContext LineOp = iota
187	// OpDelete indicates a deleted line
188	OpDelete
189	// OpAdd indicates an added line
190	OpAdd
191)
192
193func (op LineOp) String() string {
194	switch op {
195	case OpContext:
196		return " "
197	case OpDelete:
198		return "-"
199	case OpAdd:
200		return "+"
201	}
202	return "?"
203}
204
205// BinaryFragment describes changes to a binary file.
206type BinaryFragment struct {
207	Method BinaryPatchMethod
208	Size   int64
209	Data   []byte
210}
211
212// BinaryPatchMethod is the method used to create and apply the binary patch.
213type BinaryPatchMethod int
214
215const (
216	// BinaryPatchDelta indicates the data uses Git's packfile encoding
217	BinaryPatchDelta BinaryPatchMethod = iota
218	// BinaryPatchLiteral indicates the data is the exact file content
219	BinaryPatchLiteral
220)
221
222// String returns a git diff format of this fragment. Due to differences in
223// zlib implementation between Go and Git, encoded binary data in the result
224// will likely differ from what Git produces for the same input. See
225// [File.String] for more details on this format.
226func (f *BinaryFragment) String() string {
227	var diff strings.Builder
228	newFormatter(&diff).FormatBinaryFragment(f)
229	return diff.String()
230}