golang.org/x/build@v0.0.0-20240506185731-218518f32b70/devapp/reviews.go (about) 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 5 package main 6 7 import ( 8 "bytes" 9 "fmt" 10 "html/template" 11 "io" 12 "log" 13 "net/http" 14 "slices" 15 "strings" 16 "time" 17 18 "golang.org/x/build/internal/foreach" 19 "golang.org/x/build/internal/gophers" 20 "golang.org/x/build/maintner" 21 ) 22 23 type project struct { 24 *maintner.GerritProject 25 Changes []*change 26 } 27 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 41 type change struct { 42 *maintner.GerritCL 43 LastUpdate time.Time 44 FormattedLastUpdate string 45 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 55 } 56 57 type reviewsData struct { 58 Projects []*project 59 TotalChanges int 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 { 72 err := s.updateReviewsData() 73 if err != nil { 74 log.Println("updateReviewsData:", err) 75 http.Error(w, err.Error(), http.StatusInternalServerError) 76 return 77 } 78 } 79 80 s.cMu.RLock() 81 defer s.cMu.RUnlock() 82 83 projects := s.data.reviews.Projects 84 totalChanges := s.data.reviews.TotalChanges 85 86 var buf bytes.Buffer 87 if err := t.Execute(&buf, struct { 88 Projects []*project 89 TotalChanges int 90 }{ 91 Projects: projects, 92 TotalChanges: totalChanges, 93 }); err != nil { 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 103 func (s *server) updateReviewsData() error { 104 log.Println("Updating reviews data ...") 105 s.cMu.Lock() 106 defer s.cMu.Unlock() 107 var ( 108 projects []*project 109 totalChanges int 110 ) 111 err := s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error { 112 proj := &project{GerritProject: p} 113 err := p.ForeachOpenCL(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error { 114 if cl.WorkInProgress() || 115 cl.Owner() == nil || 116 strings.Contains(cl.Commit.Msg, "DO NOT REVIEW") { 117 return nil 118 } 119 var searchTerms []string 120 tags := cl.Meta.Hashtags() 121 if tags.Contains("wait-author") || 122 tags.Contains("wait-release") || 123 tags.Contains("wait-issue") { 124 return nil 125 } 126 c := &change{GerritCL: cl} 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 } 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 152 searchTerms = append(searchTerms, searchTermsFromReviewerFields(cl)...) 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 { 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") 164 case -1: 165 c.HasMinusOne = true 166 searchTerms = append(searchTerms, "t:-1") 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 } 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 } 185 } 186 } 187 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") 193 searchTerms = append(searchTerms, c.FormattedLastUpdate) 194 c.SearchTerms = strings.ToLower(strings.Join(searchTerms, " ")) 195 proj.Changes = append(proj.Changes, c) 196 totalChanges++ 197 return nil 198 })) 199 if err != nil { 200 return err 201 } 202 slices.SortFunc(proj.Changes, func(a, b *change) int { 203 return a.LastUpdate.Compare(b.LastUpdate) 204 }) 205 projects = append(projects, proj) 206 return nil 207 })) 208 if err != nil { 209 return err 210 } 211 slices.SortFunc(projects, func(a, b *project) int { 212 return strings.Compare(a.Project(), b.Project()) 213 }) 214 s.data.reviews.Projects = projects 215 s.data.reviews.TotalChanges = totalChanges 216 s.data.reviews.dirty = false 217 return nil 218 } 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 } 247 foreach.LineStr(m.Commit.Msg, func(ln string) error { 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 }