web: v1.23.0 polish follow-ups !403

merged merged by cmc on 2026-09-18 04:32 UTC · krz/gitbay:stack-221 into main

9 files changed, +48 −27

Layout: unified · split

e2e/design_test.go +25 −1
@@ -149,6 +149,30 @@ func TestReadmeRelativeLinks(t *testing.T) {
149149 if _, body := inst.get(t, "/alice/site"); !strings.Contains(body, `class="chip topic"`) {
150150 t.Error("repo home lost its topics")
151151 }
152 // The repohead is the same markup on every tab within a repository —
153 // aria-current on the tab link and the watch/bookmark toggles are the
154 // only things that could vary, and they depend on who is signed in,
155 // not which tab is shown. inst.get is anonymous, so two tabs' headers
156 // must be byte-identical.
157 inst.ssh(t, aliceKey, "", "issue", "create", "alice/site", "--title", "one")
158 header := func(body string) string {
159 i := strings.Index(body, `<header class="repohead">`)
160 j := strings.Index(body, "</header>")
161 if i < 0 || j < 0 || j < i {
162 t.Fatalf("repohead not found:\n%.2000s", body)
163 }
164 return body[i:j]
165 }
166 _, issuesBody := inst.get(t, "/alice/site/issues")
167 _, issueBody := inst.get(t, "/alice/site/issues/1")
168 if a, b := header(issuesBody), header(issueBody); a != b {
169 t.Errorf("issues list and issue page headers differ:\n%s\nvs\n%s", a, b)
170 }
171 _, homeBody := inst.get(t, "/alice/site")
172 _, treeBody := inst.get(t, "/alice/site/tree/main/")
173 if a, b := header(homeBody), header(treeBody); a != b {
174 t.Errorf("repo home and tree headers differ:\n%s\nvs\n%s", a, b)
175 }
152176}
153177
154178// TestLandingRoutes checks the landing page's copy and the two routes.
@@ -363,7 +387,7 @@ func TestTreeSearchCodeAndClone(t *testing.T) {
363387 if i, j := strings.Index(body, `<table class="tree">`), strings.Index(body, `<div class="clone">`); i < 0 || j < i {
364388 t.Error("clone block does not follow the file table")
365389 }
366 if !strings.Contains(body, `<label>SSH</label>`) || !strings.Contains(body, `<label>HTTPS</label>`) {
390 if !strings.Contains(body, `<span class="fieldname">SSH</span>`) || !strings.Contains(body, `<span class="fieldname">HTTPS</span>`) {
367391 t.Error("clone blocks are not labelled")
368392 }
369393 // Clone and about belong to the repository root, not a subdirectory.
internal/httpd/settings.go +5 −3
@@ -61,6 +61,7 @@ func (s *Server) settingsFormWith(w http.ResponseWriter, r *http.Request, u stor
6161 "description": submitted.Get("description"),
6262 "website": submitted.Get("website"),
6363 "topics": submitted.Get("topics"),
64 "key": submitted.Get("key"),
6465 }
6566 }
6667 s.render(w, "settings.html", settingsPage{
@@ -188,10 +189,11 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.
188189 return
189190 }
190191 msg, ok := s.runControlStdin(u, []string{"repo", "runner", "add", repo}, body+"\n")
191 if ok {
192 msg = ""
192 if !ok {
193 s.settingsFormWith(w, r, u, msg, r.Form)
194 return
193195 }
194 s.settingsRedirect(w, r, msg)
196 s.settingsRedirect(w, r, "Saved the runner.")
195197 return
196198 case "runner-remove":
197199 argv = []string{"repo", "runner", "remove", repo, v("fingerprint")}
internal/httpd/web.go +7 −12
@@ -8,7 +8,6 @@ import (
88 "fmt"
99 "hash/fnv"
1010 "io"
11 "io/fs"
1211 "log"
1312 "math"
1413 "os"
@@ -177,21 +176,12 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
177176 Host string
178177 Accounts bool
179178 Signup bool
180 Picture bool
181179 EmailLogin bool
182180 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, host, s.cfg.Web.Mode == "accounts",
183181 s.cfg.Web.Mode == "accounts" && s.cfg.Registration.Mode != "closed",
184 landingPicture, s.emailLoginEnabled()})
182 s.emailLoginEnabled()})
185183}
186184
187// landingPicture says whether the landing page's screenshot images exist to
188// show, checked once against the embedded images.
189var landingPicture = func() bool {
190 _, e1 := fs.Stat(web.ImageFS, "static/img/mr-dark.png")
191 _, e2 := fs.Stat(web.ImageFS, "static/img/mr-light.png")
192 return e1 == nil && e2 == nil
193}()
194
195185func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.User) {
196186 mrs, _ := s.st.DashboardMRs(viewer.ID)
197187 issues, _ := s.st.DashboardIssues(viewer.ID)
@@ -491,7 +481,12 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request) {
491481 return
492482 }
493483 p.Tab = "files"
494 s.renderTree(w, r, p, strings.Trim(r.PathValue("path"), "/"))
484 path := strings.Trim(r.PathValue("path"), "/")
485 // The root of the default branch is the same page as the bare repo
486 // URL, so its header must match: RepoHome is what picks the h1 over
487 // the p+link identity, not which route was typed.
488 p.RepoHome = path == "" && p.Ref == p.Repo.DefaultBranch
489 s.renderTree(w, r, p, path)
495490}
496491
497492// treePage is shared by the populated and empty-repository renders: two
internal/web/static/style.css +2 −2
@@ -511,7 +511,6 @@ button, .button {
511511 background: var(--fill);
512512 color: var(--fill-fg);
513513}
514button.primary, .button.primary { background: var(--fill); color: var(--fill-fg); border-color: transparent; }
515514button:hover, .button.primary:hover { filter: brightness(1.12); text-decoration: none; }
516515button.btn, .button.btn {
517516 background: var(--canvas);
@@ -581,6 +580,7 @@ input[type="radio"] { accent-color: var(--fill); }
581580label { display: block; font-weight: 500; font-size: var(--fs-2); margin-bottom: 4px; }
582581label:has(> input[type="radio"]), label:has(> input[type="checkbox"]),
583582label:has(> select) { display: inline-flex; align-items: center; gap: var(--sp-2); }
583.fieldname { display: block; font-size: var(--fs-2); font-weight: 500; }
584584.hint { display: block; color: var(--muted); font-size: var(--fs-1); font-weight: 400; margin: 0 0 6px; }
585585.field { margin-bottom: var(--sp-4); }
586586.field input[type="text"], .field input[type="email"], .field select { width: 100%; }
@@ -1331,7 +1331,7 @@ details.refmenu .refdrop a.allrefs {
13311331}
13321332.clone { margin: 0 0 var(--sp-3); }
13331333.facts .about { min-width: 0; }
1334.clone label { font-size: var(--fs-1); color: var(--muted); font-weight: 500; margin-bottom: 2px; }
1334.clone .fieldname { font-size: var(--fs-1); color: var(--muted); font-weight: 500; margin-bottom: 2px; }
13351335.clone pre { margin-bottom: var(--sp-3); user-select: all; }
13361336.factgrid { display: grid; grid-template-columns: repeat(4, auto); gap: var(--sp-2) var(--sp-4); font-size: var(--fs-2); margin-bottom: var(--sp-3); }
13371337.factgrid b { font-weight: 600; }
internal/web/templates/edit.html +1 −1
@@ -1,4 +1,4 @@
1{{define "width"}}bounded{{end}}
1{{define "width"}}wide{{end}}
22{{define "title"}}edit {{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
33{{define "content"}}
44<h1>edit {{.Repo.OwnerName}}/{{.Repo.Name}} : {{.Path}} @ {{.Ref}}</h1>
internal/web/templates/landing.html +2 −2
@@ -6,10 +6,10 @@
66<p class="lede">A git forge you drive from the terminal. Repositories, issues, merge requests and CI over SSH, with a fast, readable web view of the same state.</p>
77<pre class="quickstart">ssh git@{{.Host}} help # every command, no client to install
88git clone ssh://git@{{.Host}}/owner/repo.git</pre>
9{{if .Picture}}<div class="shot"><picture>
9<div class="shot"><picture>
1010 <source srcset="/static/img/mr-dark.png" media="(prefers-color-scheme: dark)">
1111 <img src="/static/img/mr-light.png" width="1280" height="900" alt="A merge request page: the conversation on the left, checks and reviewers on the right.">
12</picture></div>{{end}}
12</picture></div>
1313<div class="facets">
1414 <section><h2>Read</h2><p>Browse and clone any public repository over HTTPS or <code>git://</code>, no account. Every commit shows whether its signature verified.</p></section>
1515 <section><h2>Write</h2><p>Push over SSH with the key you already have. Create a repository, file an issue, open and merge a request, all as commands.</p></section>
internal/web/templates/settings.html +2 −2
@@ -36,7 +36,7 @@
3636<section id="access"><h2>Access</h2>
3737<form method="post" action="{{$base}}" class="setform">
3838 <input type="hidden" name="field" value="visibility">
39 <div><label>Visibility</label><p class="hint">Private repositories answer not found to everyone without access, including in search and on your profile.</p></div>
39 <div><span class="fieldname">Visibility</span><p class="hint">Private repositories answer not found to everyone without access, including in search and on your profile.</p></div>
4040 <div class="check">
4141 <label><input type="radio" name="visibility" value="public"{{if eq .Repo.Visibility "public"}} checked{{end}}> Public</label>
4242 <label><input type="radio" name="visibility" value="private"{{if eq .Repo.Visibility "private"}} checked{{end}}> Private</label>
@@ -169,7 +169,7 @@
169169 <input type="hidden" name="field" value="runner-add">
170170 <label for="runner-key">Attach a runner</label>
171171 <p class="hint">Install <code>gitbay-runner</code>, run <code>gitbay-runner init</code>, and paste the key it prints. The runner builds your commits with the repository's secrets; merge requests from forks wait unless it runs with <code>-untrusted</code>.</p>
172 <textarea id="runner-key" name="key" rows="3" placeholder="ssh-ed25519 AAAA… (from gitbay-runner init)"></textarea>
172 <textarea id="runner-key" name="key" rows="3" placeholder="ssh-ed25519 AAAA… (from gitbay-runner init)">{{index .Submitted "key"}}</textarea>
173173 <button type="submit" class="btn">Attach</button>
174174</form>
175175</section>
internal/web/templates/tree.html +2 −2
@@ -32,8 +32,8 @@
3232{{if and .Entries (not .DirPath)}}<div class="facts">
3333 <div class="clone">
3434 <h2>Clone</h2>
35 <label>SSH</label><pre><code>git clone {{.SSHCloneURL}}</code></pre>
36 <label>HTTPS</label><pre><code>git clone {{.CloneURL}}</code></pre>
35 <span class="fieldname">SSH</span><pre><code>git clone {{.SSHCloneURL}}</code></pre>
36 <span class="fieldname">HTTPS</span><pre><code>git clone {{.CloneURL}}</code></pre>
3737 </div>
3838 {{if .Facts.Commits}}{{$r := printf "/%s/%s" .Repo.OwnerName .Repo.Name}}<div class="about">
3939 <h2>About</h2>
internal/web/web_test.go +2 −2
@@ -89,8 +89,8 @@ func TestWhenNamesTheZone(t *testing.T) {
8989// none. The merge request page picks wide for its diff view, so it gets
9090// a per-view define instead of a fixed one.
9191func TestMainWidthClass(t *testing.T) {
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true}
93 bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "settings.html": true, "account.html": true, "admin.html": true, "edit.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true}
93 bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "settings.html": true, "account.html": true, "admin.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}
9494 perView := map[string]string{"mr.html": `{{define "width"}}{{if eq .View "diff"}}wide{{else}}reading{{end}}{{end}}`}
9595 for _, name := range Pages() {
9696 src, err := TemplateSource(name)