this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

+95 -35
+95 -35
gitdiff/apply.go
··· 1 1 package gitdiff 2 2 3 3 import ( 4 - "errors" 4 + "fmt" 5 5 "io" 6 6 ) 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 + } 7 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) ··· 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 ··· 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 } ··· 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 }