blob: 79d2de03fda6def1abfb12cab7b27ba424ad94db [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package godash
import (
"bytes"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/build/gerrit"
)
// Parsing of Gerrit information and review messages to produce CL structure.
var (
reviewerRE = regexp.MustCompile(`(?m)^R=([\w\-.@]+)\b`)
issueRefRE = regexp.MustCompile(`#\d+\b`)
goIssueRefRE = regexp.MustCompile(`\bgolang/go#\d+\b`)
)
// ParseCL takes a ChangeInfo as returned from the Gerrit API and
// applies Go-project-specific logic to turn it into a CL struct. The
// primary information that is added is the CL's state in the Go
// review process, based on parsing R= lines in the comments and the
// most recent commit message of the CL.
func ParseCL(ci *gerrit.ChangeInfo, reviewers *Reviewers, goReleaseCycle int) *CL {
// Gather information.
var (
scores = make(map[string]int)
initialReviewer = ""
firstResponder = ""
explicitReviewer = ""
closeReason = ""
)
parseReviewer := func(msg string) {
if m := reviewerRE.FindStringSubmatch(msg); m != nil {
if m[1] == "close" {
explicitReviewer = "close"
closeReason = "Closed"
} else if strings.HasPrefix(m[1], "go1.") {
n, _ := strconv.Atoi(m[1][len("go1."):])
if n > goReleaseCycle {
explicitReviewer = "close"
closeReason = "Go" + m[1][2:]
}
} else if m[1] == "golang-dev" || m[1] == "golang-codereviews" {
explicitReviewer = "golang-dev"
} else if x := reviewers.Resolve(m[1]); x != "" {
explicitReviewer = x
}
}
}
rev := ci.Revisions[ci.CurrentRevision]
for _, msg := range ci.Messages {
if msg.Author == nil { // happens for Gerrit-generated messages
continue
}
if strings.HasPrefix(msg.Message, "Uploaded patch set ") {
if explicitReviewer == "close" && !strings.HasPrefix(closeReason, "Go") {
explicitReviewer = ""
closeReason = ""
}
if msg.RevisionNumber == rev.PatchSetNumber && rev.Commit != nil {
parseReviewer(rev.Commit.Message)
}
}
parseReviewer(msg.Message)
if firstResponder == "" && reviewers.IsReviewer(msg.Author.Email) && msg.Author.Email != ci.Owner.Email {
firstResponder = msg.Author.Email
}
}
for _, score := range ci.Labels["Code-Review"].All {
scores[score.Email] = score.Value
}
cl := &CL{
Number: ci.ChangeNumber,
Subject: ci.Subject,
Project: ci.Project,
Author: reviewers.Shorten(ci.Owner.Email),
AuthorEmail: ci.Owner.Email,
Scores: scores,
Status: strings.ToLower(ci.Status),
}
// Determine reviewer, in priorty order.
// When breaking ties, give preference to R= setting.
// Otherwise compare by email address.
maybe := func(who string) {
if cl.ReviewerEmail == "" || who == explicitReviewer || cl.ReviewerEmail != explicitReviewer && cl.ReviewerEmail > who {
cl.ReviewerEmail = who
}
}
// 1. Anyone who -2'ed the CL.
if cl.ReviewerEmail == "" {
for who, score := range scores {
if score == -2 {
maybe(who)
}
}
}
// 2. Anyone who +2'ed the CL.
if cl.ReviewerEmail == "" {
for who, score := range scores {
if score == +2 {
maybe(who)
}
}
}
// 2½. Even if a CL is +2 or -2, R=closed wins,
// so that it doesn't appear in listings by default.
if explicitReviewer == "close" {
cl.ReviewerEmail = "close"
}
// 3. Last explicit R= in review message.
if cl.ReviewerEmail == "" {
cl.ReviewerEmail = explicitReviewer
}
// 4. Initial target of review requecl.
// TODO: If there's some way to figure this out, do so.
_ = initialReviewer
// 5. Whoever responds first and looks like a reviewer.
if cl.ReviewerEmail == "" {
cl.ReviewerEmail = firstResponder
}
// Allow R=golang-dev in #2 as "unassign".
if cl.ReviewerEmail == "golang-dev" {
cl.ReviewerEmail = ""
}
cl.Reviewer = reviewers.Shorten(cl.ReviewerEmail)
// Now that we know who the reviewer is,
// figure out whether the CL is in need of review
// (or else is waiting for the author to do more work).
for _, msg := range ci.Messages {
if msg.Author == nil { // happens for Gerrit-generated messages
continue
}
if cl.Start.IsZero() {
cl.Start = msg.Time.Time()
}
if strings.HasPrefix(msg.Message, "Uploaded patch set ") {
cl.NeedsReview = true
cl.NeedsReviewChanged = msg.Time.Time()
}
if msg.Author.Email == cl.ReviewerEmail {
cl.NeedsReview = false
cl.NeedsReviewChanged = msg.Time.Time()
}
}
if cl.ReviewerEmail == "close" {
cl.Reviewer = closeReason
cl.ReviewerEmail = ""
cl.Closed = true
cl.NeedsReview = false
}
// DO NOT REVIEW overrides anything in the CL state.
// We shouldn't see these, because the query always
// contains -message:do-not-review, but check anyway.
if _, ok := ci.Labels["Do-Not-Review"]; ok {
cl.Reviewer = "DoNotReview"
cl.ReviewerEmail = ""
cl.DoNotReview = true
cl.NeedsReview = false
}
// Find issue numbers.
cl.Issues = []int{} // non-nil for json
refRE := issueRefRE
if cl.Project != "go" {
refRE = goIssueRefRE
}
for file := range rev.Files {
cl.Files = append(cl.Files, file)
}
sort.Strings(cl.Files)
if rev.Commit != nil {
for _, ref := range refRE.FindAllString(rev.Commit.Message, -1) {
n, _ := strconv.Atoi(ref[strings.Index(ref, "#")+1:])
if n != 0 {
cl.Issues = append(cl.Issues, n)
}
}
}
cl.Issues = uniq(cl.Issues)
return cl
}
func uniq(x []int) []int {
sort.Ints(x)
out := x[:0]
for _, v := range x {
if len(out) == 0 || out[len(out)-1] != v {
out = append(out, v)
}
}
return out
}
// CL records information about a single CL.
// This is also used by golang.org/x/build/cmd/cl and any changes need
// to reflected in its doc comment.
type CL struct {
Number int // CL number
Subject string // subject (first line of commit message)
Project string // "go" or a subrepository name
Author string // author, short form or else full email
AuthorEmail string // author, full email
Reviewer string // expected reviewer, short form or else full email
ReviewerEmail string // expected reviewer, full email
Start time.Time // time CL was first uploaded
NeedsReview bool // CL is waiting for reviewer (otherwise author)
NeedsReviewChanged time.Time // time NeedsReview last changed
Closed bool // CL closed with R=close
DoNotReview bool // CL marked DO NOT REVIEW
Issues []int // issues referenced by commit message
Scores map[string]int // current review scores
Files []string // files changed in CL
Status string // "new", "submitted", "merged", ...
}
func (cl *CL) Age(now time.Time) time.Duration {
return now.Sub(cl.Start)
}
func (cl *CL) Delay(now time.Time) time.Duration {
return now.Sub(cl.NeedsReviewChanged)
}
func (cl *CL) Summary(now time.Time) string {
var buf bytes.Buffer
who := "author"
if cl.NeedsReview {
who = "reviewer"
}
rev := cl.Reviewer
if rev == "" {
rev = "???"
}
score := ""
if x := cl.Scores[cl.ReviewerEmail]; x != 0 {
score = fmt.Sprintf("%+d", x)
}
fmt.Fprintf(&buf, "%s → %s%s, %d/%d days, waiting for %s", cl.Author, rev, score, int(now.Sub(cl.NeedsReviewChanged).Seconds()/86400), int(now.Sub(cl.Start).Seconds()/86400), who)
for _, id := range cl.Issues {
fmt.Fprintf(&buf, " #%d", id)
}
return buf.String()
}