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