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>
This commit is contained in:
Andrew Bonventre 2017-09-20 16:27:09 -04:00
Родитель 2b993394cd
Коммит 99aa072d0c
5 изменённых файлов: 200 добавлений и 32 удалений

Просмотреть файл

@ -165,13 +165,13 @@ func (s *server) updateReleaseData() {
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 @@ func (s *server) appendOpenIssues(dirToIssues map[string][]*maintner.GitHubIssue
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 @@ func (s *server) appendPendingCLs(dirToCLs map[string][]*maintner.GerritCL) {
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 @@ func (s *server) appendPendingProposals(issueToCLs map[int32][]*maintner.GerritC
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 @@ func (s *server) appendClosedIssues() {
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) allMilestones() []milestone {
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 @@ func (s *server) handleRelease(t *template.Template, w http.ResponseWriter, r *h
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
}

120
devapp/reviews.go Normal file
Просмотреть файл

@ -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 dont have more than one parent.
}
return m
}

Просмотреть файл

@ -32,7 +32,7 @@ type server struct {
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 @@ type server struct {
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 @@ func newServer(mux *http.ServeMux, staticDir, templateDir string) *server {
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 @@ func (s *server) corpusUpdateLoop(ctx context.Context) {
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 {

Просмотреть файл

@ -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 @@ To change something about an issue here, go to GitHub.
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 (&#x2937;).
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.
The UI typically reflects data changes from both sources within a few tens of
milliseconds.

Просмотреть файл

@ -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}}