blob: 2f363b6a005727acb0468f060d17684217e80a79 [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"
14 "sort"
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 {
72 s.updateReviewsData()
73 }
74
75 s.cMu.RLock()
76 defer s.cMu.RUnlock()
77
78 ownerFilter := r.FormValue("owner")
Andrew Bonventre3498ec72017-09-22 12:19:37 -040079 var (
80 projects []*project
81 totalChanges int
82 )
Andrew Bonventre99aa0722017-09-20 16:27:09 -040083 if len(ownerFilter) > 0 {
84 for _, p := range s.data.reviews.Projects {
85 var cs []*change
86 for _, c := range p.Changes {
Andrew Bonventre5df336c2018-11-08 12:07:56 -050087 if o := c.Owner(); o != nil && o.Name() == ownerFilter {
Andrew Bonventre99aa0722017-09-20 16:27:09 -040088 cs = append(cs, c)
Andrew Bonventre3498ec72017-09-22 12:19:37 -040089 totalChanges++
Andrew Bonventre99aa0722017-09-20 16:27:09 -040090 }
91 }
92 if len(cs) > 0 {
93 projects = append(projects, &project{GerritProject: p.GerritProject, Changes: cs})
94 }
95 }
96 } else {
97 projects = s.data.reviews.Projects
Andrew Bonventre3498ec72017-09-22 12:19:37 -040098 totalChanges = s.data.reviews.TotalChanges
Andrew Bonventre99aa0722017-09-20 16:27:09 -040099 }
100
101 var buf bytes.Buffer
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400102 if err := t.Execute(&buf, struct {
103 Projects []*project
104 TotalChanges int
105 }{
106 Projects: projects,
107 TotalChanges: totalChanges,
108 }); err != nil {
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400109 http.Error(w, err.Error(), http.StatusInternalServerError)
110 return
111 }
112 if _, err := io.Copy(w, &buf); err != nil {
113 log.Printf("io.Copy(w, %+v) = %v", buf, err)
114 return
115 }
116}
117
118func (s *server) updateReviewsData() {
119 log.Println("Updating reviews data ...")
120 s.cMu.Lock()
121 defer s.cMu.Unlock()
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400122 var (
123 projects []*project
124 totalChanges int
125 )
Andrew Bonventre1389a952019-10-07 09:58:01 -0400126 s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400127 proj := &project{GerritProject: p}
Andrew Bonventre1389a952019-10-07 09:58:01 -0400128 p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
129 if cl.WorkInProgress() ||
Andrew Bonventre5df336c2018-11-08 12:07:56 -0500130 cl.Owner() == nil ||
Dmitri Shuralyov9e83d852019-02-20 14:56:03 -0500131 strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") {
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400132 return nil
133 }
Andrew Bonventre27742772019-02-20 22:38:16 -0500134 var searchTerms []string
Brad Fitzpatrick1d5048e2018-05-17 14:50:55 +0000135 tags := cl.Meta.Hashtags()
Andrew Bonventre27742772019-02-20 22:38:16 -0500136 if tags.Contains("wait-author") ||
137 tags.Contains("wait-release") ||
138 tags.Contains("wait-issue") {
Brad Fitzpatrick6e908912018-04-19 19:16:02 +0000139 return nil
140 }
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400141 c := &change{GerritCL: cl}
Andrew Bonventre27742772019-02-20 22:38:16 -0500142 searchTerms = append(searchTerms, "repo:"+p.Project())
143 searchTerms = append(searchTerms, cl.Owner().Name())
144 searchTerms = append(searchTerms, "owner:"+cl.Owner().Email())
145 searchTerms = append(searchTerms, "involves:"+cl.Owner().Email())
146 searchTerms = append(searchTerms, fmt.Sprint(cl.Number))
147 searchTerms = append(searchTerms, cl.Subject())
148
149 c.NoHumanComments = !hasHumanComments(cl)
150 if c.NoHumanComments {
151 searchTerms = append(searchTerms, "t:attn")
152 }
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400153
154 const releaseMilestonePrefix = "Go"
155 for _, ref := range cl.GitHubIssueRefs {
156 issue := ref.Repo.Issue(ref.Number)
157 if issue != nil &&
158 issue.Milestone != nil &&
159 strings.HasPrefix(issue.Milestone.Title, releaseMilestonePrefix) {
160 c.ReleaseMilestone = issue.Milestone.Title[len(releaseMilestonePrefix):]
161 }
162 }
163 if c.ReleaseMilestone != "" {
164 searchTerms = append(searchTerms, "release:"+c.ReleaseMilestone)
165 }
166
Andrew Bonventre27742772019-02-20 22:38:16 -0500167 searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...)
168 for label, votes := range cl.Metas[len(cl.Metas)-1].LabelVotes() {
169 for _, val := range votes {
170 if label == "Code-Review" {
171 switch val {
172 case -2:
173 c.HasMinusTwo = true
174 searchTerms = append(searchTerms, "t:-2")
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400175 case -1:
176 c.HasMinusOne = true
177 searchTerms = append(searchTerms, "t:-1")
Andrew Bonventre27742772019-02-20 22:38:16 -0500178 case 1:
179 c.HasPlusOne = true
180 searchTerms = append(searchTerms, "t:+1")
181 case 2:
182 c.HasPlusTwo = true
183 searchTerms = append(searchTerms, "t:+2")
184 }
185 }
Andrew Bonventre8b8ef912019-10-07 17:54:30 -0400186 if label == "TryBot-Result" {
187 switch val {
188 case -1:
189 c.TryBotMinusOne = true
190 searchTerms = append(searchTerms, "trybot:-1")
191 case 1:
192 c.TryBotPlusOne = true
193 searchTerms = append(searchTerms, "trybot:+1")
194 }
195 }
Andrew Bonventre27742772019-02-20 22:38:16 -0500196 }
197 }
198
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400199 c.LastUpdate = cl.Commit.CommitTime
200 if len(cl.Messages) > 0 {
201 c.LastUpdate = cl.Messages[len(cl.Messages)-1].Date
202 }
203 c.FormattedLastUpdate = c.LastUpdate.Format("2006-01-02")
Andrew Bonventre27742772019-02-20 22:38:16 -0500204 searchTerms = append(searchTerms, c.FormattedLastUpdate)
205 c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " "))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400206 proj.Changes = append(proj.Changes, c)
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400207 totalChanges++
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400208 return nil
Andrew Bonventre1389a952019-10-07 09:58:01 -0400209 }))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400210 sort.Slice(proj.Changes, func(i, j int) bool {
211 return proj.Changes[i].LastUpdate.Before(proj.Changes[j].LastUpdate)
212 })
213 projects = append(projects, proj)
214 return nil
Andrew Bonventre1389a952019-10-07 09:58:01 -0400215 }))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400216 sort.Slice(projects, func(i, j int) bool {
217 return projects[i].Project() < projects[j].Project()
218 })
219 s.data.reviews.Projects = projects
Andrew Bonventre3498ec72017-09-22 12:19:37 -0400220 s.data.reviews.TotalChanges = totalChanges
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400221 s.data.reviews.dirty = false
222}
Andrew Bonventre27742772019-02-20 22:38:16 -0500223
224// hasHumanComments reports whether cl has any comments from a human on it.
225func hasHumanComments(cl *maintner.GerritCL) bool {
226 const (
227 gobotID = "5976@62eb7196-b449-3ce5-99f1-c037f21e1705"
228 gerritbotID = "12446@62eb7196-b449-3ce5-99f1-c037f21e1705"
229 )
230
231 for _, m := range cl.Messages {
232 if email := m.Author.Email(); email != gobotID && email != gerritbotID {
233 return true
234 }
235 }
236 return false
237}
238
239// searchTermsFromReviewerFields returns a slice of terms generated from
240// the reviewer and cc fields of a Gerrit change.
241func searchTermsFromReviewerFields(cl *maintner.GerritCL) []string {
242 var searchTerms []string
243 reviewers := make(map[string]bool)
244 ccs := make(map[string]bool)
245 for _, m := range cl.Metas {
246 if !strings.Contains(m.Commit.Msg, "Reviewer:") &&
247 !strings.Contains(m.Commit.Msg, "CC:") &&
248 !strings.Contains(m.Commit.Msg, "Removed:") {
249 continue
250 }
Dmitri Shuralyov638dc402019-08-30 16:01:34 -0400251 foreach.LineStr(m.Commit.Msg, func(ln string) error {
Andrew Bonventre27742772019-02-20 22:38:16 -0500252 if !strings.HasPrefix(ln, "Reviewer:") &&
253 !strings.HasPrefix(ln, "CC:") &&
254 !strings.HasPrefix(ln, "Removed:") {
255 return nil
256 }
257 gerritID := ln[strings.LastIndexByte(ln, '<')+1 : strings.LastIndexByte(ln, '>')]
258 if strings.HasPrefix(ln, "Removed:") {
259 delete(reviewers, gerritID)
260 delete(ccs, gerritID)
261 } else if strings.HasPrefix(ln, "Reviewer:") {
262 delete(ccs, gerritID)
263 reviewers[gerritID] = true
264 } else if strings.HasPrefix(ln, "CC:") {
265 delete(reviewers, gerritID)
266 ccs[gerritID] = true
267 }
268 return nil
269 })
270 }
271 for r := range reviewers {
272 if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
273 searchTerms = append(searchTerms, "involves:"+p.Gerrit)
274 searchTerms = append(searchTerms, "reviewer:"+p.Gerrit)
275 }
276 }
277 for r := range ccs {
278 if p := gophers.GetPerson(r); p != nil && p.Gerrit != cl.Owner().Email() {
279 searchTerms = append(searchTerms, "involves:"+p.Gerrit)
280 searchTerms = append(searchTerms, "cc:"+p.Gerrit)
281 }
282 }
283 return searchTerms
284}