internal/httpd/builds.go
220 lines · 6497 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 statuses := make([]string, len(builds))
103 for i, b := range builds {
104 statuses[i] = b.Status
105 }
106 return worstStatus(statuses)
107}
108
109// worstStatus is combinedStatus's ordering rule, factored out so the
110// dashboard feed can apply the same worst-first precedence to a folded
111// build run (D04). A status outside runStatusPriority (a future state
112// such as "skipped") is still not "success": it is returned unchanged
113// rather than falling through and reading as green.
114func worstStatus(statuses []string) string {
115 has := map[string]bool{}
116 for _, s := range statuses {
117 has[s] = true
118 }
119 for _, s := range runStatusPriority {
120 if has[s] {
121 return s
122 }
123 }
124 for _, s := range statuses {
125 if s != "success" {
126 return s
127 }
128 }
129 return "success"
130}
131
132// groupRuns folds consecutive builds of the same commit into one run.
133// build list orders builds newest first, so one push's jobs are adjacent;
134// this does not sort or otherwise assume anything beyond that adjacency.
135func groupRuns(builds []control.BuildOut) []buildRun {
136 var runs []buildRun
137 for _, b := range builds {
138 if n := len(runs); n > 0 && runs[n-1].SHA == b.SHA {
139 runs[n-1].Builds = append(runs[n-1].Builds, b)
140 continue
141 }
142 runs = append(runs, buildRun{SHA: b.SHA, Ref: b.Ref, CreatedAt: b.CreatedAt, Builds: []control.BuildOut{b}})
143 }
144 for i := range runs {
145 runs[i].Status = combinedStatus(runs[i].Builds)
146 }
147 return runs
148}
149
150func (s *Server) builds(w http.ResponseWriter, r *http.Request) {
151 p, ok := s.repoFor(w, r, "")
152 if !ok {
153 return
154 }
155 p.Tab = "builds"
156 viewer := s.webViewer(r)
157
158 qv := r.URL.Query()
159 filter := buildFilter{Ref: qv.Get("ref"), Status: qv.Get("status"), Job: qv.Get("job")}
160 argv := []string{"build", "list", p.Repo.Path()}
161 if filter.Ref != "" {
162 argv = append(argv, "--ref", filter.Ref)
163 }
164 if filter.Status != "" {
165 argv = append(argv, "--status", filter.Status)
166 }
167 if filter.Job != "" {
168 argv = append(argv, "--job", filter.Job)
169 }
170
171 var builds []control.BuildOut
172 s.runControlInto(viewer, argv, &builds)
173
174 // The jobs a trigger can name. A repo without a CI config has none;
175 // that is not an error for this page.
176 var jobs []control.JobOut
177 s.runControlInto(viewer, []string{"build", "jobs", p.Repo.Path()}, &jobs)
178
179 s.render(w, "builds.html", struct {
180 repoPage
181 Builds []control.BuildOut
182 Jobs []control.JobOut
183 Runs []buildRun
184 Filter buildFilter
185 FilterLinks []buildFilterLink
186 Refs []string
187 CanWrite bool
188 Notice string
189 }{p, builds, jobs, groupRuns(builds), filter, filterLinks(filter, jobs), distinctRefs(builds, filter.Ref),
190 s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
191}
192
193func (s *Server) build(w http.ResponseWriter, r *http.Request) {
194 p, ok := s.repoFor(w, r, "")
195 if !ok {
196 return
197 }
198 p.Tab = "builds"
199 if _, err := strconv.ParseInt(r.PathValue("n"), 10, 64); err != nil {
200 s.notFound(w, r)
201 return
202 }
203 n := r.PathValue("n")
204 viewer := s.webViewer(r)
205
206 var b control.BuildOut
207 if _, ok := s.runControlInto(viewer, []string{"build", "show", p.Repo.Path(), n}, &b); !ok {
208 s.notFound(w, r)
209 return
210 }
211 log, _, _ := s.runControl(viewer, []string{"build", "log", p.Repo.Path(), n})
212
213 s.render(w, "build.html", struct {
214 repoPage
215 Build control.BuildOut
216 Log string
217 CanWrite bool
218 Notice string
219 }{p, b, log, s.canWriteRepo(r, p.Repo), s.takeFlash(w, r)})
220}