aboutsummaryrefslogtreecommitdiff
path: root/git-codereview/branch.go
diff options
context:
space:
mode:
authorAndrew Gerrand <adg@golang.org>2014-12-18 11:25:48 +1100
committerAndrew Gerrand <adg@golang.org>2014-12-18 00:35:19 +0000
commitf473ce13dd1bba7ce531e7800fdf018f60aa2454 (patch)
treefdc20fb8b555be78ca5a4f251e04de5e77d338ea /git-codereview/branch.go
parent6a0c83f0c935e49b841a3a880579cb07918bcb57 (diff)
downloadgo-x-review-f473ce13dd1bba7ce531e7800fdf018f60aa2454.tar.xz
git-codereview: rename from 'git-review' to 'git-codereview'
Mostly trivial search and replace, except for hooks.go which includes a special case to remove the old git-review hooks. Change-Id: Ic0792bb3e26607e5e0ead88958e46c3ac08288cd Reviewed-on: https://go-review.googlesource.com/1741 Reviewed-by: Russ Cox <rsc@golang.org>
Diffstat (limited to 'git-codereview/branch.go')
-rw-r--r--git-codereview/branch.go236
1 files changed, 236 insertions, 0 deletions
diff --git a/git-codereview/branch.go b/git-codereview/branch.go
new file mode 100644
index 0000000..ad82a46
--- /dev/null
+++ b/git-codereview/branch.go
@@ -0,0 +1,236 @@
+// 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"
+ "os"
+ "os/exec"
+ "regexp"
+ "strings"
+)
+
+// Branch describes a Git branch.
+type Branch struct {
+ Name string // branch name
+ loadedPending bool // following fields are valid
+ changeID string // Change-Id of pending commit ("" if nothing pending)
+ subject string // first line of pending commit ("" if nothing pending)
+ message string // commit message
+ commitHash string // commit hash of pending commit ("" if nothing pending)
+ shortCommitHash string // abbreviated commitHash ("" if nothing pending)
+ parentHash string // parent hash of pending commit ("" if nothing pending)
+ commitsAhead int // number of commits ahead of origin branch
+ commitsBehind int // number of commits behind origin branch
+ originBranch string // upstream origin branch
+}
+
+// CurrentBranch returns the current branch.
+func CurrentBranch() *Branch {
+ name := getOutput("git", "rev-parse", "--abbrev-ref", "HEAD")
+ return &Branch{Name: name}
+}
+
+// OriginBranch returns the name of the origin branch that branch b tracks.
+// The returned name is like "origin/master" or "origin/dev.garbage" or
+// "origin/release-branch.go1.4".
+func (b *Branch) OriginBranch() string {
+ if b.originBranch != "" {
+ return b.originBranch
+ }
+ argv := []string{"git", "rev-parse", "--abbrev-ref", b.Name + "@{u}"}
+ out, err := exec.Command(argv[0], argv[1:]...).CombinedOutput()
+ if err == nil && len(out) > 0 {
+ b.originBranch = string(bytes.TrimSpace(out))
+ return b.originBranch
+ }
+ if strings.Contains(string(out), "No upstream configured") {
+ // Assume branch was created before we set upstream correctly.
+ b.originBranch = "origin/master"
+ return b.originBranch
+ }
+ fmt.Fprintf(os.Stderr, "%v\n%s\n", commandString(argv[0], argv[1:]), out)
+ dief("%v", err)
+ panic("not reached")
+}
+
+func (b *Branch) IsLocalOnly() bool {
+ return "origin/"+b.Name != b.OriginBranch()
+}
+
+func (b *Branch) HasPendingCommit() bool {
+ b.loadPending()
+ return b.commitHash != ""
+}
+
+func (b *Branch) ChangeID() string {
+ b.loadPending()
+ return b.changeID
+}
+
+func (b *Branch) Subject() string {
+ b.loadPending()
+ return b.subject
+}
+
+func (b *Branch) CommitHash() string {
+ b.loadPending()
+ return b.commitHash
+}
+
+// Branchpoint returns an identifier for the latest revision
+// common to both this branch and its upstream branch.
+// If this branch has not split from upstream,
+// Branchpoint returns "HEAD".
+func (b *Branch) Branchpoint() string {
+ b.loadPending()
+ if b.parentHash == "" {
+ return "HEAD"
+ }
+ return b.parentHash
+}
+
+func (b *Branch) loadPending() {
+ if b.loadedPending {
+ return
+ }
+ b.loadedPending = true
+
+ const numField = 5
+ all := getOutput("git", "log", "--format=format:%H%x00%h%x00%P%x00%s%x00%B%x00", b.OriginBranch()+".."+b.Name)
+ fields := strings.Split(all, "\x00")
+ if len(fields) < numField {
+ return // nothing pending
+ }
+ for i := 0; i+numField <= len(fields); i += numField {
+ hash := fields[i]
+ shortHash := fields[i+1]
+ parent := fields[i+2]
+ subject := fields[i+3]
+ msg := fields[i+4]
+
+ // Overwrite each time through the loop.
+ // We want to save the info about the *first* commit
+ // after the branch point, and the log is ordered
+ // starting at the most recent and working backward.
+ b.commitHash = hash
+ b.shortCommitHash = shortHash
+ b.parentHash = parent
+ b.subject = subject
+ b.message = msg
+ for _, line := range strings.Split(msg, "\n") {
+ if strings.HasPrefix(line, "Change-Id: ") {
+ b.changeID = line[len("Change-Id: "):]
+ break
+ }
+ }
+ b.commitsAhead++
+ }
+ b.commitsAhead = len(fields) / numField
+ b.commitsBehind = len(getOutput("git", "log", "--format=format:x", b.Name+".."+b.OriginBranch()))
+}
+
+// Submitted reports whether some form of b's pending commit
+// has been cherry picked to origin.
+func (b *Branch) Submitted(id string) bool {
+ if id == "" {
+ return false
+ }
+ return len(getOutput("git", "log", "--grep", "Change-Id: "+id, b.Name+".."+b.OriginBranch())) > 0
+}
+
+var stagedRE = regexp.MustCompile(`^[ACDMR] `)
+
+func HasStagedChanges() bool {
+ for _, s := range getLines("git", "status", "-b", "--porcelain") {
+ if stagedRE.MatchString(s) {
+ return true
+ }
+ }
+ return false
+}
+
+var unstagedRE = regexp.MustCompile(`^.[ACDMR]`)
+
+func HasUnstagedChanges() bool {
+ for _, s := range getLines("git", "status", "-b", "--porcelain") {
+ if unstagedRE.MatchString(s) {
+ return true
+ }
+ }
+ return false
+}
+
+// LocalChanges returns a list of files containing staged, unstaged, and untracked changes.
+// The elements of the returned slices are typically file names, always relative to the root,
+// but there are a few alternate forms. First, for renaming or copying, the element takes
+// the form `from -> to`. Second, in the case of files with names that contain unusual characters,
+// the files (or the from, to fields of a rename or copy) are quoted C strings.
+// For now, we expect the caller only shows these to the user, so these exceptions are okay.
+func LocalChanges() (staged, unstaged, untracked []string) {
+ // NOTE: Cannot use getLines, because it throws away leading spaces.
+ for _, s := range strings.Split(getOutput("git", "status", "-b", "--porcelain"), "\n") {
+ if len(s) < 4 || s[2] != ' ' {
+ continue
+ }
+ switch s[0] {
+ case 'A', 'C', 'D', 'M', 'R':
+ staged = append(staged, s[3:])
+ case '?':
+ untracked = append(untracked, s[3:])
+ }
+ switch s[1] {
+ case 'A', 'C', 'D', 'M', 'R':
+ unstaged = append(unstaged, s[3:])
+ }
+ }
+ return
+}
+
+func LocalBranches() []*Branch {
+ var branches []*Branch
+ for _, s := range getLines("git", "branch", "-q") {
+ s = strings.TrimPrefix(strings.TrimSpace(s), "* ")
+ branches = append(branches, &Branch{Name: s})
+ }
+ return branches
+}
+
+func OriginBranches() []string {
+ var branches []string
+ for _, line := range getLines("git", "branch", "-a", "-q") {
+ line = strings.TrimSpace(line)
+ if i := strings.Index(line, " -> "); i >= 0 {
+ line = line[:i]
+ }
+ name := strings.TrimSpace(strings.TrimPrefix(line, "* "))
+ if strings.HasPrefix(name, "remotes/origin/") {
+ branches = append(branches, strings.TrimPrefix(name, "remotes/"))
+ }
+ }
+ return branches
+}
+
+// GerritChange returns the change metadata from the Gerrit server
+// for the branch's pending change.
+// The extra strings are passed to the Gerrit API request as o= parameters,
+// to enable additional information. Typical values include "LABELS" and "CURRENT_REVISION".
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html for details.
+func (b *Branch) GerritChange(extra ...string) (*GerritChange, error) {
+ if !b.HasPendingCommit() {
+ return nil, fmt.Errorf("no pending commit")
+ }
+ id := fullChangeID(b)
+ for i, x := range extra {
+ if i == 0 {
+ id += "?"
+ } else {
+ id += "&"
+ }
+ id += "o=" + x
+ }
+ return readGerritChange(id)
+}