fork of go-gitdiff with jj support
fork

Configure Feed

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

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

+398 -404
+1
.golangci.yml
··· 11 - golint 12 - govet 13 - ineffassign 14 - typecheck 15 - unconvert 16 - varcheck
··· 11 - golint 12 - govet 13 - ineffassign 14 + - misspell 15 - typecheck 16 - unconvert 17 - varcheck
+1 -1
README.md
··· 29 30 // apply the changes in the patch to a source file 31 var output bytes.Buffer 32 - if err := gitdiff.NewApplier(code).ApplyFile(&output, files[0]); err != nil { 33 log.Fatal(err) 34 } 35 ```
··· 29 30 // apply the changes in the patch to a source file 31 var output bytes.Buffer 32 + if err := gitdiff.Apply(&output, code, files[0]); err != nil { 33 log.Fatal(err) 34 } 35 ```
+27 -330
gitdiff/apply.go
··· 89 90 var ( 91 errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") 92 - ) 93 - 94 - const ( 95 - applyInitial = iota 96 - applyText 97 - applyBinary 98 - applyFile 99 ) 100 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. 110 // 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} 147 } 148 } 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 168 switch { 169 case f.BinaryFragment != nil: 170 - return a.ApplyBinaryFragment(dst, f.BinaryFragment) 171 172 case len(f.TextFragments) > 0: 173 frags := make([]*TextFragment, len(f.TextFragments)) ··· 181 // right now, the application fails if fragments overlap, but it should be 182 // possible to precompute the result of applying them in order 183 184 for i, frag := range frags { 185 - if err := a.ApplyTextFragment(dst, frag); err != nil { 186 return applyError(err, fragNum(i)) 187 } 188 } 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 - } 271 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 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 { 445 return err 446 } 447 - if !ok { 448 - return &Conflict{"fragment src size does not match actual src size"} 449 - } 450 - return nil 451 }
··· 89 90 var ( 91 errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") 92 + errApplierClosed = errors.New("gitdiff: applier is closed") 93 ) 94 95 + // Apply applies the changes in f to src, writing the result to dst. It can 96 + // apply both text and binary changes. 97 // 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")) 112 } 113 } 114 115 switch { 116 case f.BinaryFragment != nil: 117 + applier := NewBinaryApplier(dst, src) 118 + if err := applier.ApplyFragment(f.BinaryFragment); err != nil { 119 + return err 120 + } 121 + return applier.Close() 122 123 case len(f.TextFragments) > 0: 124 frags := make([]*TextFragment, len(f.TextFragments)) ··· 132 // right now, the application fails if fragments overlap, but it should be 133 // possible to precompute the result of applying them in order 134 135 + applier := NewTextApplier(dst, src) 136 for i, frag := range frags { 137 + if err := applier.ApplyFragment(frag); err != nil { 138 return applyError(err, fragNum(i)) 139 } 140 } 141 + return applier.Close() 142 143 default: 144 + // nothing to apply, just copy all the data 145 + _, err := copyFrom(dst, src, 0) 146 return err 147 } 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 "testing" 10 ) 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 func TestApplyTextFragment(t *testing.T) { 76 tests := map[string]applyTest{ 77 "createFile": {Files: getApplyFiles("text_fragment_new")}, ··· 127 128 for name, test := range tests { 129 t.Run(name, func(t *testing.T) { 130 - test.run(t, func(w io.Writer, applier *Applier, file *File) error { 131 if len(file.TextFragments) != 1 { 132 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(file.TextFragments)) 133 } 134 - return applier.ApplyTextFragment(w, file.TextFragments[0]) 135 }) 136 }) 137 } ··· 176 177 for name, test := range tests { 178 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) 181 }) 182 }) 183 } ··· 216 217 for name, test := range tests { 218 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) 221 }) 222 }) 223 } ··· 228 Err interface{} 229 } 230 231 - func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) error) { 232 src, patch, out := at.Files.Load(t) 233 234 files, _, err := Parse(bytes.NewReader(patch)) ··· 239 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 240 } 241 242 - applier := NewApplier(bytes.NewReader(src)) 243 - 244 var dst bytes.Buffer 245 - err = apply(&dst, applier, files[0]) 246 if at.Err != nil { 247 assertError(t, at.Err, err, "applying fragment") 248 return
··· 9 "testing" 10 ) 11 12 func TestApplyTextFragment(t *testing.T) { 13 tests := map[string]applyTest{ 14 "createFile": {Files: getApplyFiles("text_fragment_new")}, ··· 64 65 for name, test := range tests { 66 t.Run(name, func(t *testing.T) { 67 + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { 68 if len(file.TextFragments) != 1 { 69 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(file.TextFragments)) 70 } 71 + applier := NewTextApplier(dst, src) 72 + return applier.ApplyFragment(file.TextFragments[0]) 73 }) 74 }) 75 } ··· 114 115 for name, test := range tests { 116 t.Run(name, func(t *testing.T) { 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) 120 }) 121 }) 122 } ··· 155 156 for name, test := range tests { 157 t.Run(name, func(t *testing.T) { 158 + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { 159 + return Apply(dst, src, file) 160 }) 161 }) 162 } ··· 167 Err interface{} 168 } 169 170 + func (at applyTest) run(t *testing.T, apply func(io.Writer, io.ReaderAt, *File) error) { 171 src, patch, out := at.Files.Load(t) 172 173 files, _, err := Parse(bytes.NewReader(patch)) ··· 178 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 179 } 180 181 var dst bytes.Buffer 182 + err = apply(&dst, bytes.NewReader(src), files[0]) 183 if at.Err != nil { 184 assertError(t, at.Err, err, "applying fragment") 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 + }