github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/core/git.go (about)

     1  package core
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os/exec"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/atlassian/git-lob/util"
    15  )
    16  
    17  // Git specification of a commit or range of commits (a reference or reference range)
    18  type GitRefSpec struct {
    19  	// First ref
    20  	Ref1 string
    21  	// Optional range operator if this is a range refspec (".." or "...")
    22  	RangeOp string
    23  	// Optional second ref
    24  	Ref2 string
    25  }
    26  
    27  // Some top level information about a commit (only first line of message)
    28  type GitCommitSummary struct {
    29  	SHA            string
    30  	ShortSHA       string
    31  	Parents        []string
    32  	CommitDate     time.Time
    33  	AuthorDate     time.Time
    34  	AuthorName     string
    35  	AuthorEmail    string
    36  	CommitterName  string
    37  	CommitterEmail string
    38  	Subject        string
    39  }
    40  
    41  type GitRefType int
    42  
    43  const (
    44  	GitRefTypeLocalBranch  = GitRefType(iota)
    45  	GitRefTypeRemoteBranch = GitRefType(iota)
    46  	GitRefTypeLocalTag     = GitRefType(iota)
    47  	GitRefTypeRemoteTag    = GitRefType(iota)
    48  	GitRefTypeHEAD         = GitRefType(iota) // current checkout
    49  	GitRefTypeOther        = GitRefType(iota) // stash or unknown
    50  )
    51  
    52  // A git reference (branch, tag etc)
    53  type GitRef struct {
    54  	Name      string
    55  	Type      GitRefType
    56  	CommitSHA string
    57  }
    58  
    59  // Returns whether a GitRefSpec is a range or not
    60  func (r *GitRefSpec) IsRange() bool {
    61  	return (r.RangeOp == ".." || r.RangeOp == "...") &&
    62  		r.Ref1 != "" && r.Ref2 != ""
    63  }
    64  
    65  // Returns whether a GitRefSpec is an empty range (using the same ref for start & end)
    66  func (r *GitRefSpec) IsEmptyRange() bool {
    67  	return (r.RangeOp == ".." || r.RangeOp == "...") &&
    68  		r.Ref1 != "" && r.Ref1 == r.Ref2
    69  }
    70  
    71  func (r *GitRefSpec) String() string {
    72  	if r.IsRange() {
    73  		return fmt.Sprintf("%v%v%v", r.Ref1, r.RangeOp, r.Ref2)
    74  	} else {
    75  		return r.Ref1
    76  	}
    77  }
    78  
    79  // A record of a set of LOB shas that are associated with a commit
    80  type CommitLOBRef struct {
    81  	Commit  string
    82  	Parents []string
    83  	// Bare LOBs
    84  	LobSHAs []string
    85  	// LOBs with file names
    86  	FileLOBs []*FileLOB
    87  }
    88  
    89  func (self *CommitLOBRef) String() string {
    90  	return fmt.Sprintf("Commit: %v\n  Files:%v\n", self.Commit, self.LobSHAs)
    91  }
    92  
    93  // A filename & LOB SHA pair
    94  type FileLOB struct {
    95  	// Filename relative to repository root
    96  	Filename string
    97  	// LOB SHA
    98  	SHA string
    99  }
   100  
   101  // Convert a slice of FileLOBs to a map of lob sha to filename, eliminates duplicates
   102  func ConvertFileLOBSliceToMap(slice []*FileLOB) map[string]string {
   103  	ret := make(map[string]string, len(slice))
   104  	for _, filelob := range slice {
   105  		ret[filelob.SHA] = filelob.Filename
   106  	}
   107  	return ret
   108  }
   109  
   110  // Walk first parents starting from startSHA and call callback
   111  // First call will be startSHA & its parent
   112  // Parent will be blank string if there are no more parents & walk will stop after
   113  // Optimises internally to call Git only for batches of 50
   114  func WalkGitHistory(startSHA string, callback func(currentSHA, parentSHA string) (quit bool, err error)) error {
   115  
   116  	quit := false
   117  	currentLogHEAD := startSHA
   118  	var callbackError error
   119  	for !quit {
   120  		// get 250 parents
   121  		// format as <SHA> <PARENT> so we can detect the end of history
   122  		cmd := exec.Command("git", "log", "--first-parent", "--topo-order",
   123  			"-n", "250", "--format=%H %P", currentLogHEAD)
   124  
   125  		outp, err := cmd.StdoutPipe()
   126  		if err != nil {
   127  			return errors.New(fmt.Sprintf("Unable to list commits from %v: %v", currentLogHEAD, err.Error()))
   128  		}
   129  		cmd.Start()
   130  		scanner := bufio.NewScanner(outp)
   131  		var currentLine string
   132  		var parentSHA string
   133  		for scanner.Scan() {
   134  			currentLine = scanner.Text()
   135  			currentSHA := currentLine[:40]
   136  			// If we got here, we still haven't found an ancestor that was already marked
   137  			// check next batch, provided there's a parent on the last one
   138  			// 81 chars long, 2x40 SHAs + space
   139  			if len(currentLine) >= 81 {
   140  				parentSHA = strings.TrimSpace(currentLine[41:81])
   141  			} else {
   142  				parentSHA = ""
   143  			}
   144  			quit, callbackError = callback(currentSHA, parentSHA)
   145  			if quit {
   146  				cmd.Process.Kill()
   147  				break
   148  			}
   149  		}
   150  		cmd.Wait()
   151  		// End of history
   152  		if parentSHA == "" {
   153  			break
   154  		} else {
   155  			currentLogHEAD = parentSHA
   156  		}
   157  	}
   158  	return callbackError
   159  }
   160  
   161  // Walk forwards through a list of commits with LOB references based on refspec
   162  // If refspec is a range, walks that specific range of commits regardless of whether it's been pushed
   163  // If not, walks forwards from the oldest ancestor of refspec.Ref1 that's not pushed to the latest commit (including 'ref' if it includes LOBs)
   164  // Walks all ancestors including second+ parents, in topological order
   165  // remoteName can be a specific remote or "*" to count pushed ton *any* remote as OK
   166  // If recheck=true then existing pushed records are ignored (all commits are walked)
   167  func WalkGitCommitLOBsToPushForRefSpec(remoteName string, refspec *GitRefSpec, recheck bool, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) error {
   168  	if refspec.IsRange() {
   169  		// Walk a specific range
   170  		return walkGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, true, false, []string{}, []string{}, callback)
   171  
   172  	} else {
   173  		// Walk everything that hasn't been pushed before Ref1
   174  		return WalkGitCommitLOBsToPush(remoteName, refspec.Ref1, recheck, callback)
   175  	}
   176  }
   177  
   178  // Walk a list of commits with LOB references which are ancestors of 'ref' which have not been pushed
   179  // Walks forwards from the oldest commit to the latest commit (including 'ref' if it includes LOBs)
   180  // Walks all ancestors including second+ parents, in topological order
   181  // remoteName can be a specific remote or "*" to count pushed ton *any* remote as OK
   182  func WalkGitCommitLOBsToPush(remoteName, ref string, recheck bool, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) error {
   183  	// We use git's ability to log all new commits up to ref but exclude any ancestors of pushed
   184  	var pushedSHAs []string
   185  	// If rechecking, then we just log the whole thing
   186  	if !recheck {
   187  		pushedSHAs = GetPushedCommits(remoteName)
   188  	}
   189  	// Loop to allow retry
   190  	for {
   191  		args := []string{"log", `--format=commitsha: %H %P`, "-p",
   192  			"--topo-order",
   193  			"--reverse",
   194  			"-G", SHALineRegexStr,
   195  			ref}
   196  
   197  		for _, p := range pushedSHAs {
   198  			// 'not reachable from pushed commits'
   199  			args = append(args, fmt.Sprintf("^%v", p))
   200  		}
   201  
   202  		// format as <SHA> <PARENT> so we progressively work backward
   203  		cmd := exec.Command("git", args...)
   204  
   205  		outp, err := cmd.StdoutPipe()
   206  		if err != nil {
   207  			return errors.New(fmt.Sprintf("Unable to list commits from %v: %v", ref, err.Error()))
   208  		}
   209  		cmd.Start()
   210  
   211  		quit, err := walkGitLogOutputForLOBReferences(outp, true, false, []string{}, []string{}, callback)
   212  
   213  		if quit || err != nil {
   214  			// Early abort
   215  			cmd.Process.Kill()
   216  		}
   217  
   218  		procerr := cmd.Wait()
   219  		if procerr != nil {
   220  			if len(pushedSHAs) > 0 {
   221  				// This can happen because one of the pushedSHAs has been completely removed from the repo
   222  				// consolidate SHAs and try again, this deletes any non-existent SHAs
   223  				consolidated := consolidateCommitsToLatestDescendants(pushedSHAs)
   224  				if len(consolidated) != len(pushedSHAs) {
   225  					// Store the refined state
   226  					WritePushedState(remoteName, consolidated)
   227  					pushedSHAs = consolidated
   228  					// retry
   229  					continue
   230  				}
   231  			}
   232  		}
   233  
   234  		return err
   235  
   236  	}
   237  }
   238  
   239  // Internal utility for walking git-log output for git-lob references & calling callback
   240  // Log output must be formated like this: `--format=commitsha: %H %P`
   241  // outp must be output from a running git log task
   242  func walkGitLogOutputForLOBReferences(outp io.Reader, additions, removals bool,
   243  	includePaths, excludePaths []string, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) (quit bool, err error) {
   244  	// Sadly we still get more output than we actually need, but this is the minimum we can get
   245  	// For each commit we'll get something like this:
   246  	/*
   247  	   commitsha: af2607421c9fee2e430cde7e7073a7dad07be559 22be911a626eb9cf2e2760b1b8b092441771cb9d
   248  
   249  	   diff --git a/atheneNormalMap.png b/atheneNormalMap.png
   250  	   new file mode 100644
   251  	   index 0000000..272b5c1
   252  	   --- /dev/null
   253  	   +++ b/atheneNormalMap.png
   254  	   @@ -0,0 +1 @@
   255  	   +git-lob: b022770eab414c36575290c993c29799bc6610c3
   256  	*/
   257  	// There can be multiple diffs per commit (multiple binaries)
   258  	// Also when a binary is changed the diff will include a '-' line for the old SHA
   259  	// Depending on which direction in history the caller wants, they'll specify the
   260  	// parameters 'additions' and 'removals' to determine which get included
   261  
   262  	// Use 1 regex to capture all for speed
   263  	var lobregex *regexp.Regexp
   264  	if additions && !removals {
   265  		lobregex = regexp.MustCompile(`^\+git-lob: ([A-Fa-f0-9]{40})`)
   266  	} else if removals && !additions {
   267  		lobregex = regexp.MustCompile(`^\-git-lob: ([A-Fa-f0-9]{40})`)
   268  	} else {
   269  		lobregex = regexp.MustCompile(`^[\+\-]git-lob: ([A-Fa-f0-9]{40})`)
   270  	}
   271  	fileHeaderRegex := regexp.MustCompile(`diff --git a\/(.+?)\s+b\/(.+)`)
   272  	fileMergeHeaderRegex := regexp.MustCompile(`diff --cc (.+)`)
   273  	commitHeaderRegex := regexp.MustCompile(`^commitsha: ([A-Fa-f0-9]{40})(?: ([A-Fa-f0-9]{40}))*`)
   274  
   275  	scanner := bufio.NewScanner(outp)
   276  
   277  	var currentCommit *CommitLOBRef
   278  	var currentFilename string
   279  	currentFileIncluded := true
   280  	for scanner.Scan() {
   281  		line := scanner.Text()
   282  		if match := commitHeaderRegex.FindStringSubmatch(line); match != nil {
   283  			// Commit header
   284  			sha := match[1]
   285  			parentSHAs := match[2:]
   286  			// Set commit context
   287  			if currentCommit != nil {
   288  				if len(currentCommit.LobSHAs) > 0 {
   289  					quit, err := callback(currentCommit)
   290  					if err != nil {
   291  						return quit, err
   292  					} else if quit {
   293  						return true, nil
   294  					}
   295  				}
   296  				currentCommit = nil
   297  			}
   298  			currentCommit = &CommitLOBRef{Commit: sha, Parents: parentSHAs}
   299  		} else if match := fileHeaderRegex.FindStringSubmatch(line); match != nil {
   300  			// Finding a regular file header
   301  			// Pertinent file name depends on whether we're listening to additions or removals
   302  			if additions {
   303  				currentFilename = match[2]
   304  			} else {
   305  				currentFilename = match[1]
   306  			}
   307  			currentFileIncluded = util.FilenamePassesIncludeExcludeFilter(currentFilename, includePaths, excludePaths)
   308  		} else if match := fileMergeHeaderRegex.FindStringSubmatch(line); match != nil {
   309  			// Git merge file header is a little different, only one file
   310  			currentFilename = match[1]
   311  			currentFileIncluded = util.FilenamePassesIncludeExcludeFilter(currentFilename, includePaths, excludePaths)
   312  		} else if match := lobregex.FindStringSubmatch(line); match != nil {
   313  			// This is a LOB reference (+/- already matched in variant of regex)
   314  			sha := match[1]
   315  			// Use filename context to include/exclude if paths were used
   316  			if currentFileIncluded {
   317  				currentCommit.LobSHAs = append(currentCommit.LobSHAs, sha)
   318  				currentCommit.FileLOBs = append(currentCommit.FileLOBs, &FileLOB{Filename: currentFilename, SHA: sha})
   319  			}
   320  		}
   321  	}
   322  	// Final commit
   323  	if currentCommit != nil {
   324  		if len(currentCommit.LobSHAs) > 0 {
   325  			quit, err := callback(currentCommit)
   326  			if err != nil {
   327  				return quit, err
   328  			} else if quit {
   329  				return true, nil
   330  			}
   331  		}
   332  		currentCommit = nil
   333  	}
   334  
   335  	return false, nil
   336  }
   337  
   338  // Gets the default push remote for the working dir
   339  // Determined from branch.*.remote configuration for the
   340  // current branch if present, or defaults to origin.
   341  func GetGitDefaultRemoteForPush() string {
   342  
   343  	remote, ok := util.GlobalOptions.GitConfig[fmt.Sprintf("branch.%v.remote", GetGitCurrentBranch())]
   344  	if ok {
   345  		return remote
   346  	}
   347  	return "origin"
   348  
   349  }
   350  
   351  // Gets the default fetch remote for the working dir
   352  // Determined from tracking state of current branch
   353  // if present, or defaults to origin.
   354  func GetGitDefaultRemoteForPull() string {
   355  
   356  	remoteName, _ := GetGitUpstreamBranch(GetGitCurrentBranch())
   357  	if remoteName != "" {
   358  		return remoteName
   359  	}
   360  	return "origin"
   361  }
   362  
   363  // Get a list of git remotes
   364  func GetGitRemotes() ([]string, error) {
   365  	cmd := exec.Command("git", "remote")
   366  	outp, err := cmd.StdoutPipe()
   367  	if err != nil {
   368  		return []string{}, fmt.Errorf("Error calling 'git remote': %v", err.Error())
   369  	}
   370  	scanner := bufio.NewScanner(outp)
   371  	cmd.Start()
   372  	var ret []string
   373  	for scanner.Scan() {
   374  		ret = append(ret, scanner.Text())
   375  	}
   376  	cmd.Wait()
   377  	return ret, nil
   378  
   379  }
   380  
   381  func IsGitRemote(remoteName string) bool {
   382  	remotes, err := GetGitRemotes()
   383  	if err != nil {
   384  		return false
   385  	}
   386  	sort.Strings(remotes)
   387  	ret, _ := util.StringBinarySearch(remotes, remoteName)
   388  	return ret
   389  }
   390  
   391  var cachedCurrentBranch string
   392  
   393  // Get the name of the current branch
   394  func GetGitCurrentBranch() string {
   395  	// Use cache, we never switch branches ourselves within lifetime so save some
   396  	// repeat calls if queried more than once
   397  	if cachedCurrentBranch == "" {
   398  		cmd := exec.Command("git", "branch")
   399  
   400  		outp, err := cmd.StdoutPipe()
   401  		if err != nil {
   402  			util.LogErrorf("Unable to get current branch: %v", err.Error())
   403  			return ""
   404  		}
   405  		cmd.Start()
   406  		scanner := bufio.NewScanner(outp)
   407  		found := false
   408  		for scanner.Scan() {
   409  			line := scanner.Text()
   410  
   411  			if line[0] == '*' {
   412  				cachedCurrentBranch = line[2:]
   413  				found = true
   414  				break
   415  			}
   416  		}
   417  		cmd.Wait()
   418  
   419  		// There's a special case in a newly initialised repository where 'git branch' returns nothing at all
   420  		// In this case the branch really is 'master'
   421  		if !found {
   422  			cachedCurrentBranch = "master"
   423  		}
   424  	}
   425  
   426  	return cachedCurrentBranch
   427  
   428  }
   429  
   430  // Parse a single git refspec string into a GitRefSpec structure ie identify ranges if present
   431  // Does not perform any validation since refs can be symbolic anyway, up to the caller
   432  // to check whether the returned refspec actually works
   433  func ParseGitRefSpec(s string) *GitRefSpec {
   434  
   435  	if idx := strings.Index(s, "..."); idx != -1 {
   436  		// reachable from ref1 OR ref2, not both
   437  		ref1 := strings.TrimSpace(s[:idx])
   438  		ref2 := strings.TrimSpace(s[idx+3:])
   439  		return &GitRefSpec{ref1, "...", ref2}
   440  	} else if idx := strings.Index(s, ".."); idx != -1 {
   441  		// range from ref1 -> ref2
   442  		ref1 := strings.TrimSpace(s[:idx])
   443  		ref2 := strings.TrimSpace(s[idx+2:])
   444  		return &GitRefSpec{ref1, "..", ref2}
   445  	} else {
   446  		ref1 := strings.TrimSpace(s)
   447  		return &GitRefSpec{Ref1: ref1}
   448  	}
   449  
   450  }
   451  
   452  var IsSHARegex *regexp.Regexp = regexp.MustCompile("^[0-9A-Fa-f]{8,40}$")
   453  
   454  // Return whether a single git reference (not refspec, so no ranges) is a full SHA or not
   455  // SHAs can be used directly for things like lob lookup but other refs have too be converted
   456  // This version requires a full length SHA (40 characters)
   457  func GitRefIsFullSHA(ref string) bool {
   458  	return len(ref) == 40 && IsSHARegex.MatchString(ref)
   459  }
   460  
   461  // Return whether a single git reference (not refspec, so no ranges) is a SHA or not
   462  // SHAs can be used directly for things like lob lookup but other refs have too be converted
   463  // This version accepts SHAs that are 8-40 characters in length, so accepts short SHAs
   464  func GitRefIsSHA(ref string) bool {
   465  	return IsSHARegex.MatchString(ref)
   466  }
   467  
   468  func GitRefToFullSHA(ref string) (string, error) {
   469  	if GitRefIsFullSHA(ref) {
   470  		return ref, nil
   471  	}
   472  	// Otherwise use Git to expand to full 40 character SHA
   473  	cmd := exec.Command("git", "rev-parse", ref)
   474  	outp, err := cmd.Output()
   475  	if err != nil {
   476  		return ref, fmt.Errorf("Unknown or ambiguous ref %v", ref)
   477  	}
   478  	return strings.TrimSpace(string(outp)), nil
   479  }
   480  
   481  // Returns whether a ref or SHA refers to a valid, existing commit or not by asking git to resolve it
   482  func GitRefOrSHAIsValid(refOrSHA string) bool {
   483  	// --verify doesn't actually verify commit object is valid, will return OK if it's just any 40-char SHA
   484  	// Need to use <sha>^{commit} to verify it's a commit
   485  	err := exec.Command("git", "rev-parse", "--verify",
   486  		fmt.Sprintf("%v^{commit}", refOrSHA)).Run()
   487  	return err == nil
   488  }
   489  
   490  // Return a list of all local branches
   491  // Also FYI caches the current branch while we're at it so it's zero-cost to call
   492  // GetGitCurrentBranch after this
   493  func GetGitLocalBranches() ([]string, error) {
   494  	cmd := exec.Command("git", "branch")
   495  
   496  	outp, err := cmd.StdoutPipe()
   497  	if err != nil {
   498  		return []string{}, errors.New(fmt.Sprintf("Unable to get list local branches: %v", err.Error()))
   499  	}
   500  	cmd.Start()
   501  	scanner := bufio.NewScanner(outp)
   502  	foundcurrent := cachedCurrentBranch != ""
   503  	var ret []string
   504  	for scanner.Scan() {
   505  		line := scanner.Text()
   506  		if len(line) > 2 {
   507  			branch := line[2:]
   508  			ret = append(ret, branch)
   509  			// While we're at it, cache current branch
   510  			if !foundcurrent && line[0] == '*' {
   511  				cachedCurrentBranch = branch
   512  				foundcurrent = true
   513  			}
   514  
   515  		}
   516  
   517  	}
   518  	cmd.Wait()
   519  
   520  	return ret, nil
   521  
   522  }
   523  
   524  // Return a list of all remote branches for a given remote
   525  // Note this doesn't retrieve mappings between local and remote branches, just a simple list
   526  func GetGitRemoteBranches(remoteName string) ([]string, error) {
   527  	cmd := exec.Command("git", "branch", "-r")
   528  
   529  	outp, err := cmd.StdoutPipe()
   530  	if err != nil {
   531  		return []string{}, errors.New(fmt.Sprintf("Unable to get list remote branches: %v", err.Error()))
   532  	}
   533  	cmd.Start()
   534  	scanner := bufio.NewScanner(outp)
   535  	var ret []string
   536  	prefix := remoteName + "/"
   537  	for scanner.Scan() {
   538  		line := scanner.Text()
   539  		if len(line) > 2 {
   540  			line := line[2:]
   541  			if strings.HasPrefix(line, prefix) {
   542  				// Make sure we terminate at space, line may include alias
   543  				remotebranch := strings.Fields(line[len(prefix):])[0]
   544  				if remotebranch != "HEAD" {
   545  					ret = append(ret, remotebranch)
   546  				}
   547  			}
   548  		}
   549  
   550  	}
   551  	cmd.Wait()
   552  
   553  	return ret, nil
   554  
   555  }
   556  
   557  // Return a list of branches to push by default, based on push.default and local/remote branches
   558  // See push.default docs at https://www.kernel.org/pub/software/scm/git/docs/git-config.html
   559  func GetGitPushDefaultBranches(remoteName string) []string {
   560  	pushdef := util.GlobalOptions.GitConfig["push.default"]
   561  	if pushdef == "" {
   562  		// Use the git 2.0 'simple' default
   563  		pushdef = "simple"
   564  	}
   565  
   566  	if pushdef == "matching" {
   567  		// Multiple branches, but only where remote branch name matches
   568  		localbranches, err := GetGitLocalBranches()
   569  		if err != nil {
   570  			// will be logged, safe return
   571  			return []string{}
   572  		}
   573  		remotebranches, err := GetGitRemoteBranches(remoteName)
   574  		if err != nil {
   575  			// will be logged, safe return
   576  			return []string{}
   577  		}
   578  		// Probably sorted already but to be sure
   579  		sort.Strings(remotebranches)
   580  		var ret []string
   581  		for _, branch := range localbranches {
   582  			present, _ := util.StringBinarySearch(remotebranches, branch)
   583  
   584  			if present {
   585  				ret = append(ret, branch)
   586  			}
   587  		}
   588  		return ret
   589  	} else if pushdef == "current" || pushdef == "upstream" || pushdef == "simple" {
   590  		// Current, upstream, simple (in ascending complexity)
   591  		currentBranch := GetGitCurrentBranch()
   592  		if pushdef == "current" {
   593  			return []string{currentBranch}
   594  		}
   595  		// For upstream & simple we need to know what the upstream branch is
   596  		upstreamRemote, upstreamBranch := GetGitUpstreamBranch(currentBranch)
   597  		// Only proceed if the upstream is on this remote
   598  		if upstreamRemote == remoteName && upstreamBranch != "" {
   599  			if pushdef == "upstream" {
   600  				// For upstream we don't care what the remote branch is called
   601  				return []string{currentBranch}
   602  			} else {
   603  				// "simple"
   604  				// In this case git would only push if remote branch matches as well
   605  				if upstreamBranch == currentBranch {
   606  					return []string{currentBranch}
   607  				}
   608  			}
   609  		}
   610  	}
   611  
   612  	// "nothing", something we don't understand (safety), or fallthrough non-matched
   613  	return []string{}
   614  
   615  }
   616  
   617  // Get the upstream branch for a given local branch, as defined in what 'git pull' would do by default
   618  // returns the remote name and the remote branch separately for ease of use
   619  func GetGitUpstreamBranch(localbranch string) (remoteName, remoteBranch string) {
   620  	// Super-verbose mode gives us tracking branch info
   621  	cmd := exec.Command("git", "branch", "-vv")
   622  
   623  	outp, err := cmd.StdoutPipe()
   624  	if err != nil {
   625  		util.LogErrorf("Unable to get list branches: %v", err.Error())
   626  		return "", ""
   627  	}
   628  	cmd.Start()
   629  	scanner := bufio.NewScanner(outp)
   630  
   631  	// Output is like this:
   632  	//   branch1              387def9 [origin/branch1] Another new branch
   633  	// * master               aec3297 [origin/master: behind 1] Master change
   634  	// * feature1             e88c156 [origin/feature1: ahead 4, behind 6] Something something dark side
   635  	//   nottrackingbranch    f33e451 Some message
   636  
   637  	// Extract branch name and tracking branch (won't match branches with no tracking)
   638  	// Stops at ']' or ':' in tracking branch to deal with ahead/behind markers
   639  	trackRegex := regexp.MustCompile(`^[* ] (\S+)\s+[a-fA-F0-9]+\s+\[([^/]+)/([^\:]+)[\]:]`)
   640  
   641  	for scanner.Scan() {
   642  		line := scanner.Text()
   643  		if match := trackRegex.FindStringSubmatch(line); match != nil {
   644  			lbranch := match[1]
   645  			if lbranch == localbranch {
   646  				return match[2], match[3]
   647  			}
   648  		}
   649  
   650  	}
   651  	cmd.Wait()
   652  
   653  	// no tracking for this branch
   654  	return "", ""
   655  
   656  }
   657  
   658  // Returns list of commits which have LOB SHAs referenced in them, in a given commit range
   659  // Commits will be in ASCENDING order (parents before children) unlike WalkGitHistory
   660  // Either of from, to or both can be blank to have an unbounded range of commits based on current HEAD
   661  // It is required that if both are supplied, 'from' is an ancestor of 'to'
   662  // Range is exclusive of 'from' and inclusive of 'to'
   663  func GetGitCommitsReferencingLOBsInRange(from, to string, includePaths, excludePaths []string) ([]*CommitLOBRef, error) {
   664  	// We want '+' lines
   665  	return getGitCommitsReferencingLOBsInRange(from, to, true, false, includePaths, excludePaths)
   666  }
   667  
   668  // Returns list of commits which have LOB SHAs referenced in them, in a given commit range
   669  // Range is exclusive of 'from' and inclusive of 'to'
   670  // additions/removals controls whether we report only diffs with '+' lines of git-lob, '-' lines, or both
   671  func getGitCommitsReferencingLOBsInRange(from, to string, additions, removals bool, includePaths, excludePaths []string) ([]*CommitLOBRef, error) {
   672  	var ret []*CommitLOBRef
   673  	callback := func(commit *CommitLOBRef) (quit bool, err error) {
   674  		ret = append(ret, commit)
   675  		return false, nil
   676  	}
   677  	err := walkGitCommitsReferencingLOBsInRange(from, to, additions, removals, includePaths, excludePaths, callback)
   678  	return ret, err
   679  }
   680  
   681  // Walks a list of commits in ascending order which have LOB SHAs referenced in them, in a given commit range
   682  // Range is exclusive of 'from' and inclusive of 'to'
   683  // additions/removals controls whether we report only diffs with '+' lines of git-lob, '-' lines, or both
   684  func walkGitCommitsReferencingLOBsInRange(from, to string, additions, removals bool, includePaths, excludePaths []string,
   685  	callback func(commit *CommitLOBRef) (quit bool, err error)) error {
   686  
   687  	args := []string{"log", `--format=commitsha: %H %P`, "-p",
   688  		"--topo-order", "--first-parent",
   689  		"--reverse", // we want to list them in ascending order
   690  		"-G", SHALineRegexStr}
   691  
   692  	if from != "" && to != "" {
   693  		args = append(args, fmt.Sprintf("%v..%v", from, to))
   694  	} else {
   695  		if to != "" {
   696  			args = append(args, to)
   697  		} else if from != "" {
   698  			args = append(args, fmt.Sprintf("%v..HEAD", from))
   699  		}
   700  		// if from & to are both blank, just use default behaviour of git log
   701  	}
   702  
   703  	cmd := exec.Command("git", args...)
   704  	outp, err := cmd.StdoutPipe()
   705  	if err != nil {
   706  		return errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error()))
   707  	}
   708  	cmd.Start()
   709  
   710  	_, err = walkGitLogOutputForLOBReferences(outp, additions, removals, includePaths, excludePaths, callback)
   711  
   712  	cmd.Wait()
   713  
   714  	return err
   715  
   716  }
   717  
   718  // Gets a list of LOB SHAs for all binary files that are needed when checking out any of
   719  // the commits referred to by refspec.
   720  // As opposed to GetGitCommitsReferencingLOBsInRange which only picks up changes to LOBs,
   721  // this function returns the complete set of LOBs needed if you checked out a commit either at
   722  // a single commit, or any in a range (if the refspec is a range; only .. range operator allowed)
   723  // This means it will include any LOBs that were added in commits before the range, if they are still used,
   724  // while GetGitCommitsReferencingLOBsInRange wouldn't mention those.
   725  // Note that git ranges are start AND end inclusive in this case.
   726  // Note that duplicate SHAs are not eliminated for efficiency, you must do it if you need it
   727  func GetGitAllLOBsToCheckoutInRefSpec(refspec *GitRefSpec, includePaths, excludePaths []string) ([]string, error) {
   728  
   729  	var snapshotref string
   730  	if refspec.IsRange() {
   731  		if refspec.RangeOp != ".." {
   732  			return []string{}, errors.New("Only '..' range operator allowed in GetGitAllLOBsToCheckoutInRefSpec")
   733  		}
   734  		// snapshot at end of range, then look at diffs later
   735  		snapshotref = refspec.Ref2
   736  	} else {
   737  		snapshotref = refspec.Ref1
   738  	}
   739  
   740  	ret, err := GetGitAllLOBsToCheckoutAtCommit(snapshotref, includePaths, excludePaths)
   741  	if err != nil {
   742  		return ret, err
   743  	}
   744  
   745  	if refspec.IsRange() {
   746  		// Now we have all LOBs at the snapshot, find any extra ones earlier in the range
   747  		// to do this, we look for diffs in the commit range that start with "-git-lob:"
   748  		// because a removal means it was referenced before that commit therefore we need it
   749  		// to go back to that state
   750  		// git log is range start exclusive, but that's actually OK since a -git-lob diff line
   751  		// represents the state one commit earlier, giving us an inclusive start range
   752  		commits, err := getGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, false, true, includePaths, excludePaths)
   753  		if err != nil {
   754  			return ret, err
   755  		}
   756  		for _, commit := range commits {
   757  			// possible to end up with duplicates here if same SHA referenced more than once
   758  			// caller to resolve if they need uniques
   759  			ret = append(ret, commit.LobSHAs...)
   760  		}
   761  
   762  	}
   763  
   764  	return ret, nil
   765  
   766  }
   767  
   768  // Gets a list of LOB SHAs with their filenames for all binary files that are needed when checking out any of
   769  // the commits referred to by refspec.
   770  // As opposed to GetGitCommitsReferencingLOBsInRange which only picks up changes to LOBs,
   771  // this function returns the complete set of LOBs needed if you checked out a commit either at
   772  // a single commit, or any in a range (if the refspec is a range; only .. range operator allowed)
   773  // This means it will include any LOBs that were added in commits before the range, if they are still used,
   774  // while GetGitCommitsReferencingLOBsInRange wouldn't mention those.
   775  // Note that git ranges are start AND end inclusive in this case.
   776  // Note that duplicate SHAs are not eliminated for efficiency, you must do it if you need it
   777  func GetGitAllFilesAndLOBsToCheckoutInRefSpec(refspec *GitRefSpec, includePaths, excludePaths []string) ([]*FileLOB, error) {
   778  
   779  	var snapshotref string
   780  	if refspec.IsRange() {
   781  		if refspec.RangeOp != ".." {
   782  			return nil, errors.New("Only '..' range operator allowed in GetGitAllLOBsToCheckoutInRefSpec")
   783  		}
   784  		// snapshot at end of range, then look at diffs later
   785  		snapshotref = refspec.Ref2
   786  	} else {
   787  		snapshotref = refspec.Ref1
   788  	}
   789  
   790  	ret, err := GetGitAllFilesAndLOBsToCheckoutAtCommit(snapshotref, includePaths, excludePaths)
   791  	if err != nil {
   792  		return ret, err
   793  	}
   794  
   795  	if refspec.IsRange() {
   796  		// Now we have all LOBs at the snapshot, find any extra ones earlier in the range
   797  		// to do this, we look for diffs in the commit range that start with "-git-lob:"
   798  		// because a removal means it was referenced before that commit therefore we need it
   799  		// to go back to that state
   800  		// git log is range start exclusive, but that's actually OK since a -git-lob diff line
   801  		// represents the state one commit earlier, giving us an inclusive start range
   802  		commits, err := getGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, false, true, includePaths, excludePaths)
   803  		if err != nil {
   804  			return ret, err
   805  		}
   806  		for _, commit := range commits {
   807  			// possible to end up with duplicates here if same SHA referenced more than once
   808  			// caller to resolve if they need uniques
   809  			ret = append(ret, commit.FileLOBs...)
   810  		}
   811  
   812  	}
   813  
   814  	return ret, nil
   815  
   816  }
   817  
   818  // Get all the LOB SHAs that you would need to have available to check out a commit, and any other
   819  // ancestor of it within a number of days of that commit date (not today's date)
   820  // Note that if a LOB was modified to the same SHA more than once, duplicates may appear in the return
   821  // They are not routinely eliminated for performance, so perform your own dupe removal if you need it
   822  // as well as a list of LOBs, returns the commit SHA of the earliest change that was included in the scan.
   823  // Since this is the first *change* included (which would be removing the previous SHA), the earliest LOB
   824  // SHA included is from the *parent* of this commit.
   825  func GetGitAllLOBsToCheckoutAtCommitAndRecent(commit string, days int, includePaths,
   826  	excludePaths []string) (lobs []string, earliestChangeCommit string, reterr error) {
   827  	// All LOBs at the commit itself
   828  	shasAtCommit, err := GetGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths)
   829  	if err != nil {
   830  		return nil, "", err
   831  	}
   832  
   833  	// days == 0 means we only snapshot latest
   834  	if days == 0 {
   835  		earliest := commit
   836  		if !GitRefIsFullSHA(earliest) {
   837  			earliest, _ = GitRefToFullSHA(earliest)
   838  		}
   839  		return shasAtCommit, earliest, nil
   840  	} else {
   841  		ret := shasAtCommit
   842  		earliestCommit := commit
   843  		callback := func(lobcommit *CommitLOBRef) (quit bool, err error) {
   844  			ret = append(ret, lobcommit.LobSHAs...)
   845  			earliestCommit = lobcommit.Commit
   846  			return false, nil
   847  		}
   848  		err := walkGitAllLOBsInRecentCommits(commit, days, includePaths, excludePaths, callback)
   849  
   850  		return ret, earliestCommit, err
   851  	}
   852  
   853  }
   854  
   855  // Get all the Filenames & LOB SHAs that you would need to have available to check out a commit, and any other
   856  // ancestor of it within a number of days of that commit date (not today's date)
   857  // Note that if a LOB was modified to the same SHA more than once, duplicates may appear in the return
   858  // They are not routinely eliminated for performance, so perform your own dupe removal if you need it
   859  // as well as a list of LOBs, returns the commit SHA of the earliest change that was included in the scan.
   860  // Since this is the first *change* included (which would be removing the previous SHA), the earliest LOB
   861  // SHA included is from the *parent* of this commit.
   862  func GetGitAllFileLOBsToCheckoutAtCommitAndRecent(commit string, days int, includePaths,
   863  	excludePaths []string) (filelobs []*FileLOB, earliestChangeCommit string, reterr error) {
   864  	// All LOBs at the commit itself
   865  	fileshasAtCommit, err := GetGitAllFilesAndLOBsToCheckoutAtCommit(commit, includePaths, excludePaths)
   866  	if err != nil {
   867  		return nil, "", err
   868  	}
   869  
   870  	// days == 0 means we only snapshot latest
   871  	if days == 0 {
   872  		earliest := commit
   873  		if !GitRefIsFullSHA(earliest) {
   874  			earliest, _ = GitRefToFullSHA(earliest)
   875  		}
   876  		return fileshasAtCommit, earliest, nil
   877  	} else {
   878  		ret := fileshasAtCommit
   879  		earliestCommit := commit
   880  		callback := func(lobcommit *CommitLOBRef) (quit bool, err error) {
   881  			ret = append(ret, lobcommit.FileLOBs...)
   882  			earliestCommit = lobcommit.Commit
   883  			return false, nil
   884  		}
   885  		err := walkGitAllLOBsInRecentCommits(commit, days, includePaths, excludePaths, callback)
   886  
   887  		return ret, earliestCommit, err
   888  	}
   889  
   890  }
   891  
   892  // Walk backwards in history looking for all ancestors and references to LOBs in the '-' side of the diff
   893  func walkGitAllLOBsInRecentCommits(startcommit string, days int, includePaths, excludePaths []string,
   894  	callback func(lobcommit *CommitLOBRef) (quit bool, err error)) error {
   895  	// get the commit date
   896  	commitDetails, err := GetGitCommitSummary(startcommit)
   897  	if err != nil {
   898  		return err
   899  	}
   900  	sinceDate := commitDetails.CommitDate.AddDate(0, 0, -days)
   901  	// Now use git log to scan backwards
   902  	// We use git log from commit backwards, not commit^ (parent) because
   903  	// we're looking for *previous* SHAs, which means we're looking for diffs
   904  	// with a '-' line. So SHAs replaced in the latest commit are old versions too
   905  	// that we haven't included yet in fileshasAtCommit
   906  	args := []string{"log", `--format=commitsha: %H %P`, "-p",
   907  		fmt.Sprintf("--since=%v", FormatGitDate(sinceDate)),
   908  		"-G", SHALineRegexStr,
   909  		startcommit}
   910  
   911  	cmd := exec.Command("git", args...)
   912  	outp, err := cmd.StdoutPipe()
   913  	if err != nil {
   914  		return errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error()))
   915  	}
   916  	cmd.Start()
   917  
   918  	// Looking backwards, so removals
   919  	walkGitLogOutputForLOBReferences(outp, false, true, includePaths, excludePaths, callback)
   920  
   921  	cmd.Wait()
   922  
   923  	return nil
   924  }
   925  
   926  // Return a slice of LOB SHAs representing versions of filename, ordered by latest first
   927  // history is from all heads not just checked out
   928  // if shatoskip is supplied, this sha is excluded from the return if found
   929  func GetGitAllLOBHistoryForFile(filename, shatoskip string) ([]string, error) {
   930  
   931  	// Scan ALL history for this filename that includes a git-lob marker
   932  	// not just history from checked out
   933  	args := []string{"log", `--format=commitsha: %H %P`, "-p",
   934  		"--all", "--topo-order", // ALL history in reverse order
   935  		"-G", SHALineRegexStr,
   936  		"--", filename}
   937  
   938  	cmd := exec.Command("git", args...)
   939  	outp, err := cmd.StdoutPipe()
   940  	if err != nil {
   941  		return nil, errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error()))
   942  	}
   943  	cmd.Start()
   944  
   945  	// We'll just look for additions ever, walking backwards
   946  	var ret []string
   947  	callback := func(commitLOB *CommitLOBRef) (quit bool, err error) {
   948  		// Already filtered by filename so there can only be one entry, but be sure
   949  		if len(commitLOB.FileLOBs) == 1 {
   950  			sha := commitLOB.FileLOBs[0].SHA
   951  			if sha != shatoskip {
   952  				ret = append(ret, sha)
   953  			}
   954  		}
   955  		return false, nil
   956  	}
   957  	walkGitLogOutputForLOBReferences(outp, true, false, nil, nil, callback)
   958  
   959  	cmd.Wait()
   960  
   961  	return ret, nil
   962  
   963  }
   964  
   965  // Get all the binary files & their LOB SHAs that you would need to check out at a given commit (not changed in that commit)
   966  func GetGitAllFilesAndLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string) ([]*FileLOB, error) {
   967  	var ret []*FileLOB
   968  	err := WalkGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths, func(filelob *FileLOB) {
   969  		ret = append(ret, filelob)
   970  	})
   971  	return ret, err
   972  }
   973  
   974  // Get all the LOB SHAs that you would need to check out at a given commit (not changed in that commit)
   975  func GetGitAllLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string) ([]string, error) {
   976  	var ret []string
   977  	err := WalkGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths, func(filelob *FileLOB) {
   978  		ret = append(ret, filelob.SHA)
   979  	})
   980  	return ret, err
   981  }
   982  
   983  // Utility function to walk through all the LOBs which are present if checked out at a specific commit
   984  func WalkGitAllLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string,
   985  	callback func(filelob *FileLOB)) error {
   986  
   987  	// Snapshot using ls-tree
   988  	args := []string{"ls-tree",
   989  		"-r",          // recurse
   990  		"-l",          // report object size (we'll need this)
   991  		"--full-tree", // start at the root regardless of where we are in it
   992  		commit}
   993  
   994  	lstreecmd := exec.Command("git", args...)
   995  	outp, err := lstreecmd.StdoutPipe()
   996  	if err != nil {
   997  		return errors.New(fmt.Sprintf("Unable to call git ls-tree: %v", err.Error()))
   998  	}
   999  	defer outp.Close()
  1000  	lstreecmd.Start()
  1001  	lstreescanner := bufio.NewScanner(outp)
  1002  
  1003  	// We will look for objects that are *exactly* the size of the git-lob line
  1004  	regex := regexp.MustCompile(fmt.Sprintf(`^\d+\s+blob\s+([0-9a-zA-Z]{40})\s+%d\s+(.*)$`, SHALineLen))
  1005  	// This will give us object SHAs of content which is exactly the right size, we must
  1006  	// then use cat-file (in batch mode) to get the content & parse out anything that's really
  1007  	// a git-lob reference.
  1008  	// Start git cat-file in parallel and feed its stdin
  1009  	catfilecmd := exec.Command("git", "cat-file", "--batch")
  1010  	catout, err := catfilecmd.StdoutPipe()
  1011  	if err != nil {
  1012  		return errors.New(fmt.Sprintf("Unable to call git cat-file: %v", err.Error()))
  1013  	}
  1014  	defer catout.Close()
  1015  	catin, err := catfilecmd.StdinPipe()
  1016  	if err != nil {
  1017  		return errors.New(fmt.Sprintf("Unable to call git cat-file: %v", err.Error()))
  1018  	}
  1019  	defer catin.Close()
  1020  	catfilecmd.Start()
  1021  	catscanner := bufio.NewScanner(catout)
  1022  
  1023  	for lstreescanner.Scan() {
  1024  		line := lstreescanner.Text()
  1025  		if match := regex.FindStringSubmatch(line); match != nil {
  1026  			objsha := match[1]
  1027  			filename := match[2]
  1028  			// Apply filter
  1029  			if !util.FilenamePassesIncludeExcludeFilter(filename, includePaths, excludePaths) {
  1030  				continue
  1031  			}
  1032  			// Now feed object sha to cat-file to get git-lob SHA if any
  1033  			// remember we're already only finding files of exactly the right size (49 bytes)
  1034  			_, err := catin.Write([]byte(objsha))
  1035  			if err != nil {
  1036  				return errors.New(fmt.Sprintf("Unable to write to cat-file stream: %v", err.Error()))
  1037  			}
  1038  			_, err = catin.Write([]byte{'\n'})
  1039  			if err != nil {
  1040  				return errors.New(fmt.Sprintf("Unable to write to cat-file stream: %v", err.Error()))
  1041  			}
  1042  
  1043  			// Now read back response - first line is report of object sha, type & size
  1044  			// second line is content in our case
  1045  			if !catscanner.Scan() || !catscanner.Scan() {
  1046  				return errors.New(fmt.Sprintf("Couldn't read response from cat-file stream: %v", catscanner.Err()))
  1047  			}
  1048  
  1049  			// object SHA is the last 40 characters, after the prefix
  1050  			line := catscanner.Text()
  1051  			if len(line) == SHALineLen {
  1052  				lobsha := line[len(SHAPrefix):]
  1053  				// call callback to process result
  1054  				callback(&FileLOB{filename, lobsha})
  1055  			}
  1056  
  1057  		}
  1058  	}
  1059  	lstreecmd.Wait()
  1060  	catfilecmd.Process.Kill()
  1061  
  1062  	return nil
  1063  
  1064  }
  1065  
  1066  // Parse a Git date formatted in ISO 8601 format (%ci/%ai)
  1067  func ParseGitDate(str string) (time.Time, error) {
  1068  
  1069  	// Unfortunately Go and Git don't overlap in their builtin date formats
  1070  	// Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that
  1071  	// when the day is < 10 Git outputs a single digit, but Go expects a leading
  1072  	// zero - this is enough to break the parsing. Sigh.
  1073  
  1074  	// Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go
  1075  	return time.Parse("2006-01-02 15:04:05 -0700", str)
  1076  }
  1077  
  1078  // Format a date into Git format
  1079  func FormatGitDate(t time.Time) string {
  1080  	// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
  1081  	return t.Format("Mon Jan 2 15:04:05 2006 -0700")
  1082  }
  1083  
  1084  // Get summary information about a commit
  1085  func GetGitCommitSummary(commit string) (*GitCommitSummary, error) {
  1086  	cmd := exec.Command("git", "show", "-s",
  1087  		`--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit)
  1088  
  1089  	out, err := cmd.CombinedOutput()
  1090  	if err != nil {
  1091  		msg := fmt.Sprintf("Error calling git show: %v", err.Error())
  1092  		return nil, errors.New(msg)
  1093  	}
  1094  
  1095  	// At most 10 substrings so subject line is not split on anything
  1096  	fields := strings.SplitN(string(out), "|", 10)
  1097  	// Cope with the case where subject is blank
  1098  	if len(fields) >= 9 {
  1099  		ret := &GitCommitSummary{}
  1100  		// Get SHAs from output, not commit input, so we can support symbolic refs
  1101  		ret.SHA = fields[0]
  1102  		ret.ShortSHA = fields[1]
  1103  		ret.Parents = strings.Split(fields[2], " ")
  1104  		// %aD & %cD (RFC2822) matches Go's RFC1123Z format
  1105  		ret.AuthorDate, _ = ParseGitDate(fields[3])
  1106  		ret.CommitDate, _ = ParseGitDate(fields[4])
  1107  		ret.AuthorEmail = fields[5]
  1108  		ret.AuthorName = fields[6]
  1109  		ret.CommitterEmail = fields[7]
  1110  		ret.CommitterName = fields[8]
  1111  		if len(fields) > 9 {
  1112  			ret.Subject = strings.TrimRight(fields[9], "\n")
  1113  		}
  1114  		return ret, nil
  1115  	} else {
  1116  		msg := fmt.Sprintf("Unexpected output from git show: %v", out)
  1117  		return nil, errors.New(msg)
  1118  	}
  1119  
  1120  }
  1121  
  1122  // Get a list of refs (branches, tags) that have received commits in the last numdays, ordered
  1123  // by most recent first
  1124  // You can also set numdays to -1 to not have any limit but still get them in reverse order
  1125  // remoteName is optional but if specified and includeRemoteBranches is true, will only include
  1126  // remote branches on that remote
  1127  func GetGitRecentRefs(numdays int, includeRemoteBranches bool, remoteName string) ([]*GitRef, error) {
  1128  	// Include %(objectname) AND %(*objectname), the latter only returns something if it's a tag
  1129  	// and that will be the dereferenced SHA ie the actual commit SHA instead of the tag SHA
  1130  	cmd := exec.Command("git", "for-each-ref",
  1131  		`--sort=-committerdate`,
  1132  		`--format=%(refname) %(objectname) %(*objectname)`,
  1133  		"refs")
  1134  	outp, err := cmd.StdoutPipe()
  1135  	if err != nil {
  1136  		msg := fmt.Sprintf("Unable to call git for-each-ref: %v", err.Error())
  1137  		return []*GitRef{}, errors.New(msg)
  1138  	}
  1139  	cmd.Start()
  1140  	scanner := bufio.NewScanner(outp)
  1141  
  1142  	// Output is like this:
  1143  	// refs/heads/master 69d144416abf89b79f6a6fd21c2621dd9c13ead1
  1144  	// refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93
  1145  	// refs/tags/blah fa392f757dddf9fa7c3bb1717d0bf0c4762326fc c34b29b773e46ad6870fdf08796c33d97190fe93
  1146  	// note the second SHA when it's a tag but not otherwise
  1147  
  1148  	// Output is ordered by latest commit date first, so we can stop at the threshold
  1149  	var earliestDate time.Time
  1150  	if numdays >= 0 {
  1151  		earliestDate = time.Now().AddDate(0, 0, -numdays)
  1152  	}
  1153  
  1154  	regex := regexp.MustCompile(`^(refs/[^/]+/\S+)\s+([0-9A-Za-z]{40})(?:\s+([0-9A-Za-z]{40}))?`)
  1155  
  1156  	var ret []*GitRef
  1157  	for scanner.Scan() {
  1158  		line := scanner.Text()
  1159  		if match := regex.FindStringSubmatch(line); match != nil {
  1160  			fullref := match[1]
  1161  			sha := match[2]
  1162  			// test for dereferenced tags, use commit SHA
  1163  			if len(match) > 3 && match[3] != "" {
  1164  				sha = match[3]
  1165  			}
  1166  			reftype, ref := ParseGitRefToTypeAndName(fullref)
  1167  			if reftype == GitRefTypeRemoteBranch || reftype == GitRefTypeRemoteTag {
  1168  				if !includeRemoteBranches {
  1169  					continue
  1170  				}
  1171  				if remoteName != "" && !strings.HasPrefix(ref, remoteName+"/") {
  1172  					continue
  1173  				}
  1174  			}
  1175  			// This is a ref we might use
  1176  			if numdays >= 0 {
  1177  				// Check the date
  1178  				commit, err := GetGitCommitSummary(ref)
  1179  				if err != nil {
  1180  					return ret, err
  1181  				}
  1182  				if commit.CommitDate.Before(earliestDate) {
  1183  					// the end
  1184  					break
  1185  				}
  1186  			}
  1187  			ret = append(ret, &GitRef{ref, reftype, sha})
  1188  		}
  1189  	}
  1190  	cmd.Wait()
  1191  
  1192  	return ret, nil
  1193  }
  1194  
  1195  // Tell the index to refresh for files which we've modified outside of git commands
  1196  // This is necessary because git caches stat() info to provide a fast way to detect
  1197  // modifications for git-status and so can consider files modified when they're actually not
  1198  // when we've changed things that the filter would consider unmodified when called via git-diff.
  1199  // 'files' is a list of files with paths relative to the repo root
  1200  func GitRefreshIndexForFiles(files []string) error {
  1201  	var retErr error
  1202  	// Since we don't know how many there will be, potentially split into many commands
  1203  	errorFunc := func(args []string, output string, err error) (abort bool) {
  1204  		// exit status 1 is not important, it's just '<filename> needs update'
  1205  		if !strings.HasSuffix(err.Error(), "exit status 1") {
  1206  			// We actually continue anyway to make sure we try to update all files
  1207  			// but note this one because it's odd
  1208  			if retErr == nil {
  1209  				retErr = fmt.Errorf("Post-checkout index refresh failed: %v", err.Error())
  1210  			} else {
  1211  				retErr = fmt.Errorf("%v\n%v", retErr.Error(), err.Error())
  1212  			}
  1213  		}
  1214  		return false // don't abort
  1215  	}
  1216  	// Need to make file list (which files are relative to repo root) relative to cwd for git's purposes
  1217  	relfiles := util.MakeRepoFileListRelativeToCwd(files)
  1218  	util.ExecForManyFilesSplitIfRequired(relfiles, errorFunc,
  1219  		"git", "update-index", "-q", "--really-refresh", "--")
  1220  
  1221  	return retErr
  1222  
  1223  }
  1224  
  1225  // Get the type & name of a git reference
  1226  func ParseGitRefToTypeAndName(fullref string) (t GitRefType, name string) {
  1227  	const localPrefix = "refs/heads/"
  1228  	const remotePrefix = "refs/remotes/"
  1229  	const remoteTagPrefix = "refs/remotes/tags/"
  1230  	const localTagPrefix = "refs/tags/"
  1231  
  1232  	if fullref == "HEAD" {
  1233  		name = fullref
  1234  		t = GitRefTypeHEAD
  1235  	} else if strings.HasPrefix(fullref, localPrefix) {
  1236  		name = fullref[len(localPrefix):]
  1237  		t = GitRefTypeLocalBranch
  1238  	} else if strings.HasPrefix(fullref, remotePrefix) {
  1239  		name = fullref[len(remotePrefix):]
  1240  		t = GitRefTypeRemoteBranch
  1241  	} else if strings.HasPrefix(fullref, remoteTagPrefix) {
  1242  		name = fullref[len(remoteTagPrefix):]
  1243  		t = GitRefTypeRemoteTag
  1244  	} else if strings.HasPrefix(fullref, localTagPrefix) {
  1245  		name = fullref[len(localTagPrefix):]
  1246  		t = GitRefTypeLocalTag
  1247  	} else {
  1248  		name = fullref
  1249  		t = GitRefTypeOther
  1250  	}
  1251  	return
  1252  }
  1253  
  1254  // get all refs in the repo (branches, tags, stashes)
  1255  func GetGitAllRefs() ([]*GitRef, error) {
  1256  	cmd := exec.Command("git", "show-ref", "--head", "--dereference")
  1257  	outp, err := cmd.StdoutPipe()
  1258  	if err != nil {
  1259  		return []*GitRef{}, fmt.Errorf("Failure in git-show-ref: %v", err.Error())
  1260  	}
  1261  	scanner := bufio.NewScanner(outp)
  1262  	var ret []*GitRef
  1263  	cmd.Start()
  1264  
  1265  	// Output is like this:
  1266  	// <sha> HEAD
  1267  	// <sha> refs/heads/<branch>
  1268  	// <sha> refs/tags/<tag>
  1269  	// <sha> refs/tags/<tag>^{}     <- dereferenced tag, should use this one instead of original
  1270  	// <sha> refs/remotes/<remotebranch>
  1271  	// <sha> refs/stash (skipped)
  1272  
  1273  	for scanner.Scan() {
  1274  		line := scanner.Text()
  1275  
  1276  		f := strings.Fields(line)
  1277  		if len(f) == 2 {
  1278  			sha := f[0]
  1279  			fullref := f[1]
  1280  			t, name := ParseGitRefToTypeAndName(fullref)
  1281  			if t == GitRefTypeOther {
  1282  				// skip all others (including Stash)
  1283  				continue
  1284  			}
  1285  
  1286  			// Special case dereferenced tags. Non-lightweight tags refer to the tag
  1287  			// object, not the commit, but --dereference shows you the actual commit
  1288  			// with an extra ref after the tag object, called <tagname>^{}
  1289  			// This must take precedence to report the commit it applies to
  1290  			if t == GitRefTypeLocalTag && strings.HasSuffix(name, "^{}") {
  1291  				name = name[:len(name)-3]
  1292  				// now overwrite the previous tag object entry (they always come before)
  1293  				for _, ref := range ret {
  1294  					if ref.Name == name {
  1295  						ref.CommitSHA = sha
  1296  					}
  1297  				}
  1298  			} else {
  1299  				// Otherwise, new ref
  1300  				ret = append(ret, &GitRef{Name: name, Type: t, CommitSHA: sha})
  1301  			}
  1302  
  1303  		}
  1304  
  1305  	}
  1306  	cmd.Wait()
  1307  
  1308  	return ret, nil
  1309  }
  1310  
  1311  // Returns whether commit a (sha or ref) is an ancestor of commit b (sha or ref)
  1312  func GitIsAncestor(a, b string) (bool, error) {
  1313  
  1314  	if !GitRefIsSHA(a) {
  1315  		var err error
  1316  		a, err = GitRefToFullSHA(a)
  1317  		if err != nil {
  1318  			return false, err
  1319  		}
  1320  	}
  1321  	if !GitRefIsSHA(b) {
  1322  		var err error
  1323  		b, err = GitRefToFullSHA(b)
  1324  		if err != nil {
  1325  			return false, err
  1326  		}
  1327  	}
  1328  	cmd := exec.Command("git", "merge-base", a, b)
  1329  	outp, err := cmd.Output()
  1330  	if err != nil {
  1331  		return false, err
  1332  	}
  1333  	base := strings.TrimSpace(string(outp))
  1334  
  1335  	return base == a, nil
  1336  
  1337  }
  1338  
  1339  // Returns the 'best' ancestor of all the passed in refs (as a SHA)
  1340  // If a ref is listed twice the 'best' ancestor will be itself
  1341  func GetGitBestAncestor(refs []string) (ancestor string, err error) {
  1342  	args := []string{"merge-base"}
  1343  	args = append(args, refs...)
  1344  	cmd := exec.Command("git", args...)
  1345  	outp, err := cmd.Output()
  1346  	if err != nil {
  1347  		return "", err
  1348  	}
  1349  	base := strings.TrimSpace(string(outp))
  1350  	return base, nil
  1351  }
  1352  
  1353  // Gets the latest change to a specific LOB file at ref, returning the SHA and the commit details
  1354  func GetGitLatestLOBChangeDetails(filename, ref string) (summary *GitCommitSummary, lobsha string, err error) {
  1355  	cmd := exec.Command("git", "log", "-p",
  1356  		"-n", "1", // one commit
  1357  		"-G", SHALineRegexStr, // if this file was ever embedded verbatim, ignore those
  1358  		`--format=commit:%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, // standard summary info
  1359  		ref, "--", filename)
  1360  	outp, err := cmd.StdoutPipe()
  1361  	if err != nil {
  1362  		return nil, "", errors.New(fmt.Sprintf("Unable to get latest commit from %v: %v", ref, err.Error()))
  1363  	}
  1364  	cmd.Start()
  1365  	scanner := bufio.NewScanner(outp)
  1366  	summary = &GitCommitSummary{}
  1367  	lobsha = ""
  1368  	lobsharegex := regexp.MustCompile(`^\+git-lob: ([A-Fa-f0-9]{40})`)
  1369  	err = nil
  1370  	for scanner.Scan() {
  1371  		line := scanner.Text()
  1372  		if strings.HasPrefix(line, "commit:") {
  1373  			// At most 10 substrings so subject line is not split on anything
  1374  			fields := strings.SplitN(string(line[7:]), "|", 10)
  1375  			// Cope with the case where subject is blank
  1376  			if len(fields) >= 9 {
  1377  				// Get SHAs from output, not commit input, so we can support symbolic refs
  1378  				summary.SHA = fields[0]
  1379  				summary.ShortSHA = fields[1]
  1380  				summary.Parents = strings.Split(fields[2], " ")
  1381  				// %aD & %cD (RFC2822) matches Go's RFC1123Z format
  1382  				summary.AuthorDate, _ = ParseGitDate(fields[3])
  1383  				summary.CommitDate, _ = ParseGitDate(fields[4])
  1384  				summary.AuthorEmail = fields[5]
  1385  				summary.AuthorName = fields[6]
  1386  				summary.CommitterEmail = fields[7]
  1387  				summary.CommitterName = fields[8]
  1388  				if len(fields) > 9 {
  1389  					summary.Subject = strings.TrimRight(fields[9], "\n")
  1390  				}
  1391  			} else {
  1392  				msg := fmt.Sprintf("Unexpected output from git log: %v", line)
  1393  				return nil, "", errors.New(msg)
  1394  			}
  1395  		} else if match := lobsharegex.FindStringSubmatch(line); match != nil {
  1396  			lobsha = match[1]
  1397  		}
  1398  	}
  1399  	return
  1400  
  1401  }