/
apply_text.go
152 lines (132 loc) · 4.04 KB
/
apply_text.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
}