Skip to content

Commit 69e4444

Browse files
committed
Return preamble content when parsing files
This allows callers to provide patches with commit or email headers and then retrieve that leading content for additional parsing without having to identify where the first file in the patch starts.
1 parent 07e5314 commit 69e4444

File tree

3 files changed

+67
-33
lines changed

3 files changed

+67
-33
lines changed

gitdiff/parser.go

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,24 @@ import (
88
"strings"
99
)
1010

11-
// Parse parses a patch with changes for one or more files. Any content
12-
// preceding the first file header is ignored. If an error occurs while
13-
// parsing, files will contain all files parsed before the error.
14-
func Parse(r io.Reader) ([]*File, error) {
11+
// Parse parses a patch with changes to one or more files. Any content before
12+
// the first file is returned as the second value. If an error occurs while
13+
// parsing, it returns all files parsed before the error.
14+
func Parse(r io.Reader) ([]*File, string, error) {
1515
p := &parser{r: bufio.NewReader(r)}
1616
if err := p.Next(); err != nil {
1717
if err == io.EOF {
18-
return nil, nil
18+
return nil, "", nil
1919
}
20-
return nil, err
20+
return nil, "", err
2121
}
2222

23-
// TODO(bkeyes): capture non-file lines in between files
24-
23+
var preamble string
2524
var files []*File
2625
for {
27-
file, err := p.ParseNextFileHeader()
26+
file, pre, err := p.ParseNextFileHeader()
2827
if err != nil {
29-
return files, err
28+
return files, preamble, err
3029
}
3130
if file == nil {
3231
break
@@ -38,17 +37,20 @@ func Parse(r io.Reader) ([]*File, error) {
3837
} {
3938
n, err := fn(file)
4039
if err != nil {
41-
return files, err
40+
return files, preamble, err
4241
}
4342
if n > 0 {
4443
break
4544
}
4645
}
47-
// file has fragment(s) from above or the patch is empty or invalid
46+
47+
if len(files) == 0 {
48+
preamble = pre
49+
}
4850
files = append(files, file)
4951
}
5052

51-
return files, nil
53+
return files, preamble, nil
5254
}
5355

5456
// TODO(bkeyes): consider exporting the parser type with configuration
@@ -71,9 +73,11 @@ type parser struct {
7173
lines [3]string
7274
}
7375

74-
// ParseNextFileHeader finds and parses the next file header in the stream. It
75-
// returns nil if no headers are found before the end of the stream.
76-
func (p *parser) ParseNextFileHeader() (*File, error) {
76+
// ParseNextFileHeader finds and parses the next file header in the stream. If
77+
// a header is found, it returns a file and all input before the header. It
78+
// returns nil if no headers are found before the end of the input.
79+
func (p *parser) ParseNextFileHeader() (*File, string, error) {
80+
var preamble strings.Builder
7781
var file *File
7882
for {
7983
// check for disconnected fragment headers (corrupt patch)
@@ -83,36 +87,37 @@ func (p *parser) ParseNextFileHeader() (*File, error) {
8387
goto NextLine
8488
}
8589
if frag != nil {
86-
return nil, p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
90+
return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
8791
}
8892

8993
// check for a git-generated patch
9094
file, err = p.ParseGitFileHeader()
9195
if err != nil {
92-
return nil, err
96+
return nil, "", err
9397
}
9498
if file != nil {
95-
return file, nil
99+
return file, preamble.String(), nil
96100
}
97101

98102
// check for a "traditional" patch
99103
file, err = p.ParseTraditionalFileHeader()
100104
if err != nil {
101-
return nil, err
105+
return nil, "", err
102106
}
103107
if file != nil {
104-
return file, nil
108+
return file, preamble.String(), nil
105109
}
106110

107111
NextLine:
112+
preamble.WriteString(p.Line(0))
108113
if err := p.Next(); err != nil {
109114
if err == io.EOF {
110115
break
111116
}
112-
return nil, err
117+
return nil, "", err
113118
}
114119
}
115-
return nil, nil
120+
return nil, "", nil
116121
}
117122

118123
// ParseTextFragments parses text fragments until the next file header or the

gitdiff/parser_test.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,18 @@ context line
133133
}
134134

135135
if test.EndLine != p.Line(0) {
136-
t.Errorf("incorrect position after parsing\nexpected: %q\nactual: %q", test.EndLine, p.Line(0))
136+
t.Errorf("incorrect position after parsing\nexpected: %q\n actual: %q", test.EndLine, p.Line(0))
137137
}
138138
})
139139
}
140140
}
141141

142142
func TestParseNextFileHeader(t *testing.T) {
143143
tests := map[string]struct {
144-
Input string
145-
Output *File
146-
Err bool
144+
Input string
145+
Output *File
146+
Preamble string
147+
Err bool
147148
}{
148149
"gitHeader": {
149150
Input: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
@@ -165,6 +166,13 @@ index cc34da1..1acbae5 100644
165166
OldOIDPrefix: "cc34da1",
166167
NewOIDPrefix: "1acbae5",
167168
},
169+
Preamble: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
170+
Author: Morton Haypenny <mhaypenny@example.com>
171+
Date: Tue Apr 2 22:30:00 2019 -0700
172+
173+
This is a sample commit message.
174+
175+
`,
168176
},
169177
"traditionalHeader": {
170178
Input: `
@@ -176,6 +184,7 @@ index cc34da1..1acbae5 100644
176184
OldName: "file.txt",
177185
NewName: "file.txt",
178186
},
187+
Preamble: "\n",
179188
},
180189
"noHeaders": {
181190
Input: `
@@ -184,7 +193,8 @@ this is another line
184193
--- could this be a header?
185194
nope, it's just some dashes
186195
`,
187-
Output: nil,
196+
Output: nil,
197+
Preamble: "",
188198
},
189199
"detatchedFragmentLike": {
190200
Input: `
@@ -206,7 +216,7 @@ a wild fragment appears?
206216
t.Run(name, func(t *testing.T) {
207217
p := newTestParser(test.Input, true)
208218

209-
f, err := p.ParseNextFileHeader()
219+
f, pre, err := p.ParseNextFileHeader()
210220
if test.Err {
211221
if err == nil || err == io.EOF {
212222
t.Fatalf("expected error parsing next file header, but got %v", err)
@@ -217,8 +227,11 @@ a wild fragment appears?
217227
t.Fatalf("unexpected error parsing next file header: %v", err)
218228
}
219229

230+
if test.Preamble != pre {
231+
t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
232+
}
220233
if !reflect.DeepEqual(test.Output, f) {
221-
t.Errorf("incorrect file\nexpected: %+v\nactual: %+v", test.Output, f)
234+
t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
222235
}
223236
})
224237
}
@@ -266,9 +279,20 @@ func TestParse(t *testing.T) {
266279
},
267280
}
268281

282+
expectedPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
283+
Author: Morton Haypenny <mhaypenny@example.com>
284+
Date: Tue Apr 2 22:55:40 2019 -0700
285+
286+
A file with multiple fragments.
287+
288+
The content is arbitrary.
289+
290+
`
291+
269292
tests := map[string]struct {
270293
InputFile string
271294
Output []*File
295+
Preamble string
272296
Err bool
273297
}{
274298
"oneFile": {
@@ -283,6 +307,7 @@ func TestParse(t *testing.T) {
283307
Fragments: expectedFragments,
284308
},
285309
},
310+
Preamble: expectedPreamble,
286311
},
287312
"twoFiles": {
288313
InputFile: "testdata/two_files.patch",
@@ -304,6 +329,7 @@ func TestParse(t *testing.T) {
304329
Fragments: expectedFragments,
305330
},
306331
},
332+
Preamble: expectedPreamble,
307333
},
308334
}
309335

@@ -314,7 +340,7 @@ func TestParse(t *testing.T) {
314340
t.Fatalf("unexpected error opening input file: %v", err)
315341
}
316342

317-
files, err := Parse(f)
343+
files, pre, err := Parse(f)
318344
if test.Err {
319345
if err == nil || err == io.EOF {
320346
t.Fatalf("expected error parsing patch, but got %v", err)
@@ -328,11 +354,14 @@ func TestParse(t *testing.T) {
328354
if len(test.Output) != len(files) {
329355
t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files))
330356
}
357+
if test.Preamble != pre {
358+
t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
359+
}
331360
for i := range test.Output {
332361
if !reflect.DeepEqual(test.Output[i], files[i]) {
333362
exp, _ := json.MarshalIndent(test.Output[i], "", " ")
334363
act, _ := json.MarshalIndent(files[i], "", " ")
335-
t.Errorf("incorrect file at position %d\nexpected: %s\nactual: %s", i, exp, act)
364+
t.Errorf("incorrect file at position %d\nexpected: %s\n actual: %s", i, exp, act)
336365
}
337366
}
338367
})

gitdiff/testdata/one_file.patch

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
22
Author: Morton Haypenny <mhaypenny@example.com>
33
Date: Tue Apr 2 22:55:40 2019 -0700
44

5-
A single file with multiple fragments.
5+
A file with multiple fragments.
66

77
The content is arbitrary.
88

0 commit comments

Comments
 (0)