diff options
| author | Andrew Gerrand <adg@golang.org> | 2014-12-18 11:25:48 +1100 |
|---|---|---|
| committer | Andrew Gerrand <adg@golang.org> | 2014-12-18 00:35:19 +0000 |
| commit | f473ce13dd1bba7ce531e7800fdf018f60aa2454 (patch) | |
| tree | fdc20fb8b555be78ca5a4f251e04de5e77d338ea /git-codereview/branch.go | |
| parent | 6a0c83f0c935e49b841a3a880579cb07918bcb57 (diff) | |
| download | go-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.go | 236 |
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) +} |
