Commit 78b20e7d69

78b20e7d693f37f26f3ef8cc36f0bbb97b1c228f

parent: ba120a7365

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 01:04 UTC

web: a facet with nothing under it leaves the column

listFacets emitted every label and open milestone whatever its count,
so the merge request list offered all 26 of this repository's labels
and 25 of them linked to an empty list. A zero count is left out now,
unless it is the filter in force, which stays so it can be cleared.

buildFacets listed every ref distinctRefs saw. It caps at ten, keeping
the ref in force wherever it sits; the branch field below the column
still takes any ref.

Ref #237
internal/httpd/builds.go +18 −1
@@ -3,6 +3,7 @@ package httpd
33import (
44 "net/http"
55 "net/url"
6 "slices"
67 "strconv"
78
89 "gitbay.org/gitbay/internal/control"
@@ -61,6 +62,11 @@ func filterLinks(f buildFilter, jobs []control.JobOut) []buildFilterLink {
6162 return links
6263}
6364
65// maxBranchFacets caps the Branches group, the way topicFacets caps
66// topics: a page of builds names as many refs as it likes and the column
67// is not a branch listing. The field below the column takes any ref.
68const maxBranchFacets = 10
69
6470// buildFacets is the builds page's side column: filterLinks' rows split
6571// into their groups, plus one link per branch seen, which keeps status
6672// and job and clears itself when active.
@@ -77,7 +83,18 @@ func buildFacets(f buildFilter, jobs []control.JobOut, refs []string) []facetGro
7783 }
7884 branch := facetGroup{Title: "Branches"}
7985 base := url.Values{"ref": {f.Ref}, "status": {f.Status}, "job": {f.Job}}
80 for _, ref := range refs {
86 shown := refs
87 if len(shown) > maxBranchFacets {
88 shown = shown[:maxBranchFacets]
89 // the ref in force belongs in the group wherever it sits, or the
90 // filter it set cannot be cleared from the column. The reslice
91 // caps the capacity so the append copies instead of writing
92 // through to refs.
93 if i := slices.Index(refs, f.Ref); i >= maxBranchFacets {
94 shown = append(shown[:maxBranchFacets-1:maxBranchFacets-1], refs[i])
95 }
96 }
97 for _, ref := range shown {
8198 active := ref == f.Ref
8299 href := facetHref(base, "ref", ref)
83100 if active {
internal/httpd/builds_test.go +34
@@ -1,6 +1,7 @@
11package httpd
22
33import (
4 "fmt"
45 "reflect"
56 "testing"
67
@@ -174,3 +175,36 @@ func TestBuildFacetsGroups(t *testing.T) {
174175 t.Errorf("dev: %+v", b)
175176 }
176177}
178
179// The branch group caps, the way topicFacets caps topics: a page of
180// builds can name dozens of refs and the column is not a branch
181// listing. The ref in force is kept whatever its position (#237).
182func TestBuildFacetsCapsBranches(t *testing.T) {
183 var refs []string
184 for i := 0; i < 30; i++ {
185 refs = append(refs, fmt.Sprintf("b%02d", i))
186 }
187 groups := buildFacets(buildFilter{}, nil, refs)
188 branches := groups[2].Items
189 if len(branches) != maxBranchFacets {
190 t.Fatalf("branches: %d", len(branches))
191 }
192 if branches[0].Label != "b00" || branches[len(branches)-1].Label != "b09" {
193 t.Errorf("kept the wrong refs: %+v", branches)
194 }
195
196 groups = buildFacets(buildFilter{Ref: "b29"}, nil, refs)
197 branches = groups[2].Items
198 if len(branches) != maxBranchFacets {
199 t.Fatalf("branches with an active ref: %d", len(branches))
200 }
201 var active *facetItem
202 for i := range branches {
203 if branches[i].Label == "b29" {
204 active = &branches[i]
205 }
206 }
207 if active == nil || !active.Active || active.Href != "?" {
208 t.Errorf("active ref past the cap: %+v", branches)
209 }
210}
internal/httpd/facets.go +9 −1
@@ -41,7 +41,9 @@ func facetHref(base url.Values, key, value string) string {
4141// listFacets builds the issue or merge request list's column from the
4242// active parameters, the states the page offers, and the repository's
4343// labels and open milestones. Counts are the rows' own: a label's issue
44// count on the issue list, its MR count on the MR list.
44// count on the issue list, its MR count on the MR list. A count of zero
45// is left out — it links to an empty list — unless it is the filter in
46// force, which stays so it can be cleared (#237).
4547func listFacets(base url.Values, states []string, state string, labels []store.Label, ms []store.Milestone, forMRs bool) []facetGroup {
4648 var st facetGroup
4749 st.Title = "State"
@@ -55,6 +57,9 @@ func listFacets(base url.Values, states []string, state string, labels []store.L
5557 n = l.MRs
5658 }
5759 active := base.Get("label") == l.Name
60 if n == 0 && !active {
61 continue
62 }
5863 href := facetHref(base, "label", l.Name)
5964 if active {
6065 href = facetHref(base, "label", "")
@@ -64,6 +69,9 @@ func listFacets(base url.Values, states []string, state string, labels []store.L
6469 mg := facetGroup{Title: "Milestones"}
6570 for _, m := range ms {
6671 active := base.Get("milestone") == m.Title
72 if m.OpenItems == 0 && !active {
73 continue
74 }
6775 href := facetHref(base, "milestone", m.Title)
6876 if active {
6977 href = facetHref(base, "milestone", "")
internal/httpd/facets_test.go +34 −6
@@ -35,18 +35,46 @@ func TestListFacetsGroups(t *testing.T) {
3535 t.Errorf("state items: %+v", st)
3636 }
3737 lb := groups[1].Items
38 // docs has no issues, so the issue list does not offer it (#237)
39 if len(lb) != 1 {
40 t.Fatalf("labels: %+v", lb)
41 }
3842 if lb[0].Label != "bug" || lb[0].Count != 2 || !lb[0].Active || lb[0].Href != "?state=open" {
3943 t.Errorf("active label clears itself: %+v", lb[0])
4044 }
41 if lb[1].Label != "docs" || lb[1].Count != 0 || lb[1].Active || lb[1].Href != "?label=docs&state=open" {
42 t.Errorf("inactive label: %+v", lb[1])
43 }
4445 if m := groups[2].Items[0]; m.Label != "v2" || m.Count != 4 || m.Href != "?label=bug&milestone=v2&state=open" {
4546 t.Errorf("milestone: %+v", m)
4647 }
47 // on the MR list a label's count is its MR count
48 // on the MR list a label's count is its MR count, and both labels
49 // have merge requests, so both are offered
4850 mr := listFacets(base, []string{"open"}, "open", labels, nil, true)
49 if mr[1].Items[1].Count != 3 {
50 t.Errorf("mr count: %+v", mr[1].Items[1])
51 if len(mr[1].Items) != 2 || mr[1].Items[1].Count != 3 {
52 t.Errorf("mr counts: %+v", mr[1].Items)
53 }
54}
55
56// A count of zero is a link to an empty list, so it is left out — unless
57// it is the filter in force, which has to stay clickable to clear (#237).
58func TestListFacetsDropsEmpty(t *testing.T) {
59 labels := []store.Label{{Name: "bug", Issues: 0}, {Name: "docs", Issues: 0}}
60 ms := []store.Milestone{{Title: "v2", OpenItems: 0}, {Title: "v3", OpenItems: 1}}
61
62 groups := listFacets(url.Values{"state": {"open"}}, []string{"open"}, "open", labels, ms, false)
63 if n := len(groups[1].Items); n != 0 {
64 t.Errorf("empty labels offered: %+v", groups[1].Items)
65 }
66 if n := len(groups[2].Items); n != 1 || groups[2].Items[0].Label != "v3" {
67 t.Errorf("milestones: %+v", groups[2].Items)
68 }
69
70 base := url.Values{"state": {"open"}, "label": {"bug"}, "milestone": {"v2"}}
71 groups = listFacets(base, []string{"open"}, "open", labels, ms, false)
72 lb := groups[1].Items
73 if len(lb) != 1 || lb[0].Label != "bug" || !lb[0].Active || lb[0].Href != "?milestone=v2&state=open" {
74 t.Errorf("active empty label dropped: %+v", lb)
75 }
76 mg := groups[2].Items
77 if len(mg) != 2 || mg[0].Label != "v2" || !mg[0].Active || mg[0].Href != "?label=bug&state=open" {
78 t.Errorf("active empty milestone dropped: %+v", mg)
5179 }
5280}