From 69e4444419334729802520085e14b5eafa4fc10d Mon Sep 17 00:00:00 2001 From: Billy Keyes Date: Sun, 7 Apr 2019 19:57:11 -0700 Subject: [PATCH] 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. --- gitdiff/parser.go | 51 ++++++++++++++++++--------------- gitdiff/parser_test.go | 47 ++++++++++++++++++++++++------ gitdiff/testdata/one_file.patch | 2 +- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index 2d760ef..96f0f23 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -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 diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 7b79098..7ebaed7 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -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 +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 +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) } } }) diff --git a/gitdiff/testdata/one_file.patch b/gitdiff/testdata/one_file.patch index 59dc592..1aefec3 100644 --- a/gitdiff/testdata/one_file.patch +++ b/gitdiff/testdata/one_file.patch @@ -2,7 +2,7 @@ commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe Author: Morton Haypenny Date: Tue Apr 2 22:55:40 2019 -0700 - A single file with multiple fragments. + A file with multiple fragments. The content is arbitrary.