Skip to content

Commit a1f934e

Browse files
committed
Add LineReader and StringReader interfaces
LineReader is a wrapper around StringReader that counts line numbers. This is not currently used by the parser, but will be used by the apply functions.
1 parent f65047b commit a1f934e

File tree

3 files changed

+123
-10
lines changed

3 files changed

+123
-10
lines changed

gitdiff/io.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package gitdiff
2+
3+
import (
4+
"bufio"
5+
"io"
6+
)
7+
8+
// StringReader is the interface that wraps the ReadString method.
9+
type StringReader interface {
10+
// ReadString reads until the first occurrence of delim in the input,
11+
// returning a string containing the data up to and including the
12+
// delimiter. If ReadString encounters an error before finding a delimiter,
13+
// it returns the data read before the error and the error itself (often
14+
// io.EOF). ReadString returns err != nil if and only if the returned data
15+
// does not end in delim.
16+
ReadString(delim byte) (string, error)
17+
}
18+
19+
// LineReader is the interface that wraps the ReadLine method.
20+
type LineReader interface {
21+
// ReadLine reads the next full line in the input, returing the the data
22+
// including the line ending character(s) and the zero-indexed line number.
23+
// If ReadLine encounters an error before reaching the end of the line, it
24+
// returns the data read before the error and the error itself (often
25+
// io.EOF). ReadLine returns err != nil if and only if the returned data is
26+
// not a complete line.
27+
//
28+
// If the implementation defines other methods for reading the same input,
29+
// line numbers may be incorrect if calls to ReadLine are mixed with calls
30+
// to other read methods.
31+
ReadLine() (string, int, error)
32+
}
33+
34+
// NewLineReader returns a LineReader for a reader starting at a specific line
35+
// using the newline character, \n, as a line separator. If r is a
36+
// StringReader, it is used directly. Otherwise, it is wrapped in a way that
37+
// may read extra data from the underlying input.
38+
func NewLineReader(r io.Reader, lineno int) LineReader {
39+
sr, ok := r.(StringReader)
40+
if !ok {
41+
sr = bufio.NewReader(r)
42+
}
43+
return &lineReader{r: sr, n: lineno}
44+
}
45+
46+
type lineReader struct {
47+
r StringReader
48+
n int
49+
}
50+
51+
func (lr *lineReader) ReadLine() (line string, lineno int, err error) {
52+
lineno = lr.n
53+
line, err = lr.r.ReadString('\n')
54+
if err == nil {
55+
lr.n++
56+
}
57+
return
58+
}

gitdiff/io_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package gitdiff
2+
3+
import (
4+
"io"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestLineReader(t *testing.T) {
10+
const content = "first line\nsecond line\nthird line\npartial fourth line"
11+
12+
t.Run("readLine", func(t *testing.T) {
13+
r := NewLineReader(strings.NewReader(content), 0)
14+
15+
lines := []struct {
16+
Data string
17+
Err error
18+
}{
19+
{"first line\n", nil},
20+
{"second line\n", nil},
21+
{"third line\n", nil},
22+
{"partial fourth line", io.EOF},
23+
}
24+
25+
for i, line := range lines {
26+
d, n, err := r.ReadLine()
27+
if err != line.Err {
28+
if line.Err == nil {
29+
t.Fatalf("error reading line: %v", err)
30+
} else {
31+
t.Fatalf("expected %v while reading line, but got %v", line.Err, err)
32+
}
33+
}
34+
if d != line.Data {
35+
t.Errorf("incorrect line data: expected %q, actual %q", line.Data, d)
36+
}
37+
if n != i {
38+
t.Errorf("incorrect line number: expected %d, actual %d", i, n)
39+
}
40+
}
41+
})
42+
43+
t.Run("readLineOffset", func(t *testing.T) {
44+
r := NewLineReader(strings.NewReader(content), 10)
45+
46+
d, n, err := r.ReadLine()
47+
if err != nil {
48+
t.Fatalf("error reading line: %v", err)
49+
}
50+
if d != "first line\n" {
51+
t.Errorf("incorrect line data: expected %q, actual %q", "first line\n", d)
52+
}
53+
if n != 10 {
54+
t.Errorf("incorrect line number: expected %d, actual %d", 10, n)
55+
}
56+
})
57+
}

gitdiff/parser.go

+8-10
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
package gitdiff
55

66
import (
7-
"bufio"
87
"fmt"
98
"io"
109
)
1110

1211
// Parse parses a patch with changes to one or more files. Any content before
1312
// the first file is returned as the second value. If an error occurs while
1413
// parsing, it returns all files parsed before the error.
14+
//
15+
// If r is a LineReader or StringReader, it is used directly. Otherwise, it is
16+
// wrapped in a way that may read extra data from the underlying input.
1517
func Parse(r io.Reader) ([]*File, string, error) {
1618
p := newParser(r)
1719

@@ -67,23 +69,19 @@ func Parse(r io.Reader) ([]*File, string, error) {
6769
// - if returning an object, advance to the first line after the object
6870
// - any exported parsing methods must initialize the parser by calling Next()
6971

70-
type stringReader interface {
71-
ReadString(delim byte) (string, error)
72-
}
73-
7472
type parser struct {
75-
r stringReader
73+
r LineReader
7674

7775
eof bool
7876
lineno int64
7977
lines [3]string
8078
}
8179

8280
func newParser(r io.Reader) *parser {
83-
if r, ok := r.(stringReader); ok {
84-
return &parser{r: r}
81+
if lr, ok := r.(LineReader); ok {
82+
return &parser{r: lr}
8583
}
86-
return &parser{r: bufio.NewReader(r)}
84+
return &parser{r: NewLineReader(r, 0)}
8785
}
8886

8987
// Next advances the parser by one line. It returns any error encountered while
@@ -119,7 +117,7 @@ func (p *parser) shiftLines() (err error) {
119117
for i := 0; i < len(p.lines)-1; i++ {
120118
p.lines[i] = p.lines[i+1]
121119
}
122-
p.lines[len(p.lines)-1], err = p.r.ReadString('\n')
120+
p.lines[len(p.lines)-1], _, err = p.r.ReadLine()
123121
return
124122
}
125123

0 commit comments

Comments
 (0)