fork of go-gitdiff with jj support
at v0.8.1 5.9 kB view raw
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}