blob: 5e3003ac4fa97d62d29dd9800fb2f3627344f677 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"fmt"
"html/template"
"io"
"log"
"net/http"
"slices"
"strings"
"time"
"golang.org/x/build/internal/foreach"
"golang.org/x/build/internal/gophers"
"golang.org/x/build/maintner"
)
type project struct {
*maintner.GerritProject
Changes []*change
}
// ReviewServer returns the hostname of the review server for a googlesource repo,
// e.g. "go-review.googlesource.com" for a "go.googlesource.com" server. For a
// non-googlesource.com server, it will return an empty string.
func (p *project) ReviewServer() string {
const d = ".googlesource.com"
s := p.Server()
i := strings.Index(s, d)
if i == -1 {
return ""
}
return s[:i] + "-review" + d
}
type change struct {
*maintner.GerritCL
LastUpdate time.Time
FormattedLastUpdate string
HasPlusTwo bool
HasPlusOne bool
HasMinusOne bool
HasMinusTwo bool
NoHumanComments bool
TryBotMinusOne bool
TryBotPlusOne bool
SearchTerms string
ReleaseMilestone string
}
type reviewsData struct {
Projects []*project
TotalChanges int
// 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 {
err := s.updateReviewsData()
if err != nil {
log.Println("updateReviewsData:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
s.cMu.RLock()
defer s.cMu.RUnlock()
projects := s.data.reviews.Projects
totalChanges := s.data.reviews.TotalChanges
var buf bytes.Buffer
if err := t.Execute(&buf, struct {
Projects []*project
TotalChanges int
}{
Projects: projects,
TotalChanges: totalChanges,
}); 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() error {
log.Println("Updating reviews data ...")
s.cMu.Lock()
defer s.cMu.Unlock()
var (
projects []*project
totalChanges int
)
err := s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
proj := &project{GerritProject: p}
err := p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
if cl.WorkInProgress() ||
cl.Owner() == nil ||
strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") {
return nil
}
var searchTerms []string
tags := cl.Meta.Hashtags()
if tags.Contains("wait-author") ||
tags.Contains("wait-release") ||
tags.Contains("wait-issue") {
return nil
}
c := &change{GerritCL: cl}
searchTerms = append(searchTerms, "repo:"+p.Project())
searchTerms = append(searchTerms, cl.Owner().Name())
searchTerms = append(searchTerms, "owner:"+cl.Owner().Email())
searchTerms = append(searchTerms, "involves:"+cl.Owner().Email())
searchTerms = append(searchTerms, fmt.Sprint(cl.Number))
searchTerms = append(searchTerms, cl.Subject())
c.NoHumanComments = !hasHumanComments(cl)
if c.NoHumanComments {
searchTerms = append(searchTerms, "t:attn")
}
const releaseMilestonePrefix = "Go"
for _, ref := range cl.GitHubIssueRefs {
issue := ref.Repo.Issue(ref.Number)
if issue != nil &&
issue.Milestone != nil &&
strings.HasPrefix(issue.Milestone.Title, releaseMilestonePrefix) {
c.ReleaseMilestone = issue.Milestone.Title[len(releaseMilestonePrefix):]
}
}
if c.ReleaseMilestone != "" {
searchTerms = append(searchTerms, "release:"+c.ReleaseMilestone)
}
searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...)
labelVotes, err := cl.Metas[len(cl.Metas)-1].LabelVotes()
if err != nil {
return fmt.Errorf("error updating review data for CL %d: %v", cl.Number, err)
}
for label, votes := range labelVotes {
for _, val := range votes {
if label == "Code-Review" {
switch val {
case -2:
c.HasMinusTwo = true
searchTerms = append(searchTerms, "t:-2")
case -1:
c.HasMinusOne = true
searchTerms = append(searchTerms, "t:-1")
case 1:
c.HasPlusOne = true
searchTerms = append(searchTerms, "t:+1")
case 2:
c.HasPlusTwo = true
searchTerms = append(searchTerms, "t:+2")
}
}
if label == "TryBot-Result" {
switch val {
case -1:
c.TryBotMinusOne = true
searchTerms = append(searchTerms, "trybot:-1")
case 1:
c.TryBotPlusOne = true
searchTerms = append(searchTerms, "trybot:+1")
}
}
}
}
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")
searchTerms = append(searchTerms, c.FormattedLastUpdate)
c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " "))
proj.Changes = append(proj.Changes, c)
totalChanges++
return nil
}))
if err != nil {
return err
}
slices.SortFunc(proj.Changes, func(a, b *change) int {
return a.LastUpdate.Compare(b.LastUpdate)
})
projects = append(projects, proj)
return nil
}))
if err != nil {
return err
}
slices.SortFunc(projects, func(a, b *project) int {
return strings.Compare(a.Project(), b.Project())
})
s.data.reviews.Projects = projects
s.data.reviews.TotalChanges = totalChanges
s.data.reviews.dirty = false
return nil
}
// hasHumanComments reports whether cl has any comments from a human on it.
func hasHumanComments(cl *maintner.GerritCL) bool {
const (
gobotID = "5976@62eb7196-b449-3ce5-99f1-c037f21e1705"
gerritbotID = "12446@62eb7196-b449-3ce5-99f1-c037f21e1705"
)
for _, m := range cl.Messages {
if email := m.Author.Email(); email != gobotID && email != gerritbotID {
return true
}
}
return false
}
// searchTermsFromReviewerFields returns a slice of terms generated from
// the reviewer and cc fields of a Gerrit change.
func searchTermsFromReviewerFields(cl *maintner.GerritCL) []string {
var searchTerms []string
reviewers := make(map[string]bool)
ccs := make(map[string]bool)
for _, m := range cl.Metas {
if !strings.Contains(m.Commit.Msg, "Reviewer:") &&
!strings.Contains(m.Commit.Msg, "CC:") &&
!strings.Contains(m.Commit.Msg, "Removed:") {
continue
}
foreach.LineStr(m.Commit.Msg, func(ln string) error {
if !strings.HasPrefix(ln, "Reviewer:") &&
!strings.HasPrefix(ln, "CC:") &&
!strings.HasPrefix(ln, "Removed:") {
return nil
}
gerritID := ln[strings.LastIndexByte(ln, '<')+1 : strings.LastIndexByte(ln, '>')]
if strings.HasPrefix(ln, "Removed:") {
delete(reviewers, gerritID)
delete(ccs, gerritID)
} else if strings.HasPrefix(ln, "Reviewer:") {
delete(ccs, gerritID)
reviewers[gerritID] = true
} else if strings.HasPrefix(ln, "CC:") {
delete(reviewers, gerritID)
ccs[gerritID] = true
}
return nil
})
}
for r := range reviewers {
if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
searchTerms = append(searchTerms, "involves:"+p.Gerrit)
searchTerms = append(searchTerms, "reviewer:"+p.Gerrit)
}
}
for r := range ccs {
if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
searchTerms = append(searchTerms, "involves:"+p.Gerrit)
searchTerms = append(searchTerms, "cc:"+p.Gerrit)
}
}
return searchTerms
}