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

merged merged by cmc on 2026-08-26 05:14 UTC · krz/gitbay:log-polish into main

4 files changed, +63 −3

Layout: unified · split

internal/httpd/web.go +3 −1
@@ -1183,12 +1183,14 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) {
1183 type row struct { 1183 type row struct {
1184 SHA, ShortSHA, Subject, AuthorName, AuthorEmail, AuthorUser, Date string 1184 SHA, ShortSHA, Subject, AuthorName, AuthorEmail, AuthorUser, Date string
1185 Sig sigView 1185 Sig sigView
1186 Check string // combined status, "" when none ran
1186 } 1187 }
1187 names := s.authorNames() 1188 names := s.authorNames()
1189 checks, _ := s.st.CombinedStatusFor(p.Repo.ID, shas)
1188 var rows []row 1190 var rows []row
1189 for _, sha := range shas { 1191 for _, sha := range shas {
1190 v, parsed := s.sigFor(p.Repo, p.Dir, sha) 1192 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]}
1192 if parsed != nil { 1194 if parsed != nil {
1193 rw.Subject = parsed.Subject 1195 rw.Subject = parsed.Subject
1194 rw.AuthorName = names.name(parsed.AuthorEmail, parsed.AuthorName) 1196 rw.AuthorName = names.name(parsed.AuthorEmail, parsed.AuthorName)
internal/store/statuses.go +41
@@ -1,5 +1,7 @@
1package store 1package store
2 2
3import "strings"
4
3type CommitStatus struct { 5type CommitStatus struct {
4 Context string 6 Context string
5 State string // pending | success | failure | error 7 State string // pending | success | failure | error
@@ -50,6 +52,45 @@ func (s *Store) ListCommitStatuses(repoID int64, sha string) ([]CommitStatus, er
50 52
51// CombinedStatus reduces per-context states to one: error/failure dominate, 53// CombinedStatus reduces per-context states to one: error/failure dominate,
52// then pending, then success; "" when no statuses exist. 54// 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
53func CombinedStatus(statuses []CommitStatus) string { 94func CombinedStatus(statuses []CommitStatus) string {
54 if len(statuses) == 0 { 95 if len(statuses) == 0 {
55 return "" 96 return ""
internal/web/static/style.css +10
@@ -144,6 +144,10 @@ a:hover { text-decoration: underline; }
144 outline-offset: 2px; 144 outline-offset: 2px;
145 border-radius: 1px; 145 border-radius: 1px;
146} 146}
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); }
147 151
148/* visually hidden, still read aloud: for headings a sighted reader gets 152/* visually hidden, still read aloud: for headings a sighted reader gets
149 from the layout but a screen reader would otherwise miss */ 153 from the layout but a screen reader would otherwise miss */
@@ -800,6 +804,12 @@ pre.message {
800.teambody { padding: var(--sp-3); background: var(--bg); } 804.teambody { padding: var(--sp-3); background: var(--bg); }
801.teambody .memberchip { margin-right: var(--sp-2); } 805.teambody .memberchip { margin-right: var(--sp-2); }
802 806
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
803/* account settings */ 813/* account settings */
804table.keys td { padding: var(--sp-2) var(--sp-4) var(--sp-2) 0; } 814table.keys td { padding: var(--sp-2) var(--sp-4) var(--sp-2) 0; }
805table.keys td.mono, .mono { font-family: var(--mono); font-size: var(--fs-1); overflow-wrap: anywhere; } 815table.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 @@
1{{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} 1{{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
2{{define "content"}} 2{{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}}
4<h1 class="vh">Commits</h1> 3<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}}
5<ul class="loglist"> 11<ul class="loglist">
6{{range .Commits}}<li> 12{{range .Commits}}{{$c := .}}<li>
7 <div class="commitmain"> 13 <div class="commitmain">
8 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p> 14 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p>
9 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} · {{.Date}}</p> 15 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} · {{.Date}}</p>
10 </div> 16 </div>
11 <div class="commitside"> 17 <div class="commitside">
18 {{with .Check}}<a class="badge badge-{{.}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{$c.SHA}}" title="checks: {{.}}">{{.}}</a>{{end}}
12 {{template "sigbadge" .Sig}} 19 {{template "sigbadge" .Sig}}
13 <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code> 20 <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code>
14 </div> 21 </div>