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