internal/httpd/builds.go

da232bfde4952959944a074ccec455f38eab3b66
gitbay/internal/httpd/builds.go history · blame · raw

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}