mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
75930390c9
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().
154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"io"
|
|
)
|
|
|
|
// TextApplier applies changes described in text fragments to source data. If
|
|
// changes are described in multiple fragments, those fragments must be applied
|
|
// in order. The applier must be closed after use.
|
|
//
|
|
// By default, TextApplier operates in "strict" mode, where fragment content
|
|
// and positions must exactly match those of the source.
|
|
//
|
|
type TextApplier struct {
|
|
dst io.Writer
|
|
src io.ReaderAt
|
|
lineSrc LineReaderAt
|
|
nextLine int64
|
|
|
|
closed bool
|
|
dirty bool
|
|
}
|
|
|
|
// NewTextApplier creates a TextApplier that reads data from src and writes
|
|
// modified data to dst. If src implements LineReaderAt, it is used directly.
|
|
func NewTextApplier(dst io.Writer, src io.ReaderAt) *TextApplier {
|
|
a := TextApplier{
|
|
dst: dst,
|
|
src: src,
|
|
}
|
|
|
|
if lineSrc, ok := src.(LineReaderAt); ok {
|
|
a.lineSrc = lineSrc
|
|
} else {
|
|
a.lineSrc = &lineReaderAt{r: src}
|
|
}
|
|
|
|
return &a
|
|
}
|
|
|
|
// ApplyFragment applies the changes in the fragment f, writing unwritten data
|
|
// before the start of the fragment and any changes from the fragment. If
|
|
// multiple text fragments apply to the same content, ApplyFragment must be
|
|
// called in order of increasing start position. As a result, each fragment can
|
|
// be applied at most once.
|
|
//
|
|
// If an error occurs while applying, ApplyFragment returns an *ApplyError that
|
|
// annotates the error with additional information. If the error is because of
|
|
// a conflict between the fragment and the source, the wrapped error will be a
|
|
// *Conflict.
|
|
func (a *TextApplier) ApplyFragment(f *TextFragment) error {
|
|
if a.closed {
|
|
return applyError(errApplierClosed)
|
|
}
|
|
|
|
// mark an apply as in progress, even if it fails before making changes
|
|
a.dirty = true
|
|
|
|
// application code assumes fragment fields are consistent
|
|
if err := f.Validate(); err != nil {
|
|
return applyError(err)
|
|
}
|
|
|
|
// lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
|
|
fragStart := f.OldPosition - 1
|
|
if fragStart < 0 {
|
|
fragStart = 0
|
|
}
|
|
fragEnd := fragStart + f.OldLines
|
|
|
|
start := a.nextLine
|
|
if fragStart < start {
|
|
return applyError(&Conflict{"fragment overlaps with an applied fragment"})
|
|
}
|
|
|
|
if f.OldPosition == 0 {
|
|
ok, err := isLen(a.src, 0)
|
|
if err != nil {
|
|
return applyError(err)
|
|
}
|
|
if !ok {
|
|
return applyError(&Conflict{"cannot create new file from non-empty src"})
|
|
}
|
|
}
|
|
|
|
preimage := make([][]byte, fragEnd-start)
|
|
n, err := a.lineSrc.ReadLinesAt(preimage, start)
|
|
if err != nil {
|
|
return applyError(err, lineNum(start+int64(n)))
|
|
}
|
|
|
|
// copy leading data before the fragment starts
|
|
for i, line := range preimage[:fragStart-start] {
|
|
if _, err := a.dst.Write(line); err != nil {
|
|
a.nextLine = start + int64(i)
|
|
return applyError(err, lineNum(a.nextLine))
|
|
}
|
|
}
|
|
preimage = preimage[fragStart-start:]
|
|
|
|
// apply the changes in the fragment
|
|
used := int64(0)
|
|
for i, line := range f.Lines {
|
|
if err := applyTextLine(a.dst, line, preimage, used); err != nil {
|
|
a.nextLine = fragStart + used
|
|
return applyError(err, lineNum(a.nextLine), fragLineNum(i))
|
|
}
|
|
if line.Old() {
|
|
used++
|
|
}
|
|
}
|
|
a.nextLine = fragStart + used
|
|
|
|
// new position of +0,0 mean a full delete, so check for leftovers
|
|
if f.NewPosition == 0 && f.NewLines == 0 {
|
|
var b [1][]byte
|
|
n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
|
|
if err != nil && err != io.EOF {
|
|
return applyError(err, lineNum(a.nextLine))
|
|
}
|
|
if n > 0 {
|
|
return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
|
|
if line.Old() && string(preimage[i]) != line.Line {
|
|
return &Conflict{"fragment line does not match src line"}
|
|
}
|
|
if line.New() {
|
|
_, err = io.WriteString(dst, line.Line)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Close writes any data following the last applied fragment and prevents
|
|
// future calls to ApplyFragment.
|
|
func (a *TextApplier) Close() (err error) {
|
|
if a.closed {
|
|
return nil
|
|
}
|
|
|
|
a.closed = true
|
|
if !a.dirty {
|
|
_, err = copyFrom(a.dst, a.src, 0)
|
|
} else {
|
|
_, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine)
|
|
}
|
|
return err
|
|
}
|