A CLI-first git forge. cli forge git self-hosted

https://gitbay.org

Commit f99b620c29

f99b620c297a48b0a3377542faad13f885c00df3

parent: 7d5f1337c7

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-19 03:05 UTC

web: labels on the merge request page and list

POST /{owner}/{repo}/mrs/{n}/label dispatches mr label. A Labels group in
the aside after Milestone, chips on the list rows linking to ?label=, the
active label filter rendered as a chip, and an MR count on the labels
page.

Ref #231
internal/httpd/mractions.go +10
@@ -66,6 +66,16 @@ func (s *Server) mrReviewRequestSubmit(w http.ResponseWriter, r *http.Request, u
6666 s.done(w, r, code, msg, s.mrRedirect)
6767}
6868
69func (s *Server) mrLabelSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
70 args := append(fieldArgs("--add", r.FormValue("add")), fieldArgs("--remove", r.FormValue("remove"))...)
71 if len(args) == 0 {
72 s.mrRedirect(w, r, "name at least one label")
73 return
74 }
75 _, msg, code := s.runControlCode(u, mrArgs(r, "label", args...))
76 s.done(w, r, code, msg, s.mrRedirect)
77}
78
6979func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
7080 args := []string{}
7181 if st := strings.TrimSpace(r.FormValue("strategy")); st != "" && st != "auto" {
internal/httpd/routes.go +2
@@ -202,6 +202,8 @@ func (s *Server) Routes() []Route {
202202 Handler: s.checkOrigin(s.requireUser(s.mrReviewSubmit))},
203203 Route{Method: "POST", Pattern: "/{owner}/{repo}/mrs/{n}/review-request", Mutating: true,
204204 Handler: s.checkOrigin(s.requireUser(s.mrReviewRequestSubmit))},
205 Route{Method: "POST", Pattern: "/{owner}/{repo}/mrs/{n}/label", Mutating: true,
206 Handler: s.checkOrigin(s.requireUser(s.mrLabelSubmit))},
205207 Route{Method: "POST", Pattern: "/{owner}/{repo}/mrs/{n}/merge", Mutating: true,
206208 Handler: s.checkOrigin(s.requireUser(s.mrMergeSubmit))},
207209 Route{Method: "POST", Pattern: "/{owner}/{repo}/mrs/{n}/close", Mutating: true,
internal/httpd/web.go +17 −9
@@ -1814,8 +1814,8 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
18141814 state = "open"
18151815 }
18161816 qv := r.URL.Query()
1817 mf := store.MRFilter{State: state, Author: qv.Get("author"), Milestone: qv.Get("milestone"),
1818 Search: strings.TrimSpace(qv.Get("q")), Limit: listPage + 1}
1817 mf := store.MRFilter{State: state, Label: qv.Get("label"), Author: qv.Get("author"),
1818 Milestone: qv.Get("milestone"), Search: strings.TrimSpace(qv.Get("q")), Limit: listPage + 1}
18191819 mf.Before, _ = strconv.ParseInt(qv.Get("before"), 10, 64)
18201820 mrs, err := s.st.QueryMRs(p.Repo.ID, mf)
18211821 if err != nil {
@@ -1841,19 +1841,26 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
18411841 if err != nil {
18421842 comments = map[int64]int{}
18431843 }
1844 labels, err := s.st.ListMRLabels(p.Repo)
1845 if err != nil {
1846 labels = map[int64][]string{}
1847 }
18441848 rows := make([]mrRow, len(mrs))
18451849 for i, m := range mrs {
1850 m.Labels = labels[m.ID]
18461851 rows[i] = mrRow{MR: m, Check: checks[m.HeadSHA], Comments: comments[m.ID]}
18471852 }
18481853 s.render(w, "mrs.html", struct {
18491854 repoPage
1850 State string
1851 Query string
1852 Filters []listFilter
1853 MRs []mrRow
1854 Older string
1855 State string
1856 Query string
1857 Filters []listFilter
1858 MRs []mrRow
1859 LabelColors map[string]template.CSS
1860 Older string
18551861 }{p, state, mf.Search,
1856 activeFilters(state, [][2]string{{"author", mf.Author}, {"milestone", mf.Milestone}}), rows, older})
1862 activeFilters(state, [][2]string{{"label", mf.Label}, {"author", mf.Author}, {"milestone", mf.Milestone}}),
1863 rows, s.labelColors(p.Repo), older})
18571864}
18581865
18591866func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
@@ -2016,10 +2023,11 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
20162023 HeadMerged bool
20172024 HeadPruned bool
20182025 Base string
2026 LabelColors map[string]template.CSS
20192027 }{p, m, view, md(m.Body, m.BodyFormat), checks, combined, renderComments(comments, md),
20202028 reviewRows, files, diffTruncated, stat, commits, commitsTotal, branches, s.canEditItem(r, p.Repo, m.Author),
20212029 canWrite, unresolved, revisions, s.takeFlash(w, r), detachedThreads, stackedOn, stacked, supersedes, gates,
2022 sourceGone(p, m), headMerged, headPruned, base})
2030 sourceGone(p, m), headMerged, headPruned, base, s.labelColors(p.Repo)})
20232031}
20242032
20252033// sourceGone reports whether an MR's source branch no longer exists: the
internal/web/templates/labels.html +3 −2
@@ -3,7 +3,7 @@
33<h1>Labels</h1>
44{{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}}
55{{if .Labels}}<div class="tablewrap"><table class="keys">
6<tr class="cols"><th scope="col">label</th>{{if or $.CanWrite $.AnyColor}}<th scope="col">colour</th>{{end}}<th scope="col">issues</th><th scope="col"></th></tr>
6<tr class="cols"><th scope="col">label</th>{{if or $.CanWrite $.AnyColor}}<th scope="col">colour</th>{{end}}<th scope="col">issues</th><th scope="col">merge requests</th><th scope="col"></th></tr>
77{{range .Labels}}<tr>
88 <td><a class="chip label" style="{{index $.LabelColors .Name}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.Name}}">{{.Name}}</a>{{if .Org}} <span class="chip chip-neutral">org</span>{{end}}</td>
99 {{if or $.CanWrite $.AnyColor}}<td>{{if and $.CanWrite (not .Org)}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
@@ -12,6 +12,7 @@
1212 <button type="submit" class="btn">Save</button>
1313 </form>{{else}}<span class="mono">{{if .Color}}{{.Color}}{{else}}—{{end}}</span>{{end}}</td>{{end}}
1414 <td>{{.Issues}}</td>
15 <td>{{.MRs}}</td>
1516 <td class="act">{{if and $.CanWrite (not .Org)}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/labels" class="inline">
1617 <input type="hidden" name="action" value="remove">
1718 <input type="hidden" name="name" value="{{.Name}}">
@@ -32,6 +33,6 @@
3233 <button type="submit" class="btn">Create label</button>
3334 </form>
3435</details>
35<p class="meta">Removing a label takes it off every issue that carries it.</p>
36<p class="meta">Removing a label takes it off every issue and merge request that carries it.</p>
3637{{end}}
3738{{end}}
internal/web/templates/mr.html +12
@@ -166,6 +166,18 @@
166166 <h2>Milestone</h2>
167167 <p class="row"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">{{.MR.Milestone}}</a></p>
168168 </div>{{end}}
169 <div class="grp">
170 <h2>Labels</h2>
171 {{if .MR.Labels}}<p class="row">{{range .MR.Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs?label={{.}}">{{.}}</a> {{end}}</p>
172 {{else}}<p class="none">none yet</p>{{end}}
173 {{if .CanWrite}}
174 <form method="post" action="{{$base}}/label" class="actions">
175 <input type="text" name="add" aria-label="Add labels" placeholder="add, space-separated">
176 <input type="text" name="remove" aria-label="Remove labels" placeholder="remove">
177 <button type="submit" class="btn">Apply</button>
178 </form>
179 {{end}}
180 </div>
169181</aside>
170182</div>
171183{{end}}
internal/web/templates/mrs.html +3 −2
@@ -13,13 +13,14 @@
1313 <button type="submit" class="btn">Search</button>
1414 <input type="hidden" name="state" value="{{.State}}">
1515 </form>
16 {{range .Filters}}<p class="meta">{{.Key}}: <b>{{.Value}}</b> <a href="{{.Clear}}">clear</a></p>{{end}}
16 {{range .Filters}}<p class="meta">{{.Key}}: {{if eq .Key "label"}}<span class="chip label" style="{{index $.LabelColors .Value}}">{{.Value}}</span>{{else}}<b>{{.Value}}</b>{{end}} <a href="{{.Clear}}">clear</a></p>{{end}}
1717</div>
1818{{if .Viewer}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/new">New merge request</a></p>{{end}}
1919<ul class="issuelist">
2020{{range .MRs}}{{$n := .Number}}<li>
2121 <div class="issuemain">
22 <p class="title"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{.Number}}">{{.Title}}</a></p>
22 <p class="title"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{.Number}}">{{.Title}}</a>
23 {{range .Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="?label={{.}}">{{.}}</a> {{end}}</p>
2324 <p class="meta">!{{.Number}} by <a href="/{{.Author}}">{{.Author}}</a> · {{if .SourcePath}}{{.SourcePath}}:{{end}}{{.SourceRef}} → {{.TargetRef}}</p>
2425 </div>
2526 <div class="issueside">