+76
-79
gitdiff/apply.go
+76
-79
gitdiff/apply.go
···
39
39
// additional location information, if it is available.
40
40
type ApplyError struct {
41
41
// Line is the one-indexed line number in the source data
42
-
Line int
42
+
Line int64
43
43
// Fragment is the one-indexed fragment number in the file
44
44
Fragment int
45
45
// FragmentLine is the one-indexed line number in the fragment
···
75
75
for _, arg := range args {
76
76
switch v := arg.(type) {
77
77
case lineNum:
78
-
e.Line = int(v) + 1
78
+
e.Line = int64(v) + 1
79
79
case fragNum:
80
80
e.Fragment = int(v) + 1
81
81
case fragLineNum:
···
92
92
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
93
93
// Partial data may be written to dst in this case.
94
94
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
+
95
101
if f.IsBinary {
96
-
data, err := ioutil.ReadAll(src)
97
-
if err != nil {
98
-
return applyError(err)
99
-
}
100
102
if f.BinaryFragment != nil {
101
103
return f.BinaryFragment.Apply(dst, bytes.NewReader(data))
102
104
}
···
104
106
return applyError(err)
105
107
}
106
108
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"}
111
+
112
+
lra := NewLineReaderAt(bytes.NewReader(data))
111
113
114
+
var next int64
112
115
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 {
114
118
return applyError(err, fragNum(i))
115
119
}
116
120
}
117
121
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
+
}
120
141
}
121
142
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) {
131
149
// application code assumes fragment fields are consistent
132
150
if err := f.Validate(); err != nil {
133
-
return applyError(err)
151
+
return start, applyError(err)
152
+
}
153
+
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
160
+
161
+
if fragStart < start {
162
+
return start, applyError(&Conflict{"fragment overlaps with an applied fragment"})
134
163
}
135
164
136
-
// line numbers are zero-indexed, positions are one-indexed
137
-
limit := f.OldPosition - 1
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)))
172
+
}
138
173
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))
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
+
}
144
180
}
181
+
preimage = preimage[fragStart-start:]
145
182
183
+
// apply the changes in the fragment
146
184
used := int64(0)
147
185
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))
150
189
}
151
190
if line.Old() {
152
191
used++
153
192
}
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
-
}
164
193
}
165
-
166
-
return nil
194
+
return fragStart + used, nil
167
195
}
168
196
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"}
175
200
}
176
-
switch line.Op {
177
-
case OpContext, OpAdd:
201
+
if line.New() {
178
202
_, err = io.WriteString(dst, line.Line)
179
203
}
180
204
return
181
-
}
182
-
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
205
}
209
206
210
207
// Apply writes data from src to dst, modifying it as described by the
+9
-8
gitdiff/apply_test.go
+9
-8
gitdiff/apply_test.go
···
57
57
},
58
58
Err: &Conflict{},
59
59
},
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
+
// },
67
68
}
68
69
69
70
for name, test := range tests {
···
84
85
frag := files[0].TextFragments[0]
85
86
86
87
var dst bytes.Buffer
87
-
err = frag.ApplyStrict(&dst, NewLineReader(bytes.NewReader(src), 0))
88
+
_, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0)
88
89
if test.Err != nil {
89
90
checkApplyError(t, test.Err, err)
90
91
return
+2
-2
gitdiff/gitdiff.go
+2
-2
gitdiff/gitdiff.go
···
139
139
140
140
// Old returns true if the line appears in the old content of the fragment.
141
141
func (fl Line) Old() bool {
142
-
return fl.Op != OpAdd
142
+
return fl.Op == OpContext || fl.Op == OpDelete
143
143
}
144
144
145
145
// New returns true if the line appears in the new content of the fragment.
146
146
func (fl Line) New() bool {
147
-
return fl.Op == OpAdd
147
+
return fl.Op == OpContext || fl.Op == OpAdd
148
148
}
149
149
150
150
// NoEOL returns true if the line is missing a trailing newline character.
+5
-11
gitdiff/io.go
+5
-11
gitdiff/io.go
···
128
128
return 0, io.EOF
129
129
}
130
130
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)))
137
132
138
133
b := make([]byte, size)
139
134
if _, err := r.r.ReadAt(b, readOffset); err != nil {
···
144
139
}
145
140
146
141
for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ {
147
-
i := int(offset) + n
142
+
i := offset + int64(n)
148
143
start, end := readOffset, r.index[i]
149
144
if i > 0 {
150
145
start = r.index[i-1]
···
193
188
194
189
// lookupLines gets the byte offset and size of a range of lines from an index
195
190
// 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)) {
198
193
offset = index[len(index)-1]
199
194
} else if start > 0 {
200
195
offset = index[start-1]
201
196
}
202
197
if n > 0 {
203
-
// TODO(bkeyes): check types for overflow
204
-
if start+n > len(index) {
198
+
if start+n > int64(len(index)) {
205
199
size = index[len(index)-1] - offset
206
200
} else {
207
201
size = index[start+n-1] - offset