Commit 92c816c58e

92c816c58e0fb439d3392b7c8b4df80f70023f23

parent: 7720b4d5d5

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-21 20:38 UTC

builds: lead a run with its commit subject, and page the list

A run row led with a ten-character sha, so recognising a build meant
opening the commit or already knowing the sha. build list resolves each
distinct commit's subject in one git log and carries it on BuildOut, for
every surface rather than the page alone. The row leads with it and
demotes the sha to the metadata line; a build whose commit is gone falls
back to the sha.

The list was also capped at the newest 50 matching builds with the window
unstated, so filtering to a sparse status reached much further back and
appeared to raise the run count. build list takes --limit and --cursor
now, the keyset cursor the other list commands have; the page asks for 30,
offers "older" carrying every filter, and says its count is this page's
when there is another.

Closes #241
Closes #244
internal/control/build.go +46 −5
@@ -22,7 +22,7 @@ import (
2222func init() {
2323 register(Command{Path: []string{"build", "list"},
2424 Summary: "list recent builds",
25 Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>]", ReadOnly: true, Run: runBuildList})
25 Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runBuildList})
2626 register(Command{Path: []string{"build", "show"},
2727 Summary: "show one build",
2828 Usage: "build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow})
@@ -78,10 +78,15 @@ type BuildOut struct {
7878 Ref string `json:"ref"`
7979 CreatedAt string `json:"created_at"`
8080 FinishedAt string `json:"finished_at,omitempty"`
81 // Subject is the first line of the commit's message, so a build
82 // names what it ran on rather than only its sha (#241). It is empty
83 // when the commit is no longer in the repository.
84 Subject string `json:"subject,omitempty"`
8185}
8286
8387func buildToOut(b store.Build) BuildOut {
84 return BuildOut{b.Number, b.Job, b.Status, b.SHA, b.Ref, b.CreatedAt, b.FinishedAt}
88 return BuildOut{Number: b.Number, Job: b.Job, Status: b.Status, SHA: b.SHA,
89 Ref: b.Ref, CreatedAt: b.CreatedAt, FinishedAt: b.FinishedAt}
8590}
8691
8792func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
@@ -107,7 +112,16 @@ func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
107112// is told to pick from.
108113var buildStatuses = []string{"pending", "running", "success", "failure", "cancelled"}
109114
115// buildPage is how many builds one page of build list returns when no
116// --limit is given. The cap has always been there; what it is now
117// reachable past, with --cursor (#244).
118const buildPage = 50
119
110120func runBuildList(c *Ctx, args []string) int {
121 args, p, code := parsePageFlags(c, args, "build", true)
122 if code >= 0 {
123 return code
124 }
111125 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--status", "--job"}, MaxPos: 1, Usage: c.Cmd.Usage})
112126 if err != nil {
113127 return c.fail(protocol.ExitUsage, "%v", err)
@@ -124,21 +138,48 @@ func runBuildList(c *Ctx, args []string) int {
124138 if code >= 0 {
125139 return code
126140 }
127 builds, err := c.Store.ListBuilds(repo.ID, store.BuildFilter{Ref: f.Value("--ref"), Status: status, Job: f.Value("--job")}, 50)
141 limit := p.queryLimit()
142 if limit == 0 {
143 limit = buildPage
144 }
145 filter := store.BuildFilter{Ref: f.Value("--ref"), Status: status, Job: f.Value("--job"), Before: p.keyInt()}
146 builds, err := c.Store.ListBuilds(repo.ID, filter, limit)
128147 if err != nil {
129148 return c.fail(protocol.ExitFailure, "%v", err)
130149 }
150 builds, next := trimPage(p, builds, "build", func(b store.Build) string {
151 return strconv.FormatInt(b.Number, 10)
152 })
131153 var ds []BuildOut
132154 for _, b := range builds {
133155 ds = append(ds, buildToOut(b))
134156 }
135 return c.emit(ds, func(w io.Writer) {
157 subjects := buildSubjects(c, repo, ds)
158 for i := range ds {
159 ds[i].Subject = subjects[ds[i].SHA]
160 }
161 return c.emitPage(p, ds, next, func(w io.Writer) {
136162 for _, d := range ds {
137 fmt.Fprintf(w, "%d\t%s\t%s\t%.10s\t%s\n", d.Number, d.Job, d.Status, d.SHA, d.Ref)
163 fmt.Fprintf(w, "%d\t%s\t%s\t%.10s\t%s\t%s\n", d.Number, d.Job, d.Status, d.SHA, d.Ref, d.Subject)
138164 }
139165 })
140166}
141167
168// buildSubjects reads the commit subject of each distinct sha on a page
169// of builds. Several jobs of one push share a commit, so the set is
170// usually far smaller than the page.
171func buildSubjects(c *Ctx, repo store.Repo, ds []BuildOut) map[string]string {
172 seen := map[string]bool{}
173 var shas []string
174 for _, d := range ds {
175 if d.SHA != "" && !seen[d.SHA] {
176 seen[d.SHA] = true
177 shas = append(shas, d.SHA)
178 }
179 }
180 return gitutil.Subjects(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name), shas)
181}
182
142183func runBuildShow(c *Ctx, args []string) int {
143184 _, b, code := buildRef(c, args)
144185 if code >= 0 {
internal/control/buildlistpage_test.go added +139
@@ -0,0 +1,139 @@
1package control
2
3import (
4 "bytes"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15// A run row led with a sha and said nothing about what the commit was
16// (#241). build list carries the subject now, for every surface at once.
17func TestBuildListCarriesCommitSubjects(t *testing.T) {
18 st, repo, uid := newQueueTestRepo(t)
19 git := gitRunner(t)
20 root := t.TempDir()
21
22 src := filepath.Join(root, "src")
23 os.MkdirAll(src, 0o755)
24 git(root, "init", "-q", "-b", "main", "src")
25 os.WriteFile(filepath.Join(src, "a"), []byte("one\n"), 0o644)
26 git(src, "add", ".")
27 git(src, "commit", "-qm", "runner: cap a build's container")
28 sha := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
29
30 dir := RepoDir(root, repo.OwnerName, repo.Name)
31 os.MkdirAll(filepath.Dir(dir), 0o755)
32 git(root, "clone", "-q", "--bare", src, dir)
33
34 if _, err := st.CreateBuild(repo.ID, "unit", sha, "main", `["true"]`, "", "", true); err != nil {
35 t.Fatal(err)
36 }
37 // A build whose commit is gone, as a force-push leaves behind.
38 if _, err := st.CreateBuild(repo.ID, "lint", strings.Repeat("1", 40), "main", `["true"]`, "", "", true); err != nil {
39 t.Fatal(err)
40 }
41
42 c, errOut := pruneCtx(st, root, store.User{ID: uid})
43 c.JSON = true
44 if code := Dispatch(c, []string{"build", "list", repo.Path()}); code != protocol.ExitOK {
45 t.Fatalf("build list: exit %d: %s", code, errOut.String())
46 }
47 var got []BuildOut
48 decodeData(t, c.Stdout.(*bytes.Buffer).Bytes(), &got)
49 if len(got) != 2 {
50 t.Fatalf("builds: %+v", got)
51 }
52 for _, b := range got {
53 switch b.Job {
54 case "unit":
55 if b.Subject != "runner: cap a build's container" {
56 t.Errorf("unit subject = %q", b.Subject)
57 }
58 case "lint":
59 if b.Subject != "" {
60 t.Errorf("a build whose commit is gone claims a subject: %q", b.Subject)
61 }
62 }
63 }
64}
65
66// The builds list was capped at 50 with no way past it, so filtering to a
67// sparse status reached further back and appeared to raise the total
68// (#244). --limit and --cursor page it like every other list command.
69func TestBuildListPages(t *testing.T) {
70 st, repo, uid := newQueueTestRepo(t)
71 for i := 0; i < 5; i++ {
72 if _, err := st.CreateBuild(repo.ID, "unit", "aaa", "main", `["true"]`, "", "", true); err != nil {
73 t.Fatal(err)
74 }
75 }
76 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
77 c.JSON = true
78
79 page := func(args ...string) (nums []int64, next string) {
80 t.Helper()
81 out := c.Stdout.(*bytes.Buffer)
82 out.Reset()
83 errOut.Reset()
84 argv := append([]string{"build", "list", repo.Path()}, args...)
85 if code := Dispatch(c, argv); code != protocol.ExitOK {
86 t.Fatalf("build list %v: exit %d: %s", args, code, errOut.String())
87 }
88 var got struct {
89 Items []BuildOut `json:"items"`
90 Next string `json:"next"`
91 }
92 decodeData(t, out.Bytes(), &got)
93 for _, b := range got.Items {
94 nums = append(nums, b.Number)
95 }
96 return nums, got.Next
97 }
98
99 nums, next := page("--limit", "2")
100 if len(nums) != 2 || nums[0] != 5 || nums[1] != 4 {
101 t.Fatalf("first page: %v", nums)
102 }
103 if next == "" {
104 t.Fatal("first page offers no cursor with three builds left")
105 }
106 nums, next = page("--limit", "2", "--cursor", next)
107 if len(nums) != 2 || nums[0] != 3 || nums[1] != 2 {
108 t.Fatalf("second page: %v", nums)
109 }
110 nums, next = page("--limit", "2", "--cursor", next)
111 if len(nums) != 1 || nums[0] != 1 {
112 t.Fatalf("last page: %v", nums)
113 }
114 if next != "" {
115 t.Errorf("last page offers a cursor: %q", next)
116 }
117
118 // A cursor minted by another command is not a build cursor.
119 out := c.Stdout.(*bytes.Buffer)
120 out.Reset()
121 errOut.Reset()
122 if code := Dispatch(c, []string{"build", "list", repo.Path(), "--cursor", encodeCursor("issue", "3")}); code != protocol.ExitUsage {
123 t.Fatalf("foreign cursor: exit %d, want %d", code, protocol.ExitUsage)
124 }
125}
126
127// decodeData unwraps the protocol envelope the JSON emitters write.
128func decodeData(t *testing.T, b []byte, into any) {
129 t.Helper()
130 var env struct {
131 Data json.RawMessage `json:"data"`
132 }
133 if err := json.Unmarshal(b, &env); err != nil {
134 t.Fatalf("envelope: %v\n%s", err, b)
135 }
136 if err := json.Unmarshal(env.Data, into); err != nil {
137 t.Fatalf("data: %v\n%s", err, env.Data)
138 }
139}
internal/gitutil/messages.go +25
@@ -109,3 +109,28 @@ func RevListMessages(dir, old, new string, max int) ([]CommitMsg, error) {
109109 }
110110 return msgs, nil
111111}
112
113// Subjects returns the first line of each named commit's message, keyed
114// by sha. One git log for the whole set rather than one per sha: a page
115// of builds names a handful of distinct commits and a subprocess each
116// would show. --ignore-missing keeps a sha git cannot resolve from
117// failing the rest, because a build outlives the commit it ran on once a
118// branch is force-pushed; such a sha is simply absent from the map.
119func Subjects(dir string, shas []string) map[string]string {
120 if len(shas) == 0 {
121 return nil
122 }
123 args := append([]string{"-C", dir, "log", "--no-walk=unsorted", "--ignore-missing", "--format=%H%x00%s"}, shas...)
124 args = append(args, "--")
125 out, err := exec.Command(toolpath.Look("git"), args...).Output()
126 if err != nil {
127 return nil
128 }
129 subjects := map[string]string{}
130 for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
131 if sha, subject, ok := strings.Cut(line, "\x00"); ok {
132 subjects[sha] = subject
133 }
134 }
135 return subjects
136}
internal/gitutil/subjects_test.go added +50
@@ -0,0 +1,50 @@
1package gitutil
2
3import (
4 "os/exec"
5 "strings"
6 "testing"
7)
8
9func TestSubjects(t *testing.T) {
10 dir := t.TempDir()
11 git(t, dir, "init", "-q", "-b", "main")
12
13 write(t, dir, "a", "one\n")
14 git(t, dir, "add", ".")
15 git(t, dir, "commit", "-qm", "first thing")
16 first := rev(t, dir, "HEAD")
17
18 write(t, dir, "a", "two\n")
19 git(t, dir, "commit", "-qam", "second thing")
20 second := rev(t, dir, "HEAD")
21
22 got := Subjects(dir, []string{second, first})
23 if got[first] != "first thing" || got[second] != "second thing" {
24 t.Fatalf("Subjects = %v", got)
25 }
26
27 // A build outlives the commit it ran on when a branch is
28 // force-pushed. The shas that do resolve still come back.
29 gone := strings.Repeat("1", 40)
30 got = Subjects(dir, []string{gone, first})
31 if got[first] != "first thing" {
32 t.Errorf("a missing sha lost the rest: %v", got)
33 }
34 if _, ok := got[gone]; ok {
35 t.Errorf("resolved a sha that is not there: %v", got)
36 }
37
38 if Subjects(dir, nil) != nil {
39 t.Error("empty sha list should not run git")
40 }
41}
42
43func rev(t *testing.T, dir, ref string) string {
44 t.Helper()
45 out, err := exec.Command("git", "-C", dir, "rev-parse", ref).Output()
46 if err != nil {
47 t.Fatal(err)
48 }
49 return strings.TrimSpace(string(out))
50}
internal/httpd/buildpages_test.go +82 −2
@@ -36,11 +36,12 @@ func TestBuildsPageRendersCommandOutput(t *testing.T) {
3636 Filter buildFilter
3737 Facets []facetGroup
3838 Refs []string
39 Older string
3940 CanWrite bool
4041 Notice string
4142 }{
4243 testRepoPage(), builds, jobs, groupRuns(builds), filter, nil,
43 distinctRefs(builds, filter.Ref), true, "",
44 distinctRefs(builds, filter.Ref), "", true, "",
4445 })
4546 if err != nil {
4647 t.Fatalf("render: %v", err)
@@ -111,11 +112,12 @@ func TestBuildsPageCountsBuildsAndRuns(t *testing.T) {
111112 Filter buildFilter
112113 Facets []facetGroup
113114 Refs []string
115 Older string
114116 CanWrite bool
115117 Notice string
116118 }{
117119 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
118 distinctRefs(builds, filter.Ref), true, "",
120 distinctRefs(builds, filter.Ref), "", true, "",
119121 })
120122 if err != nil {
121123 t.Fatalf("render: %v", err)
@@ -124,3 +126,81 @@ func TestBuildsPageCountsBuildsAndRuns(t *testing.T) {
124126 t.Errorf("builds.html count line: %q", sb.String())
125127 }
126128}
129
130// A run row led with a ten-character sha and nothing said what the commit
131// was (#241). The subject leads now, the sha follows as metadata, and a
132// build whose commit is gone falls back to the sha alone.
133func TestBuildsPageLeadsWithTheSubject(t *testing.T) {
134 builds := []control.BuildOut{
135 {Number: 2, Job: "unit", Status: "success", SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b",
136 Ref: "main", CreatedAt: "2026-09-20T06:00:00Z", Subject: "runner: cap a build's container"},
137 {Number: 1, Job: "unit", Status: "failure", SHA: "aa11bb22cc33dd44ee55ff6677889900aabbccdd",
138 Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"},
139 }
140 out := renderBuilds(t, builds, buildFilter{}, "?cursor=abc")
141 if !strings.Contains(out, "runner: cap a build&#39;s container") {
142 t.Errorf("builds.html does not lead with the subject:\n%s", out)
143 }
144 if !strings.Contains(out, "<code>ff6271a9d4</code>") {
145 t.Errorf("builds.html drops the sha from the metadata line:\n%s", out)
146 }
147 // The commit is gone, so the sha is all there is to name the row by.
148 if !strings.Contains(out, ">aa11bb22cc</a>") {
149 t.Errorf("a subjectless run does not fall back to its sha:\n%s", out)
150 }
151}
152
153// Filtering appeared to raise the run count because the list was capped
154// at 50 with the window unstated (#244). The page is paged now: the count
155// says it counts this page, and the link to the next one carries every
156// filter.
157func TestBuildsPagePagesAndSaysSo(t *testing.T) {
158 builds := []control.BuildOut{{Number: 1, Job: "unit", Status: "failure", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"}}
159 out := renderBuilds(t, builds, buildFilter{Status: "failure"}, "?cursor=abc&status=failure")
160 if !strings.Contains(out, "1 build in 1 run on this page") {
161 t.Errorf("count line does not name the page:\n%s", out)
162 }
163 if !strings.Contains(out, `href="?cursor=abc&amp;status=failure"`) {
164 t.Errorf("pager link missing or drops the filter:\n%s", out)
165 }
166 // With everything on one page the count is the whole count.
167 if out := renderBuilds(t, builds, buildFilter{}, ""); strings.Contains(out, "on this page") {
168 t.Errorf("an unpaged listing still hedges the count:\n%s", out)
169 }
170}
171
172func TestOlderBuildsCarriesFilters(t *testing.T) {
173 if got := olderBuilds(buildFilter{Ref: "main"}, ""); got != "" {
174 t.Errorf("no next cursor should mean no link, got %q", got)
175 }
176 got := olderBuilds(buildFilter{Ref: "feature/x", Status: "failure", Job: "unit"}, "c1")
177 for _, want := range []string{"cursor=c1", "ref=feature%2Fx", "status=failure", "job=unit"} {
178 if !strings.Contains(got, want) {
179 t.Errorf("olderBuilds = %q, missing %q", got, want)
180 }
181 }
182}
183
184func renderBuilds(t *testing.T, builds []control.BuildOut, filter buildFilter, older string) string {
185 t.Helper()
186 var sb strings.Builder
187 err := web.Render(&sb, "builds.html", struct {
188 repoPage
189 Builds []control.BuildOut
190 Jobs []control.JobOut
191 Runs []buildRun
192 Filter buildFilter
193 Facets []facetGroup
194 Refs []string
195 Older string
196 CanWrite bool
197 Notice string
198 }{
199 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
200 distinctRefs(builds, filter.Ref), older, true, "",
201 })
202 if err != nil {
203 t.Fatalf("render: %v", err)
204 }
205 return sb.String()
206}
internal/httpd/builds.go +35 −5
@@ -132,6 +132,7 @@ func distinctRefs(builds []control.BuildOut, current string) []string {
132132// one queueing of a commit, not the commit — see groupRuns (#240).
133133type buildRun struct {
134134 SHA string
135 Subject string
135136 Ref string
136137 CreatedAt string
137138 Status string
@@ -194,7 +195,7 @@ func groupRuns(builds []control.BuildOut) []buildRun {
194195 runs[n-1].Builds = append(runs[n-1].Builds, b)
195196 continue
196197 }
197 runs = append(runs, buildRun{SHA: b.SHA, Ref: b.Ref, CreatedAt: b.CreatedAt, Builds: []control.BuildOut{b}})
198 runs = append(runs, buildRun{SHA: b.SHA, Subject: b.Subject, Ref: b.Ref, CreatedAt: b.CreatedAt, Builds: []control.BuildOut{b}})
198199 }
199200 for i := range runs {
200201 runs[i].Status = combinedStatus(runs[i].Builds)
@@ -202,6 +203,27 @@ func groupRuns(builds []control.BuildOut) []buildRun {
202203 return runs
203204}
204205
206// buildsPerPage is how many builds one page of the builds tab asks for.
207// Fewer than the command's own default, because the page folds them into
208// runs and a run is several builds tall (#244).
209const buildsPerPage = 30
210
211// olderBuilds is the link to the page after this one: the command's own
212// keyset cursor with the three filters carried along, so paging never
213// drops a filter and a filter never lands on page two.
214func olderBuilds(f buildFilter, next string) string {
215 if next == "" {
216 return ""
217 }
218 q := url.Values{"cursor": {next}}
219 for k, v := range map[string]string{"ref": f.Ref, "status": f.Status, "job": f.Job} {
220 if v != "" {
221 q.Set(k, v)
222 }
223 }
224 return "?" + q.Encode()
225}
226
205227func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
206228 p, ok := s.repoFor(w, r, "")
207229 if !ok {
@@ -212,7 +234,7 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
212234
213235 qv := r.URL.Query()
214236 filter := buildFilter{Ref: qv.Get("ref"), Status: qv.Get("status"), Job: qv.Get("job")}
215 argv := []string{"build", "list", p.Repo.Path()}
237 argv := []string{"build", "list", p.Repo.Path(), "--limit", strconv.Itoa(buildsPerPage)}
216238 if filter.Ref != "" {
217239 argv = append(argv, "--ref", filter.Ref)
218240 }
@@ -222,9 +244,16 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
222244 if filter.Job != "" {
223245 argv = append(argv, "--job", filter.Job)
224246 }
247 if cursor := qv.Get("cursor"); cursor != "" {
248 argv = append(argv, "--cursor", cursor)
249 }
225250
226 var builds []control.BuildOut
227 s.runControlInto(viewer, argv, &builds)
251 var page struct {
252 Items []control.BuildOut `json:"items"`
253 Next string `json:"next"`
254 }
255 s.runControlInto(viewer, argv, &page)
256 builds := page.Items
228257
229258 // The jobs a trigger can name. A repo without a CI config has none;
230259 // that is not an error for this page.
@@ -241,10 +270,11 @@ func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
241270 Filter buildFilter
242271 Facets []facetGroup
243272 Refs []string
273 Older string
244274 CanWrite bool
245275 Notice string
246276 }{p, builds, jobs, groupRuns(builds), filter, buildFacets(filter, jobs, refs), refs,
247 s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
277 olderBuilds(filter, page.Next), s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
248278}
249279
250280func (s *Server) build(w http.ResponseWriter, r *http.Request) {
internal/httpd/repohead_test.go +2 −1
@@ -26,9 +26,10 @@ func TestRepoHeaderTwoRows(t *testing.T) {
2626 Filter buildFilter
2727 Facets []facetGroup
2828 Refs []string
29 Older string
2930 CanWrite bool
3031 Notice string
31 }{p, nil, nil, nil, buildFilter{}, nil, nil, true, ""})
32 }{p, nil, nil, nil, buildFilter{}, nil, nil, "", true, ""})
3233 if err != nil {
3334 t.Fatal(err)
3435 }
internal/store/builds.go +7
@@ -275,10 +275,13 @@ func (s *Store) BuildByNumber(repoID, number int64) (Build, error) {
275275}
276276
277277// BuildFilter narrows ListBuilds to builds matching every non-empty field.
278// Before is the keyset cursor: only builds numbered below it, which with
279// the newest-first order is the page after the one that ended there.
278280type BuildFilter struct {
279281 Ref string
280282 Status string
281283 Job string
284 Before int64
282285}
283286
284287func (s *Store) ListBuilds(repoID int64, f BuildFilter, limit int) ([]Build, error) {
@@ -296,6 +299,10 @@ func (s *Store) ListBuilds(repoID int64, f BuildFilter, limit int) ([]Build, err
296299 q += " AND job = ?"
297300 args = append(args, f.Job)
298301 }
302 if f.Before > 0 {
303 q += " AND number < ?"
304 args = append(args, f.Before)
305 }
299306 q += " ORDER BY number DESC LIMIT ?"
300307 args = append(args, limit)
301308 rows, err := s.DB.Query(q, args...)
internal/web/templates/builds.html +4 −3
@@ -38,12 +38,12 @@
3838<pre class="code" tabindex="0">[![build](https://{{.Host}}/{{.Repo.OwnerName}}/{{.Repo.Name}}/badge/build.svg)](https://{{.Host}}/{{.Repo.OwnerName}}/{{.Repo.Name}}/builds)</pre>
3939<p class="meta">Add <code>?job=name</code> for one job.</p>
4040</details>
41<p class="meta">{{len .Builds}} build{{if ne (len .Builds) 1}}s{{end}} in {{len .Runs}} run{{if ne (len .Runs) 1}}s{{end}}{{if or .Filter.Ref .Filter.Status .Filter.Job}}, <a href="?">clear filters</a>{{end}}</p>
41<p class="meta">{{len .Builds}} build{{if ne (len .Builds) 1}}s{{end}} in {{len .Runs}} run{{if ne (len .Runs) 1}}s{{end}}{{if .Older}} on this page{{end}}{{if or .Filter.Ref .Filter.Status .Filter.Job}}, <a href="?">clear filters</a>{{end}}</p>
4242<ul class="loglist rows">
4343{{range .Runs}}<li>
4444 <div class="commitmain">
45 <p class="subject"><code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{printf "%.10s" .SHA}}</a></code></p>
46 <p class="meta">{{.Ref}} · {{when .CreatedAt}}</p>
45 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{if .Subject}}{{.Subject}}{{else}}{{printf "%.10s" .SHA}}{{end}}</a></p>
46 <p class="meta">{{if .Subject}}<code>{{printf "%.10s" .SHA}}</code> · {{end}}{{.Ref}} · {{when .CreatedAt}}</p>
4747 </div>
4848 <div class="commitside">
4949 <span class="badge check-{{.Status}}">{{.Status}}</span>
@@ -52,6 +52,7 @@
5252</li>
5353{{else}}<li class="empty">no builds — push a commit with a <code>.gitbay/ci.yml</code></li>{{end}}
5454</ul>
55{{if .Older}}<p class="pager"><a href="{{.Older}}">older →</a></p>{{end}}
5556</div>
5657</div>
5758{{end}}