blob: 5e3003ac4fa97d62d29dd9800fb2f3627344f677 [file] [log] [blame]
Andrew Bonventre3498ec72017-09-22 12:19:37 -04001// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Andrew Bonventre99aa0722017-09-20 16:27:09 -04005package main
6
7import (
8 "bytes"
Andrew Bonventre27742772019-02-20 22:38:16 -05009 "fmt"
Andrew Bonventre99aa0722017-09-20 16:27:09 -040010 "html/template"
11 "io"
12 "log"
13 "net/http"
Ian Lance Taylor727bcae2024-02-20 15:37:59 -080014 "slices"
Andrew Bonventre3498ec72017-09-22 12:19:37 -040015 "strings"
Andrew Bonventre99aa0722017-09-20 16:27:09 -040016 "time"
17
Dmitri Shuralyov638dc402019-08-30 16:01:34 -040018 "golang.org/x/build/internal/foreach"
Andrew Bonventre27742772019-02-20 22:38:16 -050019 "golang.org/x/build/internal/gophers"
Andrew Bonventre99aa0722017-09-20 16:27:09 -040020 "golang.org/x/build/maintner"
21)
22
23type project struct {
24 *maintner.GerritProject
25 Changes []*change
26}
27
Andrew Bonventref91a1492017-09-29 13:16:01 -040028// ReviewServer returns the hostname of the review server for a googlesource repo,
29// e.g. "go-review.googlesource.com" for a "go.googlesource.com" server. For a
30// non-googlesource.com server, it will return an empty string.
31func (p *project) ReviewServer() string {
32 const d = ".googlesource.com"
33 s := p.Server()
34 i := strings.Index(s, d)
35 if i == -1 {
36 return ""
37 }
38 return s[:i] + "-review" + d
39}
40
Andrew Bonventre99aa0722017-09-20 16:27:09 -040041type change struct {
42 *maintner.GerritCL
43 LastUpdate time.Time
44 FormattedLastUpdate string
Andrew Bonventre27742772019-02-20 22:38:16 -050045
Andrew Bonventre8b8ef912019-10-07 17:54:30 -040046 HasPlusTwo bool
47 HasPlusOne bool
48 HasMinusOne bool
49 HasMinusTwo bool
50 NoHumanComments bool
51 TryBotMinusOne bool
52 TryBotPlusOne bool
53 SearchTerms string
54 ReleaseMilestone string
Andrew Bonventre99aa0722017-09-20 16:27:09 -040055}
56
57type reviewsData struct {
Andrew Bonventre3498ec72017-09-22 12:19:37 -040058 Projects []*project
59 TotalChanges int
Andrew Bonventre99aa0722017-09-20 16:27:09 -040060
61 // dirty is set if this data needs to be updated due to a corpus change.
62 dirty bool
63}
64
65// handleReviews serves dev.golang.org/reviews.
66func (s *server) handleReviews(t *template.Template, w http.ResponseWriter, r *http.Request) {
67 w.Header().Set("Content-Type", "text/html; charset=utf-8")
68 s.cMu.RLock()
69 dirty := s.data.reviews.dirty
70 s.cMu.RUnlock()
71 if dirty {
Dmitri Shuralyovfb495b92022-05-24 12:39:51 -040072 err := s.updateReviewsData()
73 if err != nil {
74 log.Println("updateReviewsData:", err)
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -040075 http.Error(w, err.Error(), http.StatusInternalServerError)
76 return
77 }
Andrew Bonventre99aa0722017-09-20 16:27:09 -040078 }
79
80 s.cMu.RLock()
81 defer s.cMu.RUnlock()
82
Ian Lance Taylor4fe3df32024-02-20 15:26:59 -080083 projects := s.data.reviews.Projects
84 totalChanges := s.data.reviews.TotalChanges
Andrew Bonventre99aa0722017-09-20 16:27:09 -040085
86 var buf bytes.Buffer
Andrew Bonventre3498ec72017-09-22 12:19:37 -040087 if err := t.Execute(&buf, struct {
88 Projects []*project
89 TotalChanges int
90 }{
91 Projects: projects,
92 TotalChanges: totalChanges,
93 }); err != nil {
Andrew Bonventre99aa0722017-09-20 16:27:09 -040094 http.Error(w, err.Error(), http.StatusInternalServerError)
95 return
96 }
97 if _, err := io.Copy(w, &buf); err != nil {
98 log.Printf("io.Copy(w, %+v) = %v", buf, err)
99 return
100 }
101}
102
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -0400103func (s *server) updateReviewsData() error {
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400104 log.Println("Updating reviews data ...")
105 s.cMu.Lock()
106 defer s.cMu.Unlock()
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400107 var (
108 projects []*project
109 totalChanges int
110 )
Dmitri Shuralyovfb495b92022-05-24 12:39:51 -0400111 err := s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400112 proj := &project{GerritProject: p}
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -0400113 err := p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
Andrew Bonventre1389a952019-10-07 09:58:01 -0400114 if cl.WorkInProgress() ||
Andrew Bonventre5df336c2018-11-08 12:07:56 -0500115 cl.Owner() == nil ||
Dmitri Shuralyov9e83d852019-02-20 14:56:03 -0500116 strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") {
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400117 return nil
118 }
Andrew Bonventre27742772019-02-20 22:38:16 -0500119 var searchTerms []string
Brad Fitzpatrick1d5048e2018-05-17 14:50:55 +0000120 tags := cl.Meta.Hashtags()
Andrew Bonventre27742772019-02-20 22:38:16 -0500121 if tags.Contains("wait-author") ||
122 tags.Contains("wait-release") ||
123 tags.Contains("wait-issue") {
Brad Fitzpatrick6e908912018-04-19 19:16:02 +0000124 return nil
125 }
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400126 c := &change{GerritCL: cl}
Andrew Bonventre27742772019-02-20 22:38:16 -0500127 searchTerms = append(searchTerms, "repo:"+p.Project())
128 searchTerms = append(searchTerms, cl.Owner().Name())
129 searchTerms = append(searchTerms, "owner:"+cl.Owner().Email())
130 searchTerms = append(searchTerms, "involves:"+cl.Owner().Email())
131 searchTerms = append(searchTerms, fmt.Sprint(cl.Number))
132 searchTerms = append(searchTerms, cl.Subject())
133
134 c.NoHumanComments = !hasHumanComments(cl)
135 if c.NoHumanComments {
136 searchTerms = append(searchTerms, "t:attn")
137 }
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400138
139 const releaseMilestonePrefix = "Go"
140 for _, ref := range cl.GitHubIssueRefs {
141 issue := ref.Repo.Issue(ref.Number)
142 if issue != nil &&
143 issue.Milestone != nil &&
144 strings.HasPrefix(issue.Milestone.Title, releaseMilestonePrefix) {
145 c.ReleaseMilestone = issue.Milestone.Title[len(releaseMilestonePrefix):]
146 }
147 }
148 if c.ReleaseMilestone != "" {
149 searchTerms = append(searchTerms, "release:"+c.ReleaseMilestone)
150 }
151
Andrew Bonventre27742772019-02-20 22:38:16 -0500152 searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...)
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -0400153 labelVotes, err := cl.Metas[len(cl.Metas)-1].LabelVotes()
154 if err != nil {
155 return fmt.Errorf("error updating review data for CL %d: %v", cl.Number, err)
156 }
157 for label, votes := range labelVotes {
Andrew Bonventre27742772019-02-20 22:38:16 -0500158 for _, val := range votes {
159 if label == "Code-Review" {
160 switch val {
161 case -2:
162 c.HasMinusTwo = true
163 searchTerms = append(searchTerms, "t:-2")
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400164 case -1:
165 c.HasMinusOne = true
166 searchTerms = append(searchTerms, "t:-1")
Andrew Bonventre27742772019-02-20 22:38:16 -0500167 case 1:
168 c.HasPlusOne = true
169 searchTerms = append(searchTerms, "t:+1")
170 case 2:
171 c.HasPlusTwo = true
172 searchTerms = append(searchTerms, "t:+2")
173 }
174 }
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400175 if label == "TryBot-Result" {
176 switch val {
177 case -1:
178 c.TryBotMinusOne = true
179 searchTerms = append(searchTerms, "trybot:-1")
180 case 1:
181 c.TryBotPlusOne = true
182 searchTerms = append(searchTerms, "trybot:+1")
183 }
184 }
Andrew Bonventre27742772019-02-20 22:38:16 -0500185 }
186 }
187
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400188 c.LastUpdate = cl.Commit.CommitTime
189 if len(cl.Messages) > 0 {
190 c.LastUpdate = cl.Messages[len(cl.Messages)-1].Date
191 }
192 c.FormattedLastUpdate = c.LastUpdate.Format("2006-01-02")
Andrew Bonventre27742772019-02-20 22:38:16 -0500193 searchTerms = append(searchTerms, c.FormattedLastUpdate)
194 c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " "))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400195 proj.Changes = append(proj.Changes, c)
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400196 totalChanges++
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400197 return nil
Andrew Bonventre1389a952019-10-07 09:58:01 -0400198 }))
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -0400199 if err != nil {
200 return err
201 }
Ian Lance Taylor727bcae2024-02-20 15:37:59 -0800202 slices.SortFunc(proj.Changes, func(a, b *change) int {
203 return a.LastUpdate.Compare(b.LastUpdate)
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400204 })
205 projects = append(projects, proj)
206 return nil
Andrew Bonventre1389a952019-10-07 09:58:01 -0400207 }))
Dmitri Shuralyovfb495b92022-05-24 12:39:51 -0400208 if err != nil {
209 return err
210 }
Ian Lance Taylor727bcae2024-02-20 15:37:59 -0800211 slices.SortFunc(projects, func(a, b *project) int {
212 return strings.Compare(a.Project(), b.Project())
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400213 })
214 s.data.reviews.Projects = projects
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400215 s.data.reviews.TotalChanges = totalChanges
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400216 s.data.reviews.dirty = false
Bryan C. Mills24ce8bd2022-05-13 09:13:56 -0400217 return nil
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400218}
Andrew Bonventre27742772019-02-20 22:38:16 -0500219
220// hasHumanComments reports whether cl has any comments from a human on it.
221func hasHumanComments(cl *maintner.GerritCL) bool {
222 const (
223 gobotID = "5976@62eb7196-b449-3ce5-99f1-c037f21e1705"
224 gerritbotID = "12446@62eb7196-b449-3ce5-99f1-c037f21e1705"
225 )
226
227 for _, m := range cl.Messages {
228 if email := m.Author.Email(); email != gobotID && email != gerritbotID {
229 return true
230 }
231 }
232 return false
233}
234
235// searchTermsFromReviewerFields returns a slice of terms generated from
236// the reviewer and cc fields of a Gerrit change.
237func searchTermsFromReviewerFields(cl *maintner.GerritCL) []string {
238 var searchTerms []string
239 reviewers := make(map[string]bool)
240 ccs := make(map[string]bool)
241 for _, m := range cl.Metas {
242 if !strings.Contains(m.Commit.Msg, "Reviewer:") &&
243 !strings.Contains(m.Commit.Msg, "CC:") &&
244 !strings.Contains(m.Commit.Msg, "Removed:") {
245 continue
246 }
Dmitri Shuralyov638dc402019-08-30 16:01:34 -0400247 foreach.LineStr(m.Commit.Msg, func(ln string) error {
Andrew Bonventre27742772019-02-20 22:38:16 -0500248 if !strings.HasPrefix(ln, "Reviewer:") &&
249 !strings.HasPrefix(ln, "CC:") &&
250 !strings.HasPrefix(ln, "Removed:") {
251 return nil
252 }
253 gerritID := ln[strings.LastIndexByte(ln, '<')+1 : strings.LastIndexByte(ln, '>')]
254 if strings.HasPrefix(ln, "Removed:") {
255 delete(reviewers, gerritID)
256 delete(ccs, gerritID)
257 } else if strings.HasPrefix(ln, "Reviewer:") {
258 delete(ccs, gerritID)
259 reviewers[gerritID] = true
260 } else if strings.HasPrefix(ln, "CC:") {
261 delete(reviewers, gerritID)
262 ccs[gerritID] = true
263 }
264 return nil
265 })
266 }
267 for r := range reviewers {
268 if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
269 searchTerms = append(searchTerms, "involves:"+p.Gerrit)
270 searchTerms = append(searchTerms, "reviewer:"+p.Gerrit)
271 }
272 }
273 for r := range ccs {
274 if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
275 searchTerms = append(searchTerms, "involves:"+p.Gerrit)
276 searchTerms = append(searchTerms, "cc:"+p.Gerrit)
277 }
278 }
279 return searchTerms
280}