1
2
3
4
5 package diff
6
7 import (
8 "fmt"
9 "log"
10 "regexp"
11 "strconv"
12 "strings"
13 )
14
15
16
17 const DefaultContextLines = 3
18
19
20
21
22 func Unified(oldLabel, newLabel, old, new string) string {
23 edits := Strings(old, new)
24 unified, err := ToUnified(oldLabel, newLabel, old, edits, DefaultContextLines)
25 if err != nil {
26
27 log.Fatalf("internal error in diff.Unified: %v", err)
28 }
29 return unified
30 }
31
32
33
34
35
36 func ToUnified(oldLabel, newLabel, content string, edits []Edit, contextLines int) (string, error) {
37 u, err := toUnified(oldLabel, newLabel, content, edits, contextLines)
38 if err != nil {
39 return "", err
40 }
41 return u.String(), nil
42 }
43
44
45 type unified struct {
46
47 from string
48
49 to string
50
51 hunks []*hunk
52 }
53
54
55 type hunk struct {
56
57 fromLine int
58
59 toLine int
60
61 lines []line
62 }
63
64
65 type line struct {
66
67 kind opKind
68
69
70
71 content string
72 }
73
74
75 type opKind int
76
77 const (
78
79
80 opDelete opKind = iota
81
82 opInsert
83
84
85 opEqual
86 )
87
88
89
90 func (k opKind) String() string {
91 switch k {
92 case opDelete:
93 return "delete"
94 case opInsert:
95 return "insert"
96 case opEqual:
97 return "equal"
98 default:
99 panic("unknown operation kind")
100 }
101 }
102
103
104
105 func toUnified(fromName, toName string, content string, edits []Edit, contextLines int) (unified, error) {
106 gap := contextLines * 2
107 u := unified{
108 from: fromName,
109 to: toName,
110 }
111 if len(edits) == 0 {
112 return u, nil
113 }
114 var err error
115 edits, err = lineEdits(content, edits)
116 if err != nil {
117 return u, err
118 }
119 lines, _ := splitLines(content)
120 var h *hunk
121 last := 0
122 toLine := 0
123 for _, edit := range edits {
124
125
126 start := strings.Count(content[:edit.Start], "\n")
127 end := strings.Count(content[:edit.End], "\n")
128 if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' {
129 end++
130 }
131
132 switch {
133 case h != nil && start == last:
134
135 case h != nil && start <= last+gap:
136
137 addEqualLines(h, lines, last, start)
138 default:
139
140 if h != nil {
141
142 addEqualLines(h, lines, last, last+contextLines)
143 u.hunks = append(u.hunks, h)
144 }
145 toLine += start - last
146 h = &hunk{
147 fromLine: start + 1,
148 toLine: toLine + 1,
149 }
150
151 delta := addEqualLines(h, lines, start-contextLines, start)
152 h.fromLine -= delta
153 h.toLine -= delta
154 }
155 last = start
156 for i := start; i < end; i++ {
157 h.lines = append(h.lines, line{kind: opDelete, content: lines[i]})
158 last++
159 }
160 if edit.New != "" {
161 v, _ := splitLines(edit.New)
162 for _, content := range v {
163 h.lines = append(h.lines, line{kind: opInsert, content: content})
164 toLine++
165 }
166 }
167 }
168 if h != nil {
169
170 addEqualLines(h, lines, last, last+contextLines)
171 u.hunks = append(u.hunks, h)
172 }
173 return u, nil
174 }
175
176
177
178 func splitLines(text string) ([]string, []int) {
179 var lines []string
180 offsets := []int{0}
181 start := 0
182 for i, r := range text {
183 if r == '\n' {
184 lines = append(lines, text[start:i+1])
185 start = i + 1
186 offsets = append(offsets, start)
187 }
188 }
189 if start < len(text) {
190 lines = append(lines, text[start:])
191 offsets = append(offsets, len(text))
192 }
193 return lines, offsets
194 }
195
196 func addEqualLines(h *hunk, lines []string, start, end int) int {
197 delta := 0
198 for i := start; i < end; i++ {
199 if i < 0 {
200 continue
201 }
202 if i >= len(lines) {
203 return delta
204 }
205 h.lines = append(h.lines, line{kind: opEqual, content: lines[i]})
206 delta++
207 }
208 return delta
209 }
210
211
212
213 func (u unified) String() string {
214 if len(u.hunks) == 0 {
215 return ""
216 }
217 b := new(strings.Builder)
218 fmt.Fprintf(b, "--- %s\n", u.from)
219 fmt.Fprintf(b, "+++ %s\n", u.to)
220 for _, hunk := range u.hunks {
221 fromCount, toCount := 0, 0
222 for _, l := range hunk.lines {
223 switch l.kind {
224 case opDelete:
225 fromCount++
226 case opInsert:
227 toCount++
228 default:
229 fromCount++
230 toCount++
231 }
232 }
233 fmt.Fprint(b, "@@")
234 if fromCount > 1 {
235 fmt.Fprintf(b, " -%d,%d", hunk.fromLine, fromCount)
236 } else if hunk.fromLine == 1 && fromCount == 0 {
237
238 fmt.Fprintf(b, " -0,0")
239 } else {
240 fmt.Fprintf(b, " -%d", hunk.fromLine)
241 }
242 if toCount > 1 {
243 fmt.Fprintf(b, " +%d,%d", hunk.toLine, toCount)
244 } else if hunk.toLine == 1 && toCount == 0 {
245
246 fmt.Fprintf(b, " +0,0")
247 } else {
248 fmt.Fprintf(b, " +%d", hunk.toLine)
249 }
250 fmt.Fprint(b, " @@\n")
251 for _, l := range hunk.lines {
252 switch l.kind {
253 case opDelete:
254 fmt.Fprintf(b, "-%s", l.content)
255 case opInsert:
256 fmt.Fprintf(b, "+%s", l.content)
257 default:
258 fmt.Fprintf(b, " %s", l.content)
259 }
260 if !strings.HasSuffix(l.content, "\n") {
261 fmt.Fprintf(b, "\n\\ No newline at end of file\n")
262 }
263 }
264 }
265 return b.String()
266 }
267
268
269 func ApplyUnified(udiffs, bef string) (string, error) {
270 before := strings.Split(bef, "\n")
271 unif := strings.Split(udiffs, "\n")
272 var got []string
273 left := 0
274
275 for _, l := range unif {
276 if len(l) == 0 {
277 continue
278 }
279 switch l[0] {
280 case '@':
281 m := atregexp.FindStringSubmatch(l)
282 fromLine, err := strconv.Atoi(m[1])
283 if err != nil {
284 return "", fmt.Errorf("missing line number in %q", l)
285 }
286
287 for ; left < fromLine-1; left++ {
288 got = append(got, before[left])
289 }
290 case '+':
291 if strings.HasPrefix(l, "+++ ") {
292 continue
293 }
294 got = append(got, l[1:])
295 case '-':
296 if strings.HasPrefix(l, "--- ") {
297 continue
298 }
299 left++
300 case ' ':
301 return "", fmt.Errorf("unexpected line %q", l)
302 default:
303 return "", fmt.Errorf("impossible unified %q", udiffs)
304 }
305 }
306
307 for ; left < len(before); left++ {
308 got = append(got, before[left])
309 }
310 return strings.Join(got, "\n"), nil
311 }
312
313
314 var atregexp = regexp.MustCompile(`@@ -(\d+).* @@`)
315
View as plain text