+5
-6
appview/pages/pages.go
+5
-6
appview/pages/pages.go
···
863
863
Forks []db.Repo
864
864
Branches []types.Branch
865
865
Tags []*types.TagReference
866
+
Base string
867
+
Head string
866
868
867
869
Active string
868
870
}
869
871
870
872
func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
871
873
params.Active = "overview"
872
-
return p.executeRepo("repo/compare/new", w, params)
874
+
return p.executeRepo("repo/compare", w, params)
873
875
}
874
876
875
877
type RepoCompareDiffParams struct {
876
878
LoggedInUser *oauth.User
877
879
RepoInfo repoinfo.RepoInfo
878
-
FormatPatch types.RepoFormatPatchResponse
879
-
880
-
Active string
880
+
Diff types.NiceDiff
881
881
}
882
882
883
883
func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
884
-
params.Active = "overview"
885
-
return p.executeRepo("repo/compare/new", w, params)
884
+
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
886
885
}
887
886
888
887
func (p *Pages) Static() http.Handler {
+157
appview/pages/templates/repo/compare.html
+157
appview/pages/templates/repo/compare.html
···
1
+
{{ define "title" }}new comparison{{ end }}
2
+
3
+
{{ define "repoContent" }}
4
+
<section>
5
+
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
6
+
Compare changes
7
+
</h2>
8
+
<p>Choose any two refs to compare.</p>
9
+
10
+
<form id="compare-form">
11
+
<div class="flex items-center gap-2 py-4">
12
+
<div>
13
+
base:
14
+
15
+
<select
16
+
name="base"
17
+
id="base-select"
18
+
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
19
+
onchange="triggerCompare()"
20
+
>
21
+
<optgroup
22
+
label="branches ({{ len .Branches }})"
23
+
class="bold text-sm"
24
+
>
25
+
{{ range .Branches }}
26
+
<option
27
+
value="{{ .Reference.Name }}"
28
+
class="py-1"
29
+
{{ if .IsDefault }}
30
+
selected
31
+
{{ end }}
32
+
>
33
+
{{ .Reference.Name }}
34
+
</option>
35
+
{{ end }}
36
+
</optgroup>
37
+
<optgroup
38
+
label="tags ({{ len .Tags }})"
39
+
class="bold text-sm"
40
+
>
41
+
{{ range .Tags }}
42
+
<option
43
+
value="{{ .Reference.Name }}"
44
+
class="py-1"
45
+
>
46
+
{{ .Reference.Name }}
47
+
</option>
48
+
{{ else }}
49
+
<option class="py-1" disabled>
50
+
no tags found
51
+
</option>
52
+
{{ end }}
53
+
</optgroup>
54
+
</select>
55
+
</div>
56
+
57
+
{{ i "arrow-left" "w-4 h-4" }}
58
+
59
+
60
+
<div>
61
+
compare:
62
+
63
+
<select
64
+
name="head"
65
+
id="head-select"
66
+
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
67
+
onchange="triggerCompare()"
68
+
>
69
+
<option value="" selected disabled hidden>
70
+
select a branch or tag
71
+
</option>
72
+
<optgroup
73
+
label="branches ({{ len .Branches }})"
74
+
class="bold text-sm"
75
+
>
76
+
{{ range .Branches }}
77
+
<option
78
+
value="{{ .Reference.Name }}"
79
+
class="py-1"
80
+
>
81
+
{{ .Reference.Name }}
82
+
</option>
83
+
{{ end }}
84
+
</optgroup>
85
+
<optgroup
86
+
label="tags ({{ len .Tags }})"
87
+
class="bold text-sm"
88
+
>
89
+
{{ range .Tags }}
90
+
<option
91
+
value="{{ .Reference.Name }}"
92
+
class="py-1"
93
+
>
94
+
{{ .Reference.Name }}
95
+
</option>
96
+
{{ else }}
97
+
<option class="py-1" disabled>
98
+
no tags found
99
+
</option>
100
+
{{ end }}
101
+
</optgroup>
102
+
</select>
103
+
</div>
104
+
</div>
105
+
</form>
106
+
</section>
107
+
<section class="hidden"></section>
108
+
109
+
<script>
110
+
var templatedBase = `{{ .Base }}`;
111
+
var templatedHead = `{{ .Head }}`;
112
+
var selectedBase = "";
113
+
var selectedHead = "";
114
+
115
+
document.addEventListener('DOMContentLoaded', function() {
116
+
if (templatedBase && templatedHead) {
117
+
const baseSelect = document.getElementById('base-select');
118
+
const headSelect = document.getElementById('head-select');
119
+
120
+
// select the option that matches templated values
121
+
for(let i = 0; i < baseSelect.options.length; i++) {
122
+
if(baseSelect.options[i].value === templatedBase) {
123
+
baseSelect.selectedIndex = i;
124
+
break;
125
+
}
126
+
}
127
+
128
+
for(let i = 0; i < headSelect.options.length; i++) {
129
+
if(headSelect.options[i].value === templatedHead) {
130
+
headSelect.selectedIndex = i;
131
+
break;
132
+
}
133
+
}
134
+
135
+
triggerCompare();
136
+
}
137
+
});
138
+
139
+
function triggerCompare() {
140
+
// if user has selected values, use those
141
+
selectedBase = document.getElementById('base-select').value;
142
+
selectedHead = document.getElementById('head-select').value;
143
+
144
+
const baseToUse = templatedBase && !selectedBase ? templatedBase : selectedBase;
145
+
const headToUse = templatedHead && !selectedHead ? templatedHead : selectedHead;
146
+
147
+
if (baseToUse && headToUse) {
148
+
const url = `/{{ .RepoInfo.FullName }}/compare/diff/${baseToUse}/${headToUse}`;
149
+
htmx.ajax('GET', url, { target: '#compare-diff' });
150
+
}
151
+
}
152
+
</script>
153
+
{{ end }}
154
+
155
+
{{ define "repoAfter" }}
156
+
<div id="compare-diff"></div>
157
+
{{ end }}
-74
appview/pages/templates/repo/compare/new.html
-74
appview/pages/templates/repo/compare/new.html
···
1
-
{{ define "title" }}new comparison{{ end }}
2
-
3
-
{{ define "repoContent" }}
4
-
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
5
-
Compare changes
6
-
</h2>
7
-
<p>Choose any two refs to compare.</p>
8
-
9
-
<div class="flex items-center gap-2 py-4">
10
-
<div>
11
-
base:
12
-
<select
13
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
14
-
>
15
-
<optgroup
16
-
label="branches ({{ len .Branches }})"
17
-
class="bold text-sm"
18
-
>
19
-
{{ range .Branches }}
20
-
<option
21
-
value="{{ .Reference.Name }}"
22
-
class="py-1"
23
-
{{ if .IsDefault }}
24
-
selected
25
-
{{ end }}
26
-
>
27
-
{{ .Reference.Name }}
28
-
</option>
29
-
{{ end }}
30
-
</optgroup>
31
-
<optgroup label="tags ({{ len .Tags }})" class="bold text-sm">
32
-
{{ range .Tags }}
33
-
<option value="{{ .Reference.Name }}" class="py-1">
34
-
{{ .Reference.Name }}
35
-
</option>
36
-
{{ else }}
37
-
<option class="py-1" disabled>no tags found</option>
38
-
{{ end }}
39
-
</optgroup>
40
-
</select>
41
-
</div>
42
-
{{ i "arrow-left" "w-4 h-4" }}
43
-
<div>
44
-
compare:
45
-
<select
46
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
47
-
>
48
-
<optgroup
49
-
label="branches ({{ len .Branches }})"
50
-
class="bold text-sm"
51
-
>
52
-
{{ range .Branches }}
53
-
<option value="{{ .Reference.Name }}" class="py-1">
54
-
{{ .Reference.Name }}
55
-
</option>
56
-
{{ end }}
57
-
</optgroup>
58
-
<optgroup label="tags ({{ len .Tags }})" class="bold text-sm">
59
-
{{ range .Tags }}
60
-
<option value="{{ .Reference.Name }}" class="py-1">
61
-
{{ .Reference.Name }}
62
-
</option>
63
-
{{ else }}
64
-
<option class="py-1" disabled>no tags found</option>
65
-
{{ end }}
66
-
</optgroup>
67
-
</select>
68
-
</div>
69
-
</div>
70
-
{{ end }}
71
-
72
-
{{ define "repoAfter" }}
73
-
<div id="compare-diff"></div>
74
-
{{ end }}
+28
-11
appview/state/repo.go
+28
-11
appview/state/repo.go
···
25
25
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
26
26
"tangled.sh/tangled.sh/core/appview/pagination"
27
27
"tangled.sh/tangled.sh/core/knotclient"
28
+
"tangled.sh/tangled.sh/core/patchutil"
28
29
"tangled.sh/tangled.sh/core/types"
29
30
30
31
"github.com/bluesky-social/indigo/atproto/data"
···
185
186
return nil, err
186
187
}
187
188
188
-
if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
189
+
if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
189
190
return branch.Name == f.Ref
190
191
}) {
191
192
forkInfo.Status = types.MissingBranch
···
556
557
return
557
558
}
558
559
559
-
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
560
+
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
560
561
if a.IsDefault {
561
562
return -1
562
563
}
···
2063
2064
return
2064
2065
}
2065
2066
2067
+
// if user is navigating to one of
2068
+
// /compare/{base}/{head}
2069
+
// /compare/{base}...{head}
2070
+
base := chi.URLParam(r, "base")
2071
+
head := chi.URLParam(r, "head")
2072
+
if base == "" && head == "" {
2073
+
rest := chi.URLParam(r, "*") // master...feature/xyz
2074
+
parts := strings.SplitN(rest, "...", 2)
2075
+
if len(parts) == 2 {
2076
+
base = parts[0]
2077
+
head = parts[1]
2078
+
}
2079
+
}
2080
+
2066
2081
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
2067
2082
if err != nil {
2068
2083
log.Printf("failed to create unsigned client for %s", f.Knot)
···
2101
2116
Forks: forks,
2102
2117
Branches: branches.Branches,
2103
2118
Tags: tags.Tags,
2119
+
Base: base,
2120
+
Head: head,
2104
2121
})
2105
2122
}
2106
2123
2107
-
func (s *State) RepoCompareDiff(w http.ResponseWriter, r *http.Request) {
2124
+
func (s *State) RepoCompareDiffFragment(w http.ResponseWriter, r *http.Request) {
2108
2125
f, err := s.fullyResolvedRepo(r)
2109
2126
if err != nil {
2110
2127
log.Println("failed to get repo and knot", err)
···
2112
2129
}
2113
2130
user := s.oauth.GetUser(r)
2114
2131
2115
-
rest := chi.URLParam(r, "*") // master...feature/xyz
2116
-
parts := strings.SplitN(rest, "...", 2)
2117
-
if len(parts) != 2 {
2132
+
base := chi.URLParam(r, "base")
2133
+
head := chi.URLParam(r, "head")
2134
+
2135
+
if base == "" || head == "" {
2118
2136
s.pages.Notice(w, "compare-error", "Invalid ref format.")
2119
2137
return
2120
2138
}
2121
2139
2122
-
ref1 := parts[0]
2123
-
ref2 := parts[1]
2124
-
2125
2140
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
2126
2141
if err != nil {
2127
2142
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
···
2129
2144
return
2130
2145
}
2131
2146
2132
-
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, ref1, ref2)
2147
+
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
2133
2148
if err != nil {
2134
2149
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2135
2150
log.Println("failed to compare", err)
2136
2151
return
2137
2152
}
2153
+
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
2138
2154
2155
+
w.Header().Add("Hx-Push-Url", fmt.Sprintf("/%s/compare/%s...%s", f.OwnerSlashRepo(), base, head))
2139
2156
s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{
2140
2157
LoggedInUser: user,
2141
2158
RepoInfo: f.RepoInfo(s, user),
2142
-
FormatPatch: *formatPatch,
2159
+
Diff: diff,
2143
2160
})
2144
2161
}
+3
-1
appview/state/router.go
+3
-1
appview/state/router.go
···
126
126
// for example:
127
127
// /compare/master...some/feature
128
128
// /compare/master...example.com:another/feature <- this is a fork
129
-
r.Get("/*", s.RepoCompareDiff)
129
+
r.Get("/{base}/{head}", s.RepoCompare)
130
+
r.Get("/*", s.RepoCompare)
131
+
r.Get("/diff/{base}/{head}", s.RepoCompareDiffFragment)
130
132
})
131
133
132
134
r.Route("/pulls", func(r chi.Router) {