Skip to content

Commit 0b7af3f

Browse files
committed
Implement text application using LineReaderAt
This is functionally equivalent to the previous version (except for one error case), but uses the new interface. I think the code is simpler overall because it removes the line tracking.
1 parent f370211 commit 0b7af3f

File tree

4 files changed

+92
-100
lines changed

4 files changed

+92
-100
lines changed

gitdiff/apply.go

Lines changed: 76 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (c *Conflict) Is(other error) bool {
3939
// additional location information, if it is available.
4040
type ApplyError struct {
4141
// Line is the one-indexed line number in the source data
42-
Line int
42+
Line int64
4343
// Fragment is the one-indexed fragment number in the file
4444
Fragment int
4545
// FragmentLine is the one-indexed line number in the fragment
@@ -75,7 +75,7 @@ func applyError(err error, args ...interface{}) error {
7575
for _, arg := range args {
7676
switch v := arg.(type) {
7777
case lineNum:
78-
e.Line = int(v) + 1
78+
e.Line = int64(v) + 1
7979
case fragNum:
8080
e.Fragment = int(v) + 1
8181
case fragLineNum:
@@ -92,121 +92,118 @@ func applyError(err error, args ...interface{}) error {
9292
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
9393
// Partial data may be written to dst in this case.
9494
func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error {
95+
// TODO(bkeyes): take an io.ReaderAt and avoid this!
96+
data, err := ioutil.ReadAll(src)
97+
if err != nil {
98+
return applyError(err)
99+
}
100+
95101
if f.IsBinary {
96-
data, err := ioutil.ReadAll(src)
97-
if err != nil {
98-
return applyError(err)
99-
}
100102
if f.BinaryFragment != nil {
101103
return f.BinaryFragment.Apply(dst, bytes.NewReader(data))
102104
}
103105
_, err = dst.Write(data)
104106
return applyError(err)
105107
}
106108

107-
lr, ok := src.(LineReader)
108-
if !ok {
109-
lr = NewLineReader(src, 0)
110-
}
109+
// TODO(bkeyes): check for this conflict case
110+
// &Conflict{"cannot create new file from non-empty src"}
111111

112+
lra := NewLineReaderAt(bytes.NewReader(data))
113+
114+
var next int64
112115
for i, frag := range f.TextFragments {
113-
if err := frag.ApplyStrict(dst, lr); err != nil {
116+
next, err = frag.ApplyStrict(dst, lra, next)
117+
if err != nil {
114118
return applyError(err, fragNum(i))
115119
}
116120
}
117121

118-
_, err := io.Copy(dst, unwrapLineReader(lr))
119-
return applyError(err)
122+
// TODO(bkeyes): extract this to a utility
123+
buf := make([][]byte, 64)
124+
for {
125+
n, err := lra.ReadLinesAt(buf, next)
126+
if err != nil && err != io.EOF {
127+
return applyError(err, lineNum(next+int64(n)))
128+
}
129+
130+
for i := 0; i < n; i++ {
131+
if _, err := dst.Write(buf[n]); err != nil {
132+
return applyError(err, lineNum(next+int64(n)))
133+
}
134+
}
135+
136+
next += int64(n)
137+
if n < len(buf) {
138+
return nil
139+
}
140+
}
120141
}
121142

122-
// ApplyStrict writes data from src to dst, modifying it as described by the
123-
// fragment. The fragment, including all context lines, must exactly match src
124-
// at the expected line number.
125-
//
126-
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
127-
// Partial data may be written to dst in this case. If there is no error, the
128-
// next read from src returns the line immediately after the last line of the
129-
// fragment.
130-
func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
143+
// ApplyStrict copies from src to dst, from line start through then end of the
144+
// fragment, modifying the data as described by the fragment. The fragment,
145+
// including all context lines, must exactly match src at the expected line
146+
// number. ApplyStrict returns the number of the next unprocessed line in src
147+
// and any error. When the error is not non-nil, partial data may be written.
148+
func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReaderAt, start int64) (next int64, err error) {
131149
// application code assumes fragment fields are consistent
132150
if err := f.Validate(); err != nil {
133-
return applyError(err)
151+
return start, applyError(err)
134152
}
135153

136-
// line numbers are zero-indexed, positions are one-indexed
137-
limit := f.OldPosition - 1
154+
// lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
155+
fragStart := f.OldPosition - 1
156+
if fragStart < 0 {
157+
fragStart = 0
158+
}
159+
fragEnd := fragStart + f.OldLines
138160

139-
// io.EOF is acceptable here: the first line of the patch is the last of
140-
// the source and it has no newline character
141-
nextLine, n, err := copyLines(dst, src, limit)
142-
if err != nil && err != io.EOF {
143-
return applyError(err, lineNum(n))
161+
if fragStart < start {
162+
return start, applyError(&Conflict{"fragment overlaps with an applied fragment"})
163+
}
164+
165+
preimage := make([][]byte, fragEnd-start)
166+
n, err := src.ReadLinesAt(preimage, start)
167+
switch {
168+
case err == nil:
169+
case err == io.EOF && n == len(preimage): // last line of frag has no newline character
170+
default:
171+
return start, applyError(err, lineNum(start+int64(n)))
144172
}
145173

174+
// copy leading data before the fragment starts
175+
for i, line := range preimage[:fragStart-start] {
176+
if _, err := dst.Write(line); err != nil {
177+
next = start + int64(i)
178+
return next, applyError(err, lineNum(next))
179+
}
180+
}
181+
preimage = preimage[fragStart-start:]
182+
183+
// apply the changes in the fragment
146184
used := int64(0)
147185
for i, line := range f.Lines {
148-
if err := applyTextLine(dst, nextLine, line); err != nil {
149-
return applyError(err, lineNum(n), fragLineNum(i))
186+
if err := applyTextLine(dst, line, preimage, used); err != nil {
187+
next = fragStart + used
188+
return next, applyError(err, lineNum(next), fragLineNum(i))
150189
}
151190
if line.Old() {
152191
used++
153192
}
154-
// advance reader if the next fragment line appears in src and we're behind
155-
if i < len(f.Lines)-1 && f.Lines[i+1].Old() && int64(n)-limit < used {
156-
nextLine, n, err = src.ReadLine()
157-
switch {
158-
case err == io.EOF && f.Lines[i+1].NoEOL():
159-
continue
160-
case err != nil:
161-
return applyError(err, lineNum(n), fragLineNum(i+1)) // report for _next_ line in fragment
162-
}
163-
}
164193
}
165-
166-
return nil
194+
return fragStart + used, nil
167195
}
168196

169-
func applyTextLine(dst io.Writer, src string, line Line) (err error) {
170-
switch line.Op {
171-
case OpContext, OpDelete:
172-
if src != line.Line {
173-
return &Conflict{"fragment line does not match src line"}
174-
}
197+
func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
198+
if line.Old() && string(preimage[i]) != line.Line {
199+
return &Conflict{"fragment line does not match src line"}
175200
}
176-
switch line.Op {
177-
case OpContext, OpAdd:
201+
if line.New() {
178202
_, err = io.WriteString(dst, line.Line)
179203
}
180204
return
181205
}
182206

183-
// copyLines copies from src to dst until the line at limit, exclusive. Returns
184-
// the line at limit and the line number. If the error is nil or io.EOF, the
185-
// line number equals limit. A negative limit checks that the source has no
186-
// more lines to read.
187-
func copyLines(dst io.Writer, src LineReader, limit int64) (string, int64, error) {
188-
for {
189-
line, n, err := src.ReadLine()
190-
switch {
191-
case limit < 0 && err == io.EOF && line == "":
192-
return "", limit, nil
193-
case n == limit:
194-
return line, n, err
195-
case n > limit:
196-
if limit < 0 {
197-
return "", n, &Conflict{"cannot create new file from non-empty src"}
198-
}
199-
return "", n, &Conflict{"fragment overlaps with an applied fragment"}
200-
case err != nil:
201-
return line, n, wrapEOF(err)
202-
}
203-
204-
if _, err := io.WriteString(dst, line); err != nil {
205-
return "", n, err
206-
}
207-
}
208-
}
209-
210207
// Apply writes data from src to dst, modifying it as described by the
211208
// fragment.
212209
//

gitdiff/apply_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ func TestTextFragmentApplyStrict(t *testing.T) {
5757
},
5858
Err: &Conflict{},
5959
},
60-
"errorNewFile": {
61-
Files: applyFiles{
62-
Src: "text_fragment_error.src",
63-
Patch: "text_fragment_error_new_file.patch",
64-
},
65-
Err: &Conflict{},
66-
},
60+
// TODO(bkeyes): this check has moved to the file level (probably)
61+
// "errorNewFile": {
62+
// Files: applyFiles{
63+
// Src: "text_fragment_error.src",
64+
// Patch: "text_fragment_error_new_file.patch",
65+
// },
66+
// Err: &Conflict{},
67+
// },
6768
}
6869

6970
for name, test := range tests {
@@ -84,7 +85,7 @@ func TestTextFragmentApplyStrict(t *testing.T) {
8485
frag := files[0].TextFragments[0]
8586

8687
var dst bytes.Buffer
87-
err = frag.ApplyStrict(&dst, NewLineReader(bytes.NewReader(src), 0))
88+
_, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0)
8889
if test.Err != nil {
8990
checkApplyError(t, test.Err, err)
9091
return

gitdiff/gitdiff.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ func (fl Line) String() string {
139139

140140
// Old returns true if the line appears in the old content of the fragment.
141141
func (fl Line) Old() bool {
142-
return fl.Op != OpAdd
142+
return fl.Op == OpContext || fl.Op == OpDelete
143143
}
144144

145145
// New returns true if the line appears in the new content of the fragment.
146146
func (fl Line) New() bool {
147-
return fl.Op == OpAdd
147+
return fl.Op == OpContext || fl.Op == OpAdd
148148
}
149149

150150
// NoEOL returns true if the line is missing a trailing newline character.

gitdiff/io.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,7 @@ func (r *lineReaderAt) ReadLinesAt(lines [][]byte, offset int64) (n int, err err
128128
return 0, io.EOF
129129
}
130130

131-
// TODO(bkeyes): check usage of int / int64
132-
// - interface uses int64 for arbitrarily large files
133-
// - implementation is limited to int lines by index array
134-
135-
// offset <= len(r.index) means that it must fit in int without loss
136-
size, readOffset := lookupLines(r.index, int(offset), len(lines))
131+
size, readOffset := lookupLines(r.index, offset, int64(len(lines)))
137132

138133
b := make([]byte, size)
139134
if _, err := r.r.ReadAt(b, readOffset); err != nil {
@@ -144,7 +139,7 @@ func (r *lineReaderAt) ReadLinesAt(lines [][]byte, offset int64) (n int, err err
144139
}
145140

146141
for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ {
147-
i := int(offset) + n
142+
i := offset + int64(n)
148143
start, end := readOffset, r.index[i]
149144
if i > 0 {
150145
start = r.index[i-1]
@@ -193,15 +188,14 @@ func (r *lineReaderAt) indexTo(line int64) error {
193188

194189
// lookupLines gets the byte offset and size of a range of lines from an index
195190
// where the value at n is the offset of the first byte after line number n.
196-
func lookupLines(index []int64, start, n int) (size int64, offset int64) {
197-
if start > len(index) {
191+
func lookupLines(index []int64, start, n int64) (size int64, offset int64) {
192+
if start > int64(len(index)) {
198193
offset = index[len(index)-1]
199194
} else if start > 0 {
200195
offset = index[start-1]
201196
}
202197
if n > 0 {
203-
// TODO(bkeyes): check types for overflow
204-
if start+n > len(index) {
198+
if start+n > int64(len(index)) {
205199
size = index[len(index)-1] - offset
206200
} else {
207201
size = index[start+n-1] - offset

0 commit comments

Comments
 (0)