tangled
alpha
login
or
join now
moll.dev
/
core
forked from
tangled.org/core
this repo has no description
0
fork
atom
overview
issues
pulls
pipelines
move interdiff & combinediff into patchutil
oppi.li
9 months ago
b6dbf420
12ec68d5
+730
-470
11 changed files
expand all
collapse all
unified
split
appview
db
pulls.go
pages
pages.go
templates
repo
pulls
pull.html
state
pull.go
cmd
combinediff
main.go
interdiff
main.go
interdiff
interdiff.go
patchutil
combinediff.go
image.go
interdiff.go
patchutil.go
+27
-2
appview/db/pulls.go
···
150
150
return false
151
151
}
152
152
153
153
-
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
153
153
+
func (s PullSubmission) AsDiff(targetBranch string) ([]*gitdiff.File, error) {
154
154
patch := s.Patch
155
155
156
156
-
diffs, _, err := gitdiff.Parse(strings.NewReader(patch))
156
156
+
// if format-patch; then extract each patch
157
157
+
var diffs []*gitdiff.File
158
158
+
if patchutil.IsFormatPatch(patch) {
159
159
+
patches, err := patchutil.ExtractPatches(patch)
160
160
+
if err != nil {
161
161
+
return nil, err
162
162
+
}
163
163
+
var ps [][]*gitdiff.File
164
164
+
for _, p := range patches {
165
165
+
ps = append(ps, p.Files)
166
166
+
}
167
167
+
168
168
+
diffs = patchutil.CombineDiff(ps...)
169
169
+
} else {
170
170
+
d, _, err := gitdiff.Parse(strings.NewReader(patch))
171
171
+
if err != nil {
172
172
+
return nil, err
173
173
+
}
174
174
+
diffs = d
175
175
+
}
176
176
+
177
177
+
return diffs, nil
178
178
+
}
179
179
+
180
180
+
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
181
181
+
diffs, err := s.AsDiff(targetBranch)
157
182
if err != nil {
158
183
log.Println(err)
159
184
}
+2
-2
appview/pages/pages.go
···
20
20
"tangled.sh/tangled.sh/core/appview/db"
21
21
"tangled.sh/tangled.sh/core/appview/pages/markup"
22
22
"tangled.sh/tangled.sh/core/appview/state/userutil"
23
23
-
"tangled.sh/tangled.sh/core/interdiff"
23
23
+
"tangled.sh/tangled.sh/core/patchutil"
24
24
"tangled.sh/tangled.sh/core/types"
25
25
26
26
"github.com/alecthomas/chroma/v2"
···
715
715
RepoInfo RepoInfo
716
716
Pull *db.Pull
717
717
Round int
718
718
-
Interdiff *interdiff.InterdiffResult
718
718
+
Interdiff *patchutil.InterdiffResult
719
719
}
720
720
721
721
// this name is a mouthful
+7
-10
appview/pages/templates/repo/pulls/pull.html
···
51
51
</span>
52
52
</div>
53
53
54
54
-
{{ if $.Pull.IsPatchBased }}
55
55
-
<!-- view patch -->
56
54
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
57
55
hx-boost="true"
58
56
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
59
57
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">view patch</span>
60
58
</a>
61
61
-
{{ if not (eq .RoundNumber 0) }}
62
62
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
63
63
-
hx-boost="true"
64
64
-
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
65
65
-
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span>
66
66
-
</a>
67
67
-
<span id="interdiff-error-{{.RoundNumber}}"></span>
68
68
-
{{ end }}
59
59
+
{{ if not (eq .RoundNumber 0) }}
60
60
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
61
61
+
hx-boost="true"
62
62
+
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
63
63
+
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span>
64
64
+
</a>
65
65
+
<span id="interdiff-error-{{.RoundNumber}}"></span>
69
66
{{ end }}
70
67
</div>
71
68
</summary>
+3
-6
appview/state/pull.go
···
10
10
"net/http"
11
11
"net/url"
12
12
"strconv"
13
13
-
"strings"
14
13
"time"
15
14
16
15
"tangled.sh/tangled.sh/core/api/tangled"
17
16
"tangled.sh/tangled.sh/core/appview/auth"
18
17
"tangled.sh/tangled.sh/core/appview/db"
19
18
"tangled.sh/tangled.sh/core/appview/pages"
20
20
-
"tangled.sh/tangled.sh/core/interdiff"
21
19
"tangled.sh/tangled.sh/core/patchutil"
22
20
"tangled.sh/tangled.sh/core/types"
23
21
24
24
-
"github.com/bluekeyes/go-gitdiff/gitdiff"
25
22
comatproto "github.com/bluesky-social/indigo/api/atproto"
26
23
"github.com/bluesky-social/indigo/atproto/syntax"
27
24
lexutil "github.com/bluesky-social/indigo/lex/util"
···
351
348
}
352
349
}
353
350
354
354
-
currentPatch, _, err := gitdiff.Parse(strings.NewReader(pull.Submissions[roundIdInt].Patch))
351
351
+
currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch)
355
352
if err != nil {
356
353
log.Println("failed to interdiff; current patch malformed")
357
354
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
358
355
return
359
356
}
360
357
361
361
-
previousPatch, _, err := gitdiff.Parse(strings.NewReader(pull.Submissions[roundIdInt-1].Patch))
358
358
+
previousPatch, err := pull.Submissions[roundIdInt-1].AsDiff(pull.TargetBranch)
362
359
if err != nil {
363
360
log.Println("failed to interdiff; previous patch malformed")
364
361
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
365
362
return
366
363
}
367
364
368
368
-
interdiff := interdiff.Interdiff(previousPatch, currentPatch)
365
365
+
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
369
366
370
367
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
371
368
LoggedInUser: s.auth.GetUser(r),
+38
cmd/combinediff/main.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"os"
6
6
+
7
7
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
8
8
+
"tangled.sh/tangled.sh/core/patchutil"
9
9
+
)
10
10
+
11
11
+
func main() {
12
12
+
if len(os.Args) != 3 {
13
13
+
fmt.Println("Usage: combinediff <patch1> <patch2>")
14
14
+
os.Exit(1)
15
15
+
}
16
16
+
17
17
+
patch1, err := os.Open(os.Args[1])
18
18
+
if err != nil {
19
19
+
fmt.Println(err)
20
20
+
}
21
21
+
patch2, err := os.Open(os.Args[2])
22
22
+
if err != nil {
23
23
+
fmt.Println(err)
24
24
+
}
25
25
+
26
26
+
files1, _, err := gitdiff.Parse(patch1)
27
27
+
if err != nil {
28
28
+
fmt.Println(err)
29
29
+
}
30
30
+
31
31
+
files2, _, err := gitdiff.Parse(patch2)
32
32
+
if err != nil {
33
33
+
fmt.Println(err)
34
34
+
}
35
35
+
36
36
+
combined := patchutil.CombineDiff(files1, files2)
37
37
+
fmt.Println(combined)
38
38
+
}
+2
-2
cmd/interdiff/main.go
···
5
5
"os"
6
6
7
7
"github.com/bluekeyes/go-gitdiff/gitdiff"
8
8
-
"tangled.sh/tangled.sh/core/interdiff"
8
8
+
"tangled.sh/tangled.sh/core/patchutil"
9
9
)
10
10
11
11
func main() {
···
33
33
fmt.Println(err)
34
34
}
35
35
36
36
-
interDiffResult := interdiff.Interdiff(files1, files2)
36
36
+
interDiffResult := patchutil.Interdiff(files1, files2)
37
37
fmt.Println(interDiffResult)
38
38
}
-448
interdiff/interdiff.go
···
1
1
-
package interdiff
2
2
-
3
3
-
import (
4
4
-
"bytes"
5
5
-
"fmt"
6
6
-
"os"
7
7
-
"os/exec"
8
8
-
"strings"
9
9
-
10
10
-
"github.com/bluekeyes/go-gitdiff/gitdiff"
11
11
-
)
12
12
-
13
13
-
type ReconstructedLine struct {
14
14
-
LineNumber int64
15
15
-
Content string
16
16
-
IsUnknown bool
17
17
-
}
18
18
-
19
19
-
func NewLineAt(lineNumber int64, content string) ReconstructedLine {
20
20
-
return ReconstructedLine{
21
21
-
LineNumber: lineNumber,
22
22
-
Content: content,
23
23
-
IsUnknown: false,
24
24
-
}
25
25
-
}
26
26
-
27
27
-
type ReconstructedFile struct {
28
28
-
File string
29
29
-
Data []*ReconstructedLine
30
30
-
}
31
31
-
32
32
-
func (r *ReconstructedFile) String() string {
33
33
-
var i, j int64
34
34
-
var b strings.Builder
35
35
-
for {
36
36
-
i += 1
37
37
-
38
38
-
if int(j) >= (len(r.Data)) {
39
39
-
break
40
40
-
}
41
41
-
42
42
-
if r.Data[j].LineNumber == i {
43
43
-
// b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber))
44
44
-
b.WriteString(r.Data[j].Content)
45
45
-
j += 1
46
46
-
} else {
47
47
-
//b.WriteString(fmt.Sprintf("%d:\n", i))
48
48
-
b.WriteString("\n")
49
49
-
}
50
50
-
}
51
51
-
52
52
-
return b.String()
53
53
-
}
54
54
-
55
55
-
func (r *ReconstructedFile) AddLine(line *ReconstructedLine) {
56
56
-
r.Data = append(r.Data, line)
57
57
-
}
58
58
-
59
59
-
func bestName(file *gitdiff.File) string {
60
60
-
if file.IsDelete {
61
61
-
return file.OldName
62
62
-
} else {
63
63
-
return file.NewName
64
64
-
}
65
65
-
}
66
66
-
67
67
-
// in-place reverse of a diff
68
68
-
func reverseDiff(file *gitdiff.File) {
69
69
-
file.OldName, file.NewName = file.NewName, file.OldName
70
70
-
file.OldMode, file.NewMode = file.NewMode, file.OldMode
71
71
-
file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment
72
72
-
73
73
-
for _, fragment := range file.TextFragments {
74
74
-
// swap postions
75
75
-
fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition
76
76
-
fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines
77
77
-
fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded
78
78
-
79
79
-
for i := range fragment.Lines {
80
80
-
switch fragment.Lines[i].Op {
81
81
-
case gitdiff.OpAdd:
82
82
-
fragment.Lines[i].Op = gitdiff.OpDelete
83
83
-
case gitdiff.OpDelete:
84
84
-
fragment.Lines[i].Op = gitdiff.OpAdd
85
85
-
default:
86
86
-
// do nothing
87
87
-
}
88
88
-
}
89
89
-
}
90
90
-
}
91
91
-
92
92
-
// rebuild the original file from a patch
93
93
-
func CreateOriginal(file *gitdiff.File) ReconstructedFile {
94
94
-
rf := ReconstructedFile{
95
95
-
File: bestName(file),
96
96
-
}
97
97
-
98
98
-
for _, fragment := range file.TextFragments {
99
99
-
position := fragment.OldPosition
100
100
-
for _, line := range fragment.Lines {
101
101
-
switch line.Op {
102
102
-
case gitdiff.OpContext:
103
103
-
rl := NewLineAt(position, line.Line)
104
104
-
rf.Data = append(rf.Data, &rl)
105
105
-
position += 1
106
106
-
case gitdiff.OpDelete:
107
107
-
rl := NewLineAt(position, line.Line)
108
108
-
rf.Data = append(rf.Data, &rl)
109
109
-
position += 1
110
110
-
case gitdiff.OpAdd:
111
111
-
// do nothing here
112
112
-
}
113
113
-
}
114
114
-
}
115
115
-
116
116
-
return rf
117
117
-
}
118
118
-
119
119
-
type MergeError struct {
120
120
-
msg string
121
121
-
mismatchingLine int64
122
122
-
}
123
123
-
124
124
-
func (m MergeError) Error() string {
125
125
-
return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine)
126
126
-
}
127
127
-
128
128
-
// best effort merging of two reconstructed files
129
129
-
func (this *ReconstructedFile) Merge(other *ReconstructedFile) (*ReconstructedFile, error) {
130
130
-
mergedFile := ReconstructedFile{}
131
131
-
132
132
-
var i, j int64
133
133
-
134
134
-
for int(i) < len(this.Data) || int(j) < len(other.Data) {
135
135
-
if int(i) >= len(this.Data) {
136
136
-
// first file is done; the rest of the lines from file 2 can go in
137
137
-
mergedFile.AddLine(other.Data[j])
138
138
-
j++
139
139
-
continue
140
140
-
}
141
141
-
142
142
-
if int(j) >= len(other.Data) {
143
143
-
// first file is done; the rest of the lines from file 2 can go in
144
144
-
mergedFile.AddLine(this.Data[i])
145
145
-
i++
146
146
-
continue
147
147
-
}
148
148
-
149
149
-
line1 := this.Data[i]
150
150
-
line2 := other.Data[j]
151
151
-
152
152
-
if line1.LineNumber == line2.LineNumber {
153
153
-
if line1.Content != line2.Content {
154
154
-
return nil, MergeError{
155
155
-
msg: "mismatching lines, this patch might have undergone rebase",
156
156
-
mismatchingLine: line1.LineNumber,
157
157
-
}
158
158
-
} else {
159
159
-
mergedFile.AddLine(line1)
160
160
-
}
161
161
-
i++
162
162
-
j++
163
163
-
} else if line1.LineNumber < line2.LineNumber {
164
164
-
mergedFile.AddLine(line1)
165
165
-
i++
166
166
-
} else {
167
167
-
mergedFile.AddLine(line2)
168
168
-
j++
169
169
-
}
170
170
-
}
171
171
-
172
172
-
return &mergedFile, nil
173
173
-
}
174
174
-
175
175
-
func (r *ReconstructedFile) Apply(patch *gitdiff.File) (string, error) {
176
176
-
original := r.String()
177
177
-
var buffer bytes.Buffer
178
178
-
reader := strings.NewReader(original)
179
179
-
180
180
-
err := gitdiff.Apply(&buffer, reader, patch)
181
181
-
if err != nil {
182
182
-
return "", err
183
183
-
}
184
184
-
185
185
-
return buffer.String(), nil
186
186
-
}
187
187
-
188
188
-
func Unified(oldText, oldFile, newText, newFile string) (string, error) {
189
189
-
oldTemp, err := os.CreateTemp("", "old_*")
190
190
-
if err != nil {
191
191
-
return "", fmt.Errorf("failed to create temp file for oldText: %w", err)
192
192
-
}
193
193
-
defer os.Remove(oldTemp.Name())
194
194
-
if _, err := oldTemp.WriteString(oldText); err != nil {
195
195
-
return "", fmt.Errorf("failed to write to old temp file: %w", err)
196
196
-
}
197
197
-
oldTemp.Close()
198
198
-
199
199
-
newTemp, err := os.CreateTemp("", "new_*")
200
200
-
if err != nil {
201
201
-
return "", fmt.Errorf("failed to create temp file for newText: %w", err)
202
202
-
}
203
203
-
defer os.Remove(newTemp.Name())
204
204
-
if _, err := newTemp.WriteString(newText); err != nil {
205
205
-
return "", fmt.Errorf("failed to write to new temp file: %w", err)
206
206
-
}
207
207
-
newTemp.Close()
208
208
-
209
209
-
cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())
210
210
-
output, err := cmd.CombinedOutput()
211
211
-
212
212
-
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
213
213
-
return string(output), nil
214
214
-
}
215
215
-
if err != nil {
216
216
-
return "", fmt.Errorf("diff command failed: %w", err)
217
217
-
}
218
218
-
219
219
-
return string(output), nil
220
220
-
}
221
221
-
222
222
-
type InterdiffResult struct {
223
223
-
Files []*InterdiffFile
224
224
-
}
225
225
-
226
226
-
func (i *InterdiffResult) String() string {
227
227
-
var b strings.Builder
228
228
-
for _, f := range i.Files {
229
229
-
b.WriteString(f.String())
230
230
-
b.WriteString("\n")
231
231
-
}
232
232
-
233
233
-
return b.String()
234
234
-
}
235
235
-
236
236
-
type InterdiffFile struct {
237
237
-
*gitdiff.File
238
238
-
Name string
239
239
-
Status InterdiffFileStatus
240
240
-
}
241
241
-
242
242
-
func (s *InterdiffFile) String() string {
243
243
-
var b strings.Builder
244
244
-
b.WriteString(s.Status.String())
245
245
-
b.WriteString(" ")
246
246
-
247
247
-
if s.File != nil {
248
248
-
b.WriteString(bestName(s.File))
249
249
-
b.WriteString("\n")
250
250
-
b.WriteString(s.File.String())
251
251
-
}
252
252
-
253
253
-
return b.String()
254
254
-
}
255
255
-
256
256
-
type InterdiffFileStatus struct {
257
257
-
StatusKind StatusKind
258
258
-
Error error
259
259
-
}
260
260
-
261
261
-
func (s *InterdiffFileStatus) String() string {
262
262
-
kind := s.StatusKind.String()
263
263
-
if s.Error != nil {
264
264
-
return fmt.Sprintf("%s [%s]", kind, s.Error.Error())
265
265
-
} else {
266
266
-
return kind
267
267
-
}
268
268
-
}
269
269
-
270
270
-
func (s *InterdiffFileStatus) IsOk() bool {
271
271
-
return s.StatusKind == StatusOk
272
272
-
}
273
273
-
274
274
-
func (s *InterdiffFileStatus) IsUnchanged() bool {
275
275
-
return s.StatusKind == StatusUnchanged
276
276
-
}
277
277
-
278
278
-
func (s *InterdiffFileStatus) IsOnlyInOne() bool {
279
279
-
return s.StatusKind == StatusOnlyInOne
280
280
-
}
281
281
-
282
282
-
func (s *InterdiffFileStatus) IsOnlyInTwo() bool {
283
283
-
return s.StatusKind == StatusOnlyInTwo
284
284
-
}
285
285
-
286
286
-
func (s *InterdiffFileStatus) IsRebased() bool {
287
287
-
return s.StatusKind == StatusRebased
288
288
-
}
289
289
-
290
290
-
func (s *InterdiffFileStatus) IsError() bool {
291
291
-
return s.StatusKind == StatusError
292
292
-
}
293
293
-
294
294
-
type StatusKind int
295
295
-
296
296
-
func (k StatusKind) String() string {
297
297
-
switch k {
298
298
-
case StatusOnlyInOne:
299
299
-
return "only in one"
300
300
-
case StatusOnlyInTwo:
301
301
-
return "only in two"
302
302
-
case StatusUnchanged:
303
303
-
return "unchanged"
304
304
-
case StatusRebased:
305
305
-
return "rebased"
306
306
-
case StatusError:
307
307
-
return "error"
308
308
-
default:
309
309
-
return "changed"
310
310
-
}
311
311
-
}
312
312
-
313
313
-
const (
314
314
-
StatusOk StatusKind = iota
315
315
-
StatusOnlyInOne
316
316
-
StatusOnlyInTwo
317
317
-
StatusUnchanged
318
318
-
StatusRebased
319
319
-
StatusError
320
320
-
)
321
321
-
322
322
-
func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {
323
323
-
re1 := CreateOriginal(f1)
324
324
-
re2 := CreateOriginal(f2)
325
325
-
326
326
-
interdiffFile := InterdiffFile{
327
327
-
Name: bestName(f1),
328
328
-
}
329
329
-
330
330
-
merged, err := re1.Merge(&re2)
331
331
-
if err != nil {
332
332
-
interdiffFile.Status = InterdiffFileStatus{
333
333
-
StatusKind: StatusRebased,
334
334
-
Error: err,
335
335
-
}
336
336
-
return &interdiffFile
337
337
-
}
338
338
-
339
339
-
rev1, err := merged.Apply(f1)
340
340
-
if err != nil {
341
341
-
interdiffFile.Status = InterdiffFileStatus{
342
342
-
StatusKind: StatusError,
343
343
-
Error: err,
344
344
-
}
345
345
-
return &interdiffFile
346
346
-
}
347
347
-
348
348
-
rev2, err := merged.Apply(f2)
349
349
-
if err != nil {
350
350
-
interdiffFile.Status = InterdiffFileStatus{
351
351
-
StatusKind: StatusError,
352
352
-
Error: err,
353
353
-
}
354
354
-
return &interdiffFile
355
355
-
}
356
356
-
357
357
-
diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))
358
358
-
if err != nil {
359
359
-
interdiffFile.Status = InterdiffFileStatus{
360
360
-
StatusKind: StatusError,
361
361
-
Error: err,
362
362
-
}
363
363
-
return &interdiffFile
364
364
-
}
365
365
-
366
366
-
parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
367
367
-
if err != nil {
368
368
-
interdiffFile.Status = InterdiffFileStatus{
369
369
-
StatusKind: StatusError,
370
370
-
Error: err,
371
371
-
}
372
372
-
return &interdiffFile
373
373
-
}
374
374
-
375
375
-
if len(parsed) != 1 {
376
376
-
// files are identical?
377
377
-
interdiffFile.Status = InterdiffFileStatus{
378
378
-
StatusKind: StatusUnchanged,
379
379
-
}
380
380
-
return &interdiffFile
381
381
-
}
382
382
-
383
383
-
if interdiffFile.Status.StatusKind == StatusOk {
384
384
-
interdiffFile.File = parsed[0]
385
385
-
}
386
386
-
387
387
-
return &interdiffFile
388
388
-
}
389
389
-
390
390
-
func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult {
391
391
-
fileToIdx1 := make(map[string]int)
392
392
-
fileToIdx2 := make(map[string]int)
393
393
-
visited := make(map[string]struct{})
394
394
-
var result InterdiffResult
395
395
-
396
396
-
for idx, f := range patch1 {
397
397
-
fileToIdx1[bestName(f)] = idx
398
398
-
}
399
399
-
400
400
-
for idx, f := range patch2 {
401
401
-
fileToIdx2[bestName(f)] = idx
402
402
-
}
403
403
-
404
404
-
for _, f1 := range patch1 {
405
405
-
var interdiffFile *InterdiffFile
406
406
-
407
407
-
fileName := bestName(f1)
408
408
-
if idx, ok := fileToIdx2[fileName]; ok {
409
409
-
f2 := patch2[idx]
410
410
-
411
411
-
// we have f1 and f2, calculate interdiff
412
412
-
interdiffFile = interdiffFiles(f1, f2)
413
413
-
} else {
414
414
-
// only in patch 1, this change would have to be "inverted" to dissapear
415
415
-
// from patch 2, so we reverseDiff(f1)
416
416
-
reverseDiff(f1)
417
417
-
418
418
-
interdiffFile = &InterdiffFile{
419
419
-
File: f1,
420
420
-
Name: fileName,
421
421
-
Status: InterdiffFileStatus{
422
422
-
StatusKind: StatusOnlyInOne,
423
423
-
},
424
424
-
}
425
425
-
}
426
426
-
427
427
-
result.Files = append(result.Files, interdiffFile)
428
428
-
visited[fileName] = struct{}{}
429
429
-
}
430
430
-
431
431
-
// for all files in patch2 that remain unvisited; we can just add them into the output
432
432
-
for _, f2 := range patch2 {
433
433
-
fileName := bestName(f2)
434
434
-
if _, ok := visited[fileName]; ok {
435
435
-
continue
436
436
-
}
437
437
-
438
438
-
result.Files = append(result.Files, &InterdiffFile{
439
439
-
File: f2,
440
440
-
Name: fileName,
441
441
-
Status: InterdiffFileStatus{
442
442
-
StatusKind: StatusOnlyInTwo,
443
443
-
},
444
444
-
})
445
445
-
}
446
446
-
447
447
-
return &result
448
448
-
}
+168
patchutil/combinediff.go
···
1
1
+
package patchutil
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"strings"
6
6
+
7
7
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
8
8
+
)
9
9
+
10
10
+
// original1 -> patch1 -> rev1
11
11
+
// original2 -> patch2 -> rev2
12
12
+
//
13
13
+
// original2 must be equal to rev1, so we can merge them to get maximal context
14
14
+
//
15
15
+
// finally,
16
16
+
// rev2' <- apply(patch2, merged)
17
17
+
// combineddiff <- diff(rev2', original1)
18
18
+
func combineFiles(file1, file2 *gitdiff.File) (*gitdiff.File, error) {
19
19
+
fileName := bestName(file1)
20
20
+
21
21
+
o1 := CreatePreImage(file1)
22
22
+
r1 := CreatePostImage(file1)
23
23
+
o2 := CreatePreImage(file2)
24
24
+
25
25
+
merged, err := r1.Merge(&o2)
26
26
+
if err != nil {
27
27
+
return nil, err
28
28
+
}
29
29
+
30
30
+
r2Prime, err := merged.Apply(file2)
31
31
+
if err != nil {
32
32
+
return nil, err
33
33
+
}
34
34
+
35
35
+
// produce combined diff
36
36
+
diff, err := Unified(o1.String(), fileName, r2Prime, fileName)
37
37
+
if err != nil {
38
38
+
return nil, err
39
39
+
}
40
40
+
41
41
+
parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
42
42
+
43
43
+
if len(parsed) != 1 {
44
44
+
// no diff? the second commit reverted the changes from the first
45
45
+
return nil, nil
46
46
+
}
47
47
+
48
48
+
return parsed[0], nil
49
49
+
}
50
50
+
51
51
+
// use empty lines for lines we are unaware of
52
52
+
//
53
53
+
// this raises an error only if the two patches were invalid or non-contiguous
54
54
+
func mergeLines(old, new string) (string, error) {
55
55
+
var i, j int
56
56
+
57
57
+
// TODO: use strings.Lines
58
58
+
linesOld := strings.Split(old, "\n")
59
59
+
linesNew := strings.Split(new, "\n")
60
60
+
61
61
+
result := []string{}
62
62
+
63
63
+
for i < len(linesOld) || j < len(linesNew) {
64
64
+
if i >= len(linesOld) {
65
65
+
// rest of the file is populated from `new`
66
66
+
result = append(result, linesNew[j])
67
67
+
j++
68
68
+
continue
69
69
+
}
70
70
+
71
71
+
if j >= len(linesNew) {
72
72
+
// rest of the file is populated from `old`
73
73
+
result = append(result, linesOld[i])
74
74
+
i++
75
75
+
continue
76
76
+
}
77
77
+
78
78
+
oldLine := linesOld[i]
79
79
+
newLine := linesNew[j]
80
80
+
81
81
+
if oldLine != newLine && (oldLine != "" && newLine != "") {
82
82
+
// context mismatch
83
83
+
return "", fmt.Errorf("failed to merge files, found context mismatch at %d; oldLine: `%s`, newline: `%s`", i+1, oldLine, newLine)
84
84
+
}
85
85
+
86
86
+
if oldLine == newLine {
87
87
+
result = append(result, oldLine)
88
88
+
} else if oldLine == "" {
89
89
+
result = append(result, newLine)
90
90
+
} else if newLine == "" {
91
91
+
result = append(result, oldLine)
92
92
+
}
93
93
+
i++
94
94
+
j++
95
95
+
}
96
96
+
97
97
+
return strings.Join(result, "\n"), nil
98
98
+
}
99
99
+
100
100
+
func combineTwo(patch1, patch2 []*gitdiff.File) []*gitdiff.File {
101
101
+
fileToIdx1 := make(map[string]int)
102
102
+
fileToIdx2 := make(map[string]int)
103
103
+
visited := make(map[string]struct{})
104
104
+
var result []*gitdiff.File
105
105
+
106
106
+
for idx, f := range patch1 {
107
107
+
fileToIdx1[bestName(f)] = idx
108
108
+
}
109
109
+
110
110
+
for idx, f := range patch2 {
111
111
+
fileToIdx2[bestName(f)] = idx
112
112
+
}
113
113
+
114
114
+
for _, f1 := range patch1 {
115
115
+
fileName := bestName(f1)
116
116
+
if idx, ok := fileToIdx2[fileName]; ok {
117
117
+
f2 := patch2[idx]
118
118
+
119
119
+
// we have f1 and f2, combine them
120
120
+
combined, err := combineFiles(f1, f2)
121
121
+
if err != nil {
122
122
+
fmt.Println(err)
123
123
+
}
124
124
+
125
125
+
result = append(result, combined)
126
126
+
} else {
127
127
+
// only in patch1; add as-is
128
128
+
result = append(result, f1)
129
129
+
}
130
130
+
131
131
+
visited[fileName] = struct{}{}
132
132
+
}
133
133
+
134
134
+
// for all files in patch2 that remain unvisited; we can just add them into the output
135
135
+
for _, f2 := range patch2 {
136
136
+
fileName := bestName(f2)
137
137
+
if _, ok := visited[fileName]; ok {
138
138
+
continue
139
139
+
}
140
140
+
141
141
+
result = append(result, f2)
142
142
+
}
143
143
+
144
144
+
return result
145
145
+
}
146
146
+
147
147
+
// pairwise combination from first to last patch
148
148
+
func CombineDiff(patches ...[]*gitdiff.File) []*gitdiff.File {
149
149
+
if len(patches) == 0 {
150
150
+
return nil
151
151
+
}
152
152
+
153
153
+
if len(patches) == 1 {
154
154
+
return patches[0]
155
155
+
}
156
156
+
157
157
+
combined := combineTwo(patches[0], patches[1])
158
158
+
159
159
+
newPatches := [][]*gitdiff.File{}
160
160
+
newPatches = append(newPatches, combined)
161
161
+
for i, p := range patches {
162
162
+
if i >= 2 {
163
163
+
newPatches = append(newPatches, p)
164
164
+
}
165
165
+
}
166
166
+
167
167
+
return CombineDiff(newPatches...)
168
168
+
}
+178
patchutil/image.go
···
1
1
+
package patchutil
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"fmt"
6
6
+
"strings"
7
7
+
8
8
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
9
9
+
)
10
10
+
11
11
+
type Line struct {
12
12
+
LineNumber int64
13
13
+
Content string
14
14
+
IsUnknown bool
15
15
+
}
16
16
+
17
17
+
func NewLineAt(lineNumber int64, content string) Line {
18
18
+
return Line{
19
19
+
LineNumber: lineNumber,
20
20
+
Content: content,
21
21
+
IsUnknown: false,
22
22
+
}
23
23
+
}
24
24
+
25
25
+
type Image struct {
26
26
+
File string
27
27
+
Data []*Line
28
28
+
}
29
29
+
30
30
+
func (r *Image) String() string {
31
31
+
var i, j int64
32
32
+
var b strings.Builder
33
33
+
for {
34
34
+
i += 1
35
35
+
36
36
+
if int(j) >= (len(r.Data)) {
37
37
+
break
38
38
+
}
39
39
+
40
40
+
if r.Data[j].LineNumber == i {
41
41
+
// b.WriteString(fmt.Sprintf("%d:", r.Data[j].LineNumber))
42
42
+
b.WriteString(r.Data[j].Content)
43
43
+
j += 1
44
44
+
} else {
45
45
+
//b.WriteString(fmt.Sprintf("%d:\n", i))
46
46
+
b.WriteString("\n")
47
47
+
}
48
48
+
}
49
49
+
50
50
+
return b.String()
51
51
+
}
52
52
+
53
53
+
func (r *Image) AddLine(line *Line) {
54
54
+
r.Data = append(r.Data, line)
55
55
+
}
56
56
+
57
57
+
// rebuild the original file from a patch
58
58
+
func CreatePreImage(file *gitdiff.File) Image {
59
59
+
rf := Image{
60
60
+
File: bestName(file),
61
61
+
}
62
62
+
63
63
+
for _, fragment := range file.TextFragments {
64
64
+
position := fragment.OldPosition
65
65
+
for _, line := range fragment.Lines {
66
66
+
switch line.Op {
67
67
+
case gitdiff.OpContext:
68
68
+
rl := NewLineAt(position, line.Line)
69
69
+
rf.Data = append(rf.Data, &rl)
70
70
+
position += 1
71
71
+
case gitdiff.OpDelete:
72
72
+
rl := NewLineAt(position, line.Line)
73
73
+
rf.Data = append(rf.Data, &rl)
74
74
+
position += 1
75
75
+
case gitdiff.OpAdd:
76
76
+
// do nothing here
77
77
+
}
78
78
+
}
79
79
+
}
80
80
+
81
81
+
return rf
82
82
+
}
83
83
+
84
84
+
// rebuild the revised file from a patch
85
85
+
func CreatePostImage(file *gitdiff.File) Image {
86
86
+
rf := Image{
87
87
+
File: bestName(file),
88
88
+
}
89
89
+
90
90
+
for _, fragment := range file.TextFragments {
91
91
+
position := fragment.NewPosition
92
92
+
for _, line := range fragment.Lines {
93
93
+
switch line.Op {
94
94
+
case gitdiff.OpContext:
95
95
+
rl := NewLineAt(position, line.Line)
96
96
+
rf.Data = append(rf.Data, &rl)
97
97
+
position += 1
98
98
+
case gitdiff.OpAdd:
99
99
+
rl := NewLineAt(position, line.Line)
100
100
+
rf.Data = append(rf.Data, &rl)
101
101
+
position += 1
102
102
+
case gitdiff.OpDelete:
103
103
+
// do nothing here
104
104
+
}
105
105
+
}
106
106
+
}
107
107
+
108
108
+
return rf
109
109
+
}
110
110
+
111
111
+
type MergeError struct {
112
112
+
msg string
113
113
+
mismatchingLine int64
114
114
+
}
115
115
+
116
116
+
func (m MergeError) Error() string {
117
117
+
return fmt.Sprintf("%s: %v", m.msg, m.mismatchingLine)
118
118
+
}
119
119
+
120
120
+
// best effort merging of two reconstructed files
121
121
+
func (this *Image) Merge(other *Image) (*Image, error) {
122
122
+
mergedFile := Image{}
123
123
+
124
124
+
var i, j int64
125
125
+
126
126
+
for int(i) < len(this.Data) || int(j) < len(other.Data) {
127
127
+
if int(i) >= len(this.Data) {
128
128
+
// first file is done; the rest of the lines from file 2 can go in
129
129
+
mergedFile.AddLine(other.Data[j])
130
130
+
j++
131
131
+
continue
132
132
+
}
133
133
+
134
134
+
if int(j) >= len(other.Data) {
135
135
+
// first file is done; the rest of the lines from file 2 can go in
136
136
+
mergedFile.AddLine(this.Data[i])
137
137
+
i++
138
138
+
continue
139
139
+
}
140
140
+
141
141
+
line1 := this.Data[i]
142
142
+
line2 := other.Data[j]
143
143
+
144
144
+
if line1.LineNumber == line2.LineNumber {
145
145
+
if line1.Content != line2.Content {
146
146
+
return nil, MergeError{
147
147
+
msg: "mismatching lines, this patch might have undergone rebase",
148
148
+
mismatchingLine: line1.LineNumber,
149
149
+
}
150
150
+
} else {
151
151
+
mergedFile.AddLine(line1)
152
152
+
}
153
153
+
i++
154
154
+
j++
155
155
+
} else if line1.LineNumber < line2.LineNumber {
156
156
+
mergedFile.AddLine(line1)
157
157
+
i++
158
158
+
} else {
159
159
+
mergedFile.AddLine(line2)
160
160
+
j++
161
161
+
}
162
162
+
}
163
163
+
164
164
+
return &mergedFile, nil
165
165
+
}
166
166
+
167
167
+
func (r *Image) Apply(patch *gitdiff.File) (string, error) {
168
168
+
original := r.String()
169
169
+
var buffer bytes.Buffer
170
170
+
reader := strings.NewReader(original)
171
171
+
172
172
+
err := gitdiff.Apply(&buffer, reader, patch)
173
173
+
if err != nil {
174
174
+
return "", err
175
175
+
}
176
176
+
177
177
+
return buffer.String(), nil
178
178
+
}
+236
patchutil/interdiff.go
···
1
1
+
package patchutil
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"strings"
6
6
+
7
7
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
8
8
+
)
9
9
+
10
10
+
type InterdiffResult struct {
11
11
+
Files []*InterdiffFile
12
12
+
}
13
13
+
14
14
+
func (i *InterdiffResult) String() string {
15
15
+
var b strings.Builder
16
16
+
for _, f := range i.Files {
17
17
+
b.WriteString(f.String())
18
18
+
b.WriteString("\n")
19
19
+
}
20
20
+
21
21
+
return b.String()
22
22
+
}
23
23
+
24
24
+
type InterdiffFile struct {
25
25
+
*gitdiff.File
26
26
+
Name string
27
27
+
Status InterdiffFileStatus
28
28
+
}
29
29
+
30
30
+
func (s *InterdiffFile) String() string {
31
31
+
var b strings.Builder
32
32
+
b.WriteString(s.Status.String())
33
33
+
b.WriteString(" ")
34
34
+
35
35
+
if s.File != nil {
36
36
+
b.WriteString(bestName(s.File))
37
37
+
b.WriteString("\n")
38
38
+
b.WriteString(s.File.String())
39
39
+
}
40
40
+
41
41
+
return b.String()
42
42
+
}
43
43
+
44
44
+
type InterdiffFileStatus struct {
45
45
+
StatusKind StatusKind
46
46
+
Error error
47
47
+
}
48
48
+
49
49
+
func (s *InterdiffFileStatus) String() string {
50
50
+
kind := s.StatusKind.String()
51
51
+
if s.Error != nil {
52
52
+
return fmt.Sprintf("%s [%s]", kind, s.Error.Error())
53
53
+
} else {
54
54
+
return kind
55
55
+
}
56
56
+
}
57
57
+
58
58
+
func (s *InterdiffFileStatus) IsOk() bool {
59
59
+
return s.StatusKind == StatusOk
60
60
+
}
61
61
+
62
62
+
func (s *InterdiffFileStatus) IsUnchanged() bool {
63
63
+
return s.StatusKind == StatusUnchanged
64
64
+
}
65
65
+
66
66
+
func (s *InterdiffFileStatus) IsOnlyInOne() bool {
67
67
+
return s.StatusKind == StatusOnlyInOne
68
68
+
}
69
69
+
70
70
+
func (s *InterdiffFileStatus) IsOnlyInTwo() bool {
71
71
+
return s.StatusKind == StatusOnlyInTwo
72
72
+
}
73
73
+
74
74
+
func (s *InterdiffFileStatus) IsRebased() bool {
75
75
+
return s.StatusKind == StatusRebased
76
76
+
}
77
77
+
78
78
+
func (s *InterdiffFileStatus) IsError() bool {
79
79
+
return s.StatusKind == StatusError
80
80
+
}
81
81
+
82
82
+
type StatusKind int
83
83
+
84
84
+
func (k StatusKind) String() string {
85
85
+
switch k {
86
86
+
case StatusOnlyInOne:
87
87
+
return "only in one"
88
88
+
case StatusOnlyInTwo:
89
89
+
return "only in two"
90
90
+
case StatusUnchanged:
91
91
+
return "unchanged"
92
92
+
case StatusRebased:
93
93
+
return "rebased"
94
94
+
case StatusError:
95
95
+
return "error"
96
96
+
default:
97
97
+
return "changed"
98
98
+
}
99
99
+
}
100
100
+
101
101
+
const (
102
102
+
StatusOk StatusKind = iota
103
103
+
StatusOnlyInOne
104
104
+
StatusOnlyInTwo
105
105
+
StatusUnchanged
106
106
+
StatusRebased
107
107
+
StatusError
108
108
+
)
109
109
+
110
110
+
func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {
111
111
+
re1 := CreatePreImage(f1)
112
112
+
re2 := CreatePreImage(f2)
113
113
+
114
114
+
interdiffFile := InterdiffFile{
115
115
+
Name: bestName(f1),
116
116
+
}
117
117
+
118
118
+
merged, err := re1.Merge(&re2)
119
119
+
if err != nil {
120
120
+
interdiffFile.Status = InterdiffFileStatus{
121
121
+
StatusKind: StatusRebased,
122
122
+
Error: err,
123
123
+
}
124
124
+
return &interdiffFile
125
125
+
}
126
126
+
127
127
+
rev1, err := merged.Apply(f1)
128
128
+
if err != nil {
129
129
+
interdiffFile.Status = InterdiffFileStatus{
130
130
+
StatusKind: StatusError,
131
131
+
Error: err,
132
132
+
}
133
133
+
return &interdiffFile
134
134
+
}
135
135
+
136
136
+
rev2, err := merged.Apply(f2)
137
137
+
if err != nil {
138
138
+
interdiffFile.Status = InterdiffFileStatus{
139
139
+
StatusKind: StatusError,
140
140
+
Error: err,
141
141
+
}
142
142
+
return &interdiffFile
143
143
+
}
144
144
+
145
145
+
diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))
146
146
+
if err != nil {
147
147
+
interdiffFile.Status = InterdiffFileStatus{
148
148
+
StatusKind: StatusError,
149
149
+
Error: err,
150
150
+
}
151
151
+
return &interdiffFile
152
152
+
}
153
153
+
154
154
+
parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
155
155
+
if err != nil {
156
156
+
interdiffFile.Status = InterdiffFileStatus{
157
157
+
StatusKind: StatusError,
158
158
+
Error: err,
159
159
+
}
160
160
+
return &interdiffFile
161
161
+
}
162
162
+
163
163
+
if len(parsed) != 1 {
164
164
+
// files are identical?
165
165
+
interdiffFile.Status = InterdiffFileStatus{
166
166
+
StatusKind: StatusUnchanged,
167
167
+
}
168
168
+
return &interdiffFile
169
169
+
}
170
170
+
171
171
+
if interdiffFile.Status.StatusKind == StatusOk {
172
172
+
interdiffFile.File = parsed[0]
173
173
+
}
174
174
+
175
175
+
return &interdiffFile
176
176
+
}
177
177
+
178
178
+
func Interdiff(patch1, patch2 []*gitdiff.File) *InterdiffResult {
179
179
+
fileToIdx1 := make(map[string]int)
180
180
+
fileToIdx2 := make(map[string]int)
181
181
+
visited := make(map[string]struct{})
182
182
+
var result InterdiffResult
183
183
+
184
184
+
for idx, f := range patch1 {
185
185
+
fileToIdx1[bestName(f)] = idx
186
186
+
}
187
187
+
188
188
+
for idx, f := range patch2 {
189
189
+
fileToIdx2[bestName(f)] = idx
190
190
+
}
191
191
+
192
192
+
for _, f1 := range patch1 {
193
193
+
var interdiffFile *InterdiffFile
194
194
+
195
195
+
fileName := bestName(f1)
196
196
+
if idx, ok := fileToIdx2[fileName]; ok {
197
197
+
f2 := patch2[idx]
198
198
+
199
199
+
// we have f1 and f2, calculate interdiff
200
200
+
interdiffFile = interdiffFiles(f1, f2)
201
201
+
} else {
202
202
+
// only in patch 1, this change would have to be "inverted" to dissapear
203
203
+
// from patch 2, so we reverseDiff(f1)
204
204
+
reverseDiff(f1)
205
205
+
206
206
+
interdiffFile = &InterdiffFile{
207
207
+
File: f1,
208
208
+
Name: fileName,
209
209
+
Status: InterdiffFileStatus{
210
210
+
StatusKind: StatusOnlyInOne,
211
211
+
},
212
212
+
}
213
213
+
}
214
214
+
215
215
+
result.Files = append(result.Files, interdiffFile)
216
216
+
visited[fileName] = struct{}{}
217
217
+
}
218
218
+
219
219
+
// for all files in patch2 that remain unvisited; we can just add them into the output
220
220
+
for _, f2 := range patch2 {
221
221
+
fileName := bestName(f2)
222
222
+
if _, ok := visited[fileName]; ok {
223
223
+
continue
224
224
+
}
225
225
+
226
226
+
result.Files = append(result.Files, &InterdiffFile{
227
227
+
File: f2,
228
228
+
Name: fileName,
229
229
+
Status: InterdiffFileStatus{
230
230
+
StatusKind: StatusOnlyInTwo,
231
231
+
},
232
232
+
})
233
233
+
}
234
234
+
235
235
+
return &result
236
236
+
}
+69
patchutil/patchutil.go
···
2
2
3
3
import (
4
4
"fmt"
5
5
+
"os"
6
6
+
"os/exec"
5
7
"regexp"
6
8
"strings"
7
9
···
125
127
}
126
128
return patches
127
129
}
130
130
+
131
131
+
func bestName(file *gitdiff.File) string {
132
132
+
if file.IsDelete {
133
133
+
return file.OldName
134
134
+
} else {
135
135
+
return file.NewName
136
136
+
}
137
137
+
}
138
138
+
139
139
+
// in-place reverse of a diff
140
140
+
func reverseDiff(file *gitdiff.File) {
141
141
+
file.OldName, file.NewName = file.NewName, file.OldName
142
142
+
file.OldMode, file.NewMode = file.NewMode, file.OldMode
143
143
+
file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment
144
144
+
145
145
+
for _, fragment := range file.TextFragments {
146
146
+
// swap postions
147
147
+
fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition
148
148
+
fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines
149
149
+
fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded
150
150
+
151
151
+
for i := range fragment.Lines {
152
152
+
switch fragment.Lines[i].Op {
153
153
+
case gitdiff.OpAdd:
154
154
+
fragment.Lines[i].Op = gitdiff.OpDelete
155
155
+
case gitdiff.OpDelete:
156
156
+
fragment.Lines[i].Op = gitdiff.OpAdd
157
157
+
default:
158
158
+
// do nothing
159
159
+
}
160
160
+
}
161
161
+
}
162
162
+
}
163
163
+
164
164
+
func Unified(oldText, oldFile, newText, newFile string) (string, error) {
165
165
+
oldTemp, err := os.CreateTemp("", "old_*")
166
166
+
if err != nil {
167
167
+
return "", fmt.Errorf("failed to create temp file for oldText: %w", err)
168
168
+
}
169
169
+
defer os.Remove(oldTemp.Name())
170
170
+
if _, err := oldTemp.WriteString(oldText); err != nil {
171
171
+
return "", fmt.Errorf("failed to write to old temp file: %w", err)
172
172
+
}
173
173
+
oldTemp.Close()
174
174
+
175
175
+
newTemp, err := os.CreateTemp("", "new_*")
176
176
+
if err != nil {
177
177
+
return "", fmt.Errorf("failed to create temp file for newText: %w", err)
178
178
+
}
179
179
+
defer os.Remove(newTemp.Name())
180
180
+
if _, err := newTemp.WriteString(newText); err != nil {
181
181
+
return "", fmt.Errorf("failed to write to new temp file: %w", err)
182
182
+
}
183
183
+
newTemp.Close()
184
184
+
185
185
+
cmd := exec.Command("diff", "-U", "9999", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name())
186
186
+
output, err := cmd.CombinedOutput()
187
187
+
188
188
+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
189
189
+
return string(output), nil
190
190
+
}
191
191
+
if err != nil {
192
192
+
return "", fmt.Errorf("diff command failed: %w", err)
193
193
+
}
194
194
+
195
195
+
return string(output), nil
196
196
+
}