From 5a0f0ffeb33f64c010cb1e583d938c5c0de21d70 Mon Sep 17 00:00:00 2001 From: Emmanuel Antonio Cuevas Date: Wed, 11 Feb 2026 19:16:30 +0000 Subject: [PATCH 1/3] Add KeepCR to make removal of \r optional --- diff/diff.go | 172 ++++++++--------- diff/reader_util.go | 121 ++++++------ diff/reader_util_test.go | 391 +++++++++++++++++++++------------------ 3 files changed, 363 insertions(+), 321 deletions(-) diff --git a/diff/diff.go b/diff/diff.go index 81aa655..dbf56c4 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -1,115 +1,121 @@ package diff import ( - "bytes" - "time" +"bytes" +"time" ) +// ParseOptions specifies options for parsing diffs. +type ParseOptions struct { +// KeepCR specifies whether to keep trailing carriage return characters (\r) in lines. +KeepCR bool +} + // A FileDiff represents a unified diff for a single file. // // A file unified diff has a header that resembles the following: // -// --- oldname 2009-10-11 15:12:20.000000000 -0700 -// +++ newname 2009-10-11 15:12:30.000000000 -0700 +//--- oldname2009-10-11 15:12:20.000000000 -0700 +//+++ newname2009-10-11 15:12:30.000000000 -0700 type FileDiff struct { - // the original name of the file - OrigName string - // the original timestamp (nil if not present) - OrigTime *time.Time - // the new name of the file (often same as OrigName) - NewName string - // the new timestamp (nil if not present) - NewTime *time.Time - // extended header lines (e.g., git's "new mode ", "rename from ", etc.) - Extended []string - // hunks that were changed from orig to new - Hunks []*Hunk +// the original name of the file +OrigName string +// the original timestamp (nil if not present) +OrigTime *time.Time +// the new name of the file (often same as OrigName) +NewName string +// the new timestamp (nil if not present) +NewTime *time.Time +// extended header lines (e.g., git's "new mode ", "rename from ", etc.) +Extended []string +// hunks that were changed from orig to new +Hunks []*Hunk } // A Hunk represents a series of changes (additions or deletions) in a file's // unified diff. type Hunk struct { - // starting line number in original file - OrigStartLine int32 - // number of lines the hunk applies to in the original file - OrigLines int32 - // if > 0, then the original file had a 'No newline at end of file' mark at this offset - OrigNoNewlineAt int32 - // starting line number in new file - NewStartLine int32 - // number of lines the hunk applies to in the new file - NewLines int32 - // optional section heading - Section string - // 0-indexed line offset in unified file diff (including section headers); this is - // only set when Hunks are read from entire file diff (i.e., when ReadAllHunks is - // called) This accounts for hunk headers, too, so the StartPosition of the first - // hunk will be 1. - StartPosition int32 - // hunk body (lines prefixed with '-', '+', or ' ') - Body []byte +// starting line number in original file +OrigStartLine int32 +// number of lines the hunk applies to in the original file +OrigLines int32 +// if > 0, then the original file had a 'No newline at end of file' mark at this offset +OrigNoNewlineAt int32 +// starting line number in new file +NewStartLine int32 +// number of lines the hunk applies to in the new file +NewLines int32 +// optional section heading +Section string +// 0-indexed line offset in unified file diff (including section headers); this is +// only set when Hunks are read from entire file diff (i.e., when ReadAllHunks is +// called) This accounts for hunk headers, too, so the StartPosition of the first +// hunk will be 1. +StartPosition int32 +// hunk body (lines prefixed with '-', '+', or ' ') +Body []byte } // A Stat is a diff stat that represents the number of lines added/changed/deleted. type Stat struct { - // number of lines added - Added int32 - // number of lines changed - Changed int32 - // number of lines deleted - Deleted int32 +// number of lines added +Added int32 +// number of lines changed +Changed int32 +// number of lines deleted +Deleted int32 } // Stat computes the number of lines added/changed/deleted in all // hunks in this file's diff. func (d *FileDiff) Stat() Stat { - total := Stat{} - for _, h := range d.Hunks { - total.add(h.Stat()) - } - return total +total := Stat{} +for _, h := range d.Hunks { +total.add(h.Stat()) +} +return total } // Stat computes the number of lines added/changed/deleted in this // hunk. func (h *Hunk) Stat() Stat { - lines := bytes.Split(h.Body, []byte{'\n'}) - var last byte - st := Stat{} - for _, line := range lines { - if len(line) == 0 { - last = 0 - continue - } - switch line[0] { - case '-': - if last == '+' { - st.Added-- - st.Changed++ - last = 0 // next line can't change this one since this is already a change - } else { - st.Deleted++ - last = line[0] - } - case '+': - if last == '-' { - st.Deleted-- - st.Changed++ - last = 0 // next line can't change this one since this is already a change - } else { - st.Added++ - last = line[0] - } - default: - last = 0 - } - } - return st +lines := bytes.Split(h.Body, []byte{'\n'}) +var last byte +st := Stat{} +for _, line := range lines { +if len(line) == 0 { +last = 0 +continue +} +switch line[0] { +case '-': +if last == '+' { +st.Added-- +st.Changed++ +last = 0 // next line can't change this one since this is already a change +} else { +st.Deleted++ +last = line[0] +} +case '+': +if last == '-' { +st.Deleted-- +st.Changed++ +last = 0 // next line can't change this one since this is already a change +} else { +st.Added++ +last = line[0] +} +default: +last = 0 +} +} +return st } var ( - hunkPrefix = []byte("@@ ") - onlyInMessagePrefix = []byte("Only in ") +hunkPrefix = []byte("@@ ") +onlyInMessagePrefix = []byte("Only in ") ) const hunkHeader = "@@ -%d,%d +%d,%d @@" @@ -130,7 +136,7 @@ const diffTimeParseWithoutTZLayout = "2006-01-02 15:04:05" const diffTimeFormatLayout = "2006-01-02 15:04:05.000000000 -0700" func (s *Stat) add(o Stat) { - s.Added += o.Added - s.Changed += o.Changed - s.Deleted += o.Deleted +s.Added += o.Added +s.Changed += o.Changed +s.Deleted += o.Deleted } diff --git a/diff/reader_util.go b/diff/reader_util.go index 4530025..959888f 100644 --- a/diff/reader_util.go +++ b/diff/reader_util.go @@ -1,43 +1,52 @@ package diff import ( - "bufio" - "bytes" - "errors" - "io" +"bufio" +"bytes" +"errors" +"io" ) var ErrLineReaderUninitialized = errors.New("line reader not initialized") func newLineReader(r io.Reader) *lineReader { - return &lineReader{reader: bufio.NewReader(r)} +return &lineReader{reader: bufio.NewReader(r)} +} + +func newLineReaderOptions(r io.Reader, opts ParseOptions) *lineReader { +return &lineReader{ +reader: bufio.NewReader(r), +keepCR: opts.KeepCR, +} } // lineReader is a wrapper around a bufio.Reader that caches the next line to // provide lookahead functionality for the next two lines. type lineReader struct { - reader *bufio.Reader +reader *bufio.Reader + +cachedNextLine []byte +cachedNextLineErr error - cachedNextLine []byte - cachedNextLineErr error +keepCR bool } // readLine returns the next unconsumed line and advances the internal cache of // the lineReader. func (l *lineReader) readLine() ([]byte, error) { - if l.cachedNextLine == nil && l.cachedNextLineErr == nil { - l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader) - } +if l.cachedNextLine == nil && l.cachedNextLineErr == nil { +l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) +} - if l.cachedNextLineErr != nil { - return nil, l.cachedNextLineErr - } +if l.cachedNextLineErr != nil { +return nil, l.cachedNextLineErr +} - next := l.cachedNextLine +next := l.cachedNextLine - l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader) +l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) - return next, nil +return next, nil } // nextLineStartsWith looks at the line that would be returned by the next call @@ -46,11 +55,11 @@ func (l *lineReader) readLine() ([]byte, error) { // io.EOF and bufio.ErrBufferFull errors are ignored so that the function can // be used when at the end of the file. func (l *lineReader) nextLineStartsWith(prefix string) (bool, error) { - if l.cachedNextLine == nil && l.cachedNextLineErr == nil { - l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader) - } +if l.cachedNextLine == nil && l.cachedNextLineErr == nil { +l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) +} - return l.lineHasPrefix(l.cachedNextLine, prefix, l.cachedNextLineErr) +return l.lineHasPrefix(l.cachedNextLine, prefix, l.cachedNextLineErr) } // nextNextLineStartsWith checks the prefix of the line *after* the line that @@ -63,12 +72,12 @@ func (l *lineReader) nextLineStartsWith(prefix string) (bool, error) { // calling nextLineStartsWith. Otherwise ErrLineReaderUninitialized will be // returned. func (l *lineReader) nextNextLineStartsWith(prefix string) (bool, error) { - if l.cachedNextLine == nil && l.cachedNextLineErr == nil { - l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader) - } +if l.cachedNextLine == nil && l.cachedNextLineErr == nil { +l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) +} - next, err := l.reader.Peek(len(prefix)) - return l.lineHasPrefix(next, prefix, err) +next, err := l.reader.Peek(len(prefix)) +return l.lineHasPrefix(next, prefix, err) } // lineHasPrefix checks whether the given line has the given prefix with @@ -78,14 +87,14 @@ func (l *lineReader) nextNextLineStartsWith(prefix string) (bool, error) { // lineHasPrefix checks the error to adjust its return value to, e.g., return // false and ignore the error when readErr is io.EOF. func (l *lineReader) lineHasPrefix(line []byte, prefix string, readErr error) (bool, error) { - if readErr != nil { - if readErr == io.EOF || readErr == bufio.ErrBufferFull { - return false, nil - } - return false, readErr - } +if readErr != nil { +if readErr == io.EOF || readErr == bufio.ErrBufferFull { +return false, nil +} +return false, readErr +} - return bytes.HasPrefix(line, []byte(prefix)), nil +return bytes.HasPrefix(line, []byte(prefix)), nil } // readLine is a helper that mimics the functionality of calling bufio.Scanner.Scan() and @@ -93,28 +102,34 @@ func (l *lineReader) lineHasPrefix(line []byte, prefix string, readErr error) (b // the next line in the Reader with the trailing newline stripped. It will return an // io.EOF error when there is nothing left to read (at the start of the function call). It // will return any other errors it receives from the underlying call to ReadBytes. -func readLine(r *bufio.Reader) ([]byte, error) { - line_, err := r.ReadBytes('\n') - if err == io.EOF { - if len(line_) == 0 { - return nil, io.EOF - } - - // ReadBytes returned io.EOF, because it didn't find another newline, but there is - // still the remainder of the file to return as a line. - line := line_ - return line, nil - } else if err != nil { - return nil, err - } - line := line_[0 : len(line_)-1] - return dropCR(line), nil +func readLine(r *bufio.Reader, keepCR bool) ([]byte, error) { +line_, err := r.ReadBytes('\n') +if err == io.EOF { +if len(line_) == 0 { +return nil, io.EOF +} + +// ReadBytes returned io.EOF, because it didn't find another newline, but there is +// still the remainder of the file to return as a line. +line := line_ +if !keepCR { +return dropCR(line), nil +} +return line, nil +} else if err != nil { +return nil, err +} +line := line_[0 : len(line_)-1] +if !keepCR { +return dropCR(line), nil +} +return line, nil } // dropCR drops a terminal \r from the data. func dropCR(data []byte) []byte { - if len(data) > 0 && data[len(data)-1] == '\r' { - return data[0 : len(data)-1] - } - return data +if len(data) > 0 && data[len(data)-1] == '\r' { +return data[0 : len(data)-1] +} +return data } diff --git a/diff/reader_util_test.go b/diff/reader_util_test.go index 8dd0016..7dcca84 100644 --- a/diff/reader_util_test.go +++ b/diff/reader_util_test.go @@ -1,209 +1,230 @@ package diff import ( - "bufio" - "io" - "reflect" - "strings" - "testing" +"bufio" +"io" +"reflect" +"strings" +"testing" ) func TestReadLine(t *testing.T) { - tests := []struct { - name string - input string - want []string - }{ - { - name: "empty", - input: "", - want: []string{}, - }, - { - name: "single_line", - input: "@@ -0,0 +1,62 @@", - want: []string{"@@ -0,0 +1,62 @@"}, - }, - { - name: "single_lf_terminated_line", - input: "@@ -0,0 +1,62 @@\n", - want: []string{"@@ -0,0 +1,62 @@"}, - }, - { - name: "single_crlf_terminated_line", - input: "@@ -0,0 +1,62 @@\r\n", - want: []string{"@@ -0,0 +1,62 @@"}, - }, - { - name: "multi_line", - input: `diff --git a/test.go b/test.go +tests := []struct { +name string +input string +want []string +}{ +{ +name: "empty", +input: "", +want: []string{}, +}, +{ +name: "single_line", +input: "@@ -0,0 +1,62 @@", +want: []string{"@@ -0,0 +1,62 @@"}, +}, +{ +name: "single_lf_terminated_line", +input: "@@ -0,0 +1,62 @@\n", +want: []string{"@@ -0,0 +1,62 @@"}, +}, +{ +name: "single_crlf_terminated_line", +input: "@@ -0,0 +1,62 @@\r\n", +want: []string{"@@ -0,0 +1,62 @@"}, +}, +{ +name: "multi_line", +input: `diff --git a/test.go b/test.go new file mode 100644 index 0000000..3be2928`, - want: []string{ - "diff --git a/test.go b/test.go", - "new file mode 100644", - "index 0000000..3be2928", - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - in := bufio.NewReader(strings.NewReader(test.input)) - out := []string{} - for { - l, err := readLine(in) - if err == io.EOF { - break - } - if err != nil { - t.Fatal(err) - } - out = append(out, string(l)) - } - if !reflect.DeepEqual(test.want, out) { - t.Errorf("read lines not equal: want %v, got %v", test.want, out) - } - }) - } +want: []string{ +"diff --git a/test.go b/test.go", +"new file mode 100644", +"index 0000000..3be2928", +}, +}, +} +for _, test := range tests { +t.Run(test.name, func(t *testing.T) { +in := bufio.NewReader(strings.NewReader(test.input)) +out := []string{} +for { +l, err := readLine(in, false) +if err == io.EOF { +break +} +if err != nil { +t.Fatal(err) +} +out = append(out, string(l)) +} +if !reflect.DeepEqual(test.want, out) { +t.Errorf("read lines not equal: want %v, got %v", test.want, out) +} +}) +} } func TestLineReader_ReadLine(t *testing.T) { - input := `diff --git a/test.go b/test.go +input := `diff --git a/test.go b/test.go new file mode 100644 index 0000000..3be2928 ` - in := newLineReader(strings.NewReader(input)) - out := []string{} - for i := 0; i < 4; i++ { - l, err := in.readLine() - if err != nil { - t.Fatal(err) - } - out = append(out, string(l)) - } - - wantOut := strings.Split(input, "\n")[0:4] - if !reflect.DeepEqual(wantOut, out) { - t.Errorf("read lines not equal: want %v, got %v", wantOut, out) - } - - _, err := in.readLine() - if err != nil { - t.Fatal(err) - } - if in.cachedNextLineErr != io.EOF { - t.Fatalf("lineReader has wrong cachedNextLineErr: %s", in.cachedNextLineErr) - } - _, err = in.readLine() - if err != io.EOF { - t.Fatalf("readLine did not return io.EOF: %s", err) - } +in := newLineReader(strings.NewReader(input)) +out := []string{} +for i := 0; i < 4; i++ { +l, err := in.readLine() +if err != nil { +t.Fatal(err) +} +out = append(out, string(l)) +} + +wantOut := strings.Split(input, "\n")[0:4] +if !reflect.DeepEqual(wantOut, out) { +t.Errorf("read lines not equal: want %v, got %v", wantOut, out) +} + +_, err := in.readLine() +if err != nil { +t.Fatal(err) +} +if in.cachedNextLineErr != io.EOF { +t.Fatalf("lineReader has wrong cachedNextLineErr: %s", in.cachedNextLineErr) +} +_, err = in.readLine() +if err != io.EOF { +t.Fatalf("readLine did not return io.EOF: %s", err) +} } func TestLineReader_NextLine(t *testing.T) { - input := `aaa rest of line +input := `aaa rest of line bbbrest of line ccc rest of line` - in := newLineReader(strings.NewReader(input)) - - type assertion struct { - prefix string - want bool - } - - testsPerReadLine := []struct { - nextLine []assertion - nextNextLine []assertion - wantReadLineErr error - }{ - { - nextLine: []assertion{ - {prefix: "a", want: true}, - {prefix: "aa", want: true}, - {prefix: "aaa", want: true}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: false}, - }, - nextNextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: true}, - {prefix: "ccc", want: false}, - }, - }, - { - nextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: true}, - {prefix: "ccc", want: false}, - }, - nextNextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: true}, - }, - }, - { - nextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: true}, - {prefix: "ddd", want: false}, - }, - nextNextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: false}, - {prefix: "ddd", want: false}, - }, - }, - { - nextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: false}, - {prefix: "ddd", want: false}, - }, - nextNextLine: []assertion{ - {prefix: "aaa", want: false}, - {prefix: "bbb", want: false}, - {prefix: "ccc", want: false}, - {prefix: "ddd", want: false}, - }, - wantReadLineErr: io.EOF, - }, - } - - for _, tc := range testsPerReadLine { - for _, assert := range tc.nextLine { - got, err := in.nextLineStartsWith(assert.prefix) - if err != nil { - t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) - } - - if got != assert.want { - t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) - } - } - - for _, assert := range tc.nextNextLine { - got, err := in.nextNextLineStartsWith(assert.prefix) - if err != nil { - t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) - } - - if got != assert.want { - t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) - } - } - - _, err := in.readLine() - if err != tc.wantReadLineErr { - t.Fatalf("readLine returned unexpected error. got=%s, want=%s", err, tc.wantReadLineErr) - } - - } +in := newLineReader(strings.NewReader(input)) + +type assertion struct { +prefix string +want bool +} + +testsPerReadLine := []struct { +nextLine []assertion +nextNextLine []assertion +wantReadLineErr error +}{ +{ +nextLine: []assertion{ +{prefix: "a", want: true}, +{prefix: "aa", want: true}, +{prefix: "aaa", want: true}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: false}, +}, +nextNextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: true}, +{prefix: "ccc", want: false}, +}, +}, +{ +nextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: true}, +{prefix: "ccc", want: false}, +}, +nextNextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: true}, +}, +}, +{ +nextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: true}, +{prefix: "ddd", want: false}, +}, +nextNextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: false}, +{prefix: "ddd", want: false}, +}, +}, +{ +nextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: false}, +{prefix: "ddd", want: false}, +}, +nextNextLine: []assertion{ +{prefix: "aaa", want: false}, +{prefix: "bbb", want: false}, +{prefix: "ccc", want: false}, +{prefix: "ddd", want: false}, +}, +wantReadLineErr: io.EOF, +}, +} + +for _, tc := range testsPerReadLine { +for _, assert := range tc.nextLine { +got, err := in.nextLineStartsWith(assert.prefix) +if err != nil { +t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) +} + +if got != assert.want { +t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) +} +} + +for _, assert := range tc.nextNextLine { +got, err := in.nextNextLineStartsWith(assert.prefix) +if err != nil { +t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) +} + +if got != assert.want { +t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) +} +} + +_, err := in.readLine() +if err != tc.wantReadLineErr { +t.Fatalf("readLine returned unexpected error. got=%s, want=%s", err, tc.wantReadLineErr) +} + +} +} + +func TestReadLine_KeepCR(t *testing.T) { +input := "line1\r\nline2\r\n" +in := bufio.NewReader(strings.NewReader(input)) + +l, err := readLine(in, true) +if err != nil { +t.Fatal(err) +} +if string(l) != "line1\r" { +t.Errorf("expected line1\r, got %q", string(l)) +} + +l, err = readLine(in, true) +if err != nil { +t.Fatal(err) +} +if string(l) != "line2\r" { +t.Errorf("expected line2\r, got %q", string(l)) +} } From 131733f3bb2181155f60b2040385dbbdcb4af636 Mon Sep 17 00:00:00 2001 From: Emmanuel Antonio Cuevas Date: Wed, 11 Feb 2026 20:31:36 +0000 Subject: [PATCH 2/3] Restore removed tabs --- diff/diff.go | 170 ++++++++-------- diff/reader_util.go | 126 ++++++------ diff/reader_util_test.go | 406 +++++++++++++++++++-------------------- 3 files changed, 351 insertions(+), 351 deletions(-) diff --git a/diff/diff.go b/diff/diff.go index dbf56c4..70a45c1 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -1,121 +1,121 @@ package diff import ( -"bytes" -"time" + "bytes" + "time" ) // ParseOptions specifies options for parsing diffs. type ParseOptions struct { -// KeepCR specifies whether to keep trailing carriage return characters (\r) in lines. -KeepCR bool + // KeepCR specifies whether to keep trailing carriage return characters (\r) in lines. + KeepCR bool } // A FileDiff represents a unified diff for a single file. // // A file unified diff has a header that resembles the following: // -//--- oldname2009-10-11 15:12:20.000000000 -0700 -//+++ newname2009-10-11 15:12:30.000000000 -0700 +// --- oldname2009-10-11 15:12:20.000000000 -0700 +// +++ newname2009-10-11 15:12:30.000000000 -0700 type FileDiff struct { -// the original name of the file -OrigName string -// the original timestamp (nil if not present) -OrigTime *time.Time -// the new name of the file (often same as OrigName) -NewName string -// the new timestamp (nil if not present) -NewTime *time.Time -// extended header lines (e.g., git's "new mode ", "rename from ", etc.) -Extended []string -// hunks that were changed from orig to new -Hunks []*Hunk + // the original name of the file + OrigName string + // the original timestamp (nil if not present) + OrigTime *time.Time + // the new name of the file (often same as OrigName) + NewName string + // the new timestamp (nil if not present) + NewTime *time.Time + // extended header lines (e.g., git's "new mode ", "rename from ", etc.) + Extended []string + // hunks that were changed from orig to new + Hunks []*Hunk } // A Hunk represents a series of changes (additions or deletions) in a file's // unified diff. type Hunk struct { -// starting line number in original file -OrigStartLine int32 -// number of lines the hunk applies to in the original file -OrigLines int32 -// if > 0, then the original file had a 'No newline at end of file' mark at this offset -OrigNoNewlineAt int32 -// starting line number in new file -NewStartLine int32 -// number of lines the hunk applies to in the new file -NewLines int32 -// optional section heading -Section string -// 0-indexed line offset in unified file diff (including section headers); this is -// only set when Hunks are read from entire file diff (i.e., when ReadAllHunks is -// called) This accounts for hunk headers, too, so the StartPosition of the first -// hunk will be 1. -StartPosition int32 -// hunk body (lines prefixed with '-', '+', or ' ') -Body []byte + // starting line number in original file + OrigStartLine int32 + // number of lines the hunk applies to in the original file + OrigLines int32 + // if > 0, then the original file had a 'No newline at end of file' mark at this offset + OrigNoNewlineAt int32 + // starting line number in new file + NewStartLine int32 + // number of lines the hunk applies to in the new file + NewLines int32 + // optional section heading + Section string + // 0-indexed line offset in unified file diff (including section headers); this is + // only set when Hunks are read from entire file diff (i.e., when ReadAllHunks is + // called) This accounts for hunk headers, too, so the StartPosition of the first + // hunk will be 1. + StartPosition int32 + // hunk body (lines prefixed with '-', '+', or ' ') + Body []byte } // A Stat is a diff stat that represents the number of lines added/changed/deleted. type Stat struct { -// number of lines added -Added int32 -// number of lines changed -Changed int32 -// number of lines deleted -Deleted int32 + // number of lines added + Added int32 + // number of lines changed + Changed int32 + // number of lines deleted + Deleted int32 } // Stat computes the number of lines added/changed/deleted in all // hunks in this file's diff. func (d *FileDiff) Stat() Stat { -total := Stat{} -for _, h := range d.Hunks { -total.add(h.Stat()) -} -return total + total := Stat{} + for _, h := range d.Hunks { + total.add(h.Stat()) + } + return total } // Stat computes the number of lines added/changed/deleted in this // hunk. func (h *Hunk) Stat() Stat { -lines := bytes.Split(h.Body, []byte{'\n'}) -var last byte -st := Stat{} -for _, line := range lines { -if len(line) == 0 { -last = 0 -continue -} -switch line[0] { -case '-': -if last == '+' { -st.Added-- -st.Changed++ -last = 0 // next line can't change this one since this is already a change -} else { -st.Deleted++ -last = line[0] -} -case '+': -if last == '-' { -st.Deleted-- -st.Changed++ -last = 0 // next line can't change this one since this is already a change -} else { -st.Added++ -last = line[0] -} -default: -last = 0 -} -} -return st + lines := bytes.Split(h.Body, []byte{'\n'}) + var last byte + st := Stat{} + for _, line := range lines { + if len(line) == 0 { + last = 0 + continue + } + switch line[0] { + case '-': + if last == '+' { + st.Added-- + st.Changed++ + last = 0 // next line can't change this one since this is already a change + } else { + st.Deleted++ + last = line[0] + } + case '+': + if last == '-' { + st.Deleted-- + st.Changed++ + last = 0 // next line can't change this one since this is already a change + } else { + st.Added++ + last = line[0] + } + default: + last = 0 + } + } + return st } var ( -hunkPrefix = []byte("@@ ") -onlyInMessagePrefix = []byte("Only in ") + hunkPrefix = []byte("@@ ") + onlyInMessagePrefix = []byte("Only in ") ) const hunkHeader = "@@ -%d,%d +%d,%d @@" @@ -136,7 +136,7 @@ const diffTimeParseWithoutTZLayout = "2006-01-02 15:04:05" const diffTimeFormatLayout = "2006-01-02 15:04:05.000000000 -0700" func (s *Stat) add(o Stat) { -s.Added += o.Added -s.Changed += o.Changed -s.Deleted += o.Deleted + s.Added += o.Added + s.Changed += o.Changed + s.Deleted += o.Deleted } diff --git a/diff/reader_util.go b/diff/reader_util.go index 959888f..f1c17be 100644 --- a/diff/reader_util.go +++ b/diff/reader_util.go @@ -1,52 +1,52 @@ package diff import ( -"bufio" -"bytes" -"errors" -"io" + "bufio" + "bytes" + "errors" + "io" ) var ErrLineReaderUninitialized = errors.New("line reader not initialized") func newLineReader(r io.Reader) *lineReader { -return &lineReader{reader: bufio.NewReader(r)} + return &lineReader{reader: bufio.NewReader(r)} } func newLineReaderOptions(r io.Reader, opts ParseOptions) *lineReader { -return &lineReader{ -reader: bufio.NewReader(r), -keepCR: opts.KeepCR, -} + return &lineReader{ + reader: bufio.NewReader(r), + keepCR: opts.KeepCR, + } } // lineReader is a wrapper around a bufio.Reader that caches the next line to // provide lookahead functionality for the next two lines. type lineReader struct { -reader *bufio.Reader + reader *bufio.Reader -cachedNextLine []byte -cachedNextLineErr error + cachedNextLine []byte + cachedNextLineErr error -keepCR bool + keepCR bool } // readLine returns the next unconsumed line and advances the internal cache of // the lineReader. func (l *lineReader) readLine() ([]byte, error) { -if l.cachedNextLine == nil && l.cachedNextLineErr == nil { -l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) -} + if l.cachedNextLine == nil && l.cachedNextLineErr == nil { + l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) + } -if l.cachedNextLineErr != nil { -return nil, l.cachedNextLineErr -} + if l.cachedNextLineErr != nil { + return nil, l.cachedNextLineErr + } -next := l.cachedNextLine + next := l.cachedNextLine -l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) + l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) -return next, nil + return next, nil } // nextLineStartsWith looks at the line that would be returned by the next call @@ -55,11 +55,11 @@ return next, nil // io.EOF and bufio.ErrBufferFull errors are ignored so that the function can // be used when at the end of the file. func (l *lineReader) nextLineStartsWith(prefix string) (bool, error) { -if l.cachedNextLine == nil && l.cachedNextLineErr == nil { -l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) -} + if l.cachedNextLine == nil && l.cachedNextLineErr == nil { + l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) + } -return l.lineHasPrefix(l.cachedNextLine, prefix, l.cachedNextLineErr) + return l.lineHasPrefix(l.cachedNextLine, prefix, l.cachedNextLineErr) } // nextNextLineStartsWith checks the prefix of the line *after* the line that @@ -72,12 +72,12 @@ return l.lineHasPrefix(l.cachedNextLine, prefix, l.cachedNextLineErr) // calling nextLineStartsWith. Otherwise ErrLineReaderUninitialized will be // returned. func (l *lineReader) nextNextLineStartsWith(prefix string) (bool, error) { -if l.cachedNextLine == nil && l.cachedNextLineErr == nil { -l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) -} + if l.cachedNextLine == nil && l.cachedNextLineErr == nil { + l.cachedNextLine, l.cachedNextLineErr = readLine(l.reader, l.keepCR) + } -next, err := l.reader.Peek(len(prefix)) -return l.lineHasPrefix(next, prefix, err) + next, err := l.reader.Peek(len(prefix)) + return l.lineHasPrefix(next, prefix, err) } // lineHasPrefix checks whether the given line has the given prefix with @@ -87,14 +87,14 @@ return l.lineHasPrefix(next, prefix, err) // lineHasPrefix checks the error to adjust its return value to, e.g., return // false and ignore the error when readErr is io.EOF. func (l *lineReader) lineHasPrefix(line []byte, prefix string, readErr error) (bool, error) { -if readErr != nil { -if readErr == io.EOF || readErr == bufio.ErrBufferFull { -return false, nil -} -return false, readErr -} + if readErr != nil { + if readErr == io.EOF || readErr == bufio.ErrBufferFull { + return false, nil + } + return false, readErr + } -return bytes.HasPrefix(line, []byte(prefix)), nil + return bytes.HasPrefix(line, []byte(prefix)), nil } // readLine is a helper that mimics the functionality of calling bufio.Scanner.Scan() and @@ -103,33 +103,33 @@ return bytes.HasPrefix(line, []byte(prefix)), nil // io.EOF error when there is nothing left to read (at the start of the function call). It // will return any other errors it receives from the underlying call to ReadBytes. func readLine(r *bufio.Reader, keepCR bool) ([]byte, error) { -line_, err := r.ReadBytes('\n') -if err == io.EOF { -if len(line_) == 0 { -return nil, io.EOF -} - -// ReadBytes returned io.EOF, because it didn't find another newline, but there is -// still the remainder of the file to return as a line. -line := line_ -if !keepCR { -return dropCR(line), nil -} -return line, nil -} else if err != nil { -return nil, err -} -line := line_[0 : len(line_)-1] -if !keepCR { -return dropCR(line), nil -} -return line, nil + line_, err := r.ReadBytes('\n') + if err == io.EOF { + if len(line_) == 0 { + return nil, io.EOF + } + + // ReadBytes returned io.EOF, because it didn't find another newline, but there is + // still the remainder of the file to return as a line. + line := line_ + if !keepCR { + return dropCR(line), nil + } + return line, nil + } else if err != nil { + return nil, err + } + line := line_[0 : len(line_)-1] + if !keepCR { + return dropCR(line), nil + } + return line, nil } // dropCR drops a terminal \r from the data. func dropCR(data []byte) []byte { -if len(data) > 0 && data[len(data)-1] == '\r' { -return data[0 : len(data)-1] -} -return data + if len(data) > 0 && data[len(data)-1] == '\r' { + return data[0 : len(data)-1] + } + return data } diff --git a/diff/reader_util_test.go b/diff/reader_util_test.go index 7dcca84..f64ab31 100644 --- a/diff/reader_util_test.go +++ b/diff/reader_util_test.go @@ -1,230 +1,230 @@ package diff import ( -"bufio" -"io" -"reflect" -"strings" -"testing" + "bufio" + "io" + "reflect" + "strings" + "testing" ) func TestReadLine(t *testing.T) { -tests := []struct { -name string -input string -want []string -}{ -{ -name: "empty", -input: "", -want: []string{}, -}, -{ -name: "single_line", -input: "@@ -0,0 +1,62 @@", -want: []string{"@@ -0,0 +1,62 @@"}, -}, -{ -name: "single_lf_terminated_line", -input: "@@ -0,0 +1,62 @@\n", -want: []string{"@@ -0,0 +1,62 @@"}, -}, -{ -name: "single_crlf_terminated_line", -input: "@@ -0,0 +1,62 @@\r\n", -want: []string{"@@ -0,0 +1,62 @@"}, -}, -{ -name: "multi_line", -input: `diff --git a/test.go b/test.go + tests := []struct { + name string + input string + want []string + }{ + { + name: "empty", + input: "", + want: []string{}, + }, + { + name: "single_line", + input: "@@ -0,0 +1,62 @@", + want: []string{"@@ -0,0 +1,62 @@"}, + }, + { + name: "single_lf_terminated_line", + input: "@@ -0,0 +1,62 @@\n", + want: []string{"@@ -0,0 +1,62 @@"}, + }, + { + name: "single_crlf_terminated_line", + input: "@@ -0,0 +1,62 @@\r\n", + want: []string{"@@ -0,0 +1,62 @@"}, + }, + { + name: "multi_line", + input: `diff --git a/test.go b/test.go new file mode 100644 index 0000000..3be2928`, -want: []string{ -"diff --git a/test.go b/test.go", -"new file mode 100644", -"index 0000000..3be2928", -}, -}, -} -for _, test := range tests { -t.Run(test.name, func(t *testing.T) { -in := bufio.NewReader(strings.NewReader(test.input)) -out := []string{} -for { -l, err := readLine(in, false) -if err == io.EOF { -break -} -if err != nil { -t.Fatal(err) -} -out = append(out, string(l)) -} -if !reflect.DeepEqual(test.want, out) { -t.Errorf("read lines not equal: want %v, got %v", test.want, out) -} -}) -} + want: []string{ + "diff --git a/test.go b/test.go", + "new file mode 100644", + "index 0000000..3be2928", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + in := bufio.NewReader(strings.NewReader(test.input)) + out := []string{} + for { + l, err := readLine(in, false) + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) + } + out = append(out, string(l)) + } + if !reflect.DeepEqual(test.want, out) { + t.Errorf("read lines not equal: want %v, got %v", test.want, out) + } + }) + } } func TestLineReader_ReadLine(t *testing.T) { -input := `diff --git a/test.go b/test.go + input := `diff --git a/test.go b/test.go new file mode 100644 index 0000000..3be2928 ` -in := newLineReader(strings.NewReader(input)) -out := []string{} -for i := 0; i < 4; i++ { -l, err := in.readLine() -if err != nil { -t.Fatal(err) -} -out = append(out, string(l)) -} - -wantOut := strings.Split(input, "\n")[0:4] -if !reflect.DeepEqual(wantOut, out) { -t.Errorf("read lines not equal: want %v, got %v", wantOut, out) -} - -_, err := in.readLine() -if err != nil { -t.Fatal(err) -} -if in.cachedNextLineErr != io.EOF { -t.Fatalf("lineReader has wrong cachedNextLineErr: %s", in.cachedNextLineErr) -} -_, err = in.readLine() -if err != io.EOF { -t.Fatalf("readLine did not return io.EOF: %s", err) -} + in := newLineReader(strings.NewReader(input)) + out := []string{} + for i := 0; i < 4; i++ { + l, err := in.readLine() + if err != nil { + t.Fatal(err) + } + out = append(out, string(l)) + } + + wantOut := strings.Split(input, "\n")[0:4] + if !reflect.DeepEqual(wantOut, out) { + t.Errorf("read lines not equal: want %v, got %v", wantOut, out) + } + + _, err := in.readLine() + if err != nil { + t.Fatal(err) + } + if in.cachedNextLineErr != io.EOF { + t.Fatalf("lineReader has wrong cachedNextLineErr: %s", in.cachedNextLineErr) + } + _, err = in.readLine() + if err != io.EOF { + t.Fatalf("readLine did not return io.EOF: %s", err) + } } func TestLineReader_NextLine(t *testing.T) { -input := `aaa rest of line + input := `aaa rest of line bbbrest of line ccc rest of line` -in := newLineReader(strings.NewReader(input)) - -type assertion struct { -prefix string -want bool -} - -testsPerReadLine := []struct { -nextLine []assertion -nextNextLine []assertion -wantReadLineErr error -}{ -{ -nextLine: []assertion{ -{prefix: "a", want: true}, -{prefix: "aa", want: true}, -{prefix: "aaa", want: true}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: false}, -}, -nextNextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: true}, -{prefix: "ccc", want: false}, -}, -}, -{ -nextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: true}, -{prefix: "ccc", want: false}, -}, -nextNextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: true}, -}, -}, -{ -nextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: true}, -{prefix: "ddd", want: false}, -}, -nextNextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: false}, -{prefix: "ddd", want: false}, -}, -}, -{ -nextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: false}, -{prefix: "ddd", want: false}, -}, -nextNextLine: []assertion{ -{prefix: "aaa", want: false}, -{prefix: "bbb", want: false}, -{prefix: "ccc", want: false}, -{prefix: "ddd", want: false}, -}, -wantReadLineErr: io.EOF, -}, -} - -for _, tc := range testsPerReadLine { -for _, assert := range tc.nextLine { -got, err := in.nextLineStartsWith(assert.prefix) -if err != nil { -t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) -} - -if got != assert.want { -t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) -} -} - -for _, assert := range tc.nextNextLine { -got, err := in.nextNextLineStartsWith(assert.prefix) -if err != nil { -t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) -} - -if got != assert.want { -t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) -} -} - -_, err := in.readLine() -if err != tc.wantReadLineErr { -t.Fatalf("readLine returned unexpected error. got=%s, want=%s", err, tc.wantReadLineErr) -} - -} + in := newLineReader(strings.NewReader(input)) + + type assertion struct { + prefix string + want bool + } + + testsPerReadLine := []struct { + nextLine []assertion + nextNextLine []assertion + wantReadLineErr error + }{ + { + nextLine: []assertion{ + {prefix: "a", want: true}, + {prefix: "aa", want: true}, + {prefix: "aaa", want: true}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: false}, + }, + nextNextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: true}, + {prefix: "ccc", want: false}, + }, + }, + { + nextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: true}, + {prefix: "ccc", want: false}, + }, + nextNextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: true}, + }, + }, + { + nextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: true}, + {prefix: "ddd", want: false}, + }, + nextNextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: false}, + {prefix: "ddd", want: false}, + }, + }, + { + nextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: false}, + {prefix: "ddd", want: false}, + }, + nextNextLine: []assertion{ + {prefix: "aaa", want: false}, + {prefix: "bbb", want: false}, + {prefix: "ccc", want: false}, + {prefix: "ddd", want: false}, + }, + wantReadLineErr: io.EOF, + }, + } + + for _, tc := range testsPerReadLine { + for _, assert := range tc.nextLine { + got, err := in.nextLineStartsWith(assert.prefix) + if err != nil { + t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) + } + + if got != assert.want { + t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) + } + } + + for _, assert := range tc.nextNextLine { + got, err := in.nextNextLineStartsWith(assert.prefix) + if err != nil { + t.Fatalf("nextLineStartsWith returned unexpected error: %s", err) + } + + if got != assert.want { + t.Fatalf("unexpected result for prefix %q. got=%t, want=%t", assert.prefix, got, assert.want) + } + } + + _, err := in.readLine() + if err != tc.wantReadLineErr { + t.Fatalf("readLine returned unexpected error. got=%s, want=%s", err, tc.wantReadLineErr) + } + + } } func TestReadLine_KeepCR(t *testing.T) { -input := "line1\r\nline2\r\n" -in := bufio.NewReader(strings.NewReader(input)) - -l, err := readLine(in, true) -if err != nil { -t.Fatal(err) -} -if string(l) != "line1\r" { -t.Errorf("expected line1\r, got %q", string(l)) -} - -l, err = readLine(in, true) -if err != nil { -t.Fatal(err) -} -if string(l) != "line2\r" { -t.Errorf("expected line2\r, got %q", string(l)) -} + input := "line1\r\nline2\r\n" + in := bufio.NewReader(strings.NewReader(input)) + + l, err := readLine(in, true) + if err != nil { + t.Fatal(err) + } + if string(l) != "line1\r" { + t.Errorf("expected line1\r, got %q", string(l)) + } + + l, err = readLine(in, true) + if err != nil { + t.Fatal(err) + } + if string(l) != "line2\r" { + t.Errorf("expected line2\r, got %q", string(l)) + } } From 0a51a7a4ea5471bf636b5f3d72f3576f9f8a2e4d Mon Sep 17 00:00:00 2001 From: Emmanuel Antonio Cuevas Date: Wed, 11 Feb 2026 21:03:43 +0000 Subject: [PATCH 3/3] Missing tabs --- diff/diff.go | 4 ++-- diff/parse.go | 46 ++++++++++++++++++++++++++++++++++------ diff/reader_util_test.go | 4 ++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/diff/diff.go b/diff/diff.go index 70a45c1..cc19fe5 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -15,8 +15,8 @@ type ParseOptions struct { // // A file unified diff has a header that resembles the following: // -// --- oldname2009-10-11 15:12:20.000000000 -0700 -// +++ newname2009-10-11 15:12:30.000000000 -0700 +// --- oldname 2009-10-11 15:12:20.000000000 -0700 +// +++ newname 2009-10-11 15:12:30.000000000 -0700 type FileDiff struct { // the original name of the file OrigName string diff --git a/diff/parse.go b/diff/parse.go index 48eeb96..df6c140 100644 --- a/diff/parse.go +++ b/diff/parse.go @@ -1,7 +1,6 @@ package diff import ( - "bufio" "bytes" "errors" "fmt" @@ -17,13 +16,24 @@ import ( // case of per-file errors. If it cannot detect when the diff of the next file // begins, the hunks are added to the FileDiff of the previous file. func ParseMultiFileDiff(diff []byte) ([]*FileDiff, error) { - return NewMultiFileDiffReader(bytes.NewReader(diff)).ReadAllFiles() + return ParseMultiFileDiffOptions(diff, ParseOptions{}) +} + +// ParseMultiFileDiffOptions parses a multi-file unified diff with the given options. +func ParseMultiFileDiffOptions(diff []byte, opts ParseOptions) ([]*FileDiff, error) { + return NewMultiFileDiffReaderOptions(bytes.NewReader(diff), opts).ReadAllFiles() } // NewMultiFileDiffReader returns a new MultiFileDiffReader that reads // a multi-file unified diff from r. func NewMultiFileDiffReader(r io.Reader) *MultiFileDiffReader { - return &MultiFileDiffReader{reader: newLineReader(r)} + return NewMultiFileDiffReaderOptions(r, ParseOptions{}) +} + +// NewMultiFileDiffReaderOptions returns a new MultiFileDiffReader that reads +// a multi-file unified diff from r with the given options. +func NewMultiFileDiffReaderOptions(r io.Reader, opts ParseOptions) *MultiFileDiffReader { + return &MultiFileDiffReader{reader: newLineReaderOptions(r, opts)} } // MultiFileDiffReader reads a multi-file unified diff. @@ -153,13 +163,24 @@ func (r *MultiFileDiffReader) ReadAllFiles() ([]*FileDiff, error) { // ParseFileDiff parses a file unified diff. func ParseFileDiff(diff []byte) (*FileDiff, error) { - return NewFileDiffReader(bytes.NewReader(diff)).Read() + return ParseFileDiffOptions(diff, ParseOptions{}) +} + +// ParseFileDiffOptions parses a file unified diff with the given options. +func ParseFileDiffOptions(diff []byte, opts ParseOptions) (*FileDiff, error) { + return NewFileDiffReaderOptions(bytes.NewReader(diff), opts).Read() } // NewFileDiffReader returns a new FileDiffReader that reads a file // unified diff. func NewFileDiffReader(r io.Reader) *FileDiffReader { - return &FileDiffReader{reader: &lineReader{reader: bufio.NewReader(r)}} + return NewFileDiffReaderOptions(r, ParseOptions{}) +} + +// NewFileDiffReaderOptions returns a new FileDiffReader that reads a file +// unified diff with the given options. +func NewFileDiffReaderOptions(r io.Reader, opts ParseOptions) *FileDiffReader { + return &FileDiffReader{reader: newLineReaderOptions(r, opts)} } // FileDiffReader reads a unified file diff. @@ -586,7 +607,12 @@ var ( // only of hunks and not include a file header; if it has a file // header, use ParseFileDiff. func ParseHunks(diff []byte) ([]*Hunk, error) { - r := NewHunksReader(bytes.NewReader(diff)) + return ParseHunksOptions(diff, ParseOptions{}) +} + +// ParseHunksOptions parses hunks from a unified diff with the given options. +func ParseHunksOptions(diff []byte, opts ParseOptions) ([]*Hunk, error) { + r := NewHunksReaderOptions(bytes.NewReader(diff), opts) hunks, err := r.ReadAllHunks() if err != nil { return nil, err @@ -597,7 +623,13 @@ func ParseHunks(diff []byte) ([]*Hunk, error) { // NewHunksReader returns a new HunksReader that reads unified diff hunks // from r. func NewHunksReader(r io.Reader) *HunksReader { - return &HunksReader{reader: &lineReader{reader: bufio.NewReader(r)}} + return NewHunksReaderOptions(r, ParseOptions{}) +} + +// NewHunksReaderOptions returns a new HunksReader that reads unified diff hunks +// from r with the given options. +func NewHunksReaderOptions(r io.Reader, opts ParseOptions) *HunksReader { + return &HunksReader{reader: newLineReaderOptions(r, opts)} } // A HunksReader reads hunks from a unified diff. diff --git a/diff/reader_util_test.go b/diff/reader_util_test.go index f64ab31..760fcb7 100644 --- a/diff/reader_util_test.go +++ b/diff/reader_util_test.go @@ -217,7 +217,7 @@ func TestReadLine_KeepCR(t *testing.T) { t.Fatal(err) } if string(l) != "line1\r" { - t.Errorf("expected line1\r, got %q", string(l)) + t.Errorf("expected line1\\r, got %q", string(l)) } l, err = readLine(in, true) @@ -225,6 +225,6 @@ func TestReadLine_KeepCR(t *testing.T) { t.Fatal(err) } if string(l) != "line2\r" { - t.Errorf("expected line2\r, got %q", string(l)) + t.Errorf("expected line2\\r, got %q", string(l)) } }