+27
-2
appview/db/pulls.go
+27
-2
appview/db/pulls.go
···
150
150
return false
151
151
}
152
152
153
-
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
153
+
func (s PullSubmission) AsDiff(targetBranch string) ([]*gitdiff.File, error) {
154
154
patch := s.Patch
155
155
156
-
diffs, _, err := gitdiff.Parse(strings.NewReader(patch))
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)
157
182
if err != nil {
158
183
log.Println(err)
159
184
}
+21
-5
appview/pages/pages.go
+21
-5
appview/pages/pages.go
···
16
16
"slices"
17
17
"strings"
18
18
19
+
"tangled.sh/tangled.sh/core/appview/auth"
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
+
19
26
"github.com/alecthomas/chroma/v2"
20
27
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
21
28
"github.com/alecthomas/chroma/v2/lexers"
22
29
"github.com/alecthomas/chroma/v2/styles"
23
30
"github.com/bluesky-social/indigo/atproto/syntax"
24
31
"github.com/microcosm-cc/bluemonday"
25
-
"tangled.sh/tangled.sh/core/appview/auth"
26
-
"tangled.sh/tangled.sh/core/appview/db"
27
-
"tangled.sh/tangled.sh/core/appview/pages/markup"
28
-
"tangled.sh/tangled.sh/core/appview/state/userutil"
29
-
"tangled.sh/tangled.sh/core/types"
30
32
)
31
33
32
34
//go:embed templates/* static
···
705
707
// this name is a mouthful
706
708
func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error {
707
709
return p.execute("repo/pulls/patch", w, params)
710
+
}
711
+
712
+
type RepoPullInterdiffParams struct {
713
+
LoggedInUser *auth.User
714
+
DidHandleMap map[string]string
715
+
RepoInfo RepoInfo
716
+
Pull *db.Pull
717
+
Round int
718
+
Interdiff *patchutil.InterdiffResult
719
+
}
720
+
721
+
// this name is a mouthful
722
+
func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error {
723
+
return p.execute("repo/pulls/interdiff", w, params)
708
724
}
709
725
710
726
type PullPatchUploadParams struct {
+148
appview/pages/templates/repo/fragments/interdiff.html
+148
appview/pages/templates/repo/fragments/interdiff.html
···
1
+
{{ define "repo/fragments/interdiff" }}
2
+
{{ $repo := index . 0 }}
3
+
{{ $x := index . 1 }}
4
+
{{ $diff := $x.Files }}
5
+
6
+
<section class="mt-6 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
7
+
<div class="diff-stat">
8
+
<div class="flex gap-2 items-center">
9
+
<strong class="text-sm uppercase dark:text-gray-200">files</strong>
10
+
</div>
11
+
<div class="overflow-x-auto">
12
+
<ul class="dark:text-gray-200">
13
+
{{ range $diff }}
14
+
<li><a href="#file-{{ .Name }}" class="dark:hover:text-gray-300">{{ .Name }}</a></li>
15
+
{{ end }}
16
+
</ul>
17
+
</div>
18
+
</div>
19
+
</section>
20
+
21
+
{{ $last := sub (len $diff) 1 }}
22
+
{{ range $idx, $hunk := $diff }}
23
+
{{ with $hunk }}
24
+
<section class="mt-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm">
25
+
<div id="file-{{ .Name }}">
26
+
<div id="diff-file">
27
+
<details {{ if not (.Status.IsOnlyInOne) }}open{{end}}>
28
+
<summary class="list-none cursor-pointer sticky top-0">
29
+
<div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between">
30
+
<div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
31
+
<div class="flex gap-1 items-center" style="direction: ltr;">
32
+
{{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }}
33
+
{{ if .Status.IsOk }}
34
+
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">CHANGED</span>
35
+
{{ else if .Status.IsUnchanged }}
36
+
<span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">UNCHANGED</span>
37
+
{{ else if .Status.IsOnlyInOne }}
38
+
<span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">REVERTED</span>
39
+
{{ else if .Status.IsOnlyInTwo }}
40
+
<span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">NEW</span>
41
+
{{ else if .Status.IsRebased }}
42
+
<span class="bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400 {{ $markerstyle }}">REBASED</span>
43
+
{{ else }}
44
+
<span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">ERROR</span>
45
+
{{ end }}
46
+
</div>
47
+
48
+
<div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;">
49
+
<a class="dark:text-white whitespace-nowrap overflow-x-auto" href="">
50
+
{{ .Name }}
51
+
</a>
52
+
</div>
53
+
</div>
54
+
55
+
{{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }}
56
+
<div id="right-side-items" class="p-2 flex items-center">
57
+
<a title="top of file" href="#file-{{ .Name }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a>
58
+
{{ if gt $idx 0 }}
59
+
{{ $prev := index $diff (sub $idx 1) }}
60
+
<a title="previous file" href="#file-{{ $prev.Name }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a>
61
+
{{ end }}
62
+
63
+
{{ if lt $idx $last }}
64
+
{{ $next := index $diff (add $idx 1) }}
65
+
<a title="next file" href="#file-{{ $next.Name }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a>
66
+
{{ end }}
67
+
</div>
68
+
69
+
</div>
70
+
</summary>
71
+
72
+
<div class="transition-all duration-700 ease-in-out">
73
+
{{ if .Status.IsUnchanged }}
74
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
75
+
This file has not been changed.
76
+
</p>
77
+
{{ else if .Status.IsRebased }}
78
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
79
+
This patch was likely rebased, as context lines do not match.
80
+
</p>
81
+
{{ else if .Status.IsError }}
82
+
<p class="text-center text-gray-400 dark:text-gray-500 p-4">
83
+
Failed to calculate interdiff for this file.
84
+
</p>
85
+
{{ else }}
86
+
{{ $name := .Name }}
87
+
<pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">···</div>
88
+
{{- $oldStart := .OldPosition -}}
89
+
{{- $newStart := .NewPosition -}}
90
+
{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 scroll-mt-10 target:border target:border-amber-500 target:rounded " -}}
91
+
{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}}
92
+
{{- $lineNrSepStyle1 := "" -}}
93
+
{{- $lineNrSepStyle2 := "pr-2" -}}
94
+
{{- range .Lines -}}
95
+
{{- if eq .Op.String "+" -}}
96
+
<div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full items-center">
97
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div>
98
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
99
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
100
+
<div class="px-2">{{ .Line }}</div>
101
+
</div>
102
+
{{- $newStart = add64 $newStart 1 -}}
103
+
{{- end -}}
104
+
{{- if eq .Op.String "-" -}}
105
+
<div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full items-center">
106
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
107
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div>
108
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
109
+
<div class="px-2">{{ .Line }}</div>
110
+
</div>
111
+
{{- $oldStart = add64 $oldStart 1 -}}
112
+
{{- end -}}
113
+
{{- if eq .Op.String " " -}}
114
+
<div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full items-center">
115
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div>
116
+
<div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div>
117
+
<div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div>
118
+
<div class="px-2">{{ .Line }}</div>
119
+
</div>
120
+
{{- $newStart = add64 $newStart 1 -}}
121
+
{{- $oldStart = add64 $oldStart 1 -}}
122
+
{{- end -}}
123
+
{{- end -}}
124
+
{{- end -}}</div></div></pre>
125
+
{{- end -}}
126
+
</div>
127
+
128
+
</details>
129
+
130
+
</div>
131
+
</div>
132
+
</section>
133
+
{{ end }}
134
+
{{ end }}
135
+
{{ end }}
136
+
137
+
{{ define "statPill" }}
138
+
<div class="flex items-center font-mono text-sm">
139
+
{{ if and .Insertions .Deletions }}
140
+
<span class="rounded-l p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span>
141
+
<span class="rounded-r p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span>
142
+
{{ else if .Insertions }}
143
+
<span class="rounded p-1 select-none bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400">+{{ .Insertions }}</span>
144
+
{{ else if .Deletions }}
145
+
<span class="rounded p-1 select-none bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400">-{{ .Deletions }}</span>
146
+
{{ end }}
147
+
</div>
148
+
{{ end }}
+25
appview/pages/templates/repo/pulls/interdiff.html
+25
appview/pages/templates/repo/pulls/interdiff.html
···
1
+
{{ define "title" }}
2
+
interdiff of round #{{ .Round }} and #{{ sub .Round 1 }}; pull #{{ .Pull.PullId }} · {{ .RepoInfo.FullName }}
3
+
{{ end }}
4
+
5
+
{{ define "content" }}
6
+
<section class="rounded drop-shadow-sm bg-white dark:bg-gray-800 py-4 px-6 dark:text-white">
7
+
<header class="pb-2">
8
+
<div class="flex gap-3 items-center mb-3">
9
+
<a href="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/" class="flex items-center gap-2 font-medium">
10
+
{{ i "arrow-left" "w-5 h-5" }}
11
+
back
12
+
</a>
13
+
<span class="select-none before:content-['\00B7']"></span>
14
+
interdiff of round #{{ .Round }} and #{{ sub .Round 1 }}
15
+
</div>
16
+
<div class="border-t border-gray-200 dark:border-gray-700 my-2"></div>
17
+
{{ template "repo/pulls/fragments/pullHeader" . }}
18
+
</header>
19
+
</section>
20
+
21
+
<section>
22
+
{{ template "repo/fragments/interdiff" (list .RepoInfo.FullName .Interdiff) }}
23
+
</section>
24
+
{{ end }}
25
+
+7
-2
appview/pages/templates/repo/pulls/pull.html
+7
-2
appview/pages/templates/repo/pulls/pull.html
···
51
51
</span>
52
52
</div>
53
53
54
-
{{ if $.Pull.IsPatchBased }}
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>
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>
61
66
{{ end }}
62
67
</div>
63
68
</summary>
+69
-1
appview/state/pull.go
+69
-1
appview/state/pull.go
···
12
12
"strconv"
13
13
"time"
14
14
15
-
"github.com/go-chi/chi/v5"
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"
···
23
22
comatproto "github.com/bluesky-social/indigo/api/atproto"
24
23
"github.com/bluesky-social/indigo/atproto/syntax"
25
24
lexutil "github.com/bluesky-social/indigo/lex/util"
25
+
"github.com/go-chi/chi/v5"
26
26
)
27
27
28
28
// htmx fragment
···
305
305
Diff: pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch),
306
306
})
307
307
308
+
}
309
+
310
+
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
311
+
user := s.auth.GetUser(r)
312
+
313
+
f, err := fullyResolvedRepo(r)
314
+
if err != nil {
315
+
log.Println("failed to get repo and knot", err)
316
+
return
317
+
}
318
+
319
+
pull, ok := r.Context().Value("pull").(*db.Pull)
320
+
if !ok {
321
+
log.Println("failed to get pull")
322
+
s.pages.Notice(w, "pull-error", "Failed to get pull.")
323
+
return
324
+
}
325
+
326
+
roundId := chi.URLParam(r, "round")
327
+
roundIdInt, err := strconv.Atoi(roundId)
328
+
if err != nil || roundIdInt >= len(pull.Submissions) {
329
+
http.Error(w, "bad round id", http.StatusBadRequest)
330
+
log.Println("failed to parse round id", err)
331
+
return
332
+
}
333
+
334
+
if roundIdInt == 0 {
335
+
http.Error(w, "bad round id", http.StatusBadRequest)
336
+
log.Println("cannot interdiff initial submission")
337
+
return
338
+
}
339
+
340
+
identsToResolve := []string{pull.OwnerDid}
341
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
342
+
didHandleMap := make(map[string]string)
343
+
for _, identity := range resolvedIds {
344
+
if !identity.Handle.IsInvalidHandle() {
345
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
346
+
} else {
347
+
didHandleMap[identity.DID.String()] = identity.DID.String()
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),
369
+
RepoInfo: f.RepoInfo(s, user),
370
+
Pull: pull,
371
+
Round: roundIdInt,
372
+
DidHandleMap: didHandleMap,
373
+
Interdiff: interdiff,
374
+
})
375
+
return
308
376
}
309
377
310
378
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
+1
appview/state/router.go
+38
cmd/combinediff/main.go
+38
cmd/combinediff/main.go
···
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
+
}
+38
cmd/interdiff/main.go
+38
cmd/interdiff/main.go
···
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: interdiff <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
+
interDiffResult := patchutil.Interdiff(files1, files2)
37
+
fmt.Println(interDiffResult)
38
+
}
+3
-3
go.mod
+3
-3
go.mod
···
106
106
go.uber.org/atomic v1.11.0 // indirect
107
107
go.uber.org/multierr v1.11.0 // indirect
108
108
go.uber.org/zap v1.26.0 // indirect
109
-
golang.org/x/crypto v0.36.0 // indirect
110
-
golang.org/x/net v0.37.0 // indirect
111
-
golang.org/x/sys v0.31.0 // indirect
109
+
golang.org/x/crypto v0.37.0 // indirect
110
+
golang.org/x/net v0.39.0 // indirect
111
+
golang.org/x/sys v0.32.0 // indirect
112
112
golang.org/x/time v0.5.0 // indirect
113
113
google.golang.org/protobuf v1.34.2 // indirect
114
114
gopkg.in/warnings.v0 v0.1.2 // indirect
+10
-10
go.sum
+10
-10
go.sum
···
303
303
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
304
304
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
305
305
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
306
-
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
307
-
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
306
+
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
307
+
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
308
308
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
309
309
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
310
310
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
327
327
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
328
328
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
329
329
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
330
-
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
331
-
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
330
+
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
331
+
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
332
332
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
333
333
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
334
334
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
···
357
357
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
358
358
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
359
359
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
360
-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
361
-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
360
+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
361
+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
362
362
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
363
363
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
364
364
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
365
365
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
366
366
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
367
-
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
368
-
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
367
+
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
368
+
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
369
369
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
370
370
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
371
371
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
372
372
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
373
373
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
374
374
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
375
-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
376
-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
375
+
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
376
+
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
377
377
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
378
378
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
379
379
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+168
patchutil/combinediff.go
+168
patchutil/combinediff.go
···
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
+178
patchutil/image.go
···
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
+236
patchutil/interdiff.go
···
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
+69
patchutil/patchutil.go
···
2
2
3
3
import (
4
4
"fmt"
5
+
"os"
6
+
"os/exec"
5
7
"regexp"
6
8
"strings"
7
9
···
125
127
}
126
128
return patches
127
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
+
}