fork of go-gitdiff with jj support
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}