A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 73c8e291f7

73c8e291f76ca4383165230e62a311bab3d4fa74

parent: fa1784cbc9

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-26T05:14:30Z

web: check status and a path filter on the log, visible focus in the rail

The log shows each commit's combined check status, batched into one query
rather than fifty. The ?path= filter gets a form, so per-file history is
reachable without editing the URL.

The rail is black in both color schemes, where the light scheme's accent
made an invisible focus ring; it uses the shell's own mark instead.

Ref #35
internal/httpd/web.go +3 −1
@@ -1183,12 +1183,14 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) {
11831183 type row struct {
11841184 SHA, ShortSHA, Subject, AuthorName, AuthorEmail, AuthorUser, Date string
11851185 Sig sigView
1186 Check string // combined status, "" when none ran
11861187 }
11871188 names := s.authorNames()
1189 checks, _ := s.st.CombinedStatusFor(p.Repo.ID, shas)
11881190 var rows []row
11891191 for _, sha := range shas {
11901192 v, parsed := s.sigFor(p.Repo, p.Dir, sha)
1191 rw := row{SHA: sha, ShortSHA: sha[:10], Sig: v}
1193 rw := row{SHA: sha, ShortSHA: sha[:10], Sig: v, Check: checks[sha]}
11921194 if parsed != nil {
11931195 rw.Subject = parsed.Subject
11941196 rw.AuthorName = names.name(parsed.AuthorEmail, parsed.AuthorName)
internal/store/statuses.go +41
@@ -1,5 +1,7 @@
11 package store
22
3import "strings"
4
35 type CommitStatus struct {
46 Context string
57 State string // pending | success | failure | error
@@ -50,6 +52,45 @@ func (s *Store) ListCommitStatuses(repoID int64, sha string) ([]CommitStatus, er
5052
5153 // CombinedStatus reduces per-context states to one: error/failure dominate,
5254 // then pending, then success; "" when no statuses exist.
55// CombinedStatusFor returns the combined state for each of several commits
56// in one query. The log lists fifty commits at a time; asking per commit
57// turns one page into fifty round trips.
58func (s *Store) CombinedStatusFor(repoID int64, shas []string) (map[string]string, error) {
59 out := map[string]string{}
60 if len(shas) == 0 {
61 return out, nil
62 }
63 args := make([]any, 0, len(shas)+1)
64 args = append(args, repoID)
65 for _, sha := range shas {
66 args = append(args, sha)
67 }
68 q := `SELECT commit_sha, state FROM commit_statuses WHERE repo_id = ? AND commit_sha IN (?` +
69 strings.Repeat(",?", len(shas)-1) + `)`
70 rows, err := s.DB.Query(q, args...)
71 if err != nil {
72 return nil, err
73 }
74 defer rows.Close()
75 for rows.Next() {
76 var sha, state string
77 if err := rows.Scan(&sha, &state); err != nil {
78 return nil, err
79 }
80 // Same precedence as CombinedStatus, folded in as rows arrive.
81 switch {
82 case out[sha] == "failure":
83 case state == "error" || state == "failure":
84 out[sha] = "failure"
85 case state == "pending":
86 out[sha] = "pending"
87 case out[sha] == "":
88 out[sha] = "success"
89 }
90 }
91 return out, rows.Err()
92}
93
5394 func CombinedStatus(statuses []CommitStatus) string {
5495 if len(statuses) == 0 {
5596 return ""
internal/web/static/style.css +10
@@ -144,6 +144,10 @@ a:hover { text-decoration: underline; }
144144 outline-offset: 2px;
145145 border-radius: 1px;
146146 }
147/* the shell is black in both schemes, and --accent is a dark blue in the
148 light one — a ring nobody could see. The shell's own mark clears 7:1 on
149 black either way. */
150.rail :focus-visible { outline-color: var(--shell-mark); }
147151
148152 /* visually hidden, still read aloud: for headings a sighted reader gets
149153 from the layout but a screen reader would otherwise miss */
@@ -800,6 +804,12 @@ pre.message {
800804 .teambody { padding: var(--sp-3); background: var(--bg); }
801805 .teambody .memberchip { margin-right: var(--sp-2); }
802806
807form.logfilter {
808 display: flex; align-items: center; gap: var(--sp-2);
809 margin: 0 0 var(--sp-4); flex-wrap: wrap;
810}
811form.logfilter input { flex: 1; min-width: 12rem; max-width: 28rem; }
812
803813 /* account settings */
804814 table.keys td { padding: var(--sp-2) var(--sp-4) var(--sp-2) 0; }
805815 table.keys td.mono, .mono { font-family: var(--mono); font-size: var(--fs-1); overflow-wrap: anywhere; }
internal/web/templates/log.html +9 −2
@@ -1,14 +1,21 @@
11 {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
3{{if .FilePath}}<p class="meta">history of <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blob/{{.Ref}}/{{.FilePath}}"><code>{{.FilePath}}</code></a> · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log">full log</a></p>{{end}}
43 <h1 class="vh">Commits</h1>
4<form method="get" class="logfilter" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.Ref}}">
5 <label for="path">Path</label>
6 <input type="text" id="path" name="path" value="{{.FilePath}}" placeholder="filter to a file or directory">
7 <button type="submit">Filter</button>
8 {{if .FilePath}}<a class="act" href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.Ref}}">Clear</a>{{end}}
9</form>
10{{if .FilePath}}<p class="meta">history of <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blob/{{.Ref}}/{{.FilePath}}"><code>{{.FilePath}}</code></a></p>{{end}}
511 <ul class="loglist">
6{{range .Commits}}<li>
12{{range .Commits}}{{$c := .}}<li>
713 <div class="commitmain">
814 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p>
915 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} · {{.Date}}</p>
1016 </div>
1117 <div class="commitside">
18 {{with .Check}}<a class="badge badge-{{.}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{$c.SHA}}" title="checks: {{.}}">{{.}}</a>{{end}}
1219 {{template "sigbadge" .Sig}}
1320 <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code>
1421 </div>