internal/httpd/builds.go
202 lines · 5919 bytes
1package httpd
2
3import (
4 "net/http"
5 "net/url"
6 "strconv"
7
8 "gitbay.org/gitbay/internal/control"
9)
10
11// buildFilter is the builds page's GET filter: branch, status and job,
12// each optional and independent (#224).
13type buildFilter struct {
14 Ref string
15 Status string
16 Job string
17}
18
19// buildFilterLink is one entry in the nav.filters row above the build
20// list: a status or a job, with the other two parameters carried along so
21// clicking one never drops another.
22type buildFilterLink struct {
23 Label string
24 Href string
25 Active bool
26}
27
28// buildStatuses is the fixed vocabulary a build's status takes, in the
29// order the nav.filters row offers them.
30var buildStatuses = []string{"pending", "running", "success", "failure", "cancelled"}
31
32// filterLinks builds the nav.filters row: "all" (clears status and job),
33// one link per status, and one per job the repository's CI config names.
34// Each link keeps the filter's other two parameters and net/url encodes
35// them, so a branch name or job name with an odd character does not break
36// the query string it lands in.
37func filterLinks(f buildFilter, jobs []control.JobOut) []buildFilterLink {
38 href := func(status, job string) string {
39 q := url.Values{}
40 if f.Ref != "" {
41 q.Set("ref", f.Ref)
42 }
43 if status != "" {
44 q.Set("status", status)
45 }
46 if job != "" {
47 q.Set("job", job)
48 }
49 return "?" + q.Encode()
50 }
51 links := []buildFilterLink{
52 {Label: "all", Href: href("", ""), Active: f.Status == "" && f.Job == ""},
53 }
54 for _, s := range buildStatuses {
55 links = append(links, buildFilterLink{Label: s, Href: href(s, f.Job), Active: f.Status == s})
56 }
57 for _, j := range jobs {
58 links = append(links, buildFilterLink{Label: j.Name, Href: href(f.Status, j.Name), Active: f.Job == j.Name})
59 }
60 return links
61}
62
63// distinctRefs lists each ref among builds once, in order, plus the
64// current filter value if it is not already there. It backs the branch
65// field's <datalist> suggestions, not a claim about what branches exist:
66// a ref that matched nothing under the current status/job filter still
67// belongs in the list the person typed it from.
68func distinctRefs(builds []control.BuildOut, current string) []string {
69 seen := map[string]bool{}
70 var refs []string
71 add := func(ref string) {
72 if ref != "" && !seen[ref] {
73 seen[ref] = true
74 refs = append(refs, ref)
75 }
76 }
77 for _, b := range builds {
78 add(b.Ref)
79 }
80 add(current)
81 return refs
82}
83
84// buildRun is one commit's builds, grouped for display: the builds tab
85// reads by commit, not by job, so a push that runs three jobs shows as one
86// row with three chips rather than three unrelated rows (#224).
87type buildRun struct {
88 SHA string
89 Ref string
90 CreatedAt string
91 Status string
92 Builds []control.BuildOut
93}
94
95// runStatusPriority orders combinedStatus's worst-first check: a run reads
96// as its least finished or least successful build.
97var runStatusPriority = []string{"failure", "cancelled", "running", "pending"}
98
99// combinedStatus is the run's status: the worst of its builds' statuses,
100// success only when every one of them is.
101func combinedStatus(builds []control.BuildOut) string {
102 has := map[string]bool{}
103 for _, b := range builds {
104 has[b.Status] = true
105 }
106 for _, s := range runStatusPriority {
107 if has[s] {
108 return s
109 }
110 }
111 return "success"
112}
113
114// groupRuns folds consecutive builds of the same commit into one run.
115// build list orders builds newest first, so one push's jobs are adjacent;
116// this does not sort or otherwise assume anything beyond that adjacency.
117func groupRuns(builds []control.BuildOut) []buildRun {
118 var runs []buildRun
119 for _, b := range builds {
120 if n := len(runs); n > 0 && runs[n-1].SHA == b.SHA {
121 runs[n-1].Builds = append(runs[n-1].Builds, b)
122 continue
123 }
124 runs = append(runs, buildRun{SHA: b.SHA, Ref: b.Ref, CreatedAt: b.CreatedAt, Builds: []control.BuildOut{b}})
125 }
126 for i := range runs {
127 runs[i].Status = combinedStatus(runs[i].Builds)
128 }
129 return runs
130}
131
132func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
133 p, ok := s.repoFor(w, r, "")
134 if !ok {
135 return
136 }
137 p.Tab = "builds"
138 viewer := s.webViewer(r)
139
140 qv := r.URL.Query()
141 filter := buildFilter{Ref: qv.Get("ref"), Status: qv.Get("status"), Job: qv.Get("job")}
142 argv := []string{"build", "list", p.Repo.Path()}
143 if filter.Ref != "" {
144 argv = append(argv, "--ref", filter.Ref)
145 }
146 if filter.Status != "" {
147 argv = append(argv, "--status", filter.Status)
148 }
149 if filter.Job != "" {
150 argv = append(argv, "--job", filter.Job)
151 }
152
153 var builds []control.BuildOut
154 s.runControlInto(viewer, argv, &builds)
155
156 // The jobs a trigger can name. A repo without a CI config has none;
157 // that is not an error for this page.
158 var jobs []control.JobOut
159 s.runControlInto(viewer, []string{"build", "jobs", p.Repo.Path()}, &jobs)
160
161 s.render(w, "builds.html", struct {
162 repoPage
163 Builds []control.BuildOut
164 Jobs []control.JobOut
165 Runs []buildRun
166 Filter buildFilter
167 FilterLinks []buildFilterLink
168 Refs []string
169 CanWrite bool
170 Notice string
171 }{p, builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs), distinctRefs(builds, filter.Ref),
172 s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
173}
174
175func (s *Server) build(w http.ResponseWriter, r *http.Request) {
176 p, ok := s.repoFor(w, r, "")
177 if !ok {
178 return
179 }
180 p.Tab = "builds"
181 if _, err := strconv.ParseInt(r.PathValue("n"), 10, 64); err != nil {
182 s.notFound(w, r)
183 return
184 }
185 n := r.PathValue("n")
186 viewer := s.webViewer(r)
187
188 var b control.BuildOut
189 if _, ok := s.runControlInto(viewer, []string{"build", "show", p.Repo.Path(), n}, &b); !ok {
190 s.notFound(w, r)
191 return
192 }
193 log, _, _ := s.runControl(viewer, []string{"build", "log", p.Repo.Path(), n})
194
195 s.render(w, "build.html", struct {
196 repoPage
197 Build control.BuildOut
198 Log string
199 CanWrite bool
200 Notice string
201 }{p, b, log, s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
202}