x/build/devapp: implement /reviews endpoint Initial add of a page that shows all open reviews, grouped by project, in chronological order. One can click on a name to filter to that user alone. Change-Id: I521b69c65e0629689d2792d990dd2b07ef2fb9ec Reviewed-on: https://go-review.googlesource.com/65072 Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/devapp/release.go b/devapp/release.go index 45af513..182d5ef 100644 --- a/devapp/release.go +++ b/devapp/release.go
@@ -165,13 +165,13 @@ return nil }) - s.data.Sections = nil + s.data.release.Sections = nil s.appendOpenIssues(dirToIssues, issueToCLs) s.appendPendingCLs(dirToCLs) s.appendPendingProposals(issueToCLs) s.appendClosedIssues() - s.data.LastUpdated = time.Now().UTC().Format(time.UnixDate) - s.data.dirty = false + s.data.release.LastUpdated = time.Now().UTC().Format(time.UnixDate) + s.data.release.dirty = false } // requires s.cMu be locked. @@ -213,7 +213,7 @@ Items: items, }) } - s.data.Sections = append(s.data.Sections, section{ + s.data.release.Sections = append(s.data.release.Sections, section{ Title: m.title, Count: issueCount, Groups: issueGroups, @@ -241,7 +241,7 @@ clGroups = append(clGroups, g) } } - s.data.Sections = append(s.data.Sections, section{ + s.data.release.Sections = append(s.data.release.Sections, section{ Title: "Pending CLs", Count: clCount, Groups: clGroups, @@ -264,7 +264,7 @@ return nil }) sort.Sort(itemsBySummary(proposals.Items)) - s.data.Sections = append(s.data.Sections, section{ + s.data.release.Sections = append(s.data.release.Sections, section{ Title: "Pending Proposals", Count: len(proposals.Items), Groups: []group{proposals}, @@ -287,7 +287,7 @@ return nil }) sort.Sort(itemsBySummary(closed.Items)) - s.data.Sections = append(s.data.Sections, section{ + s.data.release.Sections = append(s.data.release.Sections, section{ Title: "Closed Last Week", Count: len(closed.Items), Groups: []group{closed}, @@ -322,7 +322,7 @@ func (s *server) handleRelease(t *template.Template, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") s.cMu.RLock() - dirty := s.data.dirty + dirty := s.data.release.dirty s.cMu.RUnlock() if dirty { s.updateReleaseData() @@ -330,7 +330,7 @@ s.cMu.RLock() defer s.cMu.RUnlock() - if err := t.Execute(w, s.data); err != nil { + if err := t.Execute(w, s.data.release); err != nil { log.Printf("t.Execute(w, nil) = %v", err) return }
diff --git a/devapp/reviews.go b/devapp/reviews.go new file mode 100644 index 0000000..7859fd0 --- /dev/null +++ b/devapp/reviews.go
@@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "html/template" + "io" + "log" + "net/http" + "sort" + "time" + + "golang.org/x/build/maintner" +) + +type project struct { + *maintner.GerritProject + Changes []*change +} + +type change struct { + *maintner.GerritCL + LastUpdate time.Time + FormattedLastUpdate string +} + +type reviewsData struct { + Projects []*project + + // dirty is set if this data needs to be updated due to a corpus change. + dirty bool +} + +// handleReviews serves dev.golang.org/reviews. +func (s *server) handleReviews(t *template.Template, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + s.cMu.RLock() + dirty := s.data.reviews.dirty + s.cMu.RUnlock() + if dirty { + s.updateReviewsData() + } + + s.cMu.RLock() + defer s.cMu.RUnlock() + + ownerFilter := r.FormValue("owner") + var projects []*project + if len(ownerFilter) > 0 { + for _, p := range s.data.reviews.Projects { + var cs []*change + for _, c := range p.Changes { + if c.OwnerName() == ownerFilter { + cs = append(cs, c) + } + } + if len(cs) > 0 { + projects = append(projects, &project{GerritProject: p.GerritProject, Changes: cs}) + } + } + } else { + projects = s.data.reviews.Projects + } + + var buf bytes.Buffer + if err := t.Execute(&buf, struct{ Projects []*project }{projects}); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if _, err := io.Copy(w, &buf); err != nil { + log.Printf("io.Copy(w, %+v) = %v", buf, err) + return + } +} + +func (s *server) updateReviewsData() { + log.Println("Updating reviews data ...") + s.cMu.Lock() + defer s.cMu.Unlock() + + var projects []*project + s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error { + proj := &project{GerritProject: p} + p.ForeachOpenCL(func(cl *maintner.GerritCL) error { + c := &change{GerritCL: cl} + c.LastUpdate = cl.Commit.CommitTime + if len(cl.Messages) > 0 { + c.LastUpdate = cl.Messages[len(cl.Messages)-1].Date + } + c.FormattedLastUpdate = c.LastUpdate.Format("2006-01-02") + proj.Changes = append(proj.Changes, c) + return nil + }) + sort.Slice(proj.Changes, func(i, j int) bool { + return proj.Changes[i].LastUpdate.Before(proj.Changes[j].LastUpdate) + }) + projects = append(projects, proj) + return nil + }) + sort.Slice(projects, func(i, j int) bool { + return projects[i].Project() < projects[j].Project() + }) + s.data.reviews.Projects = projects + s.data.reviews.dirty = false +} + +func (c *change) OwnerName() string { + m := c.firstMetaCommit() + if m == nil { + return "" + } + return m.Author.Name() +} + +func (c *change) firstMetaCommit() *maintner.GitCommit { + m := c.Meta + for m != nil && len(m.Parents) > 0 { + m = m.Parents[0] // Meta commits don’t have more than one parent. + } + return m +}
diff --git a/devapp/server.go b/devapp/server.go index 4ddfabd..6b62213 100644 --- a/devapp/server.go +++ b/devapp/server.go
@@ -32,7 +32,7 @@ corpus *maintner.Corpus repo *maintner.GitHubRepo helpWantedIssues []int32 - data releaseData + data pageData // GopherCon-specific fields. Must still hold cMu when reading/writing these. userMapping map[int]*maintner.GitHubUser // Gerrit Owner ID => GitHub user @@ -40,6 +40,11 @@ totalPoints int } +type pageData struct { + release releaseData + reviews reviewsData +} + func newServer(mux *http.ServeMux, staticDir, templateDir string) *server { s := &server{ mux: mux, @@ -50,6 +55,7 @@ s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir))) s.mux.HandleFunc("/favicon.ico", s.handleFavicon) s.mux.HandleFunc("/release", s.withTemplate("/release.tmpl", s.handleRelease)) + s.mux.HandleFunc("/reviews", s.withTemplate("/reviews.tmpl", s.handleReviews)) s.mux.HandleFunc("/dir/", handleDirRedirect) for _, p := range []string{"/imfeelinghelpful", "/imfeelinglucky"} { s.mux.HandleFunc(p, s.handleRandomHelpWantedIssue) @@ -89,7 +95,8 @@ log.Println("Updating activities ...") s.updateActivities() s.cMu.Lock() - s.data.dirty = true + s.data.release.dirty = true + s.data.reviews.dirty = true s.cMu.Unlock() err := s.corpus.UpdateWithLocker(ctx, &s.cMu) if err != nil {
diff --git a/devapp/static/index.html b/devapp/static/index.html index b2a81d0..d6b7c90 100644 --- a/devapp/static/index.html +++ b/devapp/static/index.html
@@ -2,11 +2,13 @@ <meta charset="utf-8"> <title>Go Development Dashboard</title> <pre> -<a href="/release">Go release dashboard</a> +<a href="/release">Releases</a> +<a href="/reviews">Open reviews</a> <b>About the Dashboards</b> -These dashboards are generated by <a href="https://godoc.org/golang.org/x/build/devapp">golang.org/x/build/devapp</a>. +These dashboards are generated by +<a href="https://godoc.org/golang.org/x/build/devapp">golang.org/x/build/devapp</a>. Issue information comes directly from GitHub. To change something about an issue here, go to GitHub. @@ -14,22 +16,5 @@ CL information comes directly from Gerrit. To change something about a CL here, go to Gerrit. -The dashboard refreshes periodically. - -<b>Release Dashboard</b> - -The Go release dashboard shows -all open issues in the milestones for the upcoming release, -plus all open CLs mentioning those issues, -plus any other open CLs in the main repository. - -The release dashboard is ordered primarily around issues in the -release milestone and the release-maybe milestone (for example, -Go1.5 and Go1.5Maybe). The maybe issues are shown in gray and -have [maybe] tags. - -If a CL refers to a release issue in its description, the CL is shown on the -dashboard below that issue, with an arrow prefix (⤷). -If a CL refers to multiple release issues, the CL is shown under each issue. - -If a CL refers to no release issues, it is shown on its own, without an arrow. \ No newline at end of file +The UI typically reflects data changes from both sources within a few tens of +milliseconds. \ No newline at end of file
diff --git a/devapp/templates/reviews.tmpl b/devapp/templates/reviews.tmpl new file mode 100644 index 0000000..a22cad3 --- /dev/null +++ b/devapp/templates/reviews.tmpl
@@ -0,0 +1,56 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1"> +<title>Open Go Code Reviews</title> +<style> +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} +body { + font: 13px monospace; + padding: 1rem; +} +h3 { + padding: 10px 0 0; +} +h3:first-of-type { + padding: 0 +} +a:link, +a:visited { + color: #00c; +} +.row { + display: flex; + white-space: nowrap; +} +.date { + min-width: 6rem; +} +.owner { + min-width: 10rem; + max-width: 10rem; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 1em; +} +.change { + overflow: hidden; + text-overflow: ellipsis; +} +</style> +<body> +{{range .Projects}} + {{if .Changes}} + <h3>{{.Project}}</h3> + {{range .Changes}} + <div class="row"> + <div class="date">{{.FormattedLastUpdate}}</div> + <a class="owner" href="?owner={{.OwnerName}}">{{.OwnerName}}</a> + <a class="change" href="https://golang.org/cl/{{.Number}}" target="_blank">{{.Number}} {{.Subject}}</a> + </div> + {{end}} + {{end}} +{{end}} \ No newline at end of file