blob: c9b99e7238ee3ec1a884d1c6fb72fd3f4d471301 [file] [log] [blame]
// Copyright 2014 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 main
import (
"bytes"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
)
var (
pendingLocal bool // -l flag, use only local operations (no network)
pendingCurrentOnly bool // -c flag, show only current branch
pendingShort bool // -s flag, short display
)
// A pendingBranch collects information about a single pending branch.
// We overlap the reading of this information for each branch.
type pendingBranch struct {
*Branch // standard Branch functionality
current bool // is this the current branch?
staged []string // files in staging area, only if current==true
unstaged []string // files unstaged in local directory, only if current==true
untracked []string // files untracked in local directory, only if current==true
}
// load populates b with information about the branch.
func (b *pendingBranch) load() {
b.loadPending()
if !b.current && b.commitsAhead == 0 {
// Won't be displayed, don't bother looking any closer.
return
}
b.OriginBranch() // cache result
if b.current {
b.staged, b.unstaged, b.untracked = LocalChanges()
}
var changeIDs []string
var commits []*Commit
for _, c := range b.Pending() {
c.committed = ListFiles(c)
if c.ChangeID == "" {
c.gerr = fmt.Errorf("missing Change-Id in commit message")
} else {
changeIDs = append(changeIDs, fullChangeID(b.Branch, c))
commits = append(commits, c)
}
}
if !pendingLocal {
gs, err := b.GerritChanges(changeIDs, "DETAILED_LABELS", "CURRENT_REVISION", "MESSAGES", "DETAILED_ACCOUNTS")
if len(gs) != len(commits) && err == nil {
err = fmt.Errorf("invalid response from Gerrit server - %d queries but %d results", len(changeIDs), len(gs))
}
if err != nil {
for _, c := range commits {
if c.gerr != nil {
c.gerr = err
}
}
} else {
for i, c := range commits {
if len(gs[i]) == 1 {
c.g = gs[i][0]
}
}
}
}
for _, c := range b.Pending() {
if c.g == nil {
c.g = new(GerritChange) // easier for formatting code
}
}
}
func cmdPending(args []string) {
// NOTE: New flags should be added to the usage message below as well as doc.go.
flags.BoolVar(&pendingCurrentOnly, "c", false, "show only current branch")
flags.BoolVar(&pendingLocal, "l", false, "use only local information - no network operations")
flags.BoolVar(&pendingShort, "s", false, "show short listing")
flags.Parse(args)
if len(flags.Args()) > 0 {
fmt.Fprintf(stderr(), "Usage: %s pending %s [-c] [-l] [-s]\n", progName, globalFlags)
exit(2)
}
// Fetch info about remote changes, so that we can say which branches need sync.
doneFetch := make(chan bool, 1)
if pendingLocal {
doneFetch <- true
} else {
http.DefaultClient.Timeout = 60 * time.Second
go func() {
run("git", "fetch", "-q")
doneFetch <- true
}()
}
// Build list of pendingBranch structs to be filled in.
// The current branch is always first.
var branches []*pendingBranch
branches = []*pendingBranch{{Branch: CurrentBranch(), current: true}}
if !pendingCurrentOnly {
current := CurrentBranch().Name
for _, b := range LocalBranches() {
if b.Name != current {
branches = append(branches, &pendingBranch{Branch: b})
}
}
}
// The various data gathering is a little slow,
// especially run in serial with a lot of branches.
// Overlap inspection of multiple branches.
// Each branch is only accessed by a single worker.
// Build work queue.
work := make(chan *pendingBranch, len(branches))
done := make(chan bool, len(branches))
for _, b := range branches {
work <- b
}
close(work)
// Kick off goroutines to do work.
n := len(branches)
if n > 10 {
n = 10
}
for i := 0; i < n; i++ {
go func() {
for b := range work {
// This b.load may be using a stale origin/master ref, which is OK.
b.load()
done <- true
}
}()
}
// Wait for goroutines to finish.
// Note: Counting work items, not goroutines (there may be fewer goroutines).
for range branches {
<-done
}
<-doneFetch
// Print output.
// If there are multiple changes in the current branch, the output splits them out into separate sections,
// in reverse commit order, to match git log output.
//
// wbshadow 7a524a1..a496c1e (current branch, all mailed, 23 behind, tracking master)
// + uncommitted changes
// Files unstaged:
// src/runtime/proc1.go
//
// + a496c1e https://go-review.googlesource.com/2064 (mailed)
// runtime: add missing write barriers in append's copy of slice data
//
// Found with GODEBUG=wbshadow=1 mode.
// Eventually that will run automatically, but right now
// it still detects other missing write barriers.
//
// Change-Id: Ic8624401d7c8225a935f719f96f2675c6f5c0d7c
//
// Code-Review:
// +0 Austin Clements, Rick Hudson
// Files in this change:
// src/runtime/slice.go
//
// + 95390c7 https://go-review.googlesource.com/2061 (mailed)
// runtime: add GODEBUG wbshadow for finding missing write barriers
//
// This is the detection code. It works well enough that I know of
// a handful of missing write barriers. However, those are subtle
// enough that I'll address them in separate followup CLs.
//
// Change-Id: If863837308e7c50d96b5bdc7d65af4969bf53a6e
//
// Code-Review:
// +0 Austin Clements, Rick Hudson
// Files in this change:
// src/runtime/extern.go
// src/runtime/malloc1.go
// src/runtime/malloc2.go
// src/runtime/mgc.go
// src/runtime/mgc0.go
// src/runtime/proc1.go
// src/runtime/runtime1.go
// src/runtime/runtime2.go
// src/runtime/stack1.go
//
// The first line only gives information that applies to the entire branch:
// the name, the commit range, whether this is the current branch, whether
// all the commits are mailed/submitted, how far behind, what remote branch
// it is tracking.
// The individual change sections have per-change information: the hash of that
// commit, the URL on the Gerrit server, whether it is mailed/submitted, the list of
// files in that commit. The uncommitted file modifications are shown as a separate
// section, at the beginning, to fit better into the reverse commit order.
//
// The short view compresses the listing down to two lines per commit:
// wbshadow 7a524a1..a496c1e (current branch, all mailed, 23 behind, tracking master)
// + uncommitted changes
// Files unstaged:
// src/runtime/proc1.go
// + a496c1e runtime: add missing write barriers in append's copy of slice data (CL 2064, mailed)
// + 95390c7 runtime: add GODEBUG wbshadow for finding missing write barriers (CL 2061, mailed)
var buf bytes.Buffer
printFileList := func(name string, list []string) {
if len(list) == 0 {
return
}
fmt.Fprintf(&buf, "\tFiles %s:\n", name)
for _, file := range list {
fmt.Fprintf(&buf, "\t\t%s\n", file)
}
}
for _, b := range branches {
if !b.current && b.commitsAhead == 0 {
// Hide branches with no work on them.
continue
}
fmt.Fprintf(&buf, "%s", b.Name)
work := b.Pending()
if len(work) > 0 {
fmt.Fprintf(&buf, " %.7s..%s", b.branchpoint, work[0].ShortHash)
}
var tags []string
if b.DetachedHead() {
tags = append(tags, "detached")
} else if b.current {
tags = append(tags, "current branch")
}
if allMailed(work) && len(work) > 0 {
tags = append(tags, "all mailed")
}
if allSubmitted(work) && len(work) > 0 {
tags = append(tags, "all submitted")
}
if n := b.CommitsBehind(); n > 0 {
tags = append(tags, fmt.Sprintf("%d behind", n))
}
if br := b.OriginBranch(); br == "" {
tags = append(tags, "remote branch unknown")
} else if br != "origin/master" && br != "origin/main" {
tags = append(tags, "tracking "+strings.TrimPrefix(b.OriginBranch(), "origin/"))
}
if len(tags) > 0 {
fmt.Fprintf(&buf, " (%s)", strings.Join(tags, ", "))
}
fmt.Fprintf(&buf, "\n")
printed := false
if b.current && len(b.staged)+len(b.unstaged)+len(b.untracked) > 0 {
printed = true
fmt.Fprintf(&buf, "+ uncommitted changes\n")
printFileList("untracked", b.untracked)
printFileList("unstaged", b.unstaged)
printFileList("staged", b.staged)
if !pendingShort {
fmt.Fprintf(&buf, "\n")
}
}
for _, c := range work {
printed = true
fmt.Fprintf(&buf, "+ ")
formatCommit(&buf, c, pendingShort)
if !pendingShort {
printFileList("in this change", c.committed)
fmt.Fprintf(&buf, "\n")
}
}
if pendingShort || !printed {
fmt.Fprintf(&buf, "\n")
}
}
stdout().Write(buf.Bytes())
}
// formatCommit writes detailed information about c to w. c.g must
// have the "CURRENT_REVISION" (or "ALL_REVISIONS") and
// "DETAILED_LABELS" options set.
//
// If short is true, this writes a single line overview.
//
// If short is false, this writes detailed information about the
// commit and its Gerrit state.
func formatCommit(w io.Writer, c *Commit, short bool) {
g := c.g
if g == nil {
g = new(GerritChange)
}
msg := strings.TrimRight(c.Message, "\r\n")
fmt.Fprintf(w, "%s", c.ShortHash)
var tags []string
if short {
if i := strings.Index(msg, "\n"); i >= 0 {
msg = msg[:i]
}
fmt.Fprintf(w, " %s", msg)
if g.Number != 0 {
tags = append(tags, fmt.Sprintf("CL %d%s", g.Number, codeReviewScores(g)))
}
} else {
if g.Number != 0 {
fmt.Fprintf(w, " %s/%d", auth.url, g.Number)
}
}
if g.CurrentRevision == c.Hash {
tags = append(tags, "mailed")
}
switch g.Status {
case "MERGED":
tags = append(tags, "submitted")
case "ABANDONED":
tags = append(tags, "abandoned")
}
if len(c.Parents) > 1 {
var h []string
for _, p := range c.Parents[1:] {
h = append(h, p[:7])
}
tags = append(tags, "merge="+strings.Join(h, ","))
}
if g.UnresolvedCommentCount > 0 {
tags = append(tags, fmt.Sprintf("%d unresolved comments", g.UnresolvedCommentCount))
}
if len(tags) > 0 {
fmt.Fprintf(w, " (%s)", strings.Join(tags, ", "))
}
fmt.Fprintf(w, "\n")
if short {
return
}
fmt.Fprintf(w, "\t%s\n", strings.Replace(msg, "\n", "\n\t", -1))
fmt.Fprintf(w, "\n")
for _, name := range g.LabelNames() {
label := g.Labels[name]
minValue := 10000
maxValue := -10000
byScore := map[int][]string{}
for _, x := range label.All {
// Hide CL owner unless owner score is nonzero.
if g.Owner != nil && x.ID == g.Owner.ID && x.Value == 0 {
continue
}
byScore[x.Value] = append(byScore[x.Value], x.Name)
if minValue > x.Value {
minValue = x.Value
}
if maxValue < x.Value {
maxValue = x.Value
}
}
// Unless there are scores to report, do not show labels other than Code-Review.
// This hides Run-TryBot and TryBot-Result.
if minValue >= 0 && maxValue <= 0 && name != "Code-Review" {
continue
}
fmt.Fprintf(w, "\t%s:\n", name)
for score := maxValue; score >= minValue; score-- {
who := byScore[score]
if len(who) == 0 || score == 0 && name != "Code-Review" {
continue
}
sort.Strings(who)
fmt.Fprintf(w, "\t\t%+d %s\n", score, strings.Join(who, ", "))
}
}
}
// codeReviewScores reports the code review scores as tags for the short output.
//
// g must have the "DETAILED_LABELS" option set.
func codeReviewScores(g *GerritChange) string {
label := g.Labels["Code-Review"]
if label == nil {
return ""
}
minValue := 10000
maxValue := -10000
for _, x := range label.All {
if minValue > x.Value {
minValue = x.Value
}
if maxValue < x.Value {
maxValue = x.Value
}
}
var scores string
if minValue < 0 {
scores += fmt.Sprintf(" %d", minValue)
}
if maxValue > 0 {
scores += fmt.Sprintf(" %+d", maxValue)
}
return scores
}
// allMailed reports whether all commits in work have been posted to Gerrit.
func allMailed(work []*Commit) bool {
for _, c := range work {
if c.Hash != c.g.CurrentRevision {
return false
}
}
return true
}
// allSubmitted reports whether all commits in work have been submitted to the origin branch.
func allSubmitted(work []*Commit) bool {
for _, c := range work {
if c.g.Status != "MERGED" {
return false
}
}
return true
}
// suffix returns an empty string if n == 1, s otherwise.
func suffix(n int, s string) string {
if n == 1 {
return ""
}
return s
}