зеркало из https://github.com/golang/build.git
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:
Родитель
2b993394cd
Коммит
99aa072d0c
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 (⤷).
|
||||
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}}
|
Загрузка…
Ссылка в новой задаче