master
  1package gitdiff
  2
  3import (
  4	"errors"
  5	"io"
  6)
  7
  8// TextApplier applies changes described in text fragments to source data. If
  9// changes are described in multiple fragments, those fragments must be applied
 10// in order. The applier must be closed after use.
 11//
 12// By default, TextApplier operates in "strict" mode, where fragment content
 13// and positions must exactly match those of the source.
 14type 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.
 26func 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.
 51func (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		// an EOF indicates that source file is shorter than the patch expects,
 90		// which should be reported as a conflict rather than a generic error
 91		if errors.Is(err, io.EOF) {
 92			err = &Conflict{"src has fewer lines than required by fragment"}
 93		}
 94		return applyError(err, lineNum(start+int64(n)))
 95	}
 96
 97	// copy leading data before the fragment starts
 98	for i, line := range preimage[:fragStart-start] {
 99		if _, err := a.dst.Write(line); err != nil {
100			a.nextLine = start + int64(i)
101			return applyError(err, lineNum(a.nextLine))
102		}
103	}
104	preimage = preimage[fragStart-start:]
105
106	// apply the changes in the fragment
107	used := int64(0)
108	for i, line := range f.Lines {
109		if err := applyTextLine(a.dst, line, preimage, used); err != nil {
110			a.nextLine = fragStart + used
111			return applyError(err, lineNum(a.nextLine), fragLineNum(i))
112		}
113		if line.Old() {
114			used++
115		}
116	}
117	a.nextLine = fragStart + used
118
119	// new position of +0,0 mean a full delete, so check for leftovers
120	if f.NewPosition == 0 && f.NewLines == 0 {
121		var b [1][]byte
122		n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
123		if err != nil && err != io.EOF {
124			return applyError(err, lineNum(a.nextLine))
125		}
126		if n > 0 {
127			return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
128		}
129	}
130
131	return nil
132}
133
134func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
135	if line.Old() && string(preimage[i]) != line.Line {
136		return &Conflict{"fragment line does not match src line"}
137	}
138	if line.New() {
139		_, err = io.WriteString(dst, line.Line)
140	}
141	return err
142}
143
144// Close writes any data following the last applied fragment and prevents
145// future calls to ApplyFragment.
146func (a *TextApplier) Close() (err error) {
147	if a.closed {
148		return nil
149	}
150
151	a.closed = true
152	if !a.dirty {
153		_, err = copyFrom(a.dst, a.src, 0)
154	} else {
155		_, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine)
156	}
157	return err
158}