| // 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" |
| "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 = "" |
| } |
| cis, err := c.QueryChanges(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 |
| } |