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.
This commit is contained in:
Billy Keyes
2019-04-07 19:57:11 -07:00
parent 07e5314a7c
commit 69e4444419
3 changed files with 67 additions and 33 deletions
+28 -23
View File
@@ -8,25 +8,24 @@ import (
"strings"
)
// Parse parses a patch with changes for one or more files. Any content
// preceding the first file header is ignored. If an error occurs while
// parsing, files will contain all files parsed before the error.
func Parse(r io.Reader) ([]*File, error) {
// Parse parses a patch with changes to one or more files. Any content before
// the first file is returned as the second value. If an error occurs while
// parsing, it returns all files parsed before the error.
func Parse(r io.Reader) ([]*File, string, error) {
p := &parser{r: bufio.NewReader(r)}
if err := p.Next(); err != nil {
if err == io.EOF {
return nil, nil
return nil, "", nil
}
return nil, err
return nil, "", err
}
// TODO(bkeyes): capture non-file lines in between files
var preamble string
var files []*File
for {
file, err := p.ParseNextFileHeader()
file, pre, err := p.ParseNextFileHeader()
if err != nil {
return files, err
return files, preamble, err
}
if file == nil {
break
@@ -38,17 +37,20 @@ func Parse(r io.Reader) ([]*File, error) {
} {
n, err := fn(file)
if err != nil {
return files, err
return files, preamble, err
}
if n > 0 {
break
}
}
// file has fragment(s) from above or the patch is empty or invalid
if len(files) == 0 {
preamble = pre
}
files = append(files, file)
}
return files, nil
return files, preamble, nil
}
// TODO(bkeyes): consider exporting the parser type with configuration
@@ -71,9 +73,11 @@ type parser struct {
lines [3]string
}
// ParseNextFileHeader finds and parses the next file header in the stream. It
// returns nil if no headers are found before the end of the stream.
func (p *parser) ParseNextFileHeader() (*File, error) {
// ParseNextFileHeader finds and parses the next file header in the stream. If
// a header is found, it returns a file and all input before the header. It
// returns nil if no headers are found before the end of the input.
func (p *parser) ParseNextFileHeader() (*File, string, error) {
var preamble strings.Builder
var file *File
for {
// check for disconnected fragment headers (corrupt patch)
@@ -83,36 +87,37 @@ func (p *parser) ParseNextFileHeader() (*File, error) {
goto NextLine
}
if frag != nil {
return nil, p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
}
// check for a git-generated patch
file, err = p.ParseGitFileHeader()
if err != nil {
return nil, err
return nil, "", err
}
if file != nil {
return file, nil
return file, preamble.String(), nil
}
// check for a "traditional" patch
file, err = p.ParseTraditionalFileHeader()
if err != nil {
return nil, err
return nil, "", err
}
if file != nil {
return file, nil
return file, preamble.String(), nil
}
NextLine:
preamble.WriteString(p.Line(0))
if err := p.Next(); err != nil {
if err == io.EOF {
break
}
return nil, err
return nil, "", err
}
}
return nil, nil
return nil, "", nil
}
// ParseTextFragments parses text fragments until the next file header or the
+38 -9
View File
@@ -133,7 +133,7 @@ context line
}
if test.EndLine != p.Line(0) {
t.Errorf("incorrect position after parsing\nexpected: %q\nactual: %q", test.EndLine, p.Line(0))
t.Errorf("incorrect position after parsing\nexpected: %q\n actual: %q", test.EndLine, p.Line(0))
}
})
}
@@ -141,9 +141,10 @@ context line
func TestParseNextFileHeader(t *testing.T) {
tests := map[string]struct {
Input string
Output *File
Err bool
Input string
Output *File
Preamble string
Err bool
}{
"gitHeader": {
Input: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
@@ -165,6 +166,13 @@ index cc34da1..1acbae5 100644
OldOIDPrefix: "cc34da1",
NewOIDPrefix: "1acbae5",
},
Preamble: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
Author: Morton Haypenny <mhaypenny@example.com>
Date: Tue Apr 2 22:30:00 2019 -0700
This is a sample commit message.
`,
},
"traditionalHeader": {
Input: `
@@ -176,6 +184,7 @@ index cc34da1..1acbae5 100644
OldName: "file.txt",
NewName: "file.txt",
},
Preamble: "\n",
},
"noHeaders": {
Input: `
@@ -184,7 +193,8 @@ this is another line
--- could this be a header?
nope, it's just some dashes
`,
Output: nil,
Output: nil,
Preamble: "",
},
"detatchedFragmentLike": {
Input: `
@@ -206,7 +216,7 @@ a wild fragment appears?
t.Run(name, func(t *testing.T) {
p := newTestParser(test.Input, true)
f, err := p.ParseNextFileHeader()
f, pre, err := p.ParseNextFileHeader()
if test.Err {
if err == nil || err == io.EOF {
t.Fatalf("expected error parsing next file header, but got %v", err)
@@ -217,8 +227,11 @@ a wild fragment appears?
t.Fatalf("unexpected error parsing next file header: %v", err)
}
if test.Preamble != pre {
t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
}
if !reflect.DeepEqual(test.Output, f) {
t.Errorf("incorrect file\nexpected: %+v\nactual: %+v", test.Output, f)
t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
}
})
}
@@ -266,9 +279,20 @@ func TestParse(t *testing.T) {
},
}
expectedPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
Author: Morton Haypenny <mhaypenny@example.com>
Date: Tue Apr 2 22:55:40 2019 -0700
A file with multiple fragments.
The content is arbitrary.
`
tests := map[string]struct {
InputFile string
Output []*File
Preamble string
Err bool
}{
"oneFile": {
@@ -283,6 +307,7 @@ func TestParse(t *testing.T) {
Fragments: expectedFragments,
},
},
Preamble: expectedPreamble,
},
"twoFiles": {
InputFile: "testdata/two_files.patch",
@@ -304,6 +329,7 @@ func TestParse(t *testing.T) {
Fragments: expectedFragments,
},
},
Preamble: expectedPreamble,
},
}
@@ -314,7 +340,7 @@ func TestParse(t *testing.T) {
t.Fatalf("unexpected error opening input file: %v", err)
}
files, err := Parse(f)
files, pre, err := Parse(f)
if test.Err {
if err == nil || err == io.EOF {
t.Fatalf("expected error parsing patch, but got %v", err)
@@ -328,11 +354,14 @@ func TestParse(t *testing.T) {
if len(test.Output) != len(files) {
t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files))
}
if test.Preamble != pre {
t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
}
for i := range test.Output {
if !reflect.DeepEqual(test.Output[i], files[i]) {
exp, _ := json.MarshalIndent(test.Output[i], "", " ")
act, _ := json.MarshalIndent(files[i], "", " ")
t.Errorf("incorrect file at position %d\nexpected: %s\nactual: %s", i, exp, act)
t.Errorf("incorrect file at position %d\nexpected: %s\n actual: %s", i, exp, act)
}
}
})
+1 -1
View File
@@ -2,7 +2,7 @@ commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
Author: Morton Haypenny <mhaypenny@example.com>
Date: Tue Apr 2 22:55:40 2019 -0700
A single file with multiple fragments.
A file with multiple fragments.
The content is arbitrary.