+5
-3
appview/pages/funcmap.go
+5
-3
appview/pages/funcmap.go
···
133
133
"sequence": func(n int) []struct{} {
134
134
return make([]struct{}, n)
135
135
},
136
-
"subslice": func(slice any, start, end int) any {
136
+
// take atmost N items from this slice
137
+
"take": func(slice any, n int) any {
137
138
v := reflect.ValueOf(slice)
138
139
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
139
140
return nil
140
141
}
141
-
if start < 0 || start > v.Len() || end > v.Len() || start > end {
142
+
if v.Len() == 0 {
142
143
return nil
143
144
}
144
-
return v.Slice(start, end).Interface()
145
+
return v.Slice(0, min(n, v.Len()-1)).Interface()
145
146
},
147
+
146
148
"markdown": func(text string) template.HTML {
147
149
rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault}
148
150
return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text)))
+19
-1
appview/pages/pages.go
+19
-1
appview/pages/pages.go
···
871
871
Tags []*types.TagReference
872
872
Base string
873
873
Head string
874
+
Diff *types.NiceDiff
874
875
875
876
Active string
876
877
}
877
878
878
879
func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
879
880
params.Active = "overview"
880
-
return p.executeRepo("repo/compare", w, params)
881
+
return p.executeRepo("repo/compare/compare", w, params)
882
+
}
883
+
884
+
type RepoCompareNewParams struct {
885
+
LoggedInUser *oauth.User
886
+
RepoInfo repoinfo.RepoInfo
887
+
Forks []db.Repo
888
+
Branches []types.Branch
889
+
Tags []*types.TagReference
890
+
Base string
891
+
Head string
892
+
893
+
Active string
894
+
}
895
+
896
+
func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
897
+
params.Active = "overview"
898
+
return p.executeRepo("repo/compare/new", w, params)
881
899
}
882
900
883
901
type RepoCompareAllowPullParams struct {
-166
appview/pages/templates/repo/compare.html
-166
appview/pages/templates/repo/compare.html
···
1
-
{{ define "title" }}
2
-
{{ if and .Head .Base }}
3
-
comparing {{ .Base }} and
4
-
{{ .Head }}
5
-
{{ else }}
6
-
new comparison
7
-
{{ end }}
8
-
{{ end }}
9
-
10
-
{{ define "repoContent" }}
11
-
<section id="compare-select">
12
-
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
13
-
Compare changes
14
-
</h2>
15
-
<p>Choose any two refs to compare.</p>
16
-
17
-
<form id="compare-form">
18
-
<div class="flex items-center gap-2 py-4">
19
-
<div>
20
-
base:
21
-
22
-
<select
23
-
name="base"
24
-
id="base-select"
25
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
26
-
onchange="triggerCompare()"
27
-
>
28
-
<optgroup
29
-
label="branches ({{ len .Branches }})"
30
-
class="bold text-sm"
31
-
>
32
-
{{ range .Branches }}
33
-
<option
34
-
value="{{ .Reference.Name }}"
35
-
class="py-1"
36
-
{{ if .IsDefault }}
37
-
selected
38
-
{{ end }}
39
-
>
40
-
{{ .Reference.Name }}
41
-
</option>
42
-
{{ end }}
43
-
</optgroup>
44
-
<optgroup
45
-
label="tags ({{ len .Tags }})"
46
-
class="bold text-sm"
47
-
>
48
-
{{ range .Tags }}
49
-
<option
50
-
value="{{ .Reference.Name }}"
51
-
class="py-1"
52
-
>
53
-
{{ .Reference.Name }}
54
-
</option>
55
-
{{ else }}
56
-
<option class="py-1" disabled>
57
-
no tags found
58
-
</option>
59
-
{{ end }}
60
-
</optgroup>
61
-
</select>
62
-
</div>
63
-
64
-
{{ i "arrow-left" "w-4 h-4" }}
65
-
66
-
67
-
<div>
68
-
compare:
69
-
70
-
<select
71
-
name="head"
72
-
id="head-select"
73
-
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
74
-
onchange="triggerCompare()"
75
-
>
76
-
<option value="" selected disabled hidden>
77
-
select a branch or tag
78
-
</option>
79
-
<optgroup
80
-
label="branches ({{ len .Branches }})"
81
-
class="bold text-sm"
82
-
>
83
-
{{ range .Branches }}
84
-
<option
85
-
value="{{ .Reference.Name }}"
86
-
class="py-1"
87
-
>
88
-
{{ .Reference.Name }}
89
-
</option>
90
-
{{ end }}
91
-
</optgroup>
92
-
<optgroup
93
-
label="tags ({{ len .Tags }})"
94
-
class="bold text-sm"
95
-
>
96
-
{{ range .Tags }}
97
-
<option
98
-
value="{{ .Reference.Name }}"
99
-
class="py-1"
100
-
>
101
-
{{ .Reference.Name }}
102
-
</option>
103
-
{{ else }}
104
-
<option class="py-1" disabled>
105
-
no tags found
106
-
</option>
107
-
{{ end }}
108
-
</optgroup>
109
-
</select>
110
-
</div>
111
-
</div>
112
-
</form>
113
-
</section>
114
-
115
-
<script>
116
-
var templatedBase = `{{ .Base }}`;
117
-
var templatedHead = `{{ .Head }}`;
118
-
var selectedBase = "";
119
-
var selectedHead = "";
120
-
121
-
document.addEventListener('DOMContentLoaded', function() {
122
-
if (templatedBase && templatedHead) {
123
-
const baseSelect = document.getElementById('base-select');
124
-
const headSelect = document.getElementById('head-select');
125
-
126
-
// select the option that matches templated values
127
-
for(let i = 0; i < baseSelect.options.length; i++) {
128
-
if(baseSelect.options[i].value === templatedBase) {
129
-
baseSelect.selectedIndex = i;
130
-
break;
131
-
}
132
-
}
133
-
134
-
for(let i = 0; i < headSelect.options.length; i++) {
135
-
if(headSelect.options[i].value === templatedHead) {
136
-
headSelect.selectedIndex = i;
137
-
break;
138
-
}
139
-
}
140
-
141
-
triggerCompare();
142
-
}
143
-
});
144
-
145
-
function triggerCompare() {
146
-
// if user has selected values, use those
147
-
selectedBase = document.getElementById('base-select').value;
148
-
selectedHead = document.getElementById('head-select').value;
149
-
150
-
const baseToUse = templatedBase && !selectedBase ? templatedBase : selectedBase;
151
-
const headToUse = templatedHead && !selectedHead ? templatedHead : selectedHead;
152
-
153
-
if (baseToUse && headToUse) {
154
-
const url = `/{{ .RepoInfo.FullName }}/compare/diff/${baseToUse}/${headToUse}`;
155
-
156
-
// htmx.ajax('GET', url, { target: '#compare-diff' })
157
-
document.title = `comparing ${baseToUse} and ${headToUse}`;
158
-
}
159
-
}
160
-
</script>
161
-
{{ end }}
162
-
163
-
{{ define "repoAfter" }}
164
-
<div id="allow-pull"></div>
165
-
<div id="compare-diff"></div>
166
-
{{ end }}
+15
appview/pages/templates/repo/compare/compare.html
+15
appview/pages/templates/repo/compare/compare.html
···
1
+
{{ define "title" }}
2
+
comparing {{ .Base }} and {{ .Head }} on {{ .RepoInfo.FullName }}
3
+
{{ end }}
4
+
5
+
{{ define "repoContent" }}
6
+
{{ template "repo/fragments/compareForm" . }}
7
+
{{ $isPushAllowed := and .LoggedInUser .RepoInfo.Roles.IsPushAllowed }}
8
+
{{ if $isPushAllowed }}
9
+
{{ template "repo/fragments/compareAllowPull" . }}
10
+
{{ end }}
11
+
{{ end }}
12
+
13
+
{{ define "repoAfter" }}
14
+
{{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }}
15
+
{{ end }}
+31
appview/pages/templates/repo/compare/new.html
+31
appview/pages/templates/repo/compare/new.html
···
1
+
{{ define "title" }}
2
+
compare refs on {{ .RepoInfo.FullName }}
3
+
{{ end }}
4
+
5
+
{{ define "repoContent" }}
6
+
{{ template "repo/fragments/compareForm" . }}
7
+
{{ end }}
8
+
9
+
{{ define "repoAfter" }}
10
+
<section class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto">
11
+
<div class="flex flex-col items-center">
12
+
<p class="text-center text-black dark:text-white">
13
+
Recently updated branches in this repository:
14
+
</p>
15
+
{{ block "recentBranchList" $ }} {{ end }}
16
+
</div>
17
+
</section>
18
+
{{ end }}
19
+
20
+
{{ define "recentBranchList" }}
21
+
<div class="mt-4 grid grid-cols-1 divide-y divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 w-full md:w-1/2">
22
+
{{ range $br := take .Branches 5 }}
23
+
<a href="/{{ $.RepoInfo.FullName }}/compare?head={{ $br.Name | urlquery }}" class="no-underline hover:no-underline">
24
+
<div class="flex items-center justify-between p-2">
25
+
{{ $br.Name }}
26
+
<time class="text-gray-500 dark:text-gray-400">{{ timeFmt $br.Commit.Committer.When }}</time>
27
+
</div>
28
+
</a>
29
+
{{ end }}
30
+
</div>
31
+
{{ end }}
+73
appview/pages/templates/repo/fragments/compareForm.html
+73
appview/pages/templates/repo/fragments/compareForm.html
···
1
+
{{ define "repo/fragments/compareForm" }}
2
+
<div id="compare-select">
3
+
<h2 class="font-bold text-sm mb-2 uppercase dark:text-white">
4
+
Compare changes
5
+
</h2>
6
+
<p>Choose any two refs to compare.</p>
7
+
8
+
<form id="compare-form" class="flex items-center gap-2 py-4">
9
+
<div>
10
+
<span class="hidden md:inline">base:</span>
11
+
{{ block "dropdown" (list $ "base" $.Base) }} {{ end }}
12
+
</div>
13
+
<span class="flex-shrink-0">
14
+
{{ i "arrow-left" "w-4 h-4" }}
15
+
</span>
16
+
<div>
17
+
<span class="hidden md:inline">compare:</span>
18
+
{{ block "dropdown" (list $ "head" $.Head) }} {{ end }}
19
+
</div>
20
+
<button
21
+
id="compare-button"
22
+
class="btn disabled:opacity-50 disabled:cursor-not-allowed"
23
+
type="button"
24
+
hx-boost="true"
25
+
onclick="
26
+
const base = document.getElementById('base-select').value;
27
+
const head = document.getElementById('head-select').value;
28
+
window.location.href = `/{{$.RepoInfo.FullName}}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
29
+
">
30
+
go
31
+
</button>
32
+
</form>
33
+
</div>
34
+
<script>
35
+
const baseSelect = document.getElementById('base-select');
36
+
const headSelect = document.getElementById('head-select');
37
+
const compareButton = document.getElementById('compare-button');
38
+
39
+
function toggleButtonState() {
40
+
compareButton.disabled = baseSelect.value === headSelect.value;
41
+
}
42
+
43
+
baseSelect.addEventListener('change', toggleButtonState);
44
+
headSelect.addEventListener('change', toggleButtonState);
45
+
46
+
// Run once on page load
47
+
toggleButtonState();
48
+
</script>
49
+
{{ end }}
50
+
51
+
{{ define "dropdown" }}
52
+
{{ $root := index . 0 }}
53
+
{{ $name := index . 1 }}
54
+
{{ $default := index . 2 }}
55
+
<select name="{{$name}}" id="{{$name}}-select" class="p-1 border max-w-32 md:max-w-64 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
56
+
<optgroup label="branches ({{ len $root.Branches }})" class="bold text-sm">
57
+
{{ range $root.Branches }}
58
+
<option value="{{ .Reference.Name }}" class="py-1" {{if eq .Reference.Name $default}}selected{{end}}>
59
+
{{ .Reference.Name }}
60
+
</option>
61
+
{{ end }}
62
+
</optgroup>
63
+
<optgroup label="tags ({{ len $root.Tags }})" class="bold text-sm">
64
+
{{ range $root.Tags }}
65
+
<option value="{{ .Reference.Name }}" class="py-1" {{if eq .Reference.Name $default}}selected{{end}}>
66
+
{{ .Reference.Name }}
67
+
</option>
68
+
{{ else }}
69
+
<option class="py-1" disabled>no tags found</option>
70
+
{{ end }}
71
+
</optgroup>
72
+
</select>
73
+
{{ end }}
+1
-1
appview/pages/templates/repo/index.html
+1
-1
appview/pages/templates/repo/index.html
+62
-74
appview/state/repo.go
+62
-74
appview/state/repo.go
···
12
12
"net/http"
13
13
"path"
14
14
"slices"
15
+
"sort"
15
16
"strconv"
16
17
"strings"
17
18
"time"
···
2056
2057
}
2057
2058
}
2058
2059
2059
-
func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) {
2060
+
func (s *State) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
2060
2061
user := s.oauth.GetUser(r)
2061
2062
f, err := s.fullyResolvedRepo(r)
2062
2063
if err != nil {
···
2064
2065
return
2065
2066
}
2066
2067
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
-
2081
2068
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
2082
2069
if err != nil {
2083
2070
log.Printf("failed to create unsigned client for %s", f.Knot)
···
2085
2072
return
2086
2073
}
2087
2074
2088
-
branches, err := us.Branches(f.OwnerDid(), f.RepoName)
2075
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
2089
2076
if err != nil {
2090
2077
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2091
2078
log.Println("failed to reach knotserver", err)
2092
2079
return
2093
2080
}
2081
+
branches := result.Branches
2082
+
sort.Slice(branches, func(i int, j int) bool {
2083
+
return branches[i].Commit.Committer.When.After(branches[j].Commit.Committer.When)
2084
+
})
2085
+
2086
+
var defaultBranch string
2087
+
for _, b := range branches {
2088
+
if b.IsDefault {
2089
+
defaultBranch = b.Name
2090
+
}
2091
+
}
2092
+
2093
+
base := defaultBranch
2094
+
head := defaultBranch
2095
+
2096
+
params := r.URL.Query()
2097
+
queryBase := params.Get("base")
2098
+
queryHead := params.Get("head")
2099
+
if queryBase != "" {
2100
+
base = queryBase
2101
+
}
2102
+
if queryHead != "" {
2103
+
head = queryHead
2104
+
}
2094
2105
2095
2106
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
2096
2107
if err != nil {
···
2099
2110
return
2100
2111
}
2101
2112
2102
-
var forks []db.Repo
2103
-
if user != nil {
2104
-
var err error
2105
-
forks, err = db.GetForksByDid(s.db, user.Did)
2106
-
if err != nil {
2107
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2108
-
log.Println("failed to get forks", err)
2109
-
return
2110
-
}
2111
-
}
2112
-
2113
2113
repoinfo := f.RepoInfo(s, user)
2114
2114
2115
-
s.pages.RepoCompare(w, pages.RepoCompareParams{
2115
+
s.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
2116
2116
LoggedInUser: user,
2117
2117
RepoInfo: repoinfo,
2118
-
Forks: forks,
2119
-
Branches: branches.Branches,
2118
+
Branches: branches,
2120
2119
Tags: tags.Tags,
2121
2120
Base: base,
2122
2121
Head: head,
2123
2122
})
2124
-
2125
2123
}
2126
2124
2127
-
func (s *State) RepoCompareAllowPullFragment(w http.ResponseWriter, r *http.Request) {
2125
+
func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) {
2128
2126
user := s.oauth.GetUser(r)
2129
2127
f, err := s.fullyResolvedRepo(r)
2130
2128
if err != nil {
···
2132
2130
return
2133
2131
}
2134
2132
2135
-
s.pages.RepoCompareAllowPullFragment(w, pages.RepoCompareAllowPullParams{
2136
-
Head: chi.URLParam(r, "head"),
2137
-
Base: chi.URLParam(r, "base"),
2138
-
RepoInfo: f.RepoInfo(s, user),
2139
-
LoggedInUser: user,
2140
-
})
2141
-
}
2142
-
2143
-
func (s *State) RepoCompareDiffFragment(w http.ResponseWriter, r *http.Request) {
2144
-
f, err := s.fullyResolvedRepo(r)
2145
-
if err != nil {
2146
-
log.Println("failed to get repo and knot", err)
2147
-
return
2148
-
}
2149
-
user := s.oauth.GetUser(r)
2150
-
2133
+
// if user is navigating to one of
2134
+
// /compare/{base}/{head}
2135
+
// /compare/{base}...{head}
2151
2136
base := chi.URLParam(r, "base")
2152
2137
head := chi.URLParam(r, "head")
2138
+
if base == "" && head == "" {
2139
+
rest := chi.URLParam(r, "*") // master...feature/xyz
2140
+
parts := strings.SplitN(rest, "...", 2)
2141
+
if len(parts) == 2 {
2142
+
base = parts[0]
2143
+
head = parts[1]
2144
+
}
2145
+
}
2153
2146
2154
2147
if base == "" || head == "" {
2155
-
s.pages.Notice(w, "compare-error", "Invalid ref format.")
2148
+
log.Printf("invalid comparison")
2149
+
s.pages.Error404(w)
2156
2150
return
2157
2151
}
2158
2152
2159
2153
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
2160
2154
if err != nil {
2155
+
log.Printf("failed to create unsigned client for %s", f.Knot)
2156
+
s.pages.Error503(w)
2157
+
return
2158
+
}
2159
+
2160
+
branches, err := us.Branches(f.OwnerDid(), f.RepoName)
2161
+
if err != nil {
2161
2162
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2162
2163
log.Println("failed to reach knotserver", err)
2163
2164
return
2164
2165
}
2165
2166
2166
-
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
2167
+
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
2167
2168
if err != nil {
2168
2169
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2169
-
log.Println("failed to compare", err)
2170
+
log.Println("failed to reach knotserver", err)
2170
2171
return
2171
2172
}
2172
-
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
2173
2173
2174
-
branches, err := us.Branches(f.OwnerDid(), f.RepoName)
2174
+
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
2175
2175
if err != nil {
2176
2176
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
2177
-
log.Println("failed to fetch branches", err)
2177
+
log.Println("failed to compare", err)
2178
2178
return
2179
2179
}
2180
+
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
2181
+
log.Println(formatPatch)
2180
2182
2181
2183
repoinfo := f.RepoInfo(s, user)
2182
2184
2183
-
w.Header().Add("Hx-Push-Url", fmt.Sprintf("/%s/compare/%s...%s", f.OwnerSlashRepo(), base, head))
2184
-
w.Header().Add("Content-Type", "text/html")
2185
-
s.pages.RepoCompareDiff(w, pages.RepoCompareDiffParams{
2185
+
s.pages.RepoCompare(w, pages.RepoCompareParams{
2186
2186
LoggedInUser: user,
2187
2187
RepoInfo: repoinfo,
2188
-
Diff: diff,
2188
+
Branches: branches.Branches,
2189
+
Tags: tags.Tags,
2190
+
Base: base,
2191
+
Head: head,
2192
+
Diff: &diff,
2189
2193
})
2190
2194
2191
-
// checks if pull is allowed and performs an htmx oob-swap
2192
-
// by writing to the same http.ResponseWriter
2193
-
if user != nil {
2194
-
if slices.ContainsFunc(branches.Branches, func(branch types.Branch) bool {
2195
-
return branch.Name == head || branch.Name == base
2196
-
}) {
2197
-
if repoinfo.Roles.IsPushAllowed() {
2198
-
s.pages.RepoCompareAllowPullFragment(w, pages.RepoCompareAllowPullParams{
2199
-
LoggedInUser: user,
2200
-
RepoInfo: repoinfo,
2201
-
Base: base,
2202
-
Head: head,
2203
-
})
2204
-
}
2205
-
}
2206
-
}
2207
2195
}
+1
-3
appview/state/router.go
+1
-3
appview/state/router.go
···
119
119
})
120
120
121
121
r.Route("/compare", func(r chi.Router) {
122
-
r.Get("/", s.RepoCompare)
122
+
r.Get("/", s.RepoCompareNew) // start an new comparison
123
123
124
124
// we have to wildcard here since we want to support GitHub's compare syntax
125
125
// /compare/{ref1}...{ref2}
···
127
127
// /compare/master...some/feature
128
128
// /compare/master...example.com:another/feature <- this is a fork
129
129
r.Get("/{base}/{head}", s.RepoCompare)
130
-
r.Get("/diff/{base}/{head}", s.RepoCompareDiffFragment)
131
-
r.Get("/allow-pull/{base}/{head}", s.RepoCompareAllowPullFragment)
132
130
r.Get("/*", s.RepoCompare)
133
131
})
134
132