github.com/golang/review@v0.0.0-20190122205339-266ee1edf5c3/git-codereview/branch.go (about)

     1  // Copyright 2014 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"net/url"
    11  	"os"
    12  	"os/exec"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  )
    17  
    18  // Branch describes a Git branch.
    19  type Branch struct {
    20  	Name          string    // branch name
    21  	loadedPending bool      // following fields are valid
    22  	originBranch  string    // upstream origin branch
    23  	commitsAhead  int       // number of commits ahead of origin branch
    24  	branchpoint   string    // latest commit hash shared with origin branch
    25  	pending       []*Commit // pending commits, newest first (children before parents)
    26  }
    27  
    28  // A Commit describes a single pending commit on a Git branch.
    29  type Commit struct {
    30  	Hash      string // commit hash
    31  	ShortHash string // abbreviated commit hash
    32  	Parent    string // parent hash
    33  	Merge     string // for merges, hash of commit being merged into Parent
    34  	Message   string // commit message
    35  	Subject   string // first line of commit message
    36  	ChangeID  string // Change-Id in commit message ("" if missing)
    37  
    38  	// For use by pending command.
    39  	g         *GerritChange // associated Gerrit change data
    40  	gerr      error         // error loading Gerrit data
    41  	committed []string      // list of files in this commit
    42  }
    43  
    44  // CurrentBranch returns the current branch.
    45  func CurrentBranch() *Branch {
    46  	name := strings.TrimPrefix(trim(cmdOutput("git", "rev-parse", "--abbrev-ref", "HEAD")), "heads/")
    47  	return &Branch{Name: name}
    48  }
    49  
    50  // DetachedHead reports whether branch b corresponds to a detached HEAD
    51  // (does not have a real branch name).
    52  func (b *Branch) DetachedHead() bool {
    53  	return b.Name == "HEAD"
    54  }
    55  
    56  // OriginBranch returns the name of the origin branch that branch b tracks.
    57  // The returned name is like "origin/master" or "origin/dev.garbage" or
    58  // "origin/release-branch.go1.4".
    59  func (b *Branch) OriginBranch() string {
    60  	if b.DetachedHead() {
    61  		// Detached head mode.
    62  		// "origin/HEAD" is clearly false, but it should be easy to find when it
    63  		// appears in other commands. Really any caller of OriginBranch
    64  		// should check for detached head mode.
    65  		return "origin/HEAD"
    66  	}
    67  
    68  	if b.originBranch != "" {
    69  		return b.originBranch
    70  	}
    71  	argv := []string{"git", "rev-parse", "--abbrev-ref", b.Name + "@{u}"}
    72  	cmd := exec.Command(argv[0], argv[1:]...)
    73  	if runtime.GOOS == "windows" {
    74  		// Workaround on windows. git for windows can't handle @{u} as same as
    75  		// given. Disable glob for this command if running on Cygwin or MSYS2.
    76  		envs := os.Environ()
    77  		envs = append(envs, "CYGWIN=noglob "+os.Getenv("CYGWIN"))
    78  		envs = append(envs, "MSYS=noglob "+os.Getenv("MSYS"))
    79  		cmd.Env = envs
    80  	}
    81  
    82  	out, err := cmd.CombinedOutput()
    83  	if err == nil && len(out) > 0 {
    84  		b.originBranch = string(bytes.TrimSpace(out))
    85  		return b.originBranch
    86  	}
    87  
    88  	// Have seen both "No upstream configured" and "no upstream configured".
    89  	if strings.Contains(string(out), "upstream configured") {
    90  		// Assume branch was created before we set upstream correctly.
    91  		b.originBranch = "origin/master"
    92  		return b.originBranch
    93  	}
    94  	fmt.Fprintf(stderr(), "%v\n%s\n", commandString(argv[0], argv[1:]), out)
    95  	dief("%v", err)
    96  	panic("not reached")
    97  }
    98  
    99  func (b *Branch) FullName() string {
   100  	if b.Name != "HEAD" {
   101  		return "refs/heads/" + b.Name
   102  	}
   103  	return b.Name
   104  }
   105  
   106  // IsLocalOnly reports whether b is a local work branch (only local, not known to remote server).
   107  func (b *Branch) IsLocalOnly() bool {
   108  	return "origin/"+b.Name != b.OriginBranch()
   109  }
   110  
   111  // HasPendingCommit reports whether b has any pending commits.
   112  func (b *Branch) HasPendingCommit() bool {
   113  	b.loadPending()
   114  	return b.commitsAhead > 0
   115  }
   116  
   117  // Pending returns b's pending commits, newest first (children before parents).
   118  func (b *Branch) Pending() []*Commit {
   119  	b.loadPending()
   120  	return b.pending
   121  }
   122  
   123  // Branchpoint returns an identifier for the latest revision
   124  // common to both this branch and its upstream branch.
   125  func (b *Branch) Branchpoint() string {
   126  	b.loadPending()
   127  	return b.branchpoint
   128  }
   129  
   130  func (b *Branch) loadPending() {
   131  	if b.loadedPending {
   132  		return
   133  	}
   134  	b.loadedPending = true
   135  
   136  	// In case of early return.
   137  	b.branchpoint = trim(cmdOutput("git", "rev-parse", "HEAD"))
   138  
   139  	if b.DetachedHead() {
   140  		return
   141  	}
   142  
   143  	// Note: This runs in parallel with "git fetch -q",
   144  	// so the commands may see a stale version of origin/master.
   145  	// The use of origin here is for identifying what the branch has
   146  	// in common with origin (what's old on the branch).
   147  	// Any new commits in origin do not affect that.
   148  
   149  	// Note: --topo-order means child first, then parent.
   150  	origin := b.OriginBranch()
   151  	const numField = 5
   152  	all := trim(cmdOutput("git", "log", "--topo-order", "--format=format:%H%x00%h%x00%P%x00%B%x00%s%x00", origin+".."+b.FullName(), "--"))
   153  	fields := strings.Split(all, "\x00")
   154  	if len(fields) < numField {
   155  		return // nothing pending
   156  	}
   157  	for i, field := range fields {
   158  		fields[i] = strings.TrimLeft(field, "\r\n")
   159  	}
   160  	foundMergeBranchpoint := false
   161  	for i := 0; i+numField <= len(fields); i += numField {
   162  		c := &Commit{
   163  			Hash:      fields[i],
   164  			ShortHash: fields[i+1],
   165  			Parent:    strings.TrimSpace(fields[i+2]), // %P starts with \n for some reason
   166  			Message:   fields[i+3],
   167  			Subject:   fields[i+4],
   168  		}
   169  		if j := strings.Index(c.Parent, " "); j >= 0 {
   170  			c.Parent, c.Merge = c.Parent[:j], c.Parent[j+1:]
   171  			// Found merge point.
   172  			// Merges break the invariant that the last shared commit (the branchpoint)
   173  			// is the parent of the final commit in the log output.
   174  			// If c.Parent is on the origin branch, then since we are reading the log
   175  			// in (reverse) topological order, we know that c.Parent is the actual branchpoint,
   176  			// even if we later see additional commits on a different branch leading down to
   177  			// a lower location on the same origin branch.
   178  			// Check c.Merge (the second parent) too, so we don't depend on the parent order.
   179  			if strings.Contains(cmdOutput("git", "branch", "-a", "--contains", c.Parent), " "+origin+"\n") {
   180  				foundMergeBranchpoint = true
   181  				b.branchpoint = c.Parent
   182  			}
   183  			if strings.Contains(cmdOutput("git", "branch", "-a", "--contains", c.Merge), " "+origin+"\n") {
   184  				foundMergeBranchpoint = true
   185  				b.branchpoint = c.Merge
   186  			}
   187  		}
   188  		for _, line := range lines(c.Message) {
   189  			// Note: Keep going even if we find one, so that
   190  			// we take the last Change-Id line, just in case
   191  			// there is a commit message quoting another
   192  			// commit message.
   193  			// I'm not sure this can come up at all, but just in case.
   194  			if strings.HasPrefix(line, "Change-Id: ") {
   195  				c.ChangeID = line[len("Change-Id: "):]
   196  			}
   197  		}
   198  
   199  		b.pending = append(b.pending, c)
   200  		if !foundMergeBranchpoint {
   201  			b.branchpoint = c.Parent
   202  		}
   203  	}
   204  	b.commitsAhead = len(b.pending)
   205  }
   206  
   207  // CommitsBehind reports the number of commits present upstream
   208  // that are not present in the current branch.
   209  func (b *Branch) CommitsBehind() int {
   210  	return len(lines(cmdOutput("git", "log", "--format=format:x", b.FullName()+".."+b.OriginBranch(), "--")))
   211  }
   212  
   213  // Submitted reports whether some form of b's pending commit
   214  // has been cherry picked to origin.
   215  func (b *Branch) Submitted(id string) bool {
   216  	if id == "" {
   217  		return false
   218  	}
   219  	line := "Change-Id: " + id
   220  	out := cmdOutput("git", "log", "-n", "1", "-F", "--grep", line, b.Name+".."+b.OriginBranch(), "--")
   221  	return strings.Contains(out, line)
   222  }
   223  
   224  var stagedRE = regexp.MustCompile(`^[ACDMR]  `)
   225  
   226  // HasStagedChanges reports whether the working directory contains staged changes.
   227  func HasStagedChanges() bool {
   228  	for _, s := range nonBlankLines(cmdOutput("git", "status", "-b", "--porcelain")) {
   229  		if stagedRE.MatchString(s) {
   230  			return true
   231  		}
   232  	}
   233  	return false
   234  }
   235  
   236  var unstagedRE = regexp.MustCompile(`^.[ACDMR]`)
   237  
   238  // HasUnstagedChanges reports whether the working directory contains unstaged changes.
   239  func HasUnstagedChanges() bool {
   240  	for _, s := range nonBlankLines(cmdOutput("git", "status", "-b", "--porcelain")) {
   241  		if unstagedRE.MatchString(s) {
   242  			return true
   243  		}
   244  	}
   245  	return false
   246  }
   247  
   248  // LocalChanges returns a list of files containing staged, unstaged, and untracked changes.
   249  // The elements of the returned slices are typically file names, always relative to the root,
   250  // but there are a few alternate forms. First, for renaming or copying, the element takes
   251  // the form `from -> to`. Second, in the case of files with names that contain unusual characters,
   252  // the files (or the from, to fields of a rename or copy) are quoted C strings.
   253  // For now, we expect the caller only shows these to the user, so these exceptions are okay.
   254  func LocalChanges() (staged, unstaged, untracked []string) {
   255  	for _, s := range lines(cmdOutput("git", "status", "-b", "--porcelain")) {
   256  		if len(s) < 4 || s[2] != ' ' {
   257  			continue
   258  		}
   259  		switch s[0] {
   260  		case 'A', 'C', 'D', 'M', 'R':
   261  			staged = append(staged, s[3:])
   262  		case '?':
   263  			untracked = append(untracked, s[3:])
   264  		}
   265  		switch s[1] {
   266  		case 'A', 'C', 'D', 'M', 'R':
   267  			unstaged = append(unstaged, s[3:])
   268  		}
   269  	}
   270  	return
   271  }
   272  
   273  // LocalBranches returns a list of all known local branches.
   274  // If the current directory is in detached HEAD mode, one returned
   275  // branch will have Name == "HEAD" and DetachedHead() == true.
   276  func LocalBranches() []*Branch {
   277  	var branches []*Branch
   278  	current := CurrentBranch()
   279  	for _, s := range nonBlankLines(cmdOutput("git", "branch", "-q")) {
   280  		s = strings.TrimSpace(s)
   281  		if strings.HasPrefix(s, "* ") {
   282  			// * marks current branch in output.
   283  			// Normally the current branch has a name like any other,
   284  			// but in detached HEAD mode the branch listing shows
   285  			// a localized (translated) textual description instead of
   286  			// a branch name. Avoid language-specific differences
   287  			// by using CurrentBranch().Name for the current branch.
   288  			// It detects detached HEAD mode in a more portable way.
   289  			// (git rev-parse --abbrev-ref HEAD returns 'HEAD').
   290  			s = current.Name
   291  		}
   292  		branches = append(branches, &Branch{Name: s})
   293  	}
   294  	return branches
   295  }
   296  
   297  func OriginBranches() []string {
   298  	var branches []string
   299  	for _, line := range nonBlankLines(cmdOutput("git", "branch", "-a", "-q")) {
   300  		line = strings.TrimSpace(line)
   301  		if i := strings.Index(line, " -> "); i >= 0 {
   302  			line = line[:i]
   303  		}
   304  		name := strings.TrimSpace(strings.TrimPrefix(line, "* "))
   305  		if strings.HasPrefix(name, "remotes/origin/") {
   306  			branches = append(branches, strings.TrimPrefix(name, "remotes/"))
   307  		}
   308  	}
   309  	return branches
   310  }
   311  
   312  // GerritChange returns the change metadata from the Gerrit server
   313  // for the branch's pending change.
   314  // The extra strings are passed to the Gerrit API request as o= parameters,
   315  // to enable additional information. Typical values include "LABELS" and "CURRENT_REVISION".
   316  // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html for details.
   317  func (b *Branch) GerritChange(c *Commit, extra ...string) (*GerritChange, error) {
   318  	if !b.HasPendingCommit() {
   319  		return nil, fmt.Errorf("no changes pending")
   320  	}
   321  	if c.ChangeID == "" {
   322  		return nil, fmt.Errorf("missing Change-Id")
   323  	}
   324  	id := fullChangeID(b, c)
   325  	for i, x := range extra {
   326  		if i == 0 {
   327  			id += "?"
   328  		} else {
   329  			id += "&"
   330  		}
   331  		id += "o=" + x
   332  	}
   333  	return readGerritChange(id)
   334  }
   335  
   336  // GerritChange returns the change metadata from the Gerrit server
   337  // for the given changes, which each be be the result of fullChangeID(b, c) for some c.
   338  // The extra strings are passed to the Gerrit API request as o= parameters,
   339  // to enable additional information. Typical values include "LABELS" and "CURRENT_REVISION".
   340  // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html for details.
   341  func (b *Branch) GerritChanges(ids []string, extra ...string) ([][]*GerritChange, error) {
   342  	q := ""
   343  	for _, id := range ids {
   344  		if q != "" {
   345  			q += "&"
   346  		}
   347  		if strings.HasSuffix(id, "~") {
   348  			// result of fullChangeID(b, c) with missing Change-Id; don't send
   349  			q += "q=is:closed+is:open" // cannot match anything
   350  			continue
   351  		}
   352  		q += "q=change:" + url.QueryEscape(id)
   353  	}
   354  	if q == "" {
   355  		return nil, fmt.Errorf("no changes found")
   356  	}
   357  	for _, x := range extra {
   358  		q += "&o=" + url.QueryEscape(x)
   359  	}
   360  	return readGerritChanges(q)
   361  }
   362  
   363  // CommitByRev finds a unique pending commit by its git <rev>.
   364  // It dies if rev cannot be resolved to a commit or that commit is not
   365  // pending on b using the action ("mail", "submit") in the failure message.
   366  func (b *Branch) CommitByRev(action, rev string) *Commit {
   367  	// Parse rev to a commit hash.
   368  	hash, err := cmdOutputErr("git", "rev-parse", "--verify", rev+"^{commit}")
   369  	if err != nil {
   370  		msg := strings.TrimPrefix(trim(err.Error()), "fatal: ")
   371  		dief("cannot %s: %s", action, msg)
   372  	}
   373  	hash = trim(hash)
   374  
   375  	// Check that hash is a pending commit.
   376  	var c *Commit
   377  	for _, c1 := range b.Pending() {
   378  		if c1.Hash == hash {
   379  			c = c1
   380  			break
   381  		}
   382  	}
   383  	if c == nil {
   384  		dief("cannot %s: commit hash %q not found in the current branch", action, hash)
   385  	}
   386  	return c
   387  }
   388  
   389  // DefaultCommit returns the default pending commit for this branch.
   390  // It dies if there is not exactly one pending commit,
   391  // using the action (e.g. "mail", "submit") and optional extra instructions
   392  // in the failure message.
   393  func (b *Branch) DefaultCommit(action, extra string) *Commit {
   394  	work := b.Pending()
   395  	if len(work) == 0 {
   396  		dief("cannot %s: no changes pending", action)
   397  	}
   398  	if len(work) >= 2 {
   399  		var buf bytes.Buffer
   400  		for _, c := range work {
   401  			fmt.Fprintf(&buf, "\n\t%s %s", c.ShortHash, c.Subject)
   402  		}
   403  		if extra != "" {
   404  			extra = "; " + extra
   405  		}
   406  		dief("cannot %s: multiple changes pending%s:%s", action, extra, buf.String())
   407  	}
   408  	return work[0]
   409  }
   410  
   411  // ListFiles returns the list of files in a given commit.
   412  func ListFiles(c *Commit) []string {
   413  	return nonBlankLines(cmdOutput("git", "diff", "--name-only", c.Parent, c.Hash, "--"))
   414  }
   415  
   416  func cmdBranchpoint(args []string) {
   417  	expectZeroArgs(args, "sync")
   418  	fmt.Fprintf(stdout(), "%s\n", CurrentBranch().Branchpoint())
   419  }
   420  
   421  func cmdRebaseWork(args []string) {
   422  	expectZeroArgs(args, "rebase-work")
   423  	b := CurrentBranch()
   424  	if HasStagedChanges() || HasUnstagedChanges() {
   425  		dief("cannot rebase with uncommitted work")
   426  	}
   427  	if len(b.Pending()) == 0 {
   428  		dief("no pending work")
   429  	}
   430  	run("git", "rebase", "-i", b.Branchpoint())
   431  }