blob: 054191f8dc2fd1620201194ee088dfcb34399c2a [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"
"os"
"os/exec"
"regexp"
"strings"
)
// Branch describes a Git branch.
type Branch struct {
Name string // branch name
changeID string // Change-Id of pending commit ("" if nothing pending)
subject string // first line of pending commit ("" if nothing pending)
commitHash string // commit hash of pending commit ("" if nothing pending)
}
// 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 {
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 {
return string(bytes.TrimSpace(out))
}
if strings.Contains(string(out), "No upstream configured") {
// Assume branch was created before we set upstream correctly.
return "origin/master"
}
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 {
head := getOutput("git", "rev-parse", b.Name)
base := getOutput("git", "merge-base", b.OriginBranch(), b.Name)
return base != head
}
func (b *Branch) ChangeID() string {
if b.changeID == "" {
if b.HasPendingCommit() {
b.changeID = headChangeID(b.Name)
b.subject = commitSubject(b.Name)
b.commitHash = commitHash(b.Name)
}
}
return b.changeID
}
func (b *Branch) Subject() string {
b.ChangeID() // page in subject
return b.subject
}
func (b *Branch) CommitHash() string {
b.ChangeID() // page in commit hash
return b.commitHash
}
func commitSubject(ref string) string {
return getOutput("git", "log", "-n", "1", "--format=format:%s", ref, "--")
}
func commitHash(ref string) string {
return getOutput("git", "log", "-n", "1", "--format=format:%H", ref, "--")
}
func headChangeID(branch string) string {
const p = "Change-Id: "
for _, s := range getLines("git", "log", "-n", "1", "--format=format:%b", branch, "--") {
if strings.HasPrefix(s, p) {
return strings.TrimSpace(strings.TrimPrefix(s, p))
}
}
dief("no Change-Id line found in HEAD commit on branch %s.", branch)
panic("unreachable")
}
// Submitted reports whether some form of b's pending commit
// has been cherry picked to origin.
func (b *Branch) Submitted(id string) bool {
return len(getOutput("git", "log", "--grep", "Change-Id: "+id, 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
}
func LocalBranches() []*Branch {
var branches []*Branch
for _, s := range getLines("git", "branch", "-q") {
branches = append(branches, &Branch{Name: strings.TrimPrefix(s, "* ")})
}
return branches
}
func OriginBranches() []string {
var branches []string
for _, line := range getLines("git", "branch", "-a", "-q") {
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.
func (b *Branch) GerritChange() (*GerritChange, error) {
if !b.HasPendingCommit() {
return nil, fmt.Errorf("no pending commit")
}
return readGerritChange(fullChangeID(b) + "?o=LABELS&o=CURRENT_REVISION")
}