Commit 584dc8d6ee

584dc8d6ee359da9453cae2c698d0dc04d5b8150

parent: 0d343c917d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 21:45 UTC

web: one empty-state register — no CLI commands, no 'yet' on finished items

Ref #270

Layout: unified · split

internal/httpd/buildpages_test.go +63
@@ -57,6 +57,69 @@ func TestBuildsPageRendersCommandOutput(t *testing.T) {
57 } 57 }
58} 58}
59 59
60// A writer sees the instruction to add CI; a reader without push access
61// sees only the fact, since the instruction is not theirs to act on
62// (#270).
63func TestBuildsEmptyStateOmitsInstructionForReaders(t *testing.T) {
64 var sb strings.Builder
65 filter := buildFilter{}
66 err := web.Render(&sb, "builds.html", struct {
67 repoPage
68 Builds []control.BuildOut
69 Jobs []control.JobOut
70 Runs []buildRun
71 Filter buildFilter
72 Facets []facetGroup
73 Refs []string
74 Older string
75 CanWrite bool
76 Notice string
77 }{
78 testRepoPage(), nil, nil, nil, filter, nil, nil, "", false, "",
79 })
80 if err != nil {
81 t.Fatalf("render: %v", err)
82 }
83 out := sb.String()
84 if !strings.Contains(out, "no builds") {
85 t.Error(`missing "no builds"`)
86 }
87 if strings.Contains(out, "ci.yml") {
88 t.Error("a reader without push access should not see the push instruction")
89 }
90}
91
92// A writer with zero builds gets the one instruction the page can give
93// them (#270).
94func TestBuildsEmptyStateShowsInstructionForWriters(t *testing.T) {
95 var sb strings.Builder
96 filter := buildFilter{}
97 err := web.Render(&sb, "builds.html", struct {
98 repoPage
99 Builds []control.BuildOut
100 Jobs []control.JobOut
101 Runs []buildRun
102 Filter buildFilter
103 Facets []facetGroup
104 Refs []string
105 Older string
106 CanWrite bool
107 Notice string
108 }{
109 testRepoPage(), nil, nil, nil, filter, nil, nil, "", true, "",
110 })
111 if err != nil {
112 t.Fatalf("render: %v", err)
113 }
114 out := sb.String()
115 if !strings.Contains(out, "no builds") {
116 t.Error(`missing "no builds"`)
117 }
118 if !strings.Contains(out, "push a commit with a .gitbay/ci.yml") {
119 t.Error("a writer with no builds should see the push instruction")
120 }
121}
122
60func TestBuildPageRendersCommandOutput(t *testing.T) { 123func TestBuildPageRendersCommandOutput(t *testing.T) {
61 var sb strings.Builder 124 var sb strings.Builder
62 err := web.Render(&sb, "build.html", buildView{ 125 err := web.Render(&sb, "build.html", buildView{
internal/httpd/mrpage_test.go +60
@@ -83,6 +83,66 @@ func testMR(state string) store.MR {
83 HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"} 83 HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"}
84} 84}
85 85
86// A finished merge request states an empty label list as a fact, not a
87// promise something is still coming (#270).
88func TestMRPageLabelsNoYetOnFinishedState(t *testing.T) {
89 var sb strings.Builder
90 if err := web.Render(&sb, "mr.html", mrPageData{
91 repoPage: testRepoPage(), MR: testMR("merged"), View: "conversation",
92 }); err != nil {
93 t.Fatalf("render: %v", err)
94 }
95 if !strings.Contains(sb.String(), "no labels") {
96 t.Error(`merged MR with no labels should read "no labels", not "none yet"`)
97 }
98}
99
100// An open MR can still gain labels, so its empty state keeps "none yet"
101// rather than the finished-item wording (#270).
102func TestMRPageLabelsNoneYetOnOpenState(t *testing.T) {
103 var sb strings.Builder
104 if err := web.Render(&sb, "mr.html", mrPageData{
105 repoPage: testRepoPage(), MR: testMR("open"), View: "conversation",
106 }); err != nil {
107 t.Fatalf("render: %v", err)
108 }
109 out := sb.String()
110 if !strings.Contains(out, "none yet") {
111 t.Error(`open MR with no labels should read "none yet"`)
112 }
113 if strings.Contains(out, "no labels") {
114 t.Error(`open MR with no labels should not read "no labels"`)
115 }
116}
117
118// A closed MR is finished the same way a merged one is (#270).
119func TestMRPageLabelsNoLabelsOnClosedState(t *testing.T) {
120 var sb strings.Builder
121 if err := web.Render(&sb, "mr.html", mrPageData{
122 repoPage: testRepoPage(), MR: testMR("closed"), View: "conversation",
123 }); err != nil {
124 t.Fatalf("render: %v", err)
125 }
126 if !strings.Contains(sb.String(), "no labels") {
127 t.Error(`closed MR with no labels should read "no labels", not "none yet"`)
128 }
129}
130
131func TestMRPageReviewersEmptyStateDropsNobody(t *testing.T) {
132 var sb strings.Builder
133 if err := web.Render(&sb, "mr.html", mrPageData{
134 repoPage: testRepoPage(), MR: testMR("open"), View: "conversation",
135 }); err != nil {
136 t.Fatalf("render: %v", err)
137 }
138 if strings.Contains(sb.String(), "nobody yet") {
139 t.Error(`reviewers empty state should read "no reviewers"`)
140 }
141 if !strings.Contains(sb.String(), "no reviewers") {
142 t.Error(`missing "no reviewers"`)
143 }
144}
145
86// The header states what happened to the MR: who opened, merged, or closed 146// The header states what happened to the MR: who opened, merged, or closed
87// it, and when. A merge or close with no stamp (imports, pre-0029 merges) 147// it, and when. A merge or close with no stamp (imports, pre-0029 merges)
88// names the state without claiming a byline it cannot back up. 148// names the state without claiming a byline it cannot back up.
internal/web/templates/builds.html +1 −1
@@ -50,7 +50,7 @@
50 {{range .Builds}}<a class="chip check-{{.Status}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/builds/{{.Number}}">{{.Job}}</a>{{end}} 50 {{range .Builds}}<a class="chip check-{{.Status}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/builds/{{.Number}}">{{.Job}}</a>{{end}}
51 </div> 51 </div>
52</li> 52</li>
53{{else}}<li class="empty">no builds — push a commit with a <code>.gitbay/ci.yml</code></li>{{end}} 53{{else}}<li class="empty">no builds{{if .CanWrite}} — push a commit with a .gitbay/ci.yml{{end}}</li>{{end}}
54</ul> 54</ul>
55{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}} 55{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}}
56</div> 56</div>
internal/web/templates/dashboard.html +1 −1
@@ -26,7 +26,7 @@
26 <ul class="pins"> 26 <ul class="pins">
27 {{range .Pins}}<li><a href="/{{.Owner}}/{{.Name}}"><span class="owner">{{.Owner}}/</span>{{.Name}}</a><span class="n" title="{{.Issues}} open issue{{if ne .Issues 1}}s{{end}}, {{.MRs}} open merge request{{if ne .MRs 1}}s{{end}}{{with .Build}}, last build {{.}}{{end}}"><b{{if .Issues}} class="wants"{{end}}>{{.Issues}} <span class="vh">open issues</span></b> <b{{if .MRs}} class="wants"{{end}}>{{.MRs}} <span class="vh">open merge requests</span></b> <span class="dot{{if eq .Build "success"}} ok{{else if or (eq .Build "failure") (eq .Build "error")}} bad{{else if or (eq .Build "pending") (eq .Build "running")}} pend{{end}}"><span class="vh">{{with .Build}}last build {{.}}{{else}}no builds{{end}}</span></span></span></li> 27 {{range .Pins}}<li><a href="/{{.Owner}}/{{.Name}}"><span class="owner">{{.Owner}}/</span>{{.Name}}</a><span class="n" title="{{.Issues}} open issue{{if ne .Issues 1}}s{{end}}, {{.MRs}} open merge request{{if ne .MRs 1}}s{{end}}{{with .Build}}, last build {{.}}{{end}}"><b{{if .Issues}} class="wants"{{end}}>{{.Issues}} <span class="vh">open issues</span></b> <b{{if .MRs}} class="wants"{{end}}>{{.MRs}} <span class="vh">open merge requests</span></b> <span class="dot{{if eq .Build "success"}} ok{{else if or (eq .Build "failure") (eq .Build "error")}} bad{{else if or (eq .Build "pending") (eq .Build "running")}} pend{{end}}"><span class="vh">{{with .Build}}last build {{.}}{{else}}no builds{{end}}</span></span></span></li>
28 {{end}}</ul> 28 {{end}}</ul>
29 {{else}}<p class="none">Nothing pinned yet. Press Pin on a repository.</p>{{end}} 29 {{else}}<p class="none">nothing pinned — press Pin on a repository you visit</p>{{end}}
30</aside> 30</aside>
31 31
32<section class="dashmain"> 32<section class="dashmain">
internal/web/templates/mr.html +2 −2
@@ -144,7 +144,7 @@
144 <div class="grp"> 144 <div class="grp">
145 <h2>Reviewers</h2> 145 <h2>Reviewers</h2>
146 {{if .MR.ReviewRequests}}<p class="row">{{range .MR.ReviewRequests}}<a href="/{{.}}">{{.}}</a> {{end}}</p> 146 {{if .MR.ReviewRequests}}<p class="row">{{range .MR.ReviewRequests}}<a href="/{{.}}">{{.}}</a> {{end}}</p>
147 {{else}}<p class="none">nobody yet</p>{{end}} 147 {{else}}<p class="none">no reviewers</p>{{end}}
148 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}} 148 {{if and .CanWrite (or (eq .MR.State "open") (eq .MR.State "source_gone"))}}
149 <form method="post" action="{{$base}}/review-request" class="actions"> 149 <form method="post" action="{{$base}}/review-request" class="actions">
150 <input type="text" name="add" aria-label="Add reviewers" autocomplete="off" spellcheck="false" placeholder="add, space-separated"> 150 <input type="text" name="add" aria-label="Add reviewers" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
@@ -175,7 +175,7 @@
175 <div class="grp"> 175 <div class="grp">
176 <h2>Labels</h2> 176 <h2>Labels</h2>
177 {{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> 177 {{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>
178 {{else}}<p class="none">none yet</p>{{end}} 178 {{else}}<p class="none">{{if or (eq .MR.State "merged") (eq .MR.State "closed")}}no labels{{else}}none yet{{end}}</p>{{end}}
179 {{if .CanWrite}} 179 {{if .CanWrite}}
180 <form method="post" action="{{$base}}/label" class="actions"> 180 <form method="post" action="{{$base}}/label" class="actions">
181 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated"> 181 <input type="text" name="add" aria-label="Add labels" autocomplete="off" spellcheck="false" placeholder="add, space-separated">
internal/web/templates/mrs.html +1 −1
@@ -26,7 +26,7 @@
26 </div> 26 </div>
27</li> 27</li>
28{{else}}{{if .Query}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests matching “{{.Query}}”</li> 28{{else}}{{if .Query}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests matching “{{.Query}}”</li>
29{{else}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests — open one with <code>gitbay mr create {{.Repo.OwnerName}}/{{.Repo.Name}} --source ... --target {{.Repo.DefaultBranch}}</code></li>{{end}}{{end}} 29{{else}}<li class="empty">no {{if ne .State "all"}}{{.State}} {{end}}merge requests</li>{{end}}{{end}}
30</ul> 30</ul>
31{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}} 31{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}}
32</div> 32</div>
internal/web/templates/notifications.html +1 −1
@@ -18,6 +18,6 @@
18 </div> 18 </div>
19 {{if not .Read}}<form method="post" action="/notifications" class="inline"><input type="hidden" name="id" value="{{.ID}}"><button type="submit" class="btn">Mark read</button></form>{{end}} 19 {{if not .Read}}<form method="post" action="/notifications" class="inline"><input type="hidden" name="id" value="{{.ID}}"><button type="submit" class="btn">Mark read</button></form>{{end}}
20</li> 20</li>
21{{else}}<li class="empty">{{if .All}}nothing here yet{{else}}nothing unread — <a href="/notifications?all=1">show all</a>{{end}}</li>{{end}} 21{{else}}<li class="empty">{{if .All}}nothing to show{{else}}no unread notifications — <a href="/notifications?all=1">show all</a>{{end}}</li>{{end}}
22</ul> 22</ul>
23{{end}} 23{{end}}