+6
-6
.github/workflows/go.yml
+6
-6
.github/workflows/go.yml
···
9
9
name: Verify
10
10
runs-on: ubuntu-latest
11
11
steps:
12
-
- name: Set up Go 1.19
13
-
uses: actions/setup-go@v3
12
+
- name: Set up Go 1.21
13
+
uses: actions/setup-go@v5
14
14
with:
15
-
go-version: 1.19
15
+
go-version: 1.21
16
16
17
17
- name: Check out code into the Go module directory
18
-
uses: actions/checkout@v2
18
+
uses: actions/checkout@v4
19
19
20
20
- name: Lint
21
-
uses: golangci/golangci-lint-action@v3
21
+
uses: golangci/golangci-lint-action@v7
22
22
with:
23
-
version: v1.49
23
+
version: v2.0
24
24
25
25
- name: Test
26
26
run: go test -v ./...
+40
-13
.golangci.yml
+40
-13
.golangci.yml
···
1
+
version: "2"
2
+
1
3
run:
2
4
tests: false
3
5
4
6
linters:
5
-
disable-all: true
7
+
default: none
6
8
enable:
7
-
- deadcode
8
9
- errcheck
9
-
- gofmt
10
-
- goimports
11
-
- golint
12
10
- govet
13
11
- ineffassign
14
12
- misspell
15
-
- typecheck
13
+
- revive
16
14
- unconvert
17
-
- varcheck
18
-
19
-
issues:
20
-
exclude-use-default: false
15
+
- unused
16
+
settings:
17
+
errcheck:
18
+
exclude-functions:
19
+
- (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).Write
20
+
- (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).WriteString
21
+
- (*github.com/bluekeyes/go-gitdiff/gitdiff.formatter).WriteByte
22
+
- fmt.Fprintf(*github.com/bluekeyes/go-gitdiff/gitdiff.formatter)
23
+
revive:
24
+
rules:
25
+
- name: context-keys-type
26
+
- name: time-naming
27
+
- name: var-declaration
28
+
- name: unexported-return
29
+
- name: errorf
30
+
- name: blank-imports
31
+
- name: context-as-argument
32
+
- name: dot-imports
33
+
- name: error-return
34
+
- name: error-strings
35
+
- name: error-naming
36
+
- name: exported
37
+
- name: increment-decrement
38
+
- name: var-naming
39
+
- name: package-comments
40
+
- name: range
41
+
- name: receiver-naming
42
+
- name: indent-error-flow
21
43
22
-
linter-settings:
23
-
goimports:
24
-
local-prefixes: github.com/bluekeyes/go-gitdiff
44
+
formatters:
45
+
enable:
46
+
- gofmt
47
+
- goimports
48
+
settings:
49
+
goimports:
50
+
local-prefixes:
51
+
- github.com/bluekeyes/go-gitdiff
+1
-1
README.md
+1
-1
README.md
···
4
4
5
5
A Go library for parsing and applying patches generated by `git diff`, `git
6
6
show`, and `git format-patch`. It can also parse and apply unified diffs
7
-
generated by the standard `diff` tool.
7
+
generated by the standard GNU `diff` tool.
8
8
9
9
It supports standard line-oriented text patches and Git binary patches, and
10
10
aims to parse anything accepted by the `git apply` command.
+1
gitdiff/apply_test.go
+1
gitdiff/apply_test.go
···
22
22
"changeStart": {Files: getApplyFiles("text_fragment_change_start")},
23
23
"changeMiddle": {Files: getApplyFiles("text_fragment_change_middle")},
24
24
"changeEnd": {Files: getApplyFiles("text_fragment_change_end")},
25
+
"changeEndEOL": {Files: getApplyFiles("text_fragment_change_end_eol")},
25
26
"changeExact": {Files: getApplyFiles("text_fragment_change_exact")},
26
27
"changeSingleNoEOL": {Files: getApplyFiles("text_fragment_change_single_noeol")},
27
28
+41
-2
gitdiff/base85.go
+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
+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
+
}
+11
-4
gitdiff/binary.go
+11
-4
gitdiff/binary.go
···
50
50
}
51
51
52
52
func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) {
53
-
switch p.Line(0) {
54
-
case "GIT binary patch\n":
53
+
line := p.Line(0)
54
+
switch {
55
+
case line == "GIT binary patch\n":
55
56
hasData = true
56
-
case "Binary files differ\n":
57
-
case "Files differ\n":
57
+
case isBinaryNoDataMarker(line):
58
58
default:
59
59
return false, false, nil
60
60
}
···
63
63
return false, false, err
64
64
}
65
65
return true, hasData, nil
66
+
}
67
+
68
+
func isBinaryNoDataMarker(line string) bool {
69
+
if strings.HasSuffix(line, " differ\n") {
70
+
return strings.HasPrefix(line, "Binary files ") || strings.HasPrefix(line, "Files ")
71
+
}
72
+
return false
66
73
}
67
74
68
75
func (p *parser) ParseBinaryFragmentHeader() (*BinaryFragment, error) {
+10
gitdiff/binary_test.go
+10
gitdiff/binary_test.go
···
25
25
IsBinary: true,
26
26
HasData: false,
27
27
},
28
+
"binaryFileNoPatchPaths": {
29
+
Input: "Binary files a/foo.bin and b/foo.bin differ\n",
30
+
IsBinary: true,
31
+
HasData: false,
32
+
},
33
+
"fileNoPatch": {
34
+
Input: "Files differ\n",
35
+
IsBinary: true,
36
+
HasData: false,
37
+
},
28
38
"textFile": {
29
39
Input: "@@ -10,14 +22,31 @@\n",
30
40
IsBinary: false,
+281
gitdiff/format.go
+281
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 ")
173
+
fm.WriteQuotedName("a/" + aName)
174
+
fm.WriteString(" and ")
175
+
fm.WriteQuotedName("b/" + bName)
176
+
fm.WriteString(" differ\n")
177
+
} else {
178
+
fm.WriteString("GIT binary patch\n")
179
+
fm.FormatBinaryFragment(f.BinaryFragment)
180
+
if f.ReverseBinaryFragment != nil {
181
+
fm.FormatBinaryFragment(f.ReverseBinaryFragment)
182
+
}
183
+
}
184
+
}
185
+
186
+
// The "---" and "+++" lines only appear for text patches with fragments
187
+
if len(f.TextFragments) > 0 {
188
+
fm.WriteString("--- ")
189
+
if f.OldName == "" {
190
+
fm.WriteString("/dev/null")
191
+
} else {
192
+
fm.WriteQuotedName("a/" + f.OldName)
193
+
}
194
+
fm.WriteByte('\n')
195
+
196
+
fm.WriteString("+++ ")
197
+
if f.NewName == "" {
198
+
fm.WriteString("/dev/null")
199
+
} else {
200
+
fm.WriteQuotedName("b/" + f.NewName)
201
+
}
202
+
fm.WriteByte('\n')
203
+
204
+
for _, frag := range f.TextFragments {
205
+
fm.FormatTextFragment(frag)
206
+
}
207
+
}
208
+
}
209
+
210
+
func (fm *formatter) FormatTextFragment(f *TextFragment) {
211
+
fm.FormatTextFragmentHeader(f)
212
+
fm.WriteByte('\n')
213
+
214
+
for _, line := range f.Lines {
215
+
fm.WriteString(line.Op.String())
216
+
fm.WriteString(line.Line)
217
+
if line.NoEOL() {
218
+
fm.WriteString("\n\\ No newline at end of file\n")
219
+
}
220
+
}
221
+
}
222
+
223
+
func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) {
224
+
fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines)
225
+
if f.Comment != "" {
226
+
fm.WriteByte(' ')
227
+
fm.WriteString(f.Comment)
228
+
}
229
+
}
230
+
231
+
func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) {
232
+
const (
233
+
maxBytesPerLine = 52
234
+
)
235
+
236
+
switch f.Method {
237
+
case BinaryPatchDelta:
238
+
fm.WriteString("delta ")
239
+
case BinaryPatchLiteral:
240
+
fm.WriteString("literal ")
241
+
}
242
+
fm.Write(strconv.AppendInt(nil, f.Size, 10))
243
+
fm.WriteByte('\n')
244
+
245
+
data := deflateBinaryChunk(f.Data)
246
+
n := (len(data) / maxBytesPerLine) * maxBytesPerLine
247
+
248
+
buf := make([]byte, base85Len(maxBytesPerLine))
249
+
for i := 0; i < n; i += maxBytesPerLine {
250
+
base85Encode(buf, data[i:i+maxBytesPerLine])
251
+
fm.WriteByte('z')
252
+
fm.Write(buf)
253
+
fm.WriteByte('\n')
254
+
}
255
+
if remainder := len(data) - n; remainder > 0 {
256
+
buf = buf[0:base85Len(remainder)]
257
+
258
+
sizeChar := byte(remainder)
259
+
if remainder <= 26 {
260
+
sizeChar = 'A' + sizeChar - 1
261
+
} else {
262
+
sizeChar = 'a' + sizeChar - 27
263
+
}
264
+
265
+
base85Encode(buf, data[n:])
266
+
fm.WriteByte(sizeChar)
267
+
fm.Write(buf)
268
+
fm.WriteByte('\n')
269
+
}
270
+
fm.WriteByte('\n')
271
+
}
272
+
273
+
func deflateBinaryChunk(data []byte) []byte {
274
+
var b bytes.Buffer
275
+
276
+
zw := zlib.NewWriter(&b)
277
+
_, _ = zw.Write(data)
278
+
_ = zw.Close()
279
+
280
+
return b.Bytes()
281
+
}
+157
gitdiff/format_roundtrip_test.go
+157
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
+
{File: "binary_modify_nodata.patch"},
35
+
}
36
+
37
+
for _, patch := range patches {
38
+
t.Run(patch.File, func(t *testing.T) {
39
+
b, err := os.ReadFile(filepath.Join("testdata", "string", patch.File))
40
+
if err != nil {
41
+
t.Fatalf("failed to read patch: %v", err)
42
+
}
43
+
44
+
original := assertParseSingleFile(t, b, "patch")
45
+
str := original.String()
46
+
47
+
if !patch.SkipTextCompare {
48
+
if string(b) != str {
49
+
t.Errorf("incorrect patch text\nexpected: %q\n actual: %q\n", string(b), str)
50
+
}
51
+
}
52
+
53
+
reparsed := assertParseSingleFile(t, []byte(str), "formatted patch")
54
+
assertFilesEqual(t, original, reparsed)
55
+
})
56
+
}
57
+
}
58
+
59
+
func assertParseSingleFile(t *testing.T, b []byte, kind string) *File {
60
+
files, _, err := Parse(bytes.NewReader(b))
61
+
if err != nil {
62
+
t.Fatalf("failed to parse %s: %v", kind, err)
63
+
}
64
+
if len(files) != 1 {
65
+
t.Fatalf("expected %s to contain a single files, but found %d", kind, len(files))
66
+
}
67
+
return files[0]
68
+
}
69
+
70
+
func assertFilesEqual(t *testing.T, expected, actual *File) {
71
+
assertEqual(t, expected.OldName, actual.OldName, "OldName")
72
+
assertEqual(t, expected.NewName, actual.NewName, "NewName")
73
+
74
+
assertEqual(t, expected.IsNew, actual.IsNew, "IsNew")
75
+
assertEqual(t, expected.IsDelete, actual.IsDelete, "IsDelete")
76
+
assertEqual(t, expected.IsCopy, actual.IsCopy, "IsCopy")
77
+
assertEqual(t, expected.IsRename, actual.IsRename, "IsRename")
78
+
79
+
assertEqual(t, expected.OldMode, actual.OldMode, "OldMode")
80
+
assertEqual(t, expected.NewMode, actual.NewMode, "NewMode")
81
+
82
+
assertEqual(t, expected.OldOIDPrefix, actual.OldOIDPrefix, "OldOIDPrefix")
83
+
assertEqual(t, expected.NewOIDPrefix, actual.NewOIDPrefix, "NewOIDPrefix")
84
+
assertEqual(t, expected.Score, actual.Score, "Score")
85
+
86
+
if len(expected.TextFragments) == len(actual.TextFragments) {
87
+
for i := range expected.TextFragments {
88
+
prefix := fmt.Sprintf("TextFragments[%d].", i)
89
+
ef := expected.TextFragments[i]
90
+
af := actual.TextFragments[i]
91
+
92
+
assertEqual(t, ef.Comment, af.Comment, prefix+"Comment")
93
+
94
+
assertEqual(t, ef.OldPosition, af.OldPosition, prefix+"OldPosition")
95
+
assertEqual(t, ef.OldLines, af.OldLines, prefix+"OldLines")
96
+
97
+
assertEqual(t, ef.NewPosition, af.NewPosition, prefix+"NewPosition")
98
+
assertEqual(t, ef.NewLines, af.NewLines, prefix+"NewLines")
99
+
100
+
assertEqual(t, ef.LinesAdded, af.LinesAdded, prefix+"LinesAdded")
101
+
assertEqual(t, ef.LinesDeleted, af.LinesDeleted, prefix+"LinesDeleted")
102
+
103
+
assertEqual(t, ef.LeadingContext, af.LeadingContext, prefix+"LeadingContext")
104
+
assertEqual(t, ef.TrailingContext, af.TrailingContext, prefix+"TrailingContext")
105
+
106
+
if !slices.Equal(ef.Lines, af.Lines) {
107
+
t.Errorf("%sLines: expected %#v, actual %#v", prefix, ef.Lines, af.Lines)
108
+
}
109
+
}
110
+
} else {
111
+
t.Errorf("TextFragments: expected length %d, actual length %d", len(expected.TextFragments), len(actual.TextFragments))
112
+
}
113
+
114
+
assertEqual(t, expected.IsBinary, actual.IsBinary, "IsBinary")
115
+
116
+
if expected.BinaryFragment != nil {
117
+
if actual.BinaryFragment == nil {
118
+
t.Errorf("BinaryFragment: expected non-nil, actual is nil")
119
+
} else {
120
+
ef := expected.BinaryFragment
121
+
af := expected.BinaryFragment
122
+
123
+
assertEqual(t, ef.Method, af.Method, "BinaryFragment.Method")
124
+
assertEqual(t, ef.Size, af.Size, "BinaryFragment.Size")
125
+
126
+
if !slices.Equal(ef.Data, af.Data) {
127
+
t.Errorf("BinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data)
128
+
}
129
+
}
130
+
} else if actual.BinaryFragment != nil {
131
+
t.Errorf("BinaryFragment: expected nil, actual is non-nil")
132
+
}
133
+
134
+
if expected.ReverseBinaryFragment != nil {
135
+
if actual.ReverseBinaryFragment == nil {
136
+
t.Errorf("ReverseBinaryFragment: expected non-nil, actual is nil")
137
+
} else {
138
+
ef := expected.ReverseBinaryFragment
139
+
af := expected.ReverseBinaryFragment
140
+
141
+
assertEqual(t, ef.Method, af.Method, "ReverseBinaryFragment.Method")
142
+
assertEqual(t, ef.Size, af.Size, "ReverseBinaryFragment.Size")
143
+
144
+
if !slices.Equal(ef.Data, af.Data) {
145
+
t.Errorf("ReverseBinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data)
146
+
}
147
+
}
148
+
} else if actual.ReverseBinaryFragment != nil {
149
+
t.Errorf("ReverseBinaryFragment: expected nil, actual is non-nil")
150
+
}
151
+
}
152
+
153
+
func assertEqual[T comparable](t *testing.T, expected, actual T, name string) {
154
+
if expected != actual {
155
+
t.Errorf("%s: expected %#v, actual %#v", name, expected, actual)
156
+
}
157
+
}
+28
gitdiff/format_test.go
+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
+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
+
}
+13
gitdiff/patch_header.go
+13
gitdiff/patch_header.go
···
52
52
// line, that line will be removed and everything after it will be
53
53
// placed in BodyAppendix.
54
54
BodyAppendix string
55
+
56
+
// All headers completely unparsed
57
+
RawHeaders map[string][]string
55
58
}
56
59
57
60
// Message returns the commit message for the header. The message consists of
···
237
240
break
238
241
}
239
242
243
+
items := strings.SplitN(line, ":", 2)
244
+
245
+
// we have "key: value"
246
+
if len(items) == 2 {
247
+
key := items[0]
248
+
val := items[1]
249
+
h.RawHeaders[key] = append(h.RawHeaders[key], val)
250
+
}
251
+
240
252
switch {
241
253
case strings.HasPrefix(line, authorPrefix):
242
254
u, err := ParsePatchIdentity(line[len(authorPrefix):])
···
361
373
}
362
374
363
375
h := &PatchHeader{}
376
+
h.RawHeaders = msg.Header
364
377
365
378
if strings.HasPrefix(mailLine, mailHeaderPrefix) {
366
379
mailLine = strings.TrimPrefix(mailLine, mailHeaderPrefix)
+10
gitdiff/testdata/apply/text_fragment_change_end_eol.patch
+10
gitdiff/testdata/apply/text_fragment_change_end_eol.patch
···
1
+
diff --git a/gitdiff/testdata/apply/text_fragment_remove_last_eol.src b/gitdiff/testdata/apply/text_fragment_remove_last_eol.src
2
+
index a92d664..8cf2f17 100644
3
+
--- a/gitdiff/testdata/apply/text_fragment_remove_last_eol.src
4
+
+++ b/gitdiff/testdata/apply/text_fragment_remove_last_eol.src
5
+
@@ -1,3 +1,3 @@
6
+
line 1
7
+
line 2
8
+
-line 3
9
+
+line 3
10
+
\ No newline at end of file
+9
gitdiff/testdata/string/binary_modify.patch
+9
gitdiff/testdata/string/binary_modify.patch
+3
gitdiff/testdata/string/binary_modify_nodata.patch
+3
gitdiff/testdata/string/binary_modify_nodata.patch
+11
gitdiff/testdata/string/binary_new.patch
+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
+4
gitdiff/testdata/string/copy.patch
+21
gitdiff/testdata/string/copy_modify.patch
+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
+16
gitdiff/testdata/string/delete.patch
+3
gitdiff/testdata/string/mode.patch
+3
gitdiff/testdata/string/mode.patch
+10
gitdiff/testdata/string/mode_modify.patch
+10
gitdiff/testdata/string/mode_modify.patch
+16
gitdiff/testdata/string/modify.patch
+16
gitdiff/testdata/string/modify.patch
+16
gitdiff/testdata/string/new.patch
+16
gitdiff/testdata/string/new.patch
+3
gitdiff/testdata/string/new_empty.patch
+3
gitdiff/testdata/string/new_empty.patch
+16
gitdiff/testdata/string/new_mode.patch
+16
gitdiff/testdata/string/new_mode.patch
+4
gitdiff/testdata/string/rename.patch
+4
gitdiff/testdata/string/rename.patch
+18
gitdiff/testdata/string/rename_modify.patch
+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