summaryrefslogtreecommitdiff
path: root/pkg/gitdiff/apply_text.go
blob: 8d0accb1157bf485f6663f690c78af50cd58b107 (plain)
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
153
154
155
156
157
158
package gitdiff

import (
	"errors"
	"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 {
		// an EOF indicates that source file is shorter than the patch expects,
		// which should be reported as a conflict rather than a generic error
		if errors.Is(err, io.EOF) {
			err = &Conflict{"src has fewer lines than required by fragment"}
		}
		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
}