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.