Skip to content

Commit ae70423

Browse files
committed
Return ApplyError when ApplyString fails
This wraps the underlying error with optional position information and provides a way to test if the error was due to a conflict. At the moment, details about the conflict are not exposed outside of the message string.
1 parent 93d46e8 commit ae70423

File tree

1 file changed

+95
-35
lines changed

1 file changed

+95
-35
lines changed

gitdiff/apply.go

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,189 @@
11
package gitdiff
22

33
import (
4-
"errors"
4+
"fmt"
55
"io"
66
)
77

8+
type conflictError string
9+
10+
func (e conflictError) Error() string {
11+
return "conflict: " + string(e)
12+
}
13+
14+
// ApplyError wraps an error that occurs during patch application with
15+
// additional location information, if it is available.
16+
type ApplyError struct {
17+
// Line is the one-indexed line number in the source data
18+
Line int
19+
// Fragment is the one-indexed fragment number in the file
20+
Fragment int
21+
// FragmentLine is the one-indexed line number in the fragment
22+
FragmentLine int
23+
24+
err error
25+
}
26+
27+
// Unwrap returns the wrapped error.
28+
func (e *ApplyError) Unwrap() error {
29+
return e.err
30+
}
31+
32+
// Conflict returns true if the error is due to a conflict between the fragment
33+
// and the source data.
34+
func (e *ApplyError) Conflict() bool {
35+
_, ok := e.err.(conflictError)
36+
return ok
37+
}
38+
39+
func (e *ApplyError) Error() string {
40+
return fmt.Sprintf("%v", e.err)
41+
}
42+
43+
type lineNum int
44+
type fragNum int
45+
type fragLineNum int
46+
47+
// applyError creates a new *ApplyError wrapping err or augments the information
48+
// in err with args if it is already an *ApplyError. Returns nil if err is nil.
49+
func applyError(err error, args ...interface{}) error {
50+
if err == nil {
51+
return nil
52+
}
53+
54+
e, ok := err.(*ApplyError)
55+
if !ok {
56+
e = &ApplyError{err: err}
57+
}
58+
for _, arg := range args {
59+
switch v := arg.(type) {
60+
case lineNum:
61+
e.Line = int(v) + 1
62+
case fragNum:
63+
e.Fragment = int(v) + 1
64+
case fragLineNum:
65+
e.FragmentLine = int(v) + 1
66+
}
67+
}
68+
return e
69+
}
70+
871
// ApplyStrict writes data from src to dst, modifying it as described by the
972
// fragments in the file. For text files, each fragment, including all context
1073
// lines, must exactly match src at the expected line number.
1174
//
12-
// If the file contains no fragments, ApplyStrict is equivalent to io.Copy.
75+
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
76+
// Partial data may be written to dst in this case.
1377
func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error {
1478
if f.IsBinary {
1579
if f.BinaryFragment != nil {
1680
return f.BinaryFragment.Apply(dst, src)
1781
}
1882
_, err := io.Copy(dst, src)
19-
return err
83+
return applyError(err)
2084
}
2185

2286
lr, ok := src.(LineReader)
2387
if !ok {
2488
lr = NewLineReader(src, 0)
2589
}
2690

27-
for _, frag := range f.TextFragments {
91+
for i, frag := range f.TextFragments {
2892
if err := frag.ApplyStrict(dst, lr); err != nil {
29-
return err
93+
return applyError(err, fragNum(i))
3094
}
3195
}
3296

3397
_, err := io.Copy(dst, unwrapLineReader(lr))
34-
return err
98+
return applyError(err)
3599
}
36100

37101
// ApplyStrict writes data from src to dst, modifying it as described by the
38102
// fragment. The fragment, including all context lines, must exactly match src
39103
// at the expected line number.
40104
//
41-
// If there is no error, the next read from src returns the line immediately
42-
// after the last line of the fragment.
105+
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
106+
// Partial data may be written to dst in this case. If there is no error, the
107+
// next read from src returns the line immediately after the last line of the
108+
// fragment.
43109
func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
44110
// application code assumes fragment fields are consistent
45111
if err := f.Validate(); err != nil {
46-
// TODO(bkeyes): wrap with additional context
47-
return err
112+
return applyError(err)
48113
}
49114

50115
// line numbers are zero-indexed, positions are one-indexed
51116
limit := f.OldPosition - 1
52117

53118
// an EOF is allowed here: the fragment applies to the last line of the
54119
// source but it does not have a newline character
55-
nextLine, err := copyLines(dst, src, limit)
120+
nextLine, n, err := copyLines(dst, src, limit)
56121
if err != nil && err != io.EOF {
57-
// TODO(bkeyes): wrap with additional context
58-
return err
122+
return applyError(err, lineNum(n))
59123
}
60124

61125
for i, line := range f.Lines {
62126
fromSrc, err := applyTextLine(dst, nextLine, line)
63127
if err != nil {
64-
// TODO(bkeyes): wrap with additional context
65-
return err
128+
return applyError(err, lineNum(n), fragLineNum(i))
66129
}
67130

68131
if fromSrc && i < len(f.Lines)-1 {
69-
nextLine, _, err = src.ReadLine()
132+
nextLine, n, err = src.ReadLine()
70133
if err != nil {
71134
if err == io.EOF {
72135
err = io.ErrUnexpectedEOF
73136
}
74-
// TODO(bkeyes): wrap with additional context
75-
return err
137+
return applyError(err, lineNum(n), fragLineNum(i+1))
76138
}
77139
}
78140
}
79141

80142
return nil
81143
}
82144

83-
func applyTextLine(dst io.Writer, srcLine string, line Line) (fromSrc bool, err error) {
145+
func applyTextLine(dst io.Writer, src string, line Line) (fromSrc bool, err error) {
84146
switch line.Op {
85147
case OpContext, OpDelete:
86148
fromSrc = true
87-
if srcLine != line.Line {
88-
// TODO(bkeyes): use special error type here
89-
// TODO(bkeyes): include line number information, etc.
90-
return fromSrc, errors.New("apply: fragment match failed: line does not match")
149+
if src != line.Line {
150+
return fromSrc, conflictError("fragment line does not match src line")
91151
}
92152
}
93-
94153
switch line.Op {
95154
case OpContext, OpAdd:
96-
// TODO(bkeyes): wrap with additional context
97155
_, err = io.WriteString(dst, line.Line)
98156
}
99157
return
100158
}
101159

102-
// copyLines copies from src to dst until the line at limit, exclusive. The
103-
// line at limit is returned. A negative limit means the first read should
104-
// return io.EOF and no data.
105-
func copyLines(dst io.Writer, src LineReader, limit int64) (string, error) {
160+
// copyLines copies from src to dst until the line at limit, exclusive. Returns
161+
// the line at limit and the line number. The line number may not equal the
162+
// limit if and only if a non-EOF error occurs. A negative limit means the
163+
// first read should return io.EOF and no data.
164+
func copyLines(dst io.Writer, src LineReader, limit int64) (string, int, error) {
165+
// TODO(bkeyes): fix int vs int64 for limit and return value
106166
for {
107167
line, n, err := src.ReadLine()
108168
switch {
109169
case limit < 0 && err == io.EOF && line == "":
110-
return "", nil
170+
return "", int(limit), nil
111171
case int64(n) == limit:
112-
return line, err
172+
return line, n, err
113173
case int64(n) > limit:
114174
if limit < 0 {
115-
return "", errors.New("src is not empty")
175+
return "", n, conflictError("cannot create new file from non-empty src")
116176
}
117-
return "", errors.New("overlapping fragments")
177+
return "", n, conflictError("fragment overlaps with an applied fragment")
118178
case err != nil:
119179
if err == io.EOF {
120180
err = io.ErrUnexpectedEOF
121181
}
122-
return line, err
182+
return line, n, err
123183
}
124184

125185
if _, err := io.WriteString(dst, line); err != nil {
126-
return "", err
186+
return "", n, err
127187
}
128188
}
129189
}

0 commit comments

Comments
 (0)