Skip to content

Commit 18058d1

Browse files
committed
Move apply methods to an Applier type
This removes the distinction between "strict" and "fuzzy" application by allowing future methods on Applier that control settings. It also avoids state tracking in the text fragment apply signature by moving it into the Applier type. While in practice, an Applier will be used once and discarded, the capability is provided to reset it for multiple uses.
1 parent 3d1274d commit 18058d1

File tree

4 files changed

+230
-100
lines changed

4 files changed

+230
-100
lines changed

gitdiff/apply.go

Lines changed: 134 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package gitdiff
22

33
import (
4-
"bytes"
54
"errors"
65
"fmt"
76
"io"
8-
"io/ioutil"
97
)
108

119
// Conflict indicates an apply failed due to a conflict between the patch and
@@ -85,70 +83,101 @@ func applyError(err error, args ...interface{}) error {
8583
return e
8684
}
8785

88-
// ApplyStrict writes data from src to dst, modifying it as described by the
89-
// fragments in the file. For text files, each fragment, including all context
90-
// lines, must exactly match src at the expected line number.
86+
var (
87+
errApplyInProgress = errors.New("gitdiff: incompatible apply in progress")
88+
)
89+
90+
const (
91+
applyInitial = iota
92+
applyText
93+
applyBinary
94+
)
95+
96+
// Applier applies changes described in fragments to source data. If changes
97+
// are described in multiple fragments, those fragments must be applied in
98+
// order, usually by calling ApplyFile.
9199
//
92-
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
93-
// Partial data may be written to dst in this case.
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+
// By default, Applier operates in "strict" mode, where fragment content and
101+
// positions must exactly match those of the source.
102+
//
103+
// If an error occurs while applying, methods on Applier return instances of
104+
// *ApplyError that annotate the wrapped error with additional information
105+
// when available. If the error is because of a conflict between a fragment and
106+
// the source, the wrapped error will be a *Conflict.
107+
//
108+
// While an Applier can apply both text and binary fragments, only one fragment
109+
// type can be used without resetting the Applier. The first fragment applied
110+
// sets the type for the Applier. Mixing fragment types or mixing
111+
// fragment-level and file-level applies results in an error.
112+
type Applier struct {
113+
src io.ReaderAt
114+
lineSrc LineReaderAt
115+
nextLine int64
116+
applyType int
117+
}
100118

101-
if f.IsBinary {
102-
if f.BinaryFragment != nil {
103-
return f.BinaryFragment.Apply(dst, bytes.NewReader(data))
119+
// NewApplier creates an Applier that reads data from src. If src is a
120+
// LineReaderAt, it is used directly to apply text fragments.
121+
func NewApplier(src io.ReaderAt) *Applier {
122+
a := new(Applier)
123+
a.Reset(src)
124+
return a
125+
}
126+
127+
// Reset resets the input and internal state of the Applier. If src is nil, the
128+
// existing source is reused.
129+
func (a *Applier) Reset(src io.ReaderAt) {
130+
if src != nil {
131+
a.src = src
132+
if lineSrc, ok := src.(LineReaderAt); ok {
133+
a.lineSrc = lineSrc
134+
} else {
135+
a.lineSrc = &lineReaderAt{r: src}
104136
}
105-
_, err = dst.Write(data)
106-
return applyError(err)
137+
}
138+
a.nextLine = 0
139+
a.applyType = applyInitial
140+
}
141+
142+
// ApplyFile applies the changes in all of the fragments of f and writes the
143+
// result to dst.
144+
func (a *Applier) ApplyFile(dst io.Writer, f *File) error {
145+
if a.applyType != applyInitial {
146+
return applyError(errApplyInProgress)
107147
}
108148

109-
// TODO(bkeyes): check for this conflict case
110-
// &Conflict{"cannot create new file from non-empty src"}
149+
if f.IsBinary && f.BinaryFragment != nil {
150+
return a.ApplyBinaryFragment(dst, f.BinaryFragment)
151+
}
111152

112-
lra := NewLineReaderAt(bytes.NewReader(data))
153+
// TODO(bkeyes): sort fragments by start position
154+
// TODO(bkeyes): merge overlapping fragments
113155

114-
var next int64
115156
for i, frag := range f.TextFragments {
116-
next, err = frag.ApplyStrict(dst, lra, next)
117-
if err != nil {
157+
if err := a.ApplyTextFragment(dst, frag); err != nil {
118158
return applyError(err, fragNum(i))
119159
}
120160
}
121161

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-
}
162+
return applyError(a.Flush(dst))
163+
}
135164

136-
next += int64(n)
137-
if n < len(buf) {
138-
return nil
139-
}
165+
// ApplyTextFragment applies the changes in the fragment f and writes unwritten
166+
// data before the start of the fragment and the result to dst. If multiple
167+
// text fragments apply to the same source, ApplyTextFragment must be called in
168+
// order of increasing start position. As a result, each fragment can be
169+
// applied at most once before a call to Reset.
170+
func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error {
171+
switch a.applyType {
172+
case applyInitial, applyText:
173+
default:
174+
return applyError(errApplyInProgress)
140175
}
141-
}
176+
a.applyType = applyText
142177

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) {
149178
// application code assumes fragment fields are consistent
150179
if err := f.Validate(); err != nil {
151-
return start, applyError(err)
180+
return applyError(err)
152181
}
153182

154183
// lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
@@ -158,24 +187,35 @@ func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReaderAt, start int64)
158187
}
159188
fragEnd := fragStart + f.OldLines
160189

190+
start := a.nextLine
161191
if fragStart < start {
162-
return start, applyError(&Conflict{"fragment overlaps with an applied fragment"})
192+
return applyError(&Conflict{"fragment overlaps with an applied fragment"})
193+
}
194+
195+
if f.OldPosition == 0 {
196+
ok, err := isLen(a.src, 0)
197+
if err != nil {
198+
return applyError(err)
199+
}
200+
if !ok {
201+
return applyError(&Conflict{"cannot create new file from non-empty src"})
202+
}
163203
}
164204

165205
preimage := make([][]byte, fragEnd-start)
166-
n, err := src.ReadLinesAt(preimage, start)
206+
n, err := a.lineSrc.ReadLinesAt(preimage, start)
167207
switch {
168208
case err == nil:
169209
case err == io.EOF && n == len(preimage): // last line of frag has no newline character
170210
default:
171-
return start, applyError(err, lineNum(start+int64(n)))
211+
return applyError(err, lineNum(start+int64(n)))
172212
}
173213

174214
// copy leading data before the fragment starts
175215
for i, line := range preimage[:fragStart-start] {
176216
if _, err := dst.Write(line); err != nil {
177-
next = start + int64(i)
178-
return next, applyError(err, lineNum(next))
217+
a.nextLine = start + int64(i)
218+
return applyError(err, lineNum(a.nextLine))
179219
}
180220
}
181221
preimage = preimage[fragStart-start:]
@@ -184,14 +224,15 @@ func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReaderAt, start int64)
184224
used := int64(0)
185225
for i, line := range f.Lines {
186226
if err := applyTextLine(dst, line, preimage, used); err != nil {
187-
next = fragStart + used
188-
return next, applyError(err, lineNum(next), fragLineNum(i))
227+
a.nextLine = fragStart + used
228+
return applyError(err, lineNum(a.nextLine), fragLineNum(i))
189229
}
190230
if line.Old() {
191231
used++
192232
}
193233
}
194-
return fragStart + used, nil
234+
a.nextLine = fragStart + used
235+
return nil
195236
}
196237

197238
func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
@@ -201,34 +242,53 @@ func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err er
201242
if line.New() {
202243
_, err = io.WriteString(dst, line.Line)
203244
}
204-
return
245+
return err
205246
}
206247

207-
// Apply writes data from src to dst, modifying it as described by the
208-
// fragment.
209-
//
210-
// Unlike text fragments, binary fragments do not distinguish between strict
211-
// and non-strict application.
212-
func (f *BinaryFragment) Apply(dst io.Writer, src io.ReaderAt) error {
248+
// Flush writes any data following the last applied fragment to dst.
249+
func (a *Applier) Flush(dst io.Writer) (err error) {
250+
switch a.applyType {
251+
case applyInitial:
252+
_, err = copyFrom(dst, a.src, 0)
253+
case applyText:
254+
_, err = copyLinesFrom(dst, a.lineSrc, a.nextLine)
255+
case applyBinary:
256+
// nothing to flush, binary apply "consumes" full source
257+
}
258+
return err
259+
}
260+
261+
// ApplyBinaryFragment applies the changes in the fragment f and writes the
262+
// result to dst. At most one binary fragment can be applied before a call to
263+
// Reset.
264+
func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error {
265+
if a.applyType != applyInitial {
266+
return applyError(errApplyInProgress)
267+
}
268+
a.applyType = applyText
269+
270+
if f == nil {
271+
return applyError(errors.New("nil fragment"))
272+
}
273+
213274
switch f.Method {
214275
case BinaryPatchLiteral:
215276
if _, err := dst.Write(f.Data); err != nil {
216277
return applyError(err)
217278
}
218279
case BinaryPatchDelta:
219-
if err := applyBinaryDeltaFragment(dst, src, f.Data); err != nil {
280+
if err := applyBinaryDeltaFragment(dst, a.src, f.Data); err != nil {
220281
return applyError(err)
221282
}
222283
default:
223284
return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method))
224285
}
225-
226286
return nil
227287
}
228288

229289
func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error {
230290
srcSize, delta := readBinaryDeltaSize(frag)
231-
if err := checkBinarySrcSize(srcSize, src); err != nil {
291+
if err := checkBinarySrcSize(src, srcSize); err != nil {
232292
return err
233293
}
234294

@@ -342,20 +402,15 @@ func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (
342402
return size, delta, err
343403
}
344404

345-
func checkBinarySrcSize(size int64, src io.ReaderAt) error {
346-
start := size
347-
if start > 0 {
348-
start--
349-
}
350-
var b [2]byte
351-
n, err := src.ReadAt(b[:], start)
352-
if err == io.EOF && (size == 0 && n == 0) || (size > 0 && n == 1) {
353-
return nil
354-
}
355-
if err != nil && err != io.EOF {
405+
func checkBinarySrcSize(r io.ReaderAt, size int64) error {
406+
ok, err := isLen(r, size)
407+
if err != nil {
356408
return err
357409
}
358-
return &Conflict{"fragment src size does not match actual src size"}
410+
if !ok {
411+
return &Conflict{"fragment src size does not match actual src size"}
412+
}
413+
return nil
359414
}
360415

361416
func wrapEOF(err error) error {

gitdiff/apply_test.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,13 @@ func TestTextFragmentApplyStrict(t *testing.T) {
5757
},
5858
Err: &Conflict{},
5959
},
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-
// },
60+
"errorNewFile": {
61+
Files: applyFiles{
62+
Src: "text_fragment_error.src",
63+
Patch: "text_fragment_error_new_file.patch",
64+
},
65+
Err: &Conflict{},
66+
},
6867
}
6968

7069
for name, test := range tests {
@@ -82,10 +81,10 @@ func TestTextFragmentApplyStrict(t *testing.T) {
8281
t.Fatalf("patch should contain exactly one fragment, but it has %d", len(files[0].TextFragments))
8382
}
8483

85-
frag := files[0].TextFragments[0]
84+
applier := NewApplier(bytes.NewReader(src))
8685

8786
var dst bytes.Buffer
88-
_, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0)
87+
err = applier.ApplyTextFragment(&dst, files[0].TextFragments[0])
8988
if test.Err != nil {
9089
checkApplyError(t, test.Err, err)
9190
return
@@ -153,13 +152,10 @@ func TestBinaryFragmentApply(t *testing.T) {
153152
t.Fatalf("patch should contain exactly one file, but it has %d", len(files))
154153
}
155154

156-
frag := files[0].BinaryFragment
157-
if frag == nil {
158-
t.Fatalf("patch should contain a binary fragment, but it was nil")
159-
}
155+
applier := NewApplier(bytes.NewReader(src))
160156

161157
var dst bytes.Buffer
162-
err = frag.Apply(&dst, bytes.NewReader(src))
158+
err = applier.ApplyBinaryFragment(&dst, files[0].BinaryFragment)
163159
if test.Err != nil {
164160
checkApplyError(t, test.Err, err)
165161
return

gitdiff/gitdiff.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gitdiff
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
)
@@ -64,6 +65,10 @@ func (f *TextFragment) Header() string {
6465
// Validate checks that the fragment is self-consistent and appliable. Validate
6566
// returns an error if and only if the fragment is invalid.
6667
func (f *TextFragment) Validate() error {
68+
if f == nil {
69+
return errors.New("nil fragment")
70+
}
71+
6772
var (
6873
oldLines, newLines int64
6974
leadingContext, trailingContext int64
@@ -117,7 +122,7 @@ func (f *TextFragment) Validate() error {
117122

118123
// if a file is being created, it can only contain additions
119124
if f.OldPosition == 0 && f.OldLines != 0 {
120-
return fmt.Errorf("file creation fragment contains context or deletion lines")
125+
return errors.New("file creation fragment contains context or deletion lines")
121126
}
122127

123128
return nil

0 commit comments

Comments
 (0)