Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 1 | // 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 Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 5 | package main |
| 6 | |
| 7 | import ( |
| 8 | "bytes" |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 9 | "fmt" |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 10 | "html/template" |
| 11 | "io" |
| 12 | "log" |
| 13 | "net/http" |
Ian Lance Taylor | 727bcae | 2024-02-20 15:37:59 -0800 | [diff] [blame] | 14 | "slices" |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 15 | "strings" |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 16 | "time" |
| 17 | |
Dmitri Shuralyov | 638dc40 | 2019-08-30 16:01:34 -0400 | [diff] [blame] | 18 | "golang.org/x/build/internal/foreach" |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 19 | "golang.org/x/build/internal/gophers" |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 20 | "golang.org/x/build/maintner" |
| 21 | ) |
| 22 | |
| 23 | type project struct { |
| 24 | *maintner.GerritProject |
| 25 | Changes []*change |
| 26 | } |
| 27 | |
Andrew Bonventre | f91a149 | 2017-09-29 13:16:01 -0400 | [diff] [blame] | 28 | // 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. |
| 31 | func (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 Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 41 | type change struct { |
| 42 | *maintner.GerritCL |
| 43 | LastUpdate time.Time |
| 44 | FormattedLastUpdate string |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 45 | |
Andrew Bonventre | 8b8ef91 | 2019-10-07 17:54:30 -0400 | [diff] [blame] | 46 | 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 Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 55 | } |
| 56 | |
| 57 | type reviewsData struct { |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 58 | Projects []*project |
| 59 | TotalChanges int |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 60 | |
| 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. |
| 66 | func (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 Shuralyov | fb495b9 | 2022-05-24 12:39:51 -0400 | [diff] [blame] | 72 | err := s.updateReviewsData() |
| 73 | if err != nil { |
| 74 | log.Println("updateReviewsData:", err) |
Bryan C. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 75 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 76 | return |
| 77 | } |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 78 | } |
| 79 | |
| 80 | s.cMu.RLock() |
| 81 | defer s.cMu.RUnlock() |
| 82 | |
Ian Lance Taylor | 4fe3df3 | 2024-02-20 15:26:59 -0800 | [diff] [blame] | 83 | projects := s.data.reviews.Projects |
| 84 | totalChanges := s.data.reviews.TotalChanges |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 85 | |
| 86 | var buf bytes.Buffer |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 87 | if err := t.Execute(&buf, struct { |
| 88 | Projects []*project |
| 89 | TotalChanges int |
| 90 | }{ |
| 91 | Projects: projects, |
| 92 | TotalChanges: totalChanges, |
| 93 | }); err != nil { |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 94 | 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. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 103 | func (s *server) updateReviewsData() error { |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 104 | log.Println("Updating reviews data ...") |
| 105 | s.cMu.Lock() |
| 106 | defer s.cMu.Unlock() |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 107 | var ( |
| 108 | projects []*project |
| 109 | totalChanges int |
| 110 | ) |
Dmitri Shuralyov | fb495b9 | 2022-05-24 12:39:51 -0400 | [diff] [blame] | 111 | err := s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error { |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 112 | proj := &project{GerritProject: p} |
Bryan C. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 113 | err := p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error { |
Andrew Bonventre | 1389a95 | 2019-10-07 09:58:01 -0400 | [diff] [blame] | 114 | if cl.WorkInProgress() || |
Andrew Bonventre | 5df336c | 2018-11-08 12:07:56 -0500 | [diff] [blame] | 115 | cl.Owner() == nil || |
Dmitri Shuralyov | 9e83d85 | 2019-02-20 14:56:03 -0500 | [diff] [blame] | 116 | strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") { |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 117 | return nil |
| 118 | } |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 119 | var searchTerms []string |
Brad Fitzpatrick | 1d5048e | 2018-05-17 14:50:55 +0000 | [diff] [blame] | 120 | tags := cl.Meta.Hashtags() |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 121 | if tags.Contains("wait-author") || |
| 122 | tags.Contains("wait-release") || |
| 123 | tags.Contains("wait-issue") { |
Brad Fitzpatrick | 6e90891 | 2018-04-19 19:16:02 +0000 | [diff] [blame] | 124 | return nil |
| 125 | } |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 126 | c := &change{GerritCL: cl} |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 127 | 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 Bonventre | 8b8ef91 | 2019-10-07 17:54:30 -0400 | [diff] [blame] | 138 | |
| 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 Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 152 | searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...) |
Bryan C. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 153 | 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 Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 158 | 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 Bonventre | 8b8ef91 | 2019-10-07 17:54:30 -0400 | [diff] [blame] | 164 | case -1: |
| 165 | c.HasMinusOne = true |
| 166 | searchTerms = append(searchTerms, "t:-1") |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 167 | 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 Bonventre | 8b8ef91 | 2019-10-07 17:54:30 -0400 | [diff] [blame] | 175 | 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 Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 185 | } |
| 186 | } |
| 187 | |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 188 | 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 Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 193 | searchTerms = append(searchTerms, c.FormattedLastUpdate) |
| 194 | c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " ")) |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 195 | proj.Changes = append(proj.Changes, c) |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 196 | totalChanges++ |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 197 | return nil |
Andrew Bonventre | 1389a95 | 2019-10-07 09:58:01 -0400 | [diff] [blame] | 198 | })) |
Bryan C. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 199 | if err != nil { |
| 200 | return err |
| 201 | } |
Ian Lance Taylor | 727bcae | 2024-02-20 15:37:59 -0800 | [diff] [blame] | 202 | slices.SortFunc(proj.Changes, func(a, b *change) int { |
| 203 | return a.LastUpdate.Compare(b.LastUpdate) |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 204 | }) |
| 205 | projects = append(projects, proj) |
| 206 | return nil |
Andrew Bonventre | 1389a95 | 2019-10-07 09:58:01 -0400 | [diff] [blame] | 207 | })) |
Dmitri Shuralyov | fb495b9 | 2022-05-24 12:39:51 -0400 | [diff] [blame] | 208 | if err != nil { |
| 209 | return err |
| 210 | } |
Ian Lance Taylor | 727bcae | 2024-02-20 15:37:59 -0800 | [diff] [blame] | 211 | slices.SortFunc(projects, func(a, b *project) int { |
| 212 | return strings.Compare(a.Project(), b.Project()) |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 213 | }) |
| 214 | s.data.reviews.Projects = projects |
Andrew Bonventre | 3498ec7 | 2017-09-22 12:19:37 -0400 | [diff] [blame] | 215 | s.data.reviews.TotalChanges = totalChanges |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 216 | s.data.reviews.dirty = false |
Bryan C. Mills | 24ce8bd | 2022-05-13 09:13:56 -0400 | [diff] [blame] | 217 | return nil |
Andrew Bonventre | 99aa072 | 2017-09-20 16:27:09 -0400 | [diff] [blame] | 218 | } |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 219 | |
| 220 | // hasHumanComments reports whether cl has any comments from a human on it. |
| 221 | func 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. |
| 237 | func 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 Shuralyov | 638dc40 | 2019-08-30 16:01:34 -0400 | [diff] [blame] | 247 | foreach.LineStr(m.Commit.Msg, func(ln string) error { |
Andrew Bonventre | 2774277 | 2019-02-20 22:38:16 -0500 | [diff] [blame] | 248 | 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 | } |