fork of go-gitdiff with jj support

Split apply logic by fragment type (#32)

Remove the Applier type and replace it with TextApplier and
BinaryApplier, both of which operate on fragments instead of on full
files. Move the logic that previously existed in Applier.ApplyFile to
the top-level Apply function.

Also restructure arguments and methods to make it clear that appliers
are one-time-use objects. The destination is now set when creating an
applier and the Reset() method was replaced by Close().

authored by Billy Keyes and committed by GitHub 75930390 8764d818

+1
.golangci.yml
··· 11 11 - golint 12 12 - govet 13 13 - ineffassign 14 + - misspell 14 15 - typecheck 15 16 - unconvert 16 17 - varcheck
+1 -1
README.md
··· 29 29 30 30 // apply the changes in the patch to a source file 31 31 var output bytes.Buffer 32 - if err := gitdiff.NewApplier(code).ApplyFile(&output, files[0]); err != nil { 32 + if err := gitdiff.Apply(&output, code, files[0]); err != nil { 33 33 log.Fatal(err) 34 34 } 35 35 ```
+27 -330
gitdiff/apply.go
··· 89 89 90 90 var ( 91 91 errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") 92 - ) 93 - 94 - const ( 95 - applyInitial = iota 96 - applyText 97 - applyBinary 98 - applyFile 92 + errApplierClosed = errors.New("gitdiff: applier is closed") 99 93 ) 100 94 101 - // Apply is a convenience function that creates an Applier for src with default 102 - // settings and applies the changes in f, writing the result to dst. 103 - func Apply(dst io.Writer, src io.ReaderAt, f *File) error { 104 - return NewApplier(src).ApplyFile(dst, f) 105 - } 106 - 107 - // Applier applies changes described in fragments to source data. If changes 108 - // are described in multiple fragments, those fragments must be applied in 109 - // order, usually by calling ApplyFile. 95 + // Apply applies the changes in f to src, writing the result to dst. It can 96 + // apply both text and binary changes. 110 97 // 111 - // By default, Applier operates in "strict" mode, where fragment content and 112 - // positions must exactly match those of the source. 113 - // 114 - // If an error occurs while applying, methods on Applier return instances of 115 - // *ApplyError that annotate the wrapped error with additional information 116 - // when available. If the error is because of a conflict between a fragment and 117 - // the source, the wrapped error will be a *Conflict. 118 - // 119 - // While an Applier can apply both text and binary fragments, only one fragment 120 - // type can be used without resetting the Applier. The first fragment applied 121 - // sets the type for the Applier. Mixing fragment types or mixing 122 - // fragment-level and file-level applies results in an error. 123 - type Applier struct { 124 - src io.ReaderAt 125 - lineSrc LineReaderAt 126 - nextLine int64 127 - applyType int 128 - } 129 - 130 - // NewApplier creates an Applier that reads data from src. If src is a 131 - // LineReaderAt, it is used directly to apply text fragments. 132 - func NewApplier(src io.ReaderAt) *Applier { 133 - a := new(Applier) 134 - a.Reset(src) 135 - return a 136 - } 137 - 138 - // Reset resets the input and internal state of the Applier. If src is nil, the 139 - // existing source is reused. 140 - func (a *Applier) Reset(src io.ReaderAt) { 141 - if src != nil { 142 - a.src = src 143 - if lineSrc, ok := src.(LineReaderAt); ok { 144 - a.lineSrc = lineSrc 145 - } else { 146 - a.lineSrc = &lineReaderAt{r: src} 98 + // If an error occurs while applying, Apply returns an *ApplyError that 99 + // annotates the error with additional information. If the error is because of 100 + // a conflict with the source, the wrapped error will be a *Conflict. 101 + func Apply(dst io.Writer, src io.ReaderAt, f *File) error { 102 + if f.IsBinary { 103 + if len(f.TextFragments) > 0 { 104 + return applyError(errors.New("binary file contains text fragments")) 105 + } 106 + if f.BinaryFragment == nil { 107 + return applyError(errors.New("binary file does not contain a binary fragment")) 108 + } 109 + } else { 110 + if f.BinaryFragment != nil { 111 + return applyError(errors.New("text file contains a binary fragment")) 147 112 } 148 113 } 149 - a.nextLine = 0 150 - a.applyType = applyInitial 151 - } 152 - 153 - // ApplyFile applies the changes in all of the fragments of f and writes the 154 - // result to dst. 155 - func (a *Applier) ApplyFile(dst io.Writer, f *File) error { 156 - if a.applyType != applyInitial { 157 - return applyError(errApplyInProgress) 158 - } 159 - defer func() { a.applyType = applyFile }() 160 - 161 - if f.IsBinary && len(f.TextFragments) > 0 { 162 - return applyError(errors.New("binary file contains text fragments")) 163 - } 164 - if !f.IsBinary && f.BinaryFragment != nil { 165 - return applyError(errors.New("text file contains binary fragment")) 166 - } 167 114 168 115 switch { 169 116 case f.BinaryFragment != nil: 170 - return a.ApplyBinaryFragment(dst, f.BinaryFragment) 117 + applier := NewBinaryApplier(dst, src) 118 + if err := applier.ApplyFragment(f.BinaryFragment); err != nil { 119 + return err 120 + } 121 + return applier.Close() 171 122 172 123 case len(f.TextFragments) > 0: 173 124 frags := make([]*TextFragment, len(f.TextFragments)) ··· 181 132 // right now, the application fails if fragments overlap, but it should be 182 133 // possible to precompute the result of applying them in order 183 134 135 + applier := NewTextApplier(dst, src) 184 136 for i, frag := range frags { 185 - if err := a.ApplyTextFragment(dst, frag); err != nil { 137 + if err := applier.ApplyFragment(frag); err != nil { 186 138 return applyError(err, fragNum(i)) 187 139 } 188 140 } 189 - } 190 - 191 - return applyError(a.Flush(dst)) 192 - } 193 - 194 - // ApplyTextFragment applies the changes in the fragment f and writes unwritten 195 - // data before the start of the fragment and the result to dst. If multiple 196 - // text fragments apply to the same source, ApplyTextFragment must be called in 197 - // order of increasing start position. As a result, each fragment can be 198 - // applied at most once before a call to Reset. 199 - func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error { 200 - if a.applyType != applyInitial && a.applyType != applyText { 201 - return applyError(errApplyInProgress) 202 - } 203 - defer func() { a.applyType = applyText }() 204 - 205 - // application code assumes fragment fields are consistent 206 - if err := f.Validate(); err != nil { 207 - return applyError(err) 208 - } 209 - 210 - // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) 211 - fragStart := f.OldPosition - 1 212 - if fragStart < 0 { 213 - fragStart = 0 214 - } 215 - fragEnd := fragStart + f.OldLines 216 - 217 - start := a.nextLine 218 - if fragStart < start { 219 - return applyError(&Conflict{"fragment overlaps with an applied fragment"}) 220 - } 221 - 222 - if f.OldPosition == 0 { 223 - ok, err := isLen(a.src, 0) 224 - if err != nil { 225 - return applyError(err) 226 - } 227 - if !ok { 228 - return applyError(&Conflict{"cannot create new file from non-empty src"}) 229 - } 230 - } 231 - 232 - preimage := make([][]byte, fragEnd-start) 233 - n, err := a.lineSrc.ReadLinesAt(preimage, start) 234 - if err != nil { 235 - return applyError(err, lineNum(start+int64(n))) 236 - } 237 - 238 - // copy leading data before the fragment starts 239 - for i, line := range preimage[:fragStart-start] { 240 - if _, err := dst.Write(line); err != nil { 241 - a.nextLine = start + int64(i) 242 - return applyError(err, lineNum(a.nextLine)) 243 - } 244 - } 245 - preimage = preimage[fragStart-start:] 246 - 247 - // apply the changes in the fragment 248 - used := int64(0) 249 - for i, line := range f.Lines { 250 - if err := applyTextLine(dst, line, preimage, used); err != nil { 251 - a.nextLine = fragStart + used 252 - return applyError(err, lineNum(a.nextLine), fragLineNum(i)) 253 - } 254 - if line.Old() { 255 - used++ 256 - } 257 - } 258 - a.nextLine = fragStart + used 259 - 260 - // new position of +0,0 mean a full delete, so check for leftovers 261 - if f.NewPosition == 0 && f.NewLines == 0 { 262 - var b [1][]byte 263 - n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine) 264 - if err != nil && err != io.EOF { 265 - return applyError(err, lineNum(a.nextLine)) 266 - } 267 - if n > 0 { 268 - return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine)) 269 - } 270 - } 141 + return applier.Close() 271 142 272 - return nil 273 - } 274 - 275 - func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) { 276 - if line.Old() && string(preimage[i]) != line.Line { 277 - return &Conflict{"fragment line does not match src line"} 278 - } 279 - if line.New() { 280 - _, err = io.WriteString(dst, line.Line) 281 - } 282 - return err 283 - } 284 - 285 - // Flush writes any data following the last applied fragment to dst. 286 - func (a *Applier) Flush(dst io.Writer) (err error) { 287 - switch a.applyType { 288 - case applyInitial: 289 - _, err = copyFrom(dst, a.src, 0) 290 - case applyText: 291 - _, err = copyLinesFrom(dst, a.lineSrc, a.nextLine) 292 - case applyBinary: 293 - // nothing to flush, binary apply "consumes" full source 294 - } 295 - return err 296 - } 297 - 298 - // ApplyBinaryFragment applies the changes in the fragment f and writes the 299 - // result to dst. At most one binary fragment can be applied before a call to 300 - // Reset. 301 - func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error { 302 - if a.applyType != applyInitial { 303 - return applyError(errApplyInProgress) 304 - } 305 - defer func() { a.applyType = applyBinary }() 306 - 307 - if f == nil { 308 - return applyError(errors.New("nil fragment")) 309 - } 310 - 311 - switch f.Method { 312 - case BinaryPatchLiteral: 313 - if _, err := dst.Write(f.Data); err != nil { 314 - return applyError(err) 315 - } 316 - case BinaryPatchDelta: 317 - if err := applyBinaryDeltaFragment(dst, a.src, f.Data); err != nil { 318 - return applyError(err) 319 - } 320 143 default: 321 - return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) 322 - } 323 - return nil 324 - } 325 - 326 - func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error { 327 - srcSize, delta := readBinaryDeltaSize(frag) 328 - if err := checkBinarySrcSize(src, srcSize); err != nil { 329 - return err 330 - } 331 - 332 - dstSize, delta := readBinaryDeltaSize(delta) 333 - 334 - for len(delta) > 0 { 335 - op := delta[0] 336 - if op == 0 { 337 - return errors.New("invalid delta opcode 0") 338 - } 339 - 340 - var n int64 341 - var err error 342 - switch op & 0x80 { 343 - case 0x80: 344 - n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src) 345 - case 0x00: 346 - n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:]) 347 - } 348 - if err != nil { 349 - return err 350 - } 351 - dstSize -= n 352 - } 353 - 354 - if dstSize != 0 { 355 - return errors.New("corrupt binary delta: insufficient or extra data") 356 - } 357 - return nil 358 - } 359 - 360 - // readBinaryDeltaSize reads a variable length size from a delta-encoded binary 361 - // fragment, returing the size and the unused data. Data is encoded as: 362 - // 363 - // [[1xxxxxxx]...] [0xxxxxxx] 364 - // 365 - // in little-endian order, with 7 bits of the value per byte. 366 - func readBinaryDeltaSize(d []byte) (size int64, rest []byte) { 367 - shift := uint(0) 368 - for i, b := range d { 369 - size |= int64(b&0x7F) << shift 370 - shift += 7 371 - if b <= 0x7F { 372 - return size, d[i+1:] 373 - } 374 - } 375 - return size, nil 376 - } 377 - 378 - // applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary 379 - // fragment, returning the amount of data written and the usused part of the 380 - // fragment. An add operation takes the form: 381 - // 382 - // [0xxxxxx][[data1]...] 383 - // 384 - // where the lower seven bits of the opcode is the number of data bytes 385 - // following the opcode. See also pack-format.txt in the Git source. 386 - func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) { 387 - size := int(op) 388 - if len(delta) < size { 389 - return 0, delta, errors.New("corrupt binary delta: incomplete add") 390 - } 391 - _, err = w.Write(delta[:size]) 392 - return int64(size), delta[size:], err 393 - } 394 - 395 - // applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary 396 - // fragment, returing the amount of data written and the unused part of the 397 - // fragment. A copy operation takes the form: 398 - // 399 - // [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3] 400 - // 401 - // where the lower seven bits of the opcode determine which non-zero offset and 402 - // size bytes are present in little-endian order: if bit 0 is set, offset1 is 403 - // present, etc. If no offset or size bytes are present, offset is 0 and size 404 - // is 0x10000. See also pack-format.txt in the Git source. 405 - func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (n int64, rest []byte, err error) { 406 - const defaultSize = 0x10000 407 - 408 - unpack := func(start, bits uint) (v int64) { 409 - for i := uint(0); i < bits; i++ { 410 - mask := byte(1 << (i + start)) 411 - if op&mask > 0 { 412 - if len(delta) == 0 { 413 - err = errors.New("corrupt binary delta: incomplete copy") 414 - return 415 - } 416 - v |= int64(delta[0]) << (8 * i) 417 - delta = delta[1:] 418 - } 419 - } 420 - return 421 - } 422 - 423 - offset := unpack(0, 4) 424 - size := unpack(4, 3) 425 - if err != nil { 426 - return 0, delta, err 427 - } 428 - if size == 0 { 429 - size = defaultSize 430 - } 431 - 432 - // TODO(bkeyes): consider pooling these buffers 433 - b := make([]byte, size) 434 - if _, err := src.ReadAt(b, offset); err != nil { 435 - return 0, delta, err 436 - } 437 - 438 - _, err = w.Write(b) 439 - return size, delta, err 440 - } 441 - 442 - func checkBinarySrcSize(r io.ReaderAt, size int64) error { 443 - ok, err := isLen(r, size) 444 - if err != nil { 144 + // nothing to apply, just copy all the data 145 + _, err := copyFrom(dst, src, 0) 445 146 return err 446 147 } 447 - if !ok { 448 - return &Conflict{"fragment src size does not match actual src size"} 449 - } 450 - return nil 451 148 }
+206
gitdiff/apply_binary.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "io" 7 + ) 8 + 9 + // BinaryApplier applies binary changes described in a fragment to source data. 10 + // The applier must be closed after use. 11 + type BinaryApplier struct { 12 + dst io.Writer 13 + src io.ReaderAt 14 + 15 + closed bool 16 + dirty bool 17 + } 18 + 19 + // NewBinaryApplier creates an BinaryApplier that reads data from src and 20 + // writes modified data to dst. 21 + func NewBinaryApplier(dst io.Writer, src io.ReaderAt) *BinaryApplier { 22 + a := BinaryApplier{ 23 + dst: dst, 24 + src: src, 25 + } 26 + return &a 27 + } 28 + 29 + // ApplyFragment applies the changes in the fragment f and writes the result to 30 + // dst. ApplyFragment can be called at most once. 31 + // 32 + // If an error occurs while applying, ApplyFragment returns an *ApplyError that 33 + // annotates the error with additional information. If the error is because of 34 + // a conflict between a fragment and the source, the wrapped error will be a 35 + // *Conflict. 36 + func (a *BinaryApplier) ApplyFragment(f *BinaryFragment) error { 37 + if f == nil { 38 + return applyError(errors.New("nil fragment")) 39 + } 40 + if a.closed { 41 + return applyError(errApplierClosed) 42 + } 43 + if a.dirty { 44 + return applyError(errApplyInProgress) 45 + } 46 + 47 + // mark an apply as in progress, even if it fails before making changes 48 + a.dirty = true 49 + 50 + switch f.Method { 51 + case BinaryPatchLiteral: 52 + if _, err := a.dst.Write(f.Data); err != nil { 53 + return applyError(err) 54 + } 55 + case BinaryPatchDelta: 56 + if err := applyBinaryDeltaFragment(a.dst, a.src, f.Data); err != nil { 57 + return applyError(err) 58 + } 59 + default: 60 + return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) 61 + } 62 + return nil 63 + } 64 + 65 + // Close writes any data following the last applied fragment and prevents 66 + // future calls to ApplyFragment. 67 + func (a *BinaryApplier) Close() (err error) { 68 + if a.closed { 69 + return nil 70 + } 71 + 72 + a.closed = true 73 + if !a.dirty { 74 + _, err = copyFrom(a.dst, a.src, 0) 75 + } else { 76 + // do nothing, applying a binary fragment copies all data 77 + } 78 + return err 79 + } 80 + 81 + func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error { 82 + srcSize, delta := readBinaryDeltaSize(frag) 83 + if err := checkBinarySrcSize(src, srcSize); err != nil { 84 + return err 85 + } 86 + 87 + dstSize, delta := readBinaryDeltaSize(delta) 88 + 89 + for len(delta) > 0 { 90 + op := delta[0] 91 + if op == 0 { 92 + return errors.New("invalid delta opcode 0") 93 + } 94 + 95 + var n int64 96 + var err error 97 + switch op & 0x80 { 98 + case 0x80: 99 + n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src) 100 + case 0x00: 101 + n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:]) 102 + } 103 + if err != nil { 104 + return err 105 + } 106 + dstSize -= n 107 + } 108 + 109 + if dstSize != 0 { 110 + return errors.New("corrupt binary delta: insufficient or extra data") 111 + } 112 + return nil 113 + } 114 + 115 + // readBinaryDeltaSize reads a variable length size from a delta-encoded binary 116 + // fragment, returing the size and the unused data. Data is encoded as: 117 + // 118 + // [[1xxxxxxx]...] [0xxxxxxx] 119 + // 120 + // in little-endian order, with 7 bits of the value per byte. 121 + func readBinaryDeltaSize(d []byte) (size int64, rest []byte) { 122 + shift := uint(0) 123 + for i, b := range d { 124 + size |= int64(b&0x7F) << shift 125 + shift += 7 126 + if b <= 0x7F { 127 + return size, d[i+1:] 128 + } 129 + } 130 + return size, nil 131 + } 132 + 133 + // applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary 134 + // fragment, returning the amount of data written and the usused part of the 135 + // fragment. An add operation takes the form: 136 + // 137 + // [0xxxxxx][[data1]...] 138 + // 139 + // where the lower seven bits of the opcode is the number of data bytes 140 + // following the opcode. See also pack-format.txt in the Git source. 141 + func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) { 142 + size := int(op) 143 + if len(delta) < size { 144 + return 0, delta, errors.New("corrupt binary delta: incomplete add") 145 + } 146 + _, err = w.Write(delta[:size]) 147 + return int64(size), delta[size:], err 148 + } 149 + 150 + // applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary 151 + // fragment, returing the amount of data written and the unused part of the 152 + // fragment. A copy operation takes the form: 153 + // 154 + // [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3] 155 + // 156 + // where the lower seven bits of the opcode determine which non-zero offset and 157 + // size bytes are present in little-endian order: if bit 0 is set, offset1 is 158 + // present, etc. If no offset or size bytes are present, offset is 0 and size 159 + // is 0x10000. See also pack-format.txt in the Git source. 160 + func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (n int64, rest []byte, err error) { 161 + const defaultSize = 0x10000 162 + 163 + unpack := func(start, bits uint) (v int64) { 164 + for i := uint(0); i < bits; i++ { 165 + mask := byte(1 << (i + start)) 166 + if op&mask > 0 { 167 + if len(delta) == 0 { 168 + err = errors.New("corrupt binary delta: incomplete copy") 169 + return 170 + } 171 + v |= int64(delta[0]) << (8 * i) 172 + delta = delta[1:] 173 + } 174 + } 175 + return 176 + } 177 + 178 + offset := unpack(0, 4) 179 + size := unpack(4, 3) 180 + if err != nil { 181 + return 0, delta, err 182 + } 183 + if size == 0 { 184 + size = defaultSize 185 + } 186 + 187 + // TODO(bkeyes): consider pooling these buffers 188 + b := make([]byte, size) 189 + if _, err := src.ReadAt(b, offset); err != nil { 190 + return 0, delta, err 191 + } 192 + 193 + _, err = w.Write(b) 194 + return size, delta, err 195 + } 196 + 197 + func checkBinarySrcSize(r io.ReaderAt, size int64) error { 198 + ok, err := isLen(r, size) 199 + if err != nil { 200 + return err 201 + } 202 + if !ok { 203 + return &Conflict{"fragment src size does not match actual src size"} 204 + } 205 + return nil 206 + }
+10 -73
gitdiff/apply_test.go
··· 9 9 "testing" 10 10 ) 11 11 12 - func TestApplierInvariants(t *testing.T) { 13 - binary := &BinaryFragment{ 14 - Method: BinaryPatchLiteral, 15 - Size: 2, 16 - Data: []byte("\xbe\xef"), 17 - } 18 - 19 - text := &TextFragment{ 20 - NewPosition: 1, 21 - NewLines: 1, 22 - LinesAdded: 1, 23 - Lines: []Line{ 24 - {Op: OpAdd, Line: "new line\n"}, 25 - }, 26 - } 27 - 28 - file := &File{ 29 - TextFragments: []*TextFragment{text}, 30 - } 31 - 32 - src := bytes.NewReader(nil) 33 - dst := ioutil.Discard 34 - 35 - assertInProgress := func(t *testing.T, kind string, err error) { 36 - if !errors.Is(err, errApplyInProgress) { 37 - t.Fatalf("expected in-progress error for %s apply, but got: %v", kind, err) 38 - } 39 - } 40 - 41 - t.Run("binaryFirst", func(t *testing.T) { 42 - a := NewApplier(src) 43 - if err := a.ApplyBinaryFragment(dst, binary); err != nil { 44 - t.Fatalf("unexpected error applying fragment: %v", err) 45 - } 46 - assertInProgress(t, "text", a.ApplyTextFragment(dst, text)) 47 - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) 48 - assertInProgress(t, "file", a.ApplyFile(dst, file)) 49 - }) 50 - 51 - t.Run("textFirst", func(t *testing.T) { 52 - a := NewApplier(src) 53 - if err := a.ApplyTextFragment(dst, text); err != nil { 54 - t.Fatalf("unexpected error applying fragment: %v", err) 55 - } 56 - // additional text fragments are allowed 57 - if err := a.ApplyTextFragment(dst, text); err != nil { 58 - t.Fatalf("unexpected error applying second fragment: %v", err) 59 - } 60 - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) 61 - assertInProgress(t, "file", a.ApplyFile(dst, file)) 62 - }) 63 - 64 - t.Run("fileFirst", func(t *testing.T) { 65 - a := NewApplier(src) 66 - if err := a.ApplyFile(dst, file); err != nil { 67 - t.Fatalf("unexpected error applying file: %v", err) 68 - } 69 - assertInProgress(t, "text", a.ApplyTextFragment(dst, text)) 70 - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) 71 - assertInProgress(t, "file", a.ApplyFile(dst, file)) 72 - }) 73 - } 74 - 75 12 func TestApplyTextFragment(t *testing.T) { 76 13 tests := map[string]applyTest{ 77 14 "createFile": {Files: getApplyFiles("text_fragment_new")}, ··· 127 64 128 65 for name, test := range tests { 129 66 t.Run(name, func(t *testing.T) { 130 - test.run(t, func(w io.Writer, applier *Applier, file *File) error { 67 + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { 131 68 if len(file.TextFragments) != 1 { 132 69 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(file.TextFragments)) 133 70 } 134 - return applier.ApplyTextFragment(w, file.TextFragments[0]) 71 + applier := NewTextApplier(dst, src) 72 + return applier.ApplyFragment(file.TextFragments[0]) 135 73 }) 136 74 }) 137 75 } ··· 176 114 177 115 for name, test := range tests { 178 116 t.Run(name, func(t *testing.T) { 179 - test.run(t, func(w io.Writer, applier *Applier, file *File) error { 180 - return applier.ApplyBinaryFragment(w, file.BinaryFragment) 117 + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { 118 + applier := NewBinaryApplier(dst, src) 119 + return applier.ApplyFragment(file.BinaryFragment) 181 120 }) 182 121 }) 183 122 } ··· 216 155 217 156 for name, test := range tests { 218 157 t.Run(name, func(t *testing.T) { 219 - test.run(t, func(w io.Writer, applier *Applier, file *File) error { 220 - return applier.ApplyFile(w, file) 158 + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { 159 + return Apply(dst, src, file) 221 160 }) 222 161 }) 223 162 } ··· 228 167 Err interface{} 229 168 } 230 169 231 - func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) error) { 170 + func (at applyTest) run(t *testing.T, apply func(io.Writer, io.ReaderAt, *File) error) { 232 171 src, patch, out := at.Files.Load(t) 233 172 234 173 files, _, err := Parse(bytes.NewReader(patch)) ··· 239 178 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 240 179 } 241 180 242 - applier := NewApplier(bytes.NewReader(src)) 243 - 244 181 var dst bytes.Buffer 245 - err = apply(&dst, applier, files[0]) 182 + err = apply(&dst, bytes.NewReader(src), files[0]) 246 183 if at.Err != nil { 247 184 assertError(t, at.Err, err, "applying fragment") 248 185 return
+153
gitdiff/apply_text.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "io" 5 + ) 6 + 7 + // TextApplier applies changes described in text fragments to source data. If 8 + // changes are described in multiple fragments, those fragments must be applied 9 + // in order. The applier must be closed after use. 10 + // 11 + // By default, TextApplier operates in "strict" mode, where fragment content 12 + // and positions must exactly match those of the source. 13 + // 14 + type TextApplier struct { 15 + dst io.Writer 16 + src io.ReaderAt 17 + lineSrc LineReaderAt 18 + nextLine int64 19 + 20 + closed bool 21 + dirty bool 22 + } 23 + 24 + // NewTextApplier creates a TextApplier that reads data from src and writes 25 + // modified data to dst. If src implements LineReaderAt, it is used directly. 26 + func NewTextApplier(dst io.Writer, src io.ReaderAt) *TextApplier { 27 + a := TextApplier{ 28 + dst: dst, 29 + src: src, 30 + } 31 + 32 + if lineSrc, ok := src.(LineReaderAt); ok { 33 + a.lineSrc = lineSrc 34 + } else { 35 + a.lineSrc = &lineReaderAt{r: src} 36 + } 37 + 38 + return &a 39 + } 40 + 41 + // ApplyFragment applies the changes in the fragment f, writing unwritten data 42 + // before the start of the fragment and any changes from the fragment. If 43 + // multiple text fragments apply to the same content, ApplyFragment must be 44 + // called in order of increasing start position. As a result, each fragment can 45 + // be applied at most once. 46 + // 47 + // If an error occurs while applying, ApplyFragment returns an *ApplyError that 48 + // annotates the error with additional information. If the error is because of 49 + // a conflict between the fragment and the source, the wrapped error will be a 50 + // *Conflict. 51 + func (a *TextApplier) ApplyFragment(f *TextFragment) error { 52 + if a.closed { 53 + return applyError(errApplierClosed) 54 + } 55 + 56 + // mark an apply as in progress, even if it fails before making changes 57 + a.dirty = true 58 + 59 + // application code assumes fragment fields are consistent 60 + if err := f.Validate(); err != nil { 61 + return applyError(err) 62 + } 63 + 64 + // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) 65 + fragStart := f.OldPosition - 1 66 + if fragStart < 0 { 67 + fragStart = 0 68 + } 69 + fragEnd := fragStart + f.OldLines 70 + 71 + start := a.nextLine 72 + if fragStart < start { 73 + return applyError(&Conflict{"fragment overlaps with an applied fragment"}) 74 + } 75 + 76 + if f.OldPosition == 0 { 77 + ok, err := isLen(a.src, 0) 78 + if err != nil { 79 + return applyError(err) 80 + } 81 + if !ok { 82 + return applyError(&Conflict{"cannot create new file from non-empty src"}) 83 + } 84 + } 85 + 86 + preimage := make([][]byte, fragEnd-start) 87 + n, err := a.lineSrc.ReadLinesAt(preimage, start) 88 + if err != nil { 89 + return applyError(err, lineNum(start+int64(n))) 90 + } 91 + 92 + // copy leading data before the fragment starts 93 + for i, line := range preimage[:fragStart-start] { 94 + if _, err := a.dst.Write(line); err != nil { 95 + a.nextLine = start + int64(i) 96 + return applyError(err, lineNum(a.nextLine)) 97 + } 98 + } 99 + preimage = preimage[fragStart-start:] 100 + 101 + // apply the changes in the fragment 102 + used := int64(0) 103 + for i, line := range f.Lines { 104 + if err := applyTextLine(a.dst, line, preimage, used); err != nil { 105 + a.nextLine = fragStart + used 106 + return applyError(err, lineNum(a.nextLine), fragLineNum(i)) 107 + } 108 + if line.Old() { 109 + used++ 110 + } 111 + } 112 + a.nextLine = fragStart + used 113 + 114 + // new position of +0,0 mean a full delete, so check for leftovers 115 + if f.NewPosition == 0 && f.NewLines == 0 { 116 + var b [1][]byte 117 + n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine) 118 + if err != nil && err != io.EOF { 119 + return applyError(err, lineNum(a.nextLine)) 120 + } 121 + if n > 0 { 122 + return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine)) 123 + } 124 + } 125 + 126 + return nil 127 + } 128 + 129 + func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) { 130 + if line.Old() && string(preimage[i]) != line.Line { 131 + return &Conflict{"fragment line does not match src line"} 132 + } 133 + if line.New() { 134 + _, err = io.WriteString(dst, line.Line) 135 + } 136 + return err 137 + } 138 + 139 + // Close writes any data following the last applied fragment and prevents 140 + // future calls to ApplyFragment. 141 + func (a *TextApplier) Close() (err error) { 142 + if a.closed { 143 + return nil 144 + } 145 + 146 + a.closed = true 147 + if !a.dirty { 148 + _, err = copyFrom(a.dst, a.src, 0) 149 + } else { 150 + _, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine) 151 + } 152 + return err 153 + }