-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhunk.go
More file actions
139 lines (129 loc) · 3.51 KB
/
Copy pathhunk.go
File metadata and controls
139 lines (129 loc) · 3.51 KB
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
package patchapply
import (
"fmt"
"strings"
"github.com/floatpane/go-mailpatch"
)
// ApplyToBytes applies a single file's hunks to orig and returns the new
// contents. It is the pure, filesystem-free core: read a file yourself, pass
// its bytes here, write the result yourself.
//
// Hunks are located at the line numbers the diff records, but a whole-hunk
// offset is tolerated (so a patch still applies when earlier, unrelated edits
// shifted the file). Context lines must match exactly — there is no fuzz. A
// hunk that cannot be placed returns an error wrapping ErrConflict.
func ApplyToBytes(orig []byte, f mailpatch.FileChange) ([]byte, error) {
src, trailingNL := splitLines(orig)
if len(orig) == 0 {
// A file built from nothing (an addition) gets a trailing newline,
// matching how git writes added text files.
trailingNL = true
}
out := make([]string, 0, len(src))
cursor := 0
for i, h := range f.Hunks {
oldBlock, newBlock := buildBlocks(h)
pos, ok := locate(src, oldBlock, h.OldStart-1, cursor)
if !ok {
return nil, fmt.Errorf("%w: %s hunk %d (@@ -%d,%d)",
ErrConflict, f.Path(), i+1, h.OldStart, h.OldLines)
}
out = append(out, src[cursor:pos]...)
out = append(out, newBlock...)
cursor = pos + len(oldBlock)
}
out = append(out, src[cursor:]...)
return joinLines(out, trailingNL), nil
}
// buildBlocks splits a hunk into the lines it expects to find (context +
// deletions) and the lines it produces (context + additions).
func buildBlocks(h mailpatch.Hunk) (oldBlock, newBlock []string) {
for _, ln := range h.Lines {
switch ln.Kind {
case mailpatch.Context:
oldBlock = append(oldBlock, ln.Text)
newBlock = append(newBlock, ln.Text)
case mailpatch.Delete:
oldBlock = append(oldBlock, ln.Text)
case mailpatch.Add:
newBlock = append(newBlock, ln.Text)
}
}
return oldBlock, newBlock
}
// locate finds where oldBlock sits in src, preferring the expected index and
// searching outward, never before minPos. For an empty oldBlock (a pure
// insertion) it returns the clamped expected index.
func locate(src, oldBlock []string, expected, minPos int) (int, bool) {
if expected < minPos {
expected = minPos
}
if len(oldBlock) == 0 {
if expected > len(src) {
expected = len(src)
}
return expected, true
}
last := len(src) - len(oldBlock)
if last < minPos {
return 0, false
}
if expected > last {
expected = last
}
// Expand outward from the expected position: 0, +1, -1, +2, -2, ...
for delta := 0; ; delta++ {
fwd := expected + delta
bwd := expected - delta
tried := false
if fwd <= last {
tried = true
if matchAt(src, oldBlock, fwd) {
return fwd, true
}
}
if delta != 0 && bwd >= minPos {
tried = true
if matchAt(src, oldBlock, bwd) {
return bwd, true
}
}
if !tried {
return 0, false
}
}
}
func matchAt(src, block []string, pos int) bool {
for i, line := range block {
if src[pos+i] != line {
return false
}
}
return true
}
// splitLines splits content into lines, reporting whether it ended with a
// newline so the result can be reconstructed faithfully.
func splitLines(b []byte) (lines []string, trailingNL bool) {
if len(b) == 0 {
return nil, false
}
s := string(b)
if strings.HasSuffix(s, "\n") {
trailingNL = true
s = s[:len(s)-1]
}
return strings.Split(s, "\n"), trailingNL
}
func joinLines(lines []string, trailingNL bool) []byte {
if len(lines) == 0 {
if trailingNL {
return []byte("\n")
}
return nil
}
s := strings.Join(lines, "\n")
if trailingNL {
s += "\n"
}
return []byte(s)
}