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}