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  }