| // 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" |
| "net/http" |
| "os" |
| "sort" |
| "strings" |
| "time" |
| ) |
| |
| var pendingLocal bool // -l flag, use only local operations (no network) |
| |
| // 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 |
| g *GerritChange // state loaded from Gerrit |
| gerr error // error loading state from Gerrit |
| current bool // is this the current branch? |
| committed []string // files committed on this 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() |
| } |
| if b.parentHash != "" && b.commitHash != "" { |
| b.committed = getLines("git", "diff", "--name-only", b.parentHash, b.commitHash, "--") |
| } |
| if !pendingLocal { |
| b.g, b.gerr = b.GerritChange("DETAILED_LABELS", "CURRENT_REVISION", "MESSAGES", "DETAILED_ACCOUNTS") |
| } |
| } |
| |
| func pending(args []string) { |
| flags.BoolVar(&pendingLocal, "l", false, "use only local information - no network operations") |
| flags.Parse(args) |
| if len(flags.Args()) > 0 { |
| fmt.Fprintf(stderr(), "Usage: %s pending %s [-l]\n", os.Args[0], globalFlags) |
| os.Exit(2) |
| } |
| |
| // Fetch info about remote changes, so that we can say which branches need sync. |
| if !pendingLocal { |
| run("git", "fetch", "-q") |
| http.DefaultClient.Timeout = 5 * time.Second |
| } |
| |
| // Build list of pendingBranch structs to be filled in. |
| current := CurrentBranch().Name |
| var branches []*pendingBranch |
| for _, b := range LocalBranches() { |
| branches = append(branches, &pendingBranch{Branch: b, current: b.Name == current}) |
| } |
| |
| // 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 { |
| b.load() |
| done <- true |
| } |
| }() |
| } |
| |
| // Wait for goroutines to finish. |
| // Note: Counting work items, not goroutines (there may be fewer goroutines). |
| for range branches { |
| <-done |
| } |
| |
| // Print output, like: |
| // pending d8fcb99 https://go-review.googlesource.com/1620 (current branch, 1 behind) |
| // git-codereview: expand pending output |
| // |
| // for pending: |
| // - show full commit message |
| // - show information about being behind upstream |
| // - show list of modified files |
| // - for current branch, show staged, unstaged, untracked files |
| // - warn about being ahead of upstream on master |
| // - warn about being multiple commits ahead of upstream |
| // |
| // - add same warnings to change |
| // - add change -a (mostly unrelated, but prompted by this work) |
| // |
| // Change-Id: Ie480ba5b66cc07faffca421ee6c9623d35204696 |
| // |
| // Code-Review: |
| // +2 Andrew Gerrand, Rob Pike |
| // Files in this change: |
| // git-codereview/api.go |
| // git-codereview/branch.go |
| // git-codereview/change.go |
| // git-codereview/pending.go |
| // git-codereview/review.go |
| // git-codereview/submit.go |
| // git-codereview/sync.go |
| // Files untracked: |
| // git-codereview/doc.go |
| // git-codereview/savedmail.go.txt |
| // |
| var buf bytes.Buffer |
| for _, b := range branches { |
| if !b.current && b.commitsAhead == 0 { |
| // Hide branches with no work on them. |
| continue |
| } |
| |
| fmt.Fprintf(&buf, "%s", b.Name) |
| if b.shortCommitHash != "" { |
| fmt.Fprintf(&buf, " %s", b.shortCommitHash) |
| } |
| if b.g != nil && b.g.Number != 0 { |
| fmt.Fprintf(&buf, " %s/%d", auth.url, b.g.Number) |
| } |
| var tags []string |
| if b.current { |
| tags = append(tags, "current branch") |
| } |
| if b.g != nil && b.g.CurrentRevision == b.commitHash { |
| tags = append(tags, "mailed") |
| } |
| if b.g != nil && b.g.Status == "MERGED" { |
| tags = append(tags, "submitted") |
| } |
| if b.commitsBehind > 0 { |
| tags = append(tags, fmt.Sprintf("%d behind", b.commitsBehind)) |
| } |
| if len(tags) > 0 { |
| fmt.Fprintf(&buf, " (%s)", strings.Join(tags, ", ")) |
| } |
| fmt.Fprintf(&buf, "\n") |
| if text := b.errors(); text != "" { |
| fmt.Fprintf(&buf, "\tERROR: %s\n\n", strings.Replace(strings.TrimSpace(text), "\n", "\n\t", -1)) |
| } |
| if b.message != "" { |
| msg := strings.TrimRight(b.message, "\r\n") |
| fmt.Fprintf(&buf, "\t%s\n", strings.Replace(msg, "\n", "\n\t", -1)) |
| fmt.Fprintf(&buf, "\n") |
| } |
| if b.g != nil { |
| for _, name := range b.g.LabelNames() { |
| label := b.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 b.g.Owner != nil && x.ID == b.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 |
| } |
| } |
| fmt.Fprintf(&buf, "\t%s:\n", name) |
| for score := maxValue; score >= minValue; score-- { |
| who := byScore[score] |
| if len(who) == 0 { |
| continue |
| } |
| sort.Strings(who) |
| fmt.Fprintf(&buf, "\t\t%+d %s\n", score, strings.Join(who, ", ")) |
| } |
| } |
| } |
| 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) |
| } |
| } |
| printFileList("in this change", b.committed) |
| printFileList("staged", b.staged) |
| printFileList("unstaged", b.unstaged) |
| printFileList("untracked", b.untracked) |
| |
| fmt.Fprintf(&buf, "\n") |
| } |
| |
| stdout().Write(buf.Bytes()) |
| } |
| |
| // errors returns any errors that should be displayed |
| // about the state of the current branch, diagnosing common mistakes. |
| func (b *Branch) errors() string { |
| b.loadPending() |
| var buf bytes.Buffer |
| if !b.IsLocalOnly() && b.commitsAhead > 0 { |
| fmt.Fprintf(&buf, "Branch contains %d commit%s not on origin/%s.\n", b.commitsAhead, suffix(b.commitsAhead, "s"), b.Name) |
| fmt.Fprintf(&buf, "\tDo not commit directly to %s branch.\n", b.Name) |
| } else if b.commitsAhead > 1 { |
| fmt.Fprintf(&buf, "Branch contains %d commits not on origin/%s.\n", b.commitsAhead, b.OriginBranch()) |
| fmt.Fprintf(&buf, "\tThere should be at most one.\n") |
| fmt.Fprintf(&buf, "\tUse 'git change', not 'git commit'.\n") |
| fmt.Fprintf(&buf, "\tRun 'git log %s..%s' to list commits.\n", b.OriginBranch(), b.Name) |
| } |
| return buf.String() |
| } |
| |
| // suffix returns an empty string if n == 1, s otherwise. |
| func suffix(n int, s string) string { |
| if n == 1 { |
| return "" |
| } |
| return s |
| } |