internal/httpd/builds.go

353f68a2e57ac692964b73ae1f9acd91fcdcae20
gitbay/internal/httpd/builds.go history · blame · raw

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}