fork of go-gitdiff with jj support

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.

Changed files
+230 -100
gitdiff
+134 -79
gitdiff/apply.go
··· 1 1 package gitdiff 2 2 3 3 import ( 4 - "bytes" 5 4 "errors" 6 5 "fmt" 7 6 "io" 8 - "io/ioutil" 9 7 ) 10 8 11 9 // Conflict indicates an apply failed due to a conflict between the patch and ··· 85 83 return e 86 84 } 87 85 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. 91 99 // 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) 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 + } 118 + 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} 136 + } 99 137 } 138 + a.nextLine = 0 139 + a.applyType = applyInitial 140 + } 100 141 101 - if f.IsBinary { 102 - if f.BinaryFragment != nil { 103 - return f.BinaryFragment.Apply(dst, bytes.NewReader(data)) 104 - } 105 - _, err = dst.Write(data) 106 - return applyError(err) 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) 107 147 } 108 148 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 + } 111 152 112 - lra := NewLineReaderAt(bytes.NewReader(data)) 153 + // TODO(bkeyes): sort fragments by start position 154 + // TODO(bkeyes): merge overlapping fragments 113 155 114 - var next int64 115 156 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 { 118 158 return applyError(err, fragNum(i)) 119 159 } 120 160 } 121 161 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 - } 162 + return applyError(a.Flush(dst)) 163 + } 129 164 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 - } 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) 140 175 } 141 - } 176 + a.applyType = applyText 142 177 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) { 149 178 // application code assumes fragment fields are consistent 150 179 if err := f.Validate(); err != nil { 151 - return start, applyError(err) 180 + return applyError(err) 152 181 } 153 182 154 183 // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) ··· 158 187 } 159 188 fragEnd := fragStart + f.OldLines 160 189 190 + start := a.nextLine 161 191 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 + } 163 203 } 164 204 165 205 preimage := make([][]byte, fragEnd-start) 166 - n, err := src.ReadLinesAt(preimage, start) 206 + n, err := a.lineSrc.ReadLinesAt(preimage, start) 167 207 switch { 168 208 case err == nil: 169 209 case err == io.EOF && n == len(preimage): // last line of frag has no newline character 170 210 default: 171 - return start, applyError(err, lineNum(start+int64(n))) 211 + return applyError(err, lineNum(start+int64(n))) 172 212 } 173 213 174 214 // copy leading data before the fragment starts 175 215 for i, line := range preimage[:fragStart-start] { 176 216 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)) 179 219 } 180 220 } 181 221 preimage = preimage[fragStart-start:] ··· 184 224 used := int64(0) 185 225 for i, line := range f.Lines { 186 226 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)) 189 229 } 190 230 if line.Old() { 191 231 used++ 192 232 } 193 233 } 194 - return fragStart + used, nil 234 + a.nextLine = fragStart + used 235 + return nil 195 236 } 196 237 197 238 func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) { ··· 201 242 if line.New() { 202 243 _, err = io.WriteString(dst, line.Line) 203 244 } 204 - return 245 + return err 205 246 } 206 247 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 + 213 274 switch f.Method { 214 275 case BinaryPatchLiteral: 215 276 if _, err := dst.Write(f.Data); err != nil { 216 277 return applyError(err) 217 278 } 218 279 case BinaryPatchDelta: 219 - if err := applyBinaryDeltaFragment(dst, src, f.Data); err != nil { 280 + if err := applyBinaryDeltaFragment(dst, a.src, f.Data); err != nil { 220 281 return applyError(err) 221 282 } 222 283 default: 223 284 return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) 224 285 } 225 - 226 286 return nil 227 287 } 228 288 229 289 func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error { 230 290 srcSize, delta := readBinaryDeltaSize(frag) 231 - if err := checkBinarySrcSize(srcSize, src); err != nil { 291 + if err := checkBinarySrcSize(src, srcSize); err != nil { 232 292 return err 233 293 } 234 294 ··· 342 402 return size, delta, err 343 403 } 344 404 345 - func checkBinarySrcSize(size int64, src io.ReaderAt) error { 346 - start := size 347 - if start > 0 { 348 - start-- 405 + func checkBinarySrcSize(r io.ReaderAt, size int64) error { 406 + ok, err := isLen(r, size) 407 + if err != nil { 408 + return err 349 409 } 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 410 + if !ok { 411 + return &Conflict{"fragment src size does not match actual src size"} 354 412 } 355 - if err != nil && err != io.EOF { 356 - return err 357 - } 358 - return &Conflict{"fragment src size does not match actual src size"} 413 + return nil 359 414 } 360 415 361 416 func wrapEOF(err error) error {
+11 -15
gitdiff/apply_test.go
··· 57 57 }, 58 58 Err: &Conflict{}, 59 59 }, 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 + }, 68 67 } 69 68 70 69 for name, test := range tests { ··· 82 81 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(files[0].TextFragments)) 83 82 } 84 83 85 - frag := files[0].TextFragments[0] 84 + applier := NewApplier(bytes.NewReader(src)) 86 85 87 86 var dst bytes.Buffer 88 - _, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0) 87 + err = applier.ApplyTextFragment(&dst, files[0].TextFragments[0]) 89 88 if test.Err != nil { 90 89 checkApplyError(t, test.Err, err) 91 90 return ··· 153 152 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 154 153 } 155 154 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)) 160 156 161 157 var dst bytes.Buffer 162 - err = frag.Apply(&dst, bytes.NewReader(src)) 158 + err = applier.ApplyBinaryFragment(&dst, files[0].BinaryFragment) 163 159 if test.Err != nil { 164 160 checkApplyError(t, test.Err, err) 165 161 return
+6 -1
gitdiff/gitdiff.go
··· 1 1 package gitdiff 2 2 3 3 import ( 4 + "errors" 4 5 "fmt" 5 6 "os" 6 7 ) ··· 64 65 // Validate checks that the fragment is self-consistent and appliable. Validate 65 66 // returns an error if and only if the fragment is invalid. 66 67 func (f *TextFragment) Validate() error { 68 + if f == nil { 69 + return errors.New("nil fragment") 70 + } 71 + 67 72 var ( 68 73 oldLines, newLines int64 69 74 leadingContext, trailingContext int64 ··· 117 122 118 123 // if a file is being created, it can only contain additions 119 124 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") 121 126 } 122 127 123 128 return nil
+79 -5
gitdiff/io.go
··· 24 24 ReadLinesAt(lines [][]byte, offset int64) (n int, err error) 25 25 } 26 26 27 - // NewLineReaderAt creates a LineReaderAt from an io.ReaderAt. 28 - func NewLineReaderAt(r io.ReaderAt) LineReaderAt { 29 - return &lineReaderAt{r: r} 30 - } 31 - 32 27 type lineReaderAt struct { 33 28 r io.ReaderAt 34 29 index []int64 ··· 129 124 } 130 125 return 131 126 } 127 + 128 + func isLen(r io.ReaderAt, n int64) (bool, error) { 129 + off := n - 1 130 + if off < 0 { 131 + off = 0 132 + } 133 + 134 + var b [2]byte 135 + nr, err := r.ReadAt(b[:], off) 136 + if err == io.EOF { 137 + return (n == 0 && nr == 0) || (n > 0 && nr == 1), nil 138 + } 139 + return false, err 140 + } 141 + 142 + // copyFrom writes bytes starting from offset off in src to dst stopping at the 143 + // end of src or at the first error. copyFrom returns the number of bytes 144 + // written and any error. 145 + func copyFrom(dst io.Writer, src io.ReaderAt, off int64) (written int64, err error) { 146 + buf := make([]byte, 32*1024) // stolen from io.Copy 147 + for { 148 + nr, rerr := src.ReadAt(buf, off) 149 + if nr > 0 { 150 + nw, werr := dst.Write(buf[0:nr]) 151 + if nw > 0 { 152 + written += int64(nw) 153 + } 154 + if werr != nil { 155 + err = werr 156 + break 157 + } 158 + if nr != nw { 159 + err = io.ErrShortWrite 160 + break 161 + } 162 + } 163 + if rerr != nil { 164 + if rerr != io.EOF { 165 + err = rerr 166 + } 167 + break 168 + } 169 + } 170 + return written, err 171 + } 172 + 173 + // copyLinesFrom writes lines starting from line off in src to dst stopping at 174 + // the end of src or at the first error. copyLinesFrom returns the number of 175 + // lines written and any error. 176 + func copyLinesFrom(dst io.Writer, src LineReaderAt, off int64) (written int64, err error) { 177 + buf := make([][]byte, 32) 178 + ReadLoop: 179 + for { 180 + nr, rerr := src.ReadLinesAt(buf, off) 181 + if nr > 0 { 182 + for _, line := range buf[0:nr] { 183 + nw, werr := dst.Write(line) 184 + if nw > 0 { 185 + written++ 186 + } 187 + if werr != nil { 188 + err = werr 189 + break ReadLoop 190 + } 191 + if len(line) != nw { 192 + err = io.ErrShortWrite 193 + break ReadLoop 194 + } 195 + } 196 + } 197 + if rerr != nil { 198 + if rerr != io.EOF { 199 + err = rerr 200 + } 201 + break 202 + } 203 + } 204 + return written, err 205 + }