From c3fbc01ef2e22d61d719e5f7e9d832385a3896af Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Wed, 20 Oct 2021 08:49:13 -0500 Subject: [PATCH 01/17] adding patchheader to files --- gitdiff/parser.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index d44465a..f3565fb 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -7,8 +7,11 @@ import ( "bufio" "fmt" "io" + "strings" ) +const commitPrefix = "commit" + // 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. @@ -24,11 +27,17 @@ func Parse(r io.Reader) ([]*File, string, error) { var preamble string var files []*File + var ph *PatchHeader for { file, pre, err := p.ParseNextFileHeader() if err != nil { return files, preamble, err } + + if strings.Contains(pre, commitPrefix) { + ph, _ = ParsePatchHeader(pre) + } + if file == nil { break } @@ -46,6 +55,7 @@ func Parse(r io.Reader) ([]*File, string, error) { } } + file.PatchHeader = ph if len(files) == 0 { preamble = pre } From 1adf7a5013601d4c8f60b459cd9eafd0dbdadbe8 Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Wed, 20 Oct 2021 08:50:13 -0500 Subject: [PATCH 02/17] Add patch to file struct --- gitdiff/gitdiff.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gitdiff/gitdiff.go b/gitdiff/gitdiff.go index 18645bd..8516344 100644 --- a/gitdiff/gitdiff.go +++ b/gitdiff/gitdiff.go @@ -24,6 +24,8 @@ type File struct { NewOIDPrefix string Score int + PatchHeader *PatchHeader + // TextFragments contains the fragments describing changes to a text file. It // may be empty if the file is empty or if only the mode changes. TextFragments []*TextFragment From b68e1cfca8177740229a2b8196c1f216925c91fe Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Wed, 20 Oct 2021 09:47:29 -0500 Subject: [PATCH 03/17] replacing package name --- .golangci.yml | 2 +- README.md | 2 +- gitdiff/testdata/apply/bin.go | 2 +- go.mod | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 12cdbd2..05db235 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,4 +20,4 @@ issues: linter-settings: goimports: - local-prefixes: github.com/bluekeyes/go-gitdiff + local-prefixes: github.com/gitleaks/go-gitdiff diff --git a/README.md b/README.md index 8f9671b..1879ef3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # go-gitdiff -[![PkgGoDev](https://pkg.go.dev/badge/github.com/bluekeyes/go-gitdiff/gitdiff)](https://pkg.go.dev/github.com/bluekeyes/go-gitdiff/gitdiff) [![Go Report Card](https://goreportcard.com/badge/github.com/bluekeyes/go-gitdiff)](https://goreportcard.com/report/github.com/bluekeyes/go-gitdiff) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/gitleaks/go-gitdiff/gitdiff)](https://pkg.go.dev/github.com/gitleaks/go-gitdiff/gitdiff) [![Go Report Card](https://goreportcard.com/badge/github.com/gitleaks/go-gitdiff)](https://goreportcard.com/report/github.com/gitleaks/go-gitdiff) A Go library for parsing and applying patches generated by `git diff`, `git show`, and `git format-patch`. It can also parse and apply unified diffs diff --git a/gitdiff/testdata/apply/bin.go b/gitdiff/testdata/apply/bin.go index 6d39ffd..7d54b4d 100644 --- a/gitdiff/testdata/apply/bin.go +++ b/gitdiff/testdata/apply/bin.go @@ -17,7 +17,7 @@ import ( "os" "strings" - "github.com/bluekeyes/go-gitdiff/gitdiff" + "github.com/gitleaks/go-gitdiff/gitdiff" ) var ( diff --git a/go.mod b/go.mod index f35826e..c547433 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/bluekeyes/go-gitdiff +module github.com/gitleaks/go-gitdiff go 1.13 From 1ee22f31ec3b22144be6e27bfcce175b44a243de Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Wed, 20 Oct 2021 13:06:30 -0500 Subject: [PATCH 04/17] raw line out and channel response from parse --- gitdiff/gitdiff.go | 13 ++++++++- gitdiff/parser.go | 68 +++++++++++++++++++++++----------------------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/gitdiff/gitdiff.go b/gitdiff/gitdiff.go index 8516344..2dbfc4c 100644 --- a/gitdiff/gitdiff.go +++ b/gitdiff/gitdiff.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "strings" ) // File describes changes to a single file. It can be either a text file or a @@ -24,7 +25,7 @@ type File struct { NewOIDPrefix string Score int - PatchHeader *PatchHeader + PatchHeader *PatchHeader // TextFragments contains the fragments describing changes to a text file. It // may be empty if the file is empty or if only the mode changes. @@ -59,6 +60,16 @@ type TextFragment struct { Lines []Line } +func (f *TextFragment) Raw(op LineOp) string { + sb := strings.Builder{} + for _, l := range f.Lines { + if l.Op == op { + sb.WriteString(l.Line) + } + } + return sb.String() +} + // Header returns the canonical header of this fragment. func (f *TextFragment) Header() string { return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index f3565fb..4f6cce8 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -15,54 +15,54 @@ const commitPrefix = "commit" // 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) { +func Parse(r io.Reader) (<-chan *File, error) { p := newParser(r) + out := make(chan *File) if err := p.Next(); err != nil { if err == io.EOF { - return nil, "", nil + return out, nil } - return nil, "", err + return out, err } - var preamble string - var files []*File - var ph *PatchHeader - for { - file, pre, err := p.ParseNextFileHeader() - if err != nil { - return files, preamble, err - } - - if strings.Contains(pre, commitPrefix) { - ph, _ = ParsePatchHeader(pre) - } - - if file == nil { - break - } - - for _, fn := range []func(*File) (int, error){ - p.ParseTextFragments, - p.ParseBinaryFragments, - } { - n, err := fn(file) + go func() { + ph := &PatchHeader{} + for { + file, pre, err := p.ParseNextFileHeader() if err != nil { - return files, preamble, err + out <- file + return } - if n > 0 { + + if strings.Contains(pre, commitPrefix) { + ph, _ = ParsePatchHeader(pre) + } + + if file == nil { break } - } - file.PatchHeader = ph - if len(files) == 0 { - preamble = pre + for _, fn := range []func(*File) (int, error){ + p.ParseTextFragments, + p.ParseBinaryFragments, + } { + n, err := fn(file) + if err != nil { + return + } + if n > 0 { + break + } + } + + file.PatchHeader = ph + out <- file } - files = append(files, file) - } + close(out) + }() - return files, preamble, nil + return out, nil } // TODO(bkeyes): consider exporting the parser type with configuration From 9487daa2c83d6d22d9548e63a897ba4bcd63d6e8 Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Thu, 28 Oct 2021 21:00:56 -0500 Subject: [PATCH 05/17] infinite loop concurrency bug --- gitdiff/parser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index 4f6cce8..f87acec 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -27,11 +27,12 @@ func Parse(r io.Reader) (<-chan *File, error) { } go func() { + defer close(out) + ph := &PatchHeader{} for { file, pre, err := p.ParseNextFileHeader() if err != nil { - out <- file return } @@ -59,7 +60,6 @@ func Parse(r io.Reader) (<-chan *File, error) { file.PatchHeader = ph out <- file } - close(out) }() return out, nil From 0c0609c7336c5a412f0177d0cbf0bc2e990da9bc Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Wed, 1 Dec 2021 20:41:53 -0600 Subject: [PATCH 06/17] add missing close on output chan --- gitdiff/parser.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index f87acec..1261d06 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -20,6 +20,7 @@ func Parse(r io.Reader) (<-chan *File, error) { out := make(chan *File) if err := p.Next(); err != nil { + close(out) if err == io.EOF { return out, nil } From 9fab4e071e3894d0d644b7bac289dedcd0423244 Mon Sep 17 00:00:00 2001 From: Zach Rice Date: Sun, 12 Dec 2021 11:57:21 -0600 Subject: [PATCH 07/17] fix premature exit on invalid syntax err --- gitdiff/parser.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index 1261d06..0fefce2 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -34,7 +34,11 @@ func Parse(r io.Reader) (<-chan *File, error) { for { file, pre, err := p.ParseNextFileHeader() if err != nil { - return + if err == io.EOF { + return + } + p.Next() + continue } if strings.Contains(pre, commitPrefix) { From 77cbdcc78f1a85e4834752af4cf0a05cd63d9f1a Mon Sep 17 00:00:00 2001 From: k0ral Date: Sun, 13 Feb 2022 12:09:10 +0100 Subject: [PATCH 08/17] fix: Empty name and/or email are valid use cases. --- gitdiff/patch_header.go | 7 ++----- gitdiff/patch_header_test.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/gitdiff/patch_header.go b/gitdiff/patch_header.go index c3c387d..a4671b7 100644 --- a/gitdiff/patch_header.go +++ b/gitdiff/patch_header.go @@ -82,9 +82,9 @@ func (i PatchIdentity) String() string { } // ParsePatchIdentity parses a patch identity string. A valid string contains a -// non-empty name followed by an email address in angle brackets. Like Git, +// name followed by an email address in angle brackets. // ParsePatchIdentity does not require that the email address is valid or -// properly formatted, only that it is non-empty. The name must not contain a +// properly formatted. The name must not contain a // left angle bracket, '<', and the email address must not contain a right // angle bracket, '>'. func ParsePatchIdentity(s string) (PatchIdentity, error) { @@ -109,9 +109,6 @@ func ParsePatchIdentity(s string) (PatchIdentity, error) { if emailStart > 0 && emailEnd > 0 { email = strings.TrimSpace(s[emailStart:emailEnd]) } - if name == "" || email == "" { - return PatchIdentity{}, fmt.Errorf("invalid identity string: %s", s) - } return PatchIdentity{Name: name, Email: email}, nil } diff --git a/gitdiff/patch_header_test.go b/gitdiff/patch_header_test.go index bda91fe..9181b5c 100644 --- a/gitdiff/patch_header_test.go +++ b/gitdiff/patch_header_test.go @@ -34,11 +34,38 @@ func TestParsePatchIdentity(t *testing.T) { }, "missingName": { Input: "", - Err: "invalid identity", + Output: PatchIdentity{ + Name: "", + Email: "mhaypenny@example.com", + }, }, "missingEmail": { Input: "Morton Haypenny", - Err: "invalid identity", + Output: PatchIdentity{ + Name: "", + Email: "", + }, + }, + "emptyEmail": { + Input: "Morton Haypenny <>", + Output: PatchIdentity{ + Name: "Morton Haypenny", + Email: "", + }, + }, + "missingNameAndEmail": { + Input: "", + Output: PatchIdentity{ + Name: "", + Email: "", + }, + }, + "emptyNameAndEmail": { + Input: " <>", + Output: PatchIdentity{ + Name: "", + Email: "", + }, }, "unclosedEmail": { Input: "Morton Haypenny Date: Fri, 4 Feb 2022 21:05:11 +0100 Subject: [PATCH 09/17] fix: Support for more kinds of binary headers --- gitdiff/binary.go | 7 ++++++- gitdiff/binary_test.go | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/gitdiff/binary.go b/gitdiff/binary.go index c65a9a6..15a5678 100644 --- a/gitdiff/binary.go +++ b/gitdiff/binary.go @@ -6,10 +6,13 @@ import ( "fmt" "io" "io/ioutil" + "regexp" "strconv" "strings" ) +var binaryRegexp = regexp.MustCompile(`^Binary files (/dev/null|a/(.+)|"a/(.+)") and (/dev/null|b/(.+)|"b/(.+)") differ\s*$`) + func (p *parser) ParseBinaryFragments(f *File) (n int, err error) { isBinary, hasData, err := p.ParseBinaryMarker() if err != nil || !isBinary { @@ -56,7 +59,9 @@ func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) { case "Binary files differ\n": case "Files differ\n": default: - return false, false, nil + if !binaryRegexp.MatchString(p.Line(0)) { + return false, false, nil + } } if err = p.Next(); err != nil && err != io.EOF { diff --git a/gitdiff/binary_test.go b/gitdiff/binary_test.go index a31a0e0..9b00cd7 100644 --- a/gitdiff/binary_test.go +++ b/gitdiff/binary_test.go @@ -30,6 +30,26 @@ func TestParseBinaryMarker(t *testing.T) { IsBinary: false, HasData: false, }, + "binaryPatchCreated": { + Input: "Binary files /dev/null and b/path/to/file.ext differ\n", + IsBinary: true, + HasData: false, + }, + "binaryPatchModified": { + Input: "Binary files a/path/to/file.ext and b/path/to/file.ext differ\n", + IsBinary: true, + HasData: false, + }, + "binaryPatchModifiedQuoted": { + Input: "Binary files \"a/path/to/file.ext\" and \"b/path/to/file.ext\" differ\n", + IsBinary: true, + HasData: false, + }, + "binaryPatchDeleted": { + Input: "Binary files a/path/to/file.ext and /dev/null differ\n", + IsBinary: true, + HasData: false, + }, } for name, test := range tests { From 496604bbfb292ed5d9a8e6b3e137bbe74f979cc7 Mon Sep 17 00:00:00 2001 From: bill-rich Date: Tue, 15 Mar 2022 16:04:58 -0700 Subject: [PATCH 10/17] Add "Binary files .* differ" marker --- gitdiff/binary.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gitdiff/binary.go b/gitdiff/binary.go index 15a5678..0a49a5e 100644 --- a/gitdiff/binary.go +++ b/gitdiff/binary.go @@ -53,11 +53,13 @@ func (p *parser) ParseBinaryFragments(f *File) (n int, err error) { } func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) { - switch p.Line(0) { - case "GIT binary patch\n": + line := p.Line(0) + switch { + case line == "GIT binary patch\n": hasData = true - case "Binary files differ\n": - case "Files differ\n": + case line == "Binary files differ\n": + case line == "Files differ\n": + case strings.HasPrefix(line, "Binary files ") && strings.HasSuffix(line, "differ\n"): default: if !binaryRegexp.MatchString(p.Line(0)) { return false, false, nil From bef745dceede90acd1b579f6b9df8a29dff9e51f Mon Sep 17 00:00:00 2001 From: Dustin Decker Date: Fri, 24 Jun 2022 14:05:59 -0700 Subject: [PATCH 11/17] wait the zombies (#1) * wait the zombies * close the reader * defer order * partially update tests --- gitdiff/apply_test.go | 5 ++++- gitdiff/parser.go | 9 ++++++--- gitdiff/parser_test.go | 8 ++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/gitdiff/apply_test.go b/gitdiff/apply_test.go index d981e96..656acf6 100644 --- a/gitdiff/apply_test.go +++ b/gitdiff/apply_test.go @@ -5,6 +5,7 @@ import ( "errors" "io" "io/ioutil" + "os/exec" "path/filepath" "testing" ) @@ -231,7 +232,9 @@ type applyTest struct { func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) error) { src, patch, out := at.Files.Load(t) - files, _, err := Parse(bytes.NewReader(patch)) + cmd := exec.Command("echo", "hello") + + files, err := Parse(cmd, io.NopCloser(bytes.NewReader(patch))) if err != nil { t.Fatalf("failed to parse patch file: %v", err) } diff --git a/gitdiff/parser.go b/gitdiff/parser.go index 0fefce2..c58d3cc 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -7,6 +7,7 @@ import ( "bufio" "fmt" "io" + "os/exec" "strings" ) @@ -15,7 +16,7 @@ const commitPrefix = "commit" // 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) (<-chan *File, error) { +func Parse(cmd *exec.Cmd, r io.ReadCloser) (<-chan *File, error) { p := newParser(r) out := make(chan *File) @@ -27,8 +28,10 @@ func Parse(r io.Reader) (<-chan *File, error) { return out, err } - go func() { + go func(cmd *exec.Cmd, out chan *File, r io.ReadCloser) { defer close(out) + defer cmd.Wait() + defer r.Close() ph := &PatchHeader{} for { @@ -65,7 +68,7 @@ func Parse(r io.Reader) (<-chan *File, error) { file.PatchHeader = ph out <- file } - }() + }(cmd, out, r) return out, nil } diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 30f59f4..20c10ac 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io" "os" + "os/exec" "reflect" "testing" ) @@ -460,7 +461,9 @@ Date: Tue Apr 2 22:55:40 2019 -0700 t.Fatalf("unexpected error opening input file: %v", err) } - files, pre, err := Parse(f) + cmd := exec.Command("echo", "hello") + + files, err := Parse(cmd, f) if test.Err { if err == nil || err == io.EOF { t.Fatalf("expected error parsing patch, but got %v", err) @@ -474,9 +477,6 @@ Date: Tue Apr 2 22:55:40 2019 -0700 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], "", " ") From 30094fc2873f700f84f2a0a40f37f8d82d3a1adc Mon Sep 17 00:00:00 2001 From: Miccah Castorina Date: Tue, 5 Jul 2022 11:36:33 -0500 Subject: [PATCH 12/17] Add benchmark and fix tests --- gitdiff/apply_test.go | 6 +++- gitdiff/parser_test.go | 63 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/gitdiff/apply_test.go b/gitdiff/apply_test.go index 656acf6..6322c02 100644 --- a/gitdiff/apply_test.go +++ b/gitdiff/apply_test.go @@ -234,10 +234,14 @@ func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) err cmd := exec.Command("echo", "hello") - files, err := Parse(cmd, io.NopCloser(bytes.NewReader(patch))) + fileChan, err := Parse(cmd, io.NopCloser(bytes.NewReader(patch))) if err != nil { t.Fatalf("failed to parse patch file: %v", err) } + var files []*File + for file := range fileChan { + files = append(files, file) + } if len(files) != 1 { t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) } diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 20c10ac..047d133 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -4,10 +4,12 @@ import ( "bytes" "encoding/binary" "encoding/json" + "fmt" "io" "os" "os/exec" "reflect" + "strings" "testing" ) @@ -463,7 +465,7 @@ Date: Tue Apr 2 22:55:40 2019 -0700 cmd := exec.Command("echo", "hello") - files, err := Parse(cmd, f) + fileChan, err := Parse(cmd, f) if test.Err { if err == nil || err == io.EOF { t.Fatalf("expected error parsing patch, but got %v", err) @@ -473,6 +475,10 @@ Date: Tue Apr 2 22:55:40 2019 -0700 if err != nil { t.Fatalf("unexpected error parsing patch: %v", err) } + var files []*File + for file := range fileChan { + files = append(files, file) + } if len(test.Output) != len(files) { t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files)) @@ -488,6 +494,61 @@ Date: Tue Apr 2 22:55:40 2019 -0700 } } +func BenchmarkParse(b *testing.B) { + var inputDiff string + { + builder := strings.Builder{} + builder.WriteString(`commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe +Author: Morton Haypenny +Date: Tue Apr 2 22:55:40 2019 -0700 + + A file with multiple fragments. + + The content is arbitrary. + +`) + fileDiff := func(i int) string { + return fmt.Sprintf(`diff --git a/dir/file%[1]d.txt b/dir/file%[1]d.txt +index ebe9fa54..fe103e1d 100644 +--- a/dir/file%[1]d.txt ++++ b/dir/file%[1]d.txt +@@ -3,6 +3,8 @@ fragment 1 + context line +-old line 1 +-old line 2 + context line ++new line 1 ++new line 2 ++new line 3 + context line +-old line 3 ++new line 4 ++new line 5 +@@ -31,2 +33,2 @@ fragment 2 + context line +-old line 4 ++new line 6 +`, i) + } + for i := 0; i < 1000; i++ { + _, err := builder.WriteString(fileDiff(i)) + if err != nil { + panic(err) + } + } + inputDiff = builder.String() + } + for i := 0; i < b.N; i++ { + reader := io.NopCloser(strings.NewReader(inputDiff)) + ch, err := Parse(&exec.Cmd{}, reader) + if err != nil { + panic(err) + } + for range ch { + } + } +} + func newTestParser(input string, init bool) *parser { p := newParser(bytes.NewBufferString(input)) if init { From d60e122a8e84dd776d92a8295ec440975eb70b66 Mon Sep 17 00:00:00 2001 From: Miccah Castorina Date: Tue, 5 Jul 2022 15:09:17 -0500 Subject: [PATCH 13/17] Fix gitdiff tests --- gitdiff/parser_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 047d133..c895796 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" "testing" + "time" ) func TestLineOperations(t *testing.T) { @@ -397,6 +398,16 @@ Date: Tue Apr 2 22:55:40 2019 -0700 InputFile: "testdata/one_file.patch", Output: []*File{ { + PatchHeader: &PatchHeader{ + SHA: "5d9790fec7d95aa223f3d20936340bf55ff3dcbe", + Author: &PatchIdentity{ + Name: "Morton Haypenny", + Email: "mhaypenny@example.com", + }, + AuthorDate: asTime("2019-04-02T22:55:40-07:00"), + Title: "A file with multiple fragments.", + Body: "The content is arbitrary.", + }, OldName: "dir/file1.txt", NewName: "dir/file1.txt", OldMode: os.FileMode(0100644), @@ -411,6 +422,16 @@ Date: Tue Apr 2 22:55:40 2019 -0700 InputFile: "testdata/two_files.patch", Output: []*File{ { + PatchHeader: &PatchHeader{ + SHA: "5d9790fec7d95aa223f3d20936340bf55ff3dcbe", + Author: &PatchIdentity{ + Name: "Morton Haypenny", + Email: "mhaypenny@example.com", + }, + AuthorDate: asTime("2019-04-02T22:55:40-07:00"), + Title: "A file with multiple fragments.", + Body: "The content is arbitrary.", + }, OldName: "dir/file1.txt", NewName: "dir/file1.txt", OldMode: os.FileMode(0100644), @@ -419,6 +440,16 @@ Date: Tue Apr 2 22:55:40 2019 -0700 TextFragments: textFragments, }, { + PatchHeader: &PatchHeader{ + SHA: "5d9790fec7d95aa223f3d20936340bf55ff3dcbe", + Author: &PatchIdentity{ + Name: "Morton Haypenny", + Email: "mhaypenny@example.com", + }, + AuthorDate: asTime("2019-04-02T22:55:40-07:00"), + Title: "A file with multiple fragments.", + Body: "The content is arbitrary.", + }, OldName: "dir/file2.txt", NewName: "dir/file2.txt", OldMode: os.FileMode(0100644), @@ -433,6 +464,15 @@ Date: Tue Apr 2 22:55:40 2019 -0700 InputFile: "testdata/new_binary_file.patch", Output: []*File{ { + PatchHeader: &PatchHeader{ + SHA: "5d9790fec7d95aa223f3d20936340bf55ff3dcbe", + Author: &PatchIdentity{ + Name: "Morton Haypenny", + Email: "mhaypenny@example.com", + }, + AuthorDate: asTime("2019-04-02T22:55:40-07:00"), + Title: "A binary file with the first 10 fibonacci numbers.", + }, OldName: "", NewName: "dir/ten.bin", NewMode: os.FileMode(0100644), @@ -556,3 +596,11 @@ func newTestParser(input string, init bool) *parser { } return p } + +func asTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t +} From 0484f3297982a94ec44787e46e2db25fb698a31c Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Fri, 11 Aug 2023 20:06:48 +0300 Subject: [PATCH 14/17] feat: parser should not know about cmd anything --- gitdiff/apply_test.go | 5 +---- gitdiff/parser.go | 9 +++------ gitdiff/parser_test.go | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/gitdiff/apply_test.go b/gitdiff/apply_test.go index 6322c02..a34ea56 100644 --- a/gitdiff/apply_test.go +++ b/gitdiff/apply_test.go @@ -5,7 +5,6 @@ import ( "errors" "io" "io/ioutil" - "os/exec" "path/filepath" "testing" ) @@ -232,9 +231,7 @@ type applyTest struct { func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) error) { src, patch, out := at.Files.Load(t) - cmd := exec.Command("echo", "hello") - - fileChan, err := Parse(cmd, io.NopCloser(bytes.NewReader(patch))) + fileChan, err := Parse(io.NopCloser(bytes.NewReader(patch))) if err != nil { t.Fatalf("failed to parse patch file: %v", err) } diff --git a/gitdiff/parser.go b/gitdiff/parser.go index c58d3cc..5ffa9bd 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -7,7 +7,6 @@ import ( "bufio" "fmt" "io" - "os/exec" "strings" ) @@ -16,7 +15,7 @@ const commitPrefix = "commit" // 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(cmd *exec.Cmd, r io.ReadCloser) (<-chan *File, error) { +func Parse(r io.Reader) (<-chan *File, error) { p := newParser(r) out := make(chan *File) @@ -28,10 +27,8 @@ func Parse(cmd *exec.Cmd, r io.ReadCloser) (<-chan *File, error) { return out, err } - go func(cmd *exec.Cmd, out chan *File, r io.ReadCloser) { + go func(out chan *File, r io.Reader) { defer close(out) - defer cmd.Wait() - defer r.Close() ph := &PatchHeader{} for { @@ -68,7 +65,7 @@ func Parse(cmd *exec.Cmd, r io.ReadCloser) (<-chan *File, error) { file.PatchHeader = ph out <- file } - }(cmd, out, r) + }(out, r) return out, nil } diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index c895796..5040b0a 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "os/exec" "reflect" "strings" "testing" @@ -503,9 +502,7 @@ Date: Tue Apr 2 22:55:40 2019 -0700 t.Fatalf("unexpected error opening input file: %v", err) } - cmd := exec.Command("echo", "hello") - - fileChan, err := Parse(cmd, f) + fileChan, err := Parse(f) if test.Err { if err == nil || err == io.EOF { t.Fatalf("expected error parsing patch, but got %v", err) @@ -580,7 +577,7 @@ index ebe9fa54..fe103e1d 100644 } for i := 0; i < b.N; i++ { reader := io.NopCloser(strings.NewReader(inputDiff)) - ch, err := Parse(&exec.Cmd{}, reader) + ch, err := Parse(reader) if err != nil { panic(err) } From ecd8e120b47d6735e1dbfa6728b8c082c1104096 Mon Sep 17 00:00:00 2001 From: Nicholas Rodine Date: Tue, 30 Jan 2024 13:44:24 -0500 Subject: [PATCH 15/17] Added `.idea` dir to .gitignore (for GoLand IDE) --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ From 664b1fd682afbd9f90f7b617a0bb234b5b5a8e29 Mon Sep 17 00:00:00 2001 From: Nicholas Rodine Date: Tue, 30 Jan 2024 13:44:47 -0500 Subject: [PATCH 16/17] Added unit test to reproduce the issue --- gitdiff/parser_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 5040b0a..2e7a91a 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -301,6 +301,42 @@ a wild fragment appears? `, Err: true, }, + "mergeHeaderFollowedByNormalHeader": { + Input: `commit f6ded7a51cf917bdb44097066fab608c0facde5b +Merge: b2cf1cd0de 0254477421 +Author: BoloniniD +Date: Thu Apr 7 01:07:38 2022 +0300 + + Merge branch 'BLAKE3' of github.com:BoloniniD/ClickHouse into BLAKE3 + +commit 645e156af6b362145fad82d714f8e70a5b5a55a8 +Author: Meena Renganathan +Date: Wed Apr 6 14:50:10 2022 -0700 + + Updated the boringssl-cmake to match the latest broingssl module update + +diff --git a/.gitmodules b/.gitmodules +index 6c9e66f9cb..9cee5f697e 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -207 +206,0 @@ +- branch = MergeWithUpstream +`, + Output: &File{ + OldName: ".gitmodules", + NewName: ".gitmodules", + OldMode: os.FileMode(0100644), + OldOIDPrefix: "6c9e66f9cb", + NewOIDPrefix: "9cee5f697e", + }, + Preamble: `commit 645e156af6b362145fad82d714f8e70a5b5a55a8 +Author: Meena Renganathan +Date: Wed Apr 6 14:50:10 2022 -0700 + + Updated the boringssl-cmake to match the latest broingssl module update + +`, + }, } for name, test := range tests { From 8d42dd9d0d5580634ddfddf9f5ee6d7c8d768bb0 Mon Sep 17 00:00:00 2001 From: Nicholas Rodine Date: Tue, 30 Jan 2024 13:45:48 -0500 Subject: [PATCH 17/17] Fixed issue by resetting the preamble if a new commit is encountered. --- gitdiff/file_header.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gitdiff/file_header.go b/gitdiff/file_header.go index 58904b4..1962e64 100644 --- a/gitdiff/file_header.go +++ b/gitdiff/file_header.go @@ -30,6 +30,12 @@ func (p *parser) ParseNextFileHeader() (*File, string, error) { return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header()) } + // check for end of merge header, and start of a new header + if strings.HasPrefix(p.Line(0), commitPrefix) { + preamble.Reset() + goto NextLine + } + // check for a git-generated patch file, err = p.ParseGitFileHeader() if err != nil {