this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add String() methods to parsed types (#48)

This enables clients to move back and forth between parsed objects and
text patches. The generated patches are semantically equal to the parsed
object and should re-parse to the same object, but may not be
byte-for-byte identical to the original input.

In my testing, formatted text patches are usually identical to the
input, but there may be cases where this is not true. Binary patches
always differ. This is because Go's 'compress/flate' package ends
streams with an empty block instead of adding the end-of-stream flag to
the last non-empty block, like Git's C implementation. Since the streams
will always be different for this reason, I chose to also enable default
compression (the test patches I generated with Git used no compression.)

The main tests for this feature involve parsing, formatting, and then
re-parsing a patch to make sure we get equal objects.

Formatting is handled by a new internal formatter type, which allows
writing all data to the same stream. This isn't exposed publicly right
now, but will be useful if there's a need for more flexible formatting
functions in the future, like formatting to a user-provided io.Writer.

authored by

Billy Keyes and committed by
GitHub
8584cd59 9e0997ef

+746 -4
+6
.golangci.yml
··· 19 19 exclude-use-default: false 20 20 21 21 linters-settings: 22 + errcheck: 23 + exclude-functions: 24 + - (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).Write 25 + - (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).WriteString 26 + - (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).WriteByte 27 + - fmt.Fprintf(*github.com/bluekeyes/go-gitdiff/gitdiff.formatter) 22 28 goimports: 23 29 local-prefixes: github.com/bluekeyes/go-gitdiff 24 30 revive:
+41 -2
gitdiff/base85.go
··· 19 19 } 20 20 21 21 // base85Decode decodes Base85-encoded data from src into dst. It uses the 22 - // alphabet defined by base85.c in the Git source tree, which appears to be 23 - // unique. src must contain at least len(dst) bytes of encoded data. 22 + // alphabet defined by base85.c in the Git source tree. src must contain at 23 + // least len(dst) bytes of encoded data. 24 24 func base85Decode(dst, src []byte) error { 25 25 var v uint32 26 26 var n, ndst int ··· 50 50 } 51 51 return nil 52 52 } 53 + 54 + // base85Encode encodes src in Base85, writing the result to dst. It uses the 55 + // alphabet defined by base85.c in the Git source tree. 56 + func base85Encode(dst, src []byte) { 57 + var di, si int 58 + 59 + encode := func(v uint32) { 60 + dst[di+0] = b85Alpha[(v/(85*85*85*85))%85] 61 + dst[di+1] = b85Alpha[(v/(85*85*85))%85] 62 + dst[di+2] = b85Alpha[(v/(85*85))%85] 63 + dst[di+3] = b85Alpha[(v/85)%85] 64 + dst[di+4] = b85Alpha[v%85] 65 + } 66 + 67 + n := (len(src) / 4) * 4 68 + for si < n { 69 + encode(uint32(src[si+0])<<24 | uint32(src[si+1])<<16 | uint32(src[si+2])<<8 | uint32(src[si+3])) 70 + si += 4 71 + di += 5 72 + } 73 + 74 + var v uint32 75 + switch len(src) - si { 76 + case 3: 77 + v |= uint32(src[si+2]) << 8 78 + fallthrough 79 + case 2: 80 + v |= uint32(src[si+1]) << 16 81 + fallthrough 82 + case 1: 83 + v |= uint32(src[si+0]) << 24 84 + encode(v) 85 + } 86 + } 87 + 88 + // base85Len returns the length of n bytes of Base85 encoded data. 89 + func base85Len(n int) int { 90 + return (n + 3) / 4 * 5 91 + }
+58
gitdiff/base85_test.go
··· 1 1 package gitdiff 2 2 3 3 import ( 4 + "bytes" 4 5 "testing" 5 6 ) 6 7 ··· 58 59 }) 59 60 } 60 61 } 62 + 63 + func TestBase85Encode(t *testing.T) { 64 + tests := map[string]struct { 65 + Input []byte 66 + Output string 67 + }{ 68 + "zeroBytes": { 69 + Input: []byte{}, 70 + Output: "", 71 + }, 72 + "twoBytes": { 73 + Input: []byte{0xCA, 0xFE}, 74 + Output: "%KiWV", 75 + }, 76 + "fourBytes": { 77 + Input: []byte{0x0, 0x0, 0xCA, 0xFE}, 78 + Output: "007GV", 79 + }, 80 + "sixBytes": { 81 + Input: []byte{0x0, 0x0, 0xCA, 0xFE, 0xCA, 0xFE}, 82 + Output: "007GV%KiWV", 83 + }, 84 + } 85 + 86 + for name, test := range tests { 87 + t.Run(name, func(t *testing.T) { 88 + dst := make([]byte, len(test.Output)) 89 + base85Encode(dst, test.Input) 90 + for i, b := range test.Output { 91 + if dst[i] != byte(b) { 92 + t.Errorf("incorrect character at index %d: expected '%c', actual '%c'", i, b, dst[i]) 93 + } 94 + } 95 + }) 96 + } 97 + } 98 + 99 + func FuzzBase85Roundtrip(f *testing.F) { 100 + f.Add([]byte{0x2b, 0x0d}) 101 + f.Add([]byte{0xbc, 0xb4, 0x3f}) 102 + f.Add([]byte{0xfa, 0x62, 0x05, 0x83, 0x24, 0x39, 0xd5, 0x25}) 103 + f.Add([]byte{0x31, 0x59, 0x02, 0xa0, 0x61, 0x12, 0xd9, 0x43, 0xb8, 0x23, 0x1a, 0xb4, 0x02, 0xae, 0xfa, 0xcc, 0x22, 0xad, 0x41, 0xb9, 0xb8}) 104 + 105 + f.Fuzz(func(t *testing.T, in []byte) { 106 + n := len(in) 107 + dst := make([]byte, base85Len(n)) 108 + out := make([]byte, n) 109 + 110 + base85Encode(dst, in) 111 + if err := base85Decode(out, dst); err != nil { 112 + t.Fatalf("unexpected error decoding base85 data: %v", err) 113 + } 114 + if !bytes.Equal(in, out) { 115 + t.Errorf("decoded data differed from input data:\n input: %x\n output: %x\nencoding: %s\n", in, out, string(dst)) 116 + } 117 + }) 118 + }
+277
gitdiff/format.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "bytes" 5 + "compress/zlib" 6 + "fmt" 7 + "io" 8 + "strconv" 9 + ) 10 + 11 + type formatter struct { 12 + w io.Writer 13 + err error 14 + } 15 + 16 + func newFormatter(w io.Writer) *formatter { 17 + return &formatter{w: w} 18 + } 19 + 20 + func (fm *formatter) Write(p []byte) (int, error) { 21 + if fm.err != nil { 22 + return len(p), nil 23 + } 24 + if _, err := fm.w.Write(p); err != nil { 25 + fm.err = err 26 + } 27 + return len(p), nil 28 + } 29 + 30 + func (fm *formatter) WriteString(s string) (int, error) { 31 + fm.Write([]byte(s)) 32 + return len(s), nil 33 + } 34 + 35 + func (fm *formatter) WriteByte(c byte) error { 36 + fm.Write([]byte{c}) 37 + return nil 38 + } 39 + 40 + func (fm *formatter) WriteQuotedName(s string) { 41 + qpos := 0 42 + for i := 0; i < len(s); i++ { 43 + ch := s[i] 44 + if q, quoted := quoteByte(ch); quoted { 45 + if qpos == 0 { 46 + fm.WriteByte('"') 47 + } 48 + fm.WriteString(s[qpos:i]) 49 + fm.Write(q) 50 + qpos = i + 1 51 + } 52 + } 53 + fm.WriteString(s[qpos:]) 54 + if qpos > 0 { 55 + fm.WriteByte('"') 56 + } 57 + } 58 + 59 + var quoteEscapeTable = map[byte]byte{ 60 + '\a': 'a', 61 + '\b': 'b', 62 + '\t': 't', 63 + '\n': 'n', 64 + '\v': 'v', 65 + '\f': 'f', 66 + '\r': 'r', 67 + '"': '"', 68 + '\\': '\\', 69 + } 70 + 71 + func quoteByte(b byte) ([]byte, bool) { 72 + if q, ok := quoteEscapeTable[b]; ok { 73 + return []byte{'\\', q}, true 74 + } 75 + if b < 0x20 || b >= 0x7F { 76 + return []byte{ 77 + '\\', 78 + '0' + (b>>6)&0o3, 79 + '0' + (b>>3)&0o7, 80 + '0' + (b>>0)&0o7, 81 + }, true 82 + } 83 + return nil, false 84 + } 85 + 86 + func (fm *formatter) FormatFile(f *File) { 87 + fm.WriteString("diff --git ") 88 + 89 + var aName, bName string 90 + switch { 91 + case f.OldName == "": 92 + aName = f.NewName 93 + bName = f.NewName 94 + 95 + case f.NewName == "": 96 + aName = f.OldName 97 + bName = f.OldName 98 + 99 + default: 100 + aName = f.OldName 101 + bName = f.NewName 102 + } 103 + 104 + fm.WriteQuotedName("a/" + aName) 105 + fm.WriteByte(' ') 106 + fm.WriteQuotedName("b/" + bName) 107 + fm.WriteByte('\n') 108 + 109 + if f.OldMode != 0 { 110 + if f.IsDelete { 111 + fmt.Fprintf(fm, "deleted file mode %o\n", f.OldMode) 112 + } else if f.NewMode != 0 { 113 + fmt.Fprintf(fm, "old mode %o\n", f.OldMode) 114 + } 115 + } 116 + 117 + if f.NewMode != 0 { 118 + if f.IsNew { 119 + fmt.Fprintf(fm, "new file mode %o\n", f.NewMode) 120 + } else if f.OldMode != 0 { 121 + fmt.Fprintf(fm, "new mode %o\n", f.NewMode) 122 + } 123 + } 124 + 125 + if f.Score > 0 { 126 + if f.IsCopy || f.IsRename { 127 + fmt.Fprintf(fm, "similarity index %d%%\n", f.Score) 128 + } else { 129 + fmt.Fprintf(fm, "dissimilarity index %d%%\n", f.Score) 130 + } 131 + } 132 + 133 + if f.IsCopy { 134 + if f.OldName != "" { 135 + fm.WriteString("copy from ") 136 + fm.WriteQuotedName(f.OldName) 137 + fm.WriteByte('\n') 138 + } 139 + if f.NewName != "" { 140 + fm.WriteString("copy to ") 141 + fm.WriteQuotedName(f.NewName) 142 + fm.WriteByte('\n') 143 + } 144 + } 145 + 146 + if f.IsRename { 147 + if f.OldName != "" { 148 + fm.WriteString("rename from ") 149 + fm.WriteQuotedName(f.OldName) 150 + fm.WriteByte('\n') 151 + } 152 + if f.NewName != "" { 153 + fm.WriteString("rename to ") 154 + fm.WriteQuotedName(f.NewName) 155 + fm.WriteByte('\n') 156 + } 157 + } 158 + 159 + if f.OldOIDPrefix != "" && f.NewOIDPrefix != "" { 160 + fmt.Fprintf(fm, "index %s..%s", f.OldOIDPrefix, f.NewOIDPrefix) 161 + 162 + // Mode is only included on the index line when it is not changing 163 + if f.OldMode != 0 && ((f.NewMode == 0 && !f.IsDelete) || f.OldMode == f.NewMode) { 164 + fmt.Fprintf(fm, " %o", f.OldMode) 165 + } 166 + 167 + fm.WriteByte('\n') 168 + } 169 + 170 + if f.IsBinary { 171 + if f.BinaryFragment == nil { 172 + fm.WriteString("Binary files fmer\n") 173 + } else { 174 + fm.WriteString("GIT binary patch\n") 175 + fm.FormatBinaryFragment(f.BinaryFragment) 176 + if f.ReverseBinaryFragment != nil { 177 + fm.FormatBinaryFragment(f.ReverseBinaryFragment) 178 + } 179 + } 180 + } 181 + 182 + // The "---" and "+++" lines only appear for text patches with fragments 183 + if len(f.TextFragments) > 0 { 184 + fm.WriteString("--- ") 185 + if f.OldName == "" { 186 + fm.WriteString("/dev/null") 187 + } else { 188 + fm.WriteQuotedName("a/" + f.OldName) 189 + } 190 + fm.WriteByte('\n') 191 + 192 + fm.WriteString("+++ ") 193 + if f.NewName == "" { 194 + fm.WriteString("/dev/null") 195 + } else { 196 + fm.WriteQuotedName("b/" + f.NewName) 197 + } 198 + fm.WriteByte('\n') 199 + 200 + for _, frag := range f.TextFragments { 201 + fm.FormatTextFragment(frag) 202 + } 203 + } 204 + } 205 + 206 + func (fm *formatter) FormatTextFragment(f *TextFragment) { 207 + fm.FormatTextFragmentHeader(f) 208 + fm.WriteByte('\n') 209 + 210 + for _, line := range f.Lines { 211 + fm.WriteString(line.Op.String()) 212 + fm.WriteString(line.Line) 213 + if line.NoEOL() { 214 + fm.WriteString("\n\\ No newline at end of file\n") 215 + } 216 + } 217 + } 218 + 219 + func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) { 220 + fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines) 221 + if f.Comment != "" { 222 + fm.WriteByte(' ') 223 + fm.WriteString(f.Comment) 224 + } 225 + } 226 + 227 + func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) { 228 + const ( 229 + maxBytesPerLine = 52 230 + ) 231 + 232 + switch f.Method { 233 + case BinaryPatchDelta: 234 + fm.WriteString("delta ") 235 + case BinaryPatchLiteral: 236 + fm.WriteString("literal ") 237 + } 238 + fm.Write(strconv.AppendInt(nil, f.Size, 10)) 239 + fm.WriteByte('\n') 240 + 241 + data := deflateBinaryChunk(f.Data) 242 + n := (len(data) / maxBytesPerLine) * maxBytesPerLine 243 + 244 + buf := make([]byte, base85Len(maxBytesPerLine)) 245 + for i := 0; i < n; i += maxBytesPerLine { 246 + base85Encode(buf, data[i:i+maxBytesPerLine]) 247 + fm.WriteByte('z') 248 + fm.Write(buf) 249 + fm.WriteByte('\n') 250 + } 251 + if remainder := len(data) - n; remainder > 0 { 252 + buf = buf[0:base85Len(remainder)] 253 + 254 + sizeChar := byte(remainder) 255 + if remainder <= 26 { 256 + sizeChar = 'A' + sizeChar - 1 257 + } else { 258 + sizeChar = 'a' + sizeChar - 27 259 + } 260 + 261 + base85Encode(buf, data[n:]) 262 + fm.WriteByte(sizeChar) 263 + fm.Write(buf) 264 + fm.WriteByte('\n') 265 + } 266 + fm.WriteByte('\n') 267 + } 268 + 269 + func deflateBinaryChunk(data []byte) []byte { 270 + var b bytes.Buffer 271 + 272 + zw := zlib.NewWriter(&b) 273 + _, _ = zw.Write(data) 274 + _ = zw.Close() 275 + 276 + return b.Bytes() 277 + }
+156
gitdiff/format_roundtrip_test.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "slices" 9 + "testing" 10 + ) 11 + 12 + func TestFormatRoundtrip(t *testing.T) { 13 + patches := []struct { 14 + File string 15 + SkipTextCompare bool 16 + }{ 17 + {File: "copy.patch"}, 18 + {File: "copy_modify.patch"}, 19 + {File: "delete.patch"}, 20 + {File: "mode.patch"}, 21 + {File: "mode_modify.patch"}, 22 + {File: "modify.patch"}, 23 + {File: "new.patch"}, 24 + {File: "new_empty.patch"}, 25 + {File: "new_mode.patch"}, 26 + {File: "rename.patch"}, 27 + {File: "rename_modify.patch"}, 28 + 29 + // Due to differences between Go's 'encoding/zlib' package and the zlib 30 + // C library, binary patches cannot be compared directly as the patch 31 + // data is slightly different when re-encoded by Go. 32 + {File: "binary_modify.patch", SkipTextCompare: true}, 33 + {File: "binary_new.patch", SkipTextCompare: true}, 34 + } 35 + 36 + for _, patch := range patches { 37 + t.Run(patch.File, func(t *testing.T) { 38 + b, err := os.ReadFile(filepath.Join("testdata", "string", patch.File)) 39 + if err != nil { 40 + t.Fatalf("failed to read patch: %v", err) 41 + } 42 + 43 + original := assertParseSingleFile(t, b, "patch") 44 + str := original.String() 45 + 46 + if !patch.SkipTextCompare { 47 + if string(b) != str { 48 + t.Errorf("incorrect patch text\nexpected: %q\n actual: %q\n", string(b), str) 49 + } 50 + } 51 + 52 + reparsed := assertParseSingleFile(t, []byte(str), "formatted patch") 53 + assertFilesEqual(t, original, reparsed) 54 + }) 55 + } 56 + } 57 + 58 + func assertParseSingleFile(t *testing.T, b []byte, kind string) *File { 59 + files, _, err := Parse(bytes.NewReader(b)) 60 + if err != nil { 61 + t.Fatalf("failed to parse %s: %v", kind, err) 62 + } 63 + if len(files) != 1 { 64 + t.Fatalf("expected %s to contain a single files, but found %d", kind, len(files)) 65 + } 66 + return files[0] 67 + } 68 + 69 + func assertFilesEqual(t *testing.T, expected, actual *File) { 70 + assertEqual(t, expected.OldName, actual.OldName, "OldName") 71 + assertEqual(t, expected.NewName, actual.NewName, "NewName") 72 + 73 + assertEqual(t, expected.IsNew, actual.IsNew, "IsNew") 74 + assertEqual(t, expected.IsDelete, actual.IsDelete, "IsDelete") 75 + assertEqual(t, expected.IsCopy, actual.IsCopy, "IsCopy") 76 + assertEqual(t, expected.IsRename, actual.IsRename, "IsRename") 77 + 78 + assertEqual(t, expected.OldMode, actual.OldMode, "OldMode") 79 + assertEqual(t, expected.NewMode, actual.NewMode, "NewMode") 80 + 81 + assertEqual(t, expected.OldOIDPrefix, actual.OldOIDPrefix, "OldOIDPrefix") 82 + assertEqual(t, expected.NewOIDPrefix, actual.NewOIDPrefix, "NewOIDPrefix") 83 + assertEqual(t, expected.Score, actual.Score, "Score") 84 + 85 + if len(expected.TextFragments) == len(actual.TextFragments) { 86 + for i := range expected.TextFragments { 87 + prefix := fmt.Sprintf("TextFragments[%d].", i) 88 + ef := expected.TextFragments[i] 89 + af := actual.TextFragments[i] 90 + 91 + assertEqual(t, ef.Comment, af.Comment, prefix+"Comment") 92 + 93 + assertEqual(t, ef.OldPosition, af.OldPosition, prefix+"OldPosition") 94 + assertEqual(t, ef.OldLines, af.OldLines, prefix+"OldLines") 95 + 96 + assertEqual(t, ef.NewPosition, af.NewPosition, prefix+"NewPosition") 97 + assertEqual(t, ef.NewLines, af.NewLines, prefix+"NewLines") 98 + 99 + assertEqual(t, ef.LinesAdded, af.LinesAdded, prefix+"LinesAdded") 100 + assertEqual(t, ef.LinesDeleted, af.LinesDeleted, prefix+"LinesDeleted") 101 + 102 + assertEqual(t, ef.LeadingContext, af.LeadingContext, prefix+"LeadingContext") 103 + assertEqual(t, ef.TrailingContext, af.TrailingContext, prefix+"TrailingContext") 104 + 105 + if !slices.Equal(ef.Lines, af.Lines) { 106 + t.Errorf("%sLines: expected %#v, actual %#v", prefix, ef.Lines, af.Lines) 107 + } 108 + } 109 + } else { 110 + t.Errorf("TextFragments: expected length %d, actual length %d", len(expected.TextFragments), len(actual.TextFragments)) 111 + } 112 + 113 + assertEqual(t, expected.IsBinary, actual.IsBinary, "IsBinary") 114 + 115 + if expected.BinaryFragment != nil { 116 + if actual.BinaryFragment == nil { 117 + t.Errorf("BinaryFragment: expected non-nil, actual is nil") 118 + } else { 119 + ef := expected.BinaryFragment 120 + af := expected.BinaryFragment 121 + 122 + assertEqual(t, ef.Method, af.Method, "BinaryFragment.Method") 123 + assertEqual(t, ef.Size, af.Size, "BinaryFragment.Size") 124 + 125 + if !slices.Equal(ef.Data, af.Data) { 126 + t.Errorf("BinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data) 127 + } 128 + } 129 + } else if actual.BinaryFragment != nil { 130 + t.Errorf("BinaryFragment: expected nil, actual is non-nil") 131 + } 132 + 133 + if expected.ReverseBinaryFragment != nil { 134 + if actual.ReverseBinaryFragment == nil { 135 + t.Errorf("ReverseBinaryFragment: expected non-nil, actual is nil") 136 + } else { 137 + ef := expected.ReverseBinaryFragment 138 + af := expected.ReverseBinaryFragment 139 + 140 + assertEqual(t, ef.Method, af.Method, "ReverseBinaryFragment.Method") 141 + assertEqual(t, ef.Size, af.Size, "ReverseBinaryFragment.Size") 142 + 143 + if !slices.Equal(ef.Data, af.Data) { 144 + t.Errorf("ReverseBinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data) 145 + } 146 + } 147 + } else if actual.ReverseBinaryFragment != nil { 148 + t.Errorf("ReverseBinaryFragment: expected nil, actual is non-nil") 149 + } 150 + } 151 + 152 + func assertEqual[T comparable](t *testing.T, expected, actual T, name string) { 153 + if expected != actual { 154 + t.Errorf("%s: expected %#v, actual %#v", name, expected, actual) 155 + } 156 + }
+28
gitdiff/format_test.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + ) 7 + 8 + func TestFormatter_WriteQuotedName(t *testing.T) { 9 + tests := []struct { 10 + Input string 11 + Expected string 12 + }{ 13 + {"noquotes.txt", `noquotes.txt`}, 14 + {"no quotes.txt", `no quotes.txt`}, 15 + {"new\nline", `"new\nline"`}, 16 + {"escape\x1B null\x00", `"escape\033 null\000"`}, 17 + {"snowman \u2603 snowman", `"snowman \342\230\203 snowman"`}, 18 + {"\"already quoted\"", `"\"already quoted\""`}, 19 + } 20 + 21 + for _, test := range tests { 22 + var b strings.Builder 23 + newFormatter(&b).WriteQuotedName(test.Input) 24 + if b.String() != test.Expected { 25 + t.Errorf("expected %q, got %q", test.Expected, b.String()) 26 + } 27 + } 28 + }
+33 -2
gitdiff/gitdiff.go
··· 4 4 "errors" 5 5 "fmt" 6 6 "os" 7 + "strings" 7 8 ) 8 9 9 10 // File describes changes to a single file. It can be either a text file or a ··· 38 39 ReverseBinaryFragment *BinaryFragment 39 40 } 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. 45 + func (f *File) String() string { 46 + var diff strings.Builder 47 + newFormatter(&diff).FormatFile(f) 48 + return diff.String() 49 + } 50 + 41 51 // TextFragment describes changed lines starting at a specific line in a text file. 42 52 type TextFragment struct { 43 53 Comment string ··· 57 67 Lines []Line 58 68 } 59 69 60 - // Header returns the canonical header of this fragment. 70 + // String returns a git diff format of this fragment. See [File.String] for 71 + // more details on this format. 72 + func (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. 61 80 func (f *TextFragment) Header() string { 62 - return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment) 81 + var hdr strings.Builder 82 + newFormatter(&hdr).FormatTextFragmentHeader(f) 83 + return hdr.String() 63 84 } 64 85 65 86 // Validate checks that the fragment is self-consistent and appliable. Validate ··· 197 218 // BinaryPatchLiteral indicates the data is the exact file content 198 219 BinaryPatchLiteral 199 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. 226 + func (f *BinaryFragment) String() string { 227 + var diff strings.Builder 228 + newFormatter(&diff).FormatBinaryFragment(f) 229 + return diff.String() 230 + }
+9
gitdiff/testdata/string/binary_modify.patch
··· 1 + diff --git a/file.bin b/file.bin 2 + index a7f4d5d6975ec021016c02b6d58345ebf434f38c..bdc9a70f055892146612dcdb413f0e339faaa0df 100644 3 + GIT binary patch 4 + delta 66 5 + QcmeZhVVvM$!$1K50C&Ox;s5{u 6 + 7 + delta 5 8 + McmZo+^qAlQ00i9urT_o{ 9 +
+11
gitdiff/testdata/string/binary_new.patch
··· 1 + diff --git a/file.bin b/file.bin 2 + new file mode 100644 3 + index 0000000000000000000000000000000000000000..a7f4d5d6975ec021016c02b6d58345ebf434f38c 4 + GIT binary patch 5 + literal 72 6 + zcmV-O0Jr~td-`u6JcK&{KDK=<a#;v1^LR5&K)zQ0=Goz82(?nJ6_nD`f#8O9p}}{P 7 + eiXim+rDI+BDadMQmMsO5Sw@;DbrCA+PamP;Ng_@F 8 + 9 + literal 0 10 + HcmV?d00001 11 +
+4
gitdiff/testdata/string/copy.patch
··· 1 + diff --git a/file.txt b/numbers.txt 2 + similarity index 100% 3 + copy from file.txt 4 + copy to numbers.txt
+21
gitdiff/testdata/string/copy_modify.patch
··· 1 + diff --git a/file.txt b/numbers.txt 2 + similarity index 57% 3 + copy from file.txt 4 + copy to numbers.txt 5 + index c9e9e05..6c4a3e0 100644 6 + --- a/file.txt 7 + +++ b/numbers.txt 8 + @@ -1,6 +1,6 @@ 9 + one 10 + two 11 + -three 12 + +three three three 13 + four 14 + five 15 + six 16 + @@ -8,3 +8,5 @@ seven 17 + eight 18 + nine 19 + ten 20 + +eleven 21 + +twelve
+16
gitdiff/testdata/string/delete.patch
··· 1 + diff --git a/file.txt b/file.txt 2 + deleted file mode 100644 3 + index c9e9e05..0000000 4 + --- a/file.txt 5 + +++ /dev/null 6 + @@ -1,10 +0,0 @@ 7 + -one 8 + -two 9 + -three 10 + -four 11 + -five 12 + -six 13 + -seven 14 + -eight 15 + -nine 16 + -ten
+3
gitdiff/testdata/string/mode.patch
··· 1 + diff --git a/file.txt b/file.txt 2 + old mode 100644 3 + new mode 100755
+10
gitdiff/testdata/string/mode_modify.patch
··· 1 + diff --git a/script.sh b/script.sh 2 + old mode 100644 3 + new mode 100755 4 + index 7a870bd..68d501e 5 + --- a/script.sh 6 + +++ b/script.sh 7 + @@ -1,2 +1,2 @@ 8 + #!/bin/bash 9 + -echo "Hello World" 10 + +echo "Hello, World!"
+16
gitdiff/testdata/string/modify.patch
··· 1 + diff --git a/file.txt b/file.txt 2 + index c9e9e05..7d5fdc6 100644 3 + --- a/file.txt 4 + +++ b/file.txt 5 + @@ -3,8 +3,10 @@ two 6 + three 7 + four 8 + five 9 + -six 10 + +six six six six six six 11 + seven 12 + eight 13 + nine 14 + ten 15 + +eleven 16 + +twelve
+16
gitdiff/testdata/string/new.patch
··· 1 + diff --git a/file.txt b/file.txt 2 + new file mode 100644 3 + index 0000000..c9e9e05 4 + --- /dev/null 5 + +++ b/file.txt 6 + @@ -0,0 +1,10 @@ 7 + +one 8 + +two 9 + +three 10 + +four 11 + +five 12 + +six 13 + +seven 14 + +eight 15 + +nine 16 + +ten
+3
gitdiff/testdata/string/new_empty.patch
··· 1 + diff --git a/file.txt b/file.txt 2 + new file mode 100644 3 + index 0000000..e69de29
+16
gitdiff/testdata/string/new_mode.patch
··· 1 + diff --git a/file.sh b/file.sh 2 + new file mode 100755 3 + index 0000000..c9e9e05 4 + --- /dev/null 5 + +++ b/file.sh 6 + @@ -0,0 +1,10 @@ 7 + +one 8 + +two 9 + +three 10 + +four 11 + +five 12 + +six 13 + +seven 14 + +eight 15 + +nine 16 + +ten
+4
gitdiff/testdata/string/rename.patch
··· 1 + diff --git a/file.txt b/numbers.txt 2 + similarity index 100% 3 + rename from file.txt 4 + rename to numbers.txt
+18
gitdiff/testdata/string/rename_modify.patch
··· 1 + diff --git a/file.txt b/numbers.txt 2 + similarity index 77% 3 + rename from file.txt 4 + rename to numbers.txt 5 + index c9e9e05..a6b31d6 100644 6 + --- a/file.txt 7 + +++ b/numbers.txt 8 + @@ -3,8 +3,9 @@ two 9 + three 10 + four 11 + five 12 + -six 13 + + six 14 + seven 15 + eight 16 + nine 17 + ten 18 + +eleven