|
1 | 1 | package gitdiff |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "errors" |
| 4 | + "fmt" |
5 | 5 | "io" |
6 | 6 | ) |
7 | 7 |
|
| 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 | + |
8 | 71 | // ApplyStrict writes data from src to dst, modifying it as described by the |
9 | 72 | // fragments in the file. For text files, each fragment, including all context |
10 | 73 | // lines, must exactly match src at the expected line number. |
11 | 74 | // |
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. |
13 | 77 | func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error { |
14 | 78 | if f.IsBinary { |
15 | 79 | if f.BinaryFragment != nil { |
16 | 80 | return f.BinaryFragment.Apply(dst, src) |
17 | 81 | } |
18 | 82 | _, err := io.Copy(dst, src) |
19 | | - return err |
| 83 | + return applyError(err) |
20 | 84 | } |
21 | 85 |
|
22 | 86 | lr, ok := src.(LineReader) |
23 | 87 | if !ok { |
24 | 88 | lr = NewLineReader(src, 0) |
25 | 89 | } |
26 | 90 |
|
27 | | - for _, frag := range f.TextFragments { |
| 91 | + for i, frag := range f.TextFragments { |
28 | 92 | if err := frag.ApplyStrict(dst, lr); err != nil { |
29 | | - return err |
| 93 | + return applyError(err, fragNum(i)) |
30 | 94 | } |
31 | 95 | } |
32 | 96 |
|
33 | 97 | _, err := io.Copy(dst, unwrapLineReader(lr)) |
34 | | - return err |
| 98 | + return applyError(err) |
35 | 99 | } |
36 | 100 |
|
37 | 101 | // ApplyStrict writes data from src to dst, modifying it as described by the |
38 | 102 | // fragment. The fragment, including all context lines, must exactly match src |
39 | 103 | // at the expected line number. |
40 | 104 | // |
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. |
43 | 109 | func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error { |
44 | 110 | // application code assumes fragment fields are consistent |
45 | 111 | if err := f.Validate(); err != nil { |
46 | | - // TODO(bkeyes): wrap with additional context |
47 | | - return err |
| 112 | + return applyError(err) |
48 | 113 | } |
49 | 114 |
|
50 | 115 | // line numbers are zero-indexed, positions are one-indexed |
51 | 116 | limit := f.OldPosition - 1 |
52 | 117 |
|
53 | 118 | // an EOF is allowed here: the fragment applies to the last line of the |
54 | 119 | // source but it does not have a newline character |
55 | | - nextLine, err := copyLines(dst, src, limit) |
| 120 | + nextLine, n, err := copyLines(dst, src, limit) |
56 | 121 | if err != nil && err != io.EOF { |
57 | | - // TODO(bkeyes): wrap with additional context |
58 | | - return err |
| 122 | + return applyError(err, lineNum(n)) |
59 | 123 | } |
60 | 124 |
|
61 | 125 | for i, line := range f.Lines { |
62 | 126 | fromSrc, err := applyTextLine(dst, nextLine, line) |
63 | 127 | if err != nil { |
64 | | - // TODO(bkeyes): wrap with additional context |
65 | | - return err |
| 128 | + return applyError(err, lineNum(n), fragLineNum(i)) |
66 | 129 | } |
67 | 130 |
|
68 | 131 | if fromSrc && i < len(f.Lines)-1 { |
69 | | - nextLine, _, err = src.ReadLine() |
| 132 | + nextLine, n, err = src.ReadLine() |
70 | 133 | if err != nil { |
71 | 134 | if err == io.EOF { |
72 | 135 | err = io.ErrUnexpectedEOF |
73 | 136 | } |
74 | | - // TODO(bkeyes): wrap with additional context |
75 | | - return err |
| 137 | + return applyError(err, lineNum(n), fragLineNum(i+1)) |
76 | 138 | } |
77 | 139 | } |
78 | 140 | } |
79 | 141 |
|
80 | 142 | return nil |
81 | 143 | } |
82 | 144 |
|
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) { |
84 | 146 | switch line.Op { |
85 | 147 | case OpContext, OpDelete: |
86 | 148 | 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") |
91 | 151 | } |
92 | 152 | } |
93 | | - |
94 | 153 | switch line.Op { |
95 | 154 | case OpContext, OpAdd: |
96 | | - // TODO(bkeyes): wrap with additional context |
97 | 155 | _, err = io.WriteString(dst, line.Line) |
98 | 156 | } |
99 | 157 | return |
100 | 158 | } |
101 | 159 |
|
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 |
106 | 166 | for { |
107 | 167 | line, n, err := src.ReadLine() |
108 | 168 | switch { |
109 | 169 | case limit < 0 && err == io.EOF && line == "": |
110 | | - return "", nil |
| 170 | + return "", int(limit), nil |
111 | 171 | case int64(n) == limit: |
112 | | - return line, err |
| 172 | + return line, n, err |
113 | 173 | case int64(n) > limit: |
114 | 174 | if limit < 0 { |
115 | | - return "", errors.New("src is not empty") |
| 175 | + return "", n, conflictError("cannot create new file from non-empty src") |
116 | 176 | } |
117 | | - return "", errors.New("overlapping fragments") |
| 177 | + return "", n, conflictError("fragment overlaps with an applied fragment") |
118 | 178 | case err != nil: |
119 | 179 | if err == io.EOF { |
120 | 180 | err = io.ErrUnexpectedEOF |
121 | 181 | } |
122 | | - return line, err |
| 182 | + return line, n, err |
123 | 183 | } |
124 | 184 |
|
125 | 185 | if _, err := io.WriteString(dst, line); err != nil { |
126 | | - return "", err |
| 186 | + return "", n, err |
127 | 187 | } |
128 | 188 | } |
129 | 189 | } |
|
0 commit comments