blob: fefaef7d05654cc52e918bfa8bb526d862f247b5 [file] [log] [blame]
// Copyright 2015 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.
/*
CL prints a list of open Go code reviews (also known as change lists, or CLs).
Usage:
cl [-closed] [-dnr] [-json] [-r] [-url] [query ...]
CL searches Gerrit for CLs matching the query and then
prints a line for each CL that is waiting for review
(as opposed to waiting for revisions by the author).
The output line looks like:
CL 9225 0/ 2d go rsc austin* cmd/internal/gc: emit write barrier
From left to right, the columns show the CL number,
the number of days the CL has been in the current waiting state
(waiting for author or waiting for review),
the number of days since the CL was created,
the project name ("go" or the name of a subrepository),
the author, the reviewer, and the subject.
If the query restricts results to a single project
(for example, "cl project:go"), the project column is elided.
If the CL is waiting for revisions by the author,
the author column has an asterisk.
If the CL is waiting for a reviewer, the reviewer column
has an asterisk.
If the CL has been reviewed by the reviewer,
the reviewer column shows the current score.
By default, CL omits closed CLs, those with an R=close reply
and no subsequent upload of a new patch set.
If the -closed flag is specified, CL adds closed CLs to the output.
By default, CL omits CLs containing ``DO NOT REVIEW'' in the
latest patch's commit message.
If the -dnr flag is specified, CL includes those CLs in its output.
If the -r flag is specified, CL shows only CLs that need review,
not those waiting for the author. In this mode, the
redundant ``waiting for reviewer'' asterisk is elided.
If the -url flag is specified, CL replaces "CL 1234" at the beginning
of each output line with a full URL, "https://golang.org/cl/1234".
By default, CL sorts the output first by the combination of
project name and change subject.
The -sort flag changes the sort order. The choices are
"delay", to sort by the time the change has been in the current
waiting state, and "age", to sort by creation time.
When sorting, ties are broken by CL number.
If the -json flag is specified, CL does not print the usual listing.
Instead, it prints a JSON array holding CL objects, one for each
matched CL. Each of the CL objects is generated by this Go struct:
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
}
*/
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sort"
"strings"
"time"
"golang.org/x/build/gerrit"
"golang.org/x/build/godash"
)
var (
flagClosed = flag.Bool("closed", false, "include CLs that are closed or DO NOT REVIEW")
flagDoNotReview = flag.Bool("dnr", false, "print only CLs in need of review")
flagNeedsReview = flag.Bool("r", false, "print only CLs in need of review")
flagJSON = flag.Bool("json", false, "print CLs in JSON format")
flagURL = flag.Bool("url", false, "print full URLs for CLs")
flagSort = flag.String("sort", "", "sort by `order` (age or delay) instead of project+subject")
)
func usage() {
fmt.Fprintf(os.Stderr, "usage: cl [query]\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
os.Exit(2)
}
var now = time.Now() // so time stays the same during computations
func main() {
log.SetFlags(0)
log.SetPrefix("cl: ")
flag.Usage = usage
flag.Parse()
switch *flagSort {
case "", "age", "delay":
// ok
default:
log.Fatal("unknown sort order")
}
c := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
query := strings.Join(flag.Args(), " ")
open := "is:open"
if strings.Contains(query, " is:") || strings.HasPrefix(query, "is:") {
open = ""
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cis, err := c.QueryChanges(ctx, open+" -project:scratch -message:do-not-review "+query, gerrit.QueryChangesOpt{
N: 5000,
Fields: []string{
"LABELS",
"CURRENT_FILES",
"CURRENT_REVISION",
"CURRENT_COMMIT",
"MESSAGES",
"DETAILED_ACCOUNTS", // fill out Owner.AuthorInfo, etc
"DETAILED_LABELS",
},
})
if err != nil {
log.Fatalf("error querying changes: %v", err)
}
cls := []*godash.CL{} // non-nil for json
for _, ci := range cis {
cl := parseCL(ci)
if *flagNeedsReview && !cl.NeedsReview || !*flagClosed && (cl.Closed || cl.DoNotReview) {
continue
}
cls = append(cls, cl)
}
switch *flagSort {
case "":
sort.Sort(byRepoAndSubject(cls))
case "age":
sort.Sort(byAge(cls))
case "delay":
sort.Sort(byDelay(cls))
}
if *flagJSON {
data, err := json.MarshalIndent(cls, "", "\t")
if err != nil {
log.Fatal(err)
}
data = append(data, '\n')
os.Stdout.Write(data)
return
}
clPrefix := "CL "
if *flagURL {
clPrefix = "https://golang.org/cl/"
}
var projectLen, authorLen, reviewerLen int
for _, cl := range cls {
projectLen = max(projectLen, len(cl.Project))
authorLen = max(authorLen, len(cl.Author))
reviewerLen = max(reviewerLen, len(cl.Reviewer))
}
if authorLen > 12 {
authorLen = 12
}
if reviewerLen > 12 {
reviewerLen = 12
}
authorLen += 1 // for *
reviewerLen += 3 // for +2*
var buf bytes.Buffer
for _, cl := range cls {
fmt.Fprintf(&buf, "%s%-5d %3.0f/%3.0fd %-*s %-*s %-*s %s%s\n",
clPrefix, cl.Number,
cl.Delay(now).Seconds()/86400, cl.Age(now).Seconds()/86400,
projectLen, cl.Project,
authorLen, authorString(cl, authorLen),
reviewerLen, reviewerString(cl, reviewerLen),
cl.Subject, issuesString(cl))
}
os.Stdout.Write(buf.Bytes())
}
func max(i, j int) int {
if i < j {
return j
}
return i
}
// authorString returns the author column, limited to n bytes.
func authorString(cl *godash.CL, n int) string {
suffix := ""
if !cl.NeedsReview {
suffix = "*"
}
return truncate(cl.Author, n-len(suffix)) + suffix
}
// reviewerString returns the reviewer column, limited to n bytes.
func reviewerString(cl *godash.CL, n int) string {
suffix := ""
if cl.NeedsReview && !*flagNeedsReview {
suffix = "*"
}
if score := cl.Scores[cl.ReviewerEmail]; score != 0 {
suffix = fmt.Sprintf("%+d", score) + suffix
}
return truncate(cl.Reviewer, n-len(suffix)) + suffix
}
// truncate returns the name truncated to n bytes.
func truncate(text string, n int) string {
if len(text) <= n {
return text
}
return text[:n-3] + "..."
}
func issuesString(cl *godash.CL) string {
s := ""
for _, id := range cl.Issues {
s += fmt.Sprintf(" #%d", id)
}
return s
}
const goReleaseCycle = 8 // working on Go 1.x
var reviewers *godash.Reviewers
func parseCL(ci *gerrit.ChangeInfo) *godash.CL {
if reviewers == nil {
reviewers = &godash.Reviewers{}
reviewers.LoadLocal()
}
return godash.ParseCL(ci, reviewers, goReleaseCycle)
}
// Sort interfaces.
type byRepoAndSubject []*godash.CL
func (x byRepoAndSubject) Len() int { return len(x) }
func (x byRepoAndSubject) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byRepoAndSubject) Less(i, j int) bool {
if x[i].Project != x[j].Project {
return projectOrder(x[i].Project) < projectOrder(x[j].Project)
}
if x[i].Subject != x[j].Subject {
return x[i].Subject < x[j].Subject
}
return x[i].Number < x[j].Number
}
type byAge []*godash.CL
func (x byAge) Len() int { return len(x) }
func (x byAge) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byAge) Less(i, j int) bool {
if !x[i].Start.Equal(x[j].Start) {
return x[i].Start.Before(x[j].Start)
}
return x[i].Number > x[j].Number
}
type byDelay []*godash.CL
func (x byDelay) Len() int { return len(x) }
func (x byDelay) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byDelay) Less(i, j int) bool {
if !x[i].NeedsReviewChanged.Equal(x[j].NeedsReviewChanged) {
return x[i].NeedsReviewChanged.Before(x[j].NeedsReviewChanged)
}
return x[i].Number < x[j].Number
}
func projectOrder(name string) string {
if name == "go" {
return "\x00" // sort before everything except empty string
}
return name
}