github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/git/git.go (about)

     1  // Package git contains various commands that shell out to git
     2  // NOTE: Subject to change, do not rely on this package from outside git-lfs source
     3  package git
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	lfserrors "github.com/git-lfs/git-lfs/errors"
    21  	"github.com/git-lfs/git-lfs/subprocess"
    22  	"github.com/git-lfs/git-lfs/tools"
    23  	"github.com/rubyist/tracerx"
    24  )
    25  
    26  type RefType int
    27  
    28  const (
    29  	RefTypeLocalBranch  = RefType(iota)
    30  	RefTypeRemoteBranch = RefType(iota)
    31  	RefTypeLocalTag     = RefType(iota)
    32  	RefTypeRemoteTag    = RefType(iota)
    33  	RefTypeHEAD         = RefType(iota) // current checkout
    34  	RefTypeOther        = RefType(iota) // stash or unknown
    35  
    36  	// A ref which can be used as a placeholder for before the first commit
    37  	// Equivalent to git mktree < /dev/null, useful for diffing before first commit
    38  	RefBeforeFirstCommit = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
    39  )
    40  
    41  // A git reference (branch, tag etc)
    42  type Ref struct {
    43  	Name string
    44  	Type RefType
    45  	Sha  string
    46  }
    47  
    48  // Some top level information about a commit (only first line of message)
    49  type CommitSummary struct {
    50  	Sha            string
    51  	ShortSha       string
    52  	Parents        []string
    53  	CommitDate     time.Time
    54  	AuthorDate     time.Time
    55  	AuthorName     string
    56  	AuthorEmail    string
    57  	CommitterName  string
    58  	CommitterEmail string
    59  	Subject        string
    60  }
    61  
    62  func LsRemote(remote, remoteRef string) (string, error) {
    63  	if remote == "" {
    64  		return "", errors.New("remote required")
    65  	}
    66  	if remoteRef == "" {
    67  		return subprocess.SimpleExec("git", "ls-remote", remote)
    68  
    69  	}
    70  	return subprocess.SimpleExec("git", "ls-remote", remote, remoteRef)
    71  }
    72  
    73  func ResolveRef(ref string) (*Ref, error) {
    74  	outp, err := subprocess.SimpleExec("git", "rev-parse", ref, "--symbolic-full-name", ref)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
    77  	}
    78  	if outp == "" {
    79  		return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
    80  	}
    81  
    82  	lines := strings.Split(outp, "\n")
    83  	fullref := &Ref{Sha: lines[0]}
    84  
    85  	if len(lines) == 1 {
    86  		// ref is a sha1 and has no symbolic-full-name
    87  		fullref.Name = lines[0] // fullref.Sha
    88  		fullref.Type = RefTypeOther
    89  		return fullref, nil
    90  	}
    91  
    92  	// parse the symbolic-full-name
    93  	fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1])
    94  	return fullref, nil
    95  }
    96  
    97  func ResolveRefs(refnames []string) ([]*Ref, error) {
    98  	refs := make([]*Ref, len(refnames))
    99  	for i, name := range refnames {
   100  		ref, err := ResolveRef(name)
   101  		if err != nil {
   102  			return refs, err
   103  		}
   104  
   105  		refs[i] = ref
   106  	}
   107  	return refs, nil
   108  }
   109  
   110  func CurrentRef() (*Ref, error) {
   111  	return ResolveRef("HEAD")
   112  }
   113  
   114  func CurrentRemoteRef() (*Ref, error) {
   115  	remoteref, err := RemoteRefNameForCurrentBranch()
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	return ResolveRef(remoteref)
   121  }
   122  
   123  // RemoteForCurrentBranch returns the name of the remote that the current branch is tracking
   124  func RemoteForCurrentBranch() (string, error) {
   125  	ref, err := CurrentRef()
   126  	if err != nil {
   127  		return "", err
   128  	}
   129  	remote := RemoteForBranch(ref.Name)
   130  	if remote == "" {
   131  		return "", fmt.Errorf("remote not found for branch %q", ref.Name)
   132  	}
   133  	return remote, nil
   134  }
   135  
   136  // RemoteRefForCurrentBranch returns the full remote ref (refs/remotes/{remote}/{remotebranch})
   137  // that the current branch is tracking.
   138  func RemoteRefNameForCurrentBranch() (string, error) {
   139  	ref, err := CurrentRef()
   140  	if err != nil {
   141  		return "", err
   142  	}
   143  
   144  	if ref.Type == RefTypeHEAD || ref.Type == RefTypeOther {
   145  		return "", errors.New("not on a branch")
   146  	}
   147  
   148  	remote := RemoteForBranch(ref.Name)
   149  	if remote == "" {
   150  		return "", fmt.Errorf("remote not found for branch %q", ref.Name)
   151  	}
   152  
   153  	remotebranch := RemoteBranchForLocalBranch(ref.Name)
   154  
   155  	return fmt.Sprintf("refs/remotes/%s/%s", remote, remotebranch), nil
   156  }
   157  
   158  // RemoteForBranch returns the remote name that a given local branch is tracking (blank if none)
   159  func RemoteForBranch(localBranch string) string {
   160  	return Config.Find(fmt.Sprintf("branch.%s.remote", localBranch))
   161  }
   162  
   163  // RemoteBranchForLocalBranch returns the name (only) of the remote branch that the local branch is tracking
   164  // If no specific branch is configured, returns local branch name
   165  func RemoteBranchForLocalBranch(localBranch string) string {
   166  	// get remote ref to track, may not be same name
   167  	merge := Config.Find(fmt.Sprintf("branch.%s.merge", localBranch))
   168  	if strings.HasPrefix(merge, "refs/heads/") {
   169  		return merge[11:]
   170  	} else {
   171  		return localBranch
   172  	}
   173  
   174  }
   175  
   176  func RemoteList() ([]string, error) {
   177  	cmd := subprocess.ExecCommand("git", "remote")
   178  
   179  	outp, err := cmd.StdoutPipe()
   180  	if err != nil {
   181  		return nil, fmt.Errorf("Failed to call git remote: %v", err)
   182  	}
   183  	cmd.Start()
   184  	defer cmd.Wait()
   185  
   186  	scanner := bufio.NewScanner(outp)
   187  
   188  	var ret []string
   189  	for scanner.Scan() {
   190  		ret = append(ret, strings.TrimSpace(scanner.Text()))
   191  	}
   192  
   193  	return ret, nil
   194  }
   195  
   196  // Refs returns all of the local and remote branches and tags for the current
   197  // repository. Other refs (HEAD, refs/stash, git notes) are ignored.
   198  func LocalRefs() ([]*Ref, error) {
   199  	cmd := subprocess.ExecCommand("git", "show-ref", "--heads", "--tags")
   200  
   201  	outp, err := cmd.StdoutPipe()
   202  	if err != nil {
   203  		return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
   204  	}
   205  
   206  	var refs []*Ref
   207  
   208  	if err := cmd.Start(); err != nil {
   209  		return refs, err
   210  	}
   211  
   212  	scanner := bufio.NewScanner(outp)
   213  	for scanner.Scan() {
   214  		line := strings.TrimSpace(scanner.Text())
   215  		parts := strings.SplitN(line, " ", 2)
   216  		if len(parts) != 2 || len(parts[0]) != 40 || len(parts[1]) < 1 {
   217  			tracerx.Printf("Invalid line from git show-ref: %q", line)
   218  			continue
   219  		}
   220  
   221  		rtype, name := ParseRefToTypeAndName(parts[1])
   222  		if rtype != RefTypeLocalBranch && rtype != RefTypeLocalTag {
   223  			continue
   224  		}
   225  
   226  		refs = append(refs, &Ref{name, rtype, parts[0]})
   227  	}
   228  
   229  	return refs, cmd.Wait()
   230  }
   231  
   232  // ValidateRemote checks that a named remote is valid for use
   233  // Mainly to check user-supplied remotes & fail more nicely
   234  func ValidateRemote(remote string) error {
   235  	remotes, err := RemoteList()
   236  	if err != nil {
   237  		return err
   238  	}
   239  	for _, r := range remotes {
   240  		if r == remote {
   241  			return nil
   242  		}
   243  	}
   244  
   245  	if err = ValidateRemoteURL(remote); err == nil {
   246  		return nil
   247  	}
   248  
   249  	return fmt.Errorf("Invalid remote name: %q", remote)
   250  }
   251  
   252  // ValidateRemoteURL checks that a string is a valid Git remote URL
   253  func ValidateRemoteURL(remote string) error {
   254  	u, _ := url.Parse(remote)
   255  	if u == nil || u.Scheme == "" {
   256  		// This is either an invalid remote name (maybe the user made a typo
   257  		// when selecting a named remote) or a bare SSH URL like
   258  		// "x@y.com:path/to/resource.git". Guess that this is a URL in the latter
   259  		// form if the string contains a colon ":", and an invalid remote if it
   260  		// does not.
   261  		if strings.Contains(remote, ":") {
   262  			return nil
   263  		} else {
   264  			return fmt.Errorf("Invalid remote name: %q", remote)
   265  		}
   266  	}
   267  
   268  	switch u.Scheme {
   269  	case "ssh", "http", "https", "git":
   270  		return nil
   271  	default:
   272  		return fmt.Errorf("Invalid remote url protocol %q in %q", u.Scheme, remote)
   273  	}
   274  }
   275  
   276  // DefaultRemote returns the default remote based on:
   277  // 1. The currently tracked remote branch, if present
   278  // 2. "origin", if defined
   279  // 3. Any other SINGLE remote defined in .git/config
   280  // Returns an error if all of these fail, i.e. no tracked remote branch, no
   281  // "origin", and either no remotes defined or 2+ non-"origin" remotes
   282  func DefaultRemote() (string, error) {
   283  	tracked, err := RemoteForCurrentBranch()
   284  	if err == nil {
   285  		return tracked, nil
   286  	}
   287  
   288  	// Otherwise, check what remotes are defined
   289  	remotes, err := RemoteList()
   290  	if err != nil {
   291  		return "", err
   292  	}
   293  	switch len(remotes) {
   294  	case 0:
   295  		return "", errors.New("No remotes defined")
   296  	case 1: // always use a single remote whether it's origin or otherwise
   297  		return remotes[0], nil
   298  	default:
   299  		for _, remote := range remotes {
   300  			// Use origin if present
   301  			if remote == "origin" {
   302  				return remote, nil
   303  			}
   304  		}
   305  	}
   306  	return "", errors.New("Unable to pick default remote, too ambiguous")
   307  }
   308  
   309  func UpdateIndex(file string) error {
   310  	_, err := subprocess.SimpleExec("git", "update-index", "-q", "--refresh", file)
   311  	return err
   312  }
   313  
   314  type gitConfig struct {
   315  	gitVersion string
   316  	mu         sync.Mutex
   317  }
   318  
   319  var Config = &gitConfig{}
   320  
   321  // Find returns the git config value for the key
   322  func (c *gitConfig) Find(val string) string {
   323  	output, _ := subprocess.SimpleExec("git", "config", val)
   324  	return output
   325  }
   326  
   327  // FindGlobal returns the git config value global scope for the key
   328  func (c *gitConfig) FindGlobal(val string) string {
   329  	output, _ := subprocess.SimpleExec("git", "config", "--global", val)
   330  	return output
   331  }
   332  
   333  // FindSystem returns the git config value in system scope for the key
   334  func (c *gitConfig) FindSystem(val string) string {
   335  	output, _ := subprocess.SimpleExec("git", "config", "--system", val)
   336  	return output
   337  }
   338  
   339  // Find returns the git config value for the key
   340  func (c *gitConfig) FindLocal(val string) string {
   341  	output, _ := subprocess.SimpleExec("git", "config", "--local", val)
   342  	return output
   343  }
   344  
   345  // SetGlobal sets the git config value for the key in the global config
   346  func (c *gitConfig) SetGlobal(key, val string) (string, error) {
   347  	return subprocess.SimpleExec("git", "config", "--global", key, val)
   348  }
   349  
   350  // SetSystem sets the git config value for the key in the system config
   351  func (c *gitConfig) SetSystem(key, val string) (string, error) {
   352  	return subprocess.SimpleExec("git", "config", "--system", key, val)
   353  }
   354  
   355  // UnsetGlobal removes the git config value for the key from the global config
   356  func (c *gitConfig) UnsetGlobal(key string) (string, error) {
   357  	return subprocess.SimpleExec("git", "config", "--global", "--unset", key)
   358  }
   359  
   360  // UnsetSystem removes the git config value for the key from the system config
   361  func (c *gitConfig) UnsetSystem(key string) (string, error) {
   362  	return subprocess.SimpleExec("git", "config", "--system", "--unset", key)
   363  }
   364  
   365  // UnsetGlobalSection removes the entire named section from the global config
   366  func (c *gitConfig) UnsetGlobalSection(key string) (string, error) {
   367  	return subprocess.SimpleExec("git", "config", "--global", "--remove-section", key)
   368  }
   369  
   370  // UnsetSystemSection removes the entire named section from the system config
   371  func (c *gitConfig) UnsetSystemSection(key string) (string, error) {
   372  	return subprocess.SimpleExec("git", "config", "--system", "--remove-section", key)
   373  }
   374  
   375  // UnsetLocalSection removes the entire named section from the system config
   376  func (c *gitConfig) UnsetLocalSection(key string) (string, error) {
   377  	return subprocess.SimpleExec("git", "config", "--local", "--remove-section", key)
   378  }
   379  
   380  // SetLocal sets the git config value for the key in the specified config file
   381  func (c *gitConfig) SetLocal(file, key, val string) (string, error) {
   382  	args := make([]string, 1, 5)
   383  	args[0] = "config"
   384  	if len(file) > 0 {
   385  		args = append(args, "--file", file)
   386  	}
   387  	args = append(args, key, val)
   388  	return subprocess.SimpleExec("git", args...)
   389  }
   390  
   391  // UnsetLocalKey removes the git config value for the key from the specified config file
   392  func (c *gitConfig) UnsetLocalKey(file, key string) (string, error) {
   393  	args := make([]string, 1, 5)
   394  	args[0] = "config"
   395  	if len(file) > 0 {
   396  		args = append(args, "--file", file)
   397  	}
   398  	args = append(args, "--unset", key)
   399  	return subprocess.SimpleExec("git", args...)
   400  }
   401  
   402  // List lists all of the git config values
   403  func (c *gitConfig) List() (string, error) {
   404  	return subprocess.SimpleExec("git", "config", "-l")
   405  }
   406  
   407  // ListFromFile lists all of the git config values in the given config file
   408  func (c *gitConfig) ListFromFile(f string) (string, error) {
   409  	return subprocess.SimpleExec("git", "config", "-l", "-f", f)
   410  }
   411  
   412  // Version returns the git version
   413  func (c *gitConfig) Version() (string, error) {
   414  	c.mu.Lock()
   415  	defer c.mu.Unlock()
   416  
   417  	if len(c.gitVersion) == 0 {
   418  		v, err := subprocess.SimpleExec("git", "version")
   419  		if err != nil {
   420  			return v, err
   421  		}
   422  		c.gitVersion = v
   423  	}
   424  
   425  	return c.gitVersion, nil
   426  }
   427  
   428  // IsVersionAtLeast returns whether the git version is the one specified or higher
   429  // argument is plain version string separated by '.' e.g. "2.3.1" but can omit minor/patch
   430  func (c *gitConfig) IsGitVersionAtLeast(ver string) bool {
   431  	gitver, err := c.Version()
   432  	if err != nil {
   433  		tracerx.Printf("Error getting git version: %v", err)
   434  		return false
   435  	}
   436  	return IsVersionAtLeast(gitver, ver)
   437  }
   438  
   439  // RecentBranches returns branches with commit dates on or after the given date/time
   440  // Return full Ref type for easier detection of duplicate SHAs etc
   441  // since: refs with commits on or after this date will be included
   442  // includeRemoteBranches: true to include refs on remote branches
   443  // onlyRemote: set to non-blank to only include remote branches on a single remote
   444  func RecentBranches(since time.Time, includeRemoteBranches bool, onlyRemote string) ([]*Ref, error) {
   445  	cmd := subprocess.ExecCommand("git", "for-each-ref",
   446  		`--sort=-committerdate`,
   447  		`--format=%(refname) %(objectname) %(committerdate:iso)`,
   448  		"refs")
   449  	outp, err := cmd.StdoutPipe()
   450  	if err != nil {
   451  		return nil, fmt.Errorf("Failed to call git for-each-ref: %v", err)
   452  	}
   453  	cmd.Start()
   454  	defer cmd.Wait()
   455  
   456  	scanner := bufio.NewScanner(outp)
   457  
   458  	// Output is like this:
   459  	// refs/heads/master f03686b324b29ff480591745dbfbbfa5e5ac1bd5 2015-08-19 16:50:37 +0100
   460  	// refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 2015-08-13 16:50:37 +0100
   461  
   462  	// Output is ordered by latest commit date first, so we can stop at the threshold
   463  	regex := regexp.MustCompile(`^(refs/[^/]+/\S+)\s+([0-9A-Za-z]{40})\s+(\d{4}-\d{2}-\d{2}\s+\d{2}\:\d{2}\:\d{2}\s+[\+\-]\d{4})`)
   464  	tracerx.Printf("RECENT: Getting refs >= %v", since)
   465  	var ret []*Ref
   466  	for scanner.Scan() {
   467  		line := scanner.Text()
   468  		if match := regex.FindStringSubmatch(line); match != nil {
   469  			fullref := match[1]
   470  			sha := match[2]
   471  			reftype, ref := ParseRefToTypeAndName(fullref)
   472  			if reftype == RefTypeRemoteBranch || reftype == RefTypeRemoteTag {
   473  				if !includeRemoteBranches {
   474  					continue
   475  				}
   476  				if onlyRemote != "" && !strings.HasPrefix(ref, onlyRemote+"/") {
   477  					continue
   478  				}
   479  			}
   480  			// This is a ref we might use
   481  			// Check the date
   482  			commitDate, err := ParseGitDate(match[3])
   483  			if err != nil {
   484  				return ret, err
   485  			}
   486  			if commitDate.Before(since) {
   487  				// the end
   488  				break
   489  			}
   490  			tracerx.Printf("RECENT: %v (%v)", ref, commitDate)
   491  			ret = append(ret, &Ref{ref, reftype, sha})
   492  		}
   493  	}
   494  
   495  	return ret, nil
   496  
   497  }
   498  
   499  // Get the type & name of a git reference
   500  func ParseRefToTypeAndName(fullref string) (t RefType, name string) {
   501  	const localPrefix = "refs/heads/"
   502  	const remotePrefix = "refs/remotes/"
   503  	const remoteTagPrefix = "refs/remotes/tags/"
   504  	const localTagPrefix = "refs/tags/"
   505  
   506  	if fullref == "HEAD" {
   507  		name = fullref
   508  		t = RefTypeHEAD
   509  	} else if strings.HasPrefix(fullref, localPrefix) {
   510  		name = fullref[len(localPrefix):]
   511  		t = RefTypeLocalBranch
   512  	} else if strings.HasPrefix(fullref, remotePrefix) {
   513  		name = fullref[len(remotePrefix):]
   514  		t = RefTypeRemoteBranch
   515  	} else if strings.HasPrefix(fullref, remoteTagPrefix) {
   516  		name = fullref[len(remoteTagPrefix):]
   517  		t = RefTypeRemoteTag
   518  	} else if strings.HasPrefix(fullref, localTagPrefix) {
   519  		name = fullref[len(localTagPrefix):]
   520  		t = RefTypeLocalTag
   521  	} else {
   522  		name = fullref
   523  		t = RefTypeOther
   524  	}
   525  	return
   526  }
   527  
   528  // Parse a Git date formatted in ISO 8601 format (%ci/%ai)
   529  func ParseGitDate(str string) (time.Time, error) {
   530  
   531  	// Unfortunately Go and Git don't overlap in their builtin date formats
   532  	// Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that
   533  	// when the day is < 10 Git outputs a single digit, but Go expects a leading
   534  	// zero - this is enough to break the parsing. Sigh.
   535  
   536  	// Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go
   537  	return time.Parse("2006-01-02 15:04:05 -0700", str)
   538  }
   539  
   540  // FormatGitDate converts a Go date into a git command line format date
   541  func FormatGitDate(tm time.Time) string {
   542  	// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
   543  	return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
   544  }
   545  
   546  // Get summary information about a commit
   547  func GetCommitSummary(commit string) (*CommitSummary, error) {
   548  	cmd := subprocess.ExecCommand("git", "show", "-s",
   549  		`--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit)
   550  
   551  	out, err := cmd.CombinedOutput()
   552  	if err != nil {
   553  		return nil, fmt.Errorf("Failed to call git show: %v %v", err, string(out))
   554  	}
   555  
   556  	// At most 10 substrings so subject line is not split on anything
   557  	fields := strings.SplitN(string(out), "|", 10)
   558  	// Cope with the case where subject is blank
   559  	if len(fields) >= 9 {
   560  		ret := &CommitSummary{}
   561  		// Get SHAs from output, not commit input, so we can support symbolic refs
   562  		ret.Sha = fields[0]
   563  		ret.ShortSha = fields[1]
   564  		ret.Parents = strings.Split(fields[2], " ")
   565  		// %aD & %cD (RFC2822) matches Go's RFC1123Z format
   566  		ret.AuthorDate, _ = ParseGitDate(fields[3])
   567  		ret.CommitDate, _ = ParseGitDate(fields[4])
   568  		ret.AuthorEmail = fields[5]
   569  		ret.AuthorName = fields[6]
   570  		ret.CommitterEmail = fields[7]
   571  		ret.CommitterName = fields[8]
   572  		if len(fields) > 9 {
   573  			ret.Subject = strings.TrimRight(fields[9], "\n")
   574  		}
   575  		return ret, nil
   576  	} else {
   577  		msg := fmt.Sprintf("Unexpected output from git show: %v", string(out))
   578  		return nil, errors.New(msg)
   579  	}
   580  }
   581  
   582  func GitAndRootDirs() (string, string, error) {
   583  	cmd := subprocess.ExecCommand("git", "rev-parse", "--git-dir", "--show-toplevel")
   584  	buf := &bytes.Buffer{}
   585  	cmd.Stderr = buf
   586  
   587  	out, err := cmd.Output()
   588  	output := string(out)
   589  	if err != nil {
   590  		return "", "", fmt.Errorf("Failed to call git rev-parse --git-dir --show-toplevel: %q", buf.String())
   591  	}
   592  
   593  	paths := strings.Split(output, "\n")
   594  	pathLen := len(paths)
   595  
   596  	for i := 0; i < pathLen; i++ {
   597  		paths[i], err = tools.TranslateCygwinPath(paths[i])
   598  	}
   599  
   600  	if pathLen == 0 {
   601  		return "", "", fmt.Errorf("Bad git rev-parse output: %q", output)
   602  	}
   603  
   604  	absGitDir, err := filepath.Abs(paths[0])
   605  	if err != nil {
   606  		return "", "", fmt.Errorf("Error converting %q to absolute: %s", paths[0], err)
   607  	}
   608  
   609  	if pathLen == 1 || len(paths[1]) == 0 {
   610  		return absGitDir, "", nil
   611  	}
   612  
   613  	absRootDir := paths[1]
   614  	return absGitDir, absRootDir, nil
   615  }
   616  
   617  func RootDir() (string, error) {
   618  	cmd := subprocess.ExecCommand("git", "rev-parse", "--show-toplevel")
   619  	out, err := cmd.Output()
   620  	if err != nil {
   621  		return "", fmt.Errorf("Failed to call git rev-parse --show-toplevel: %v %v", err, string(out))
   622  	}
   623  
   624  	path := strings.TrimSpace(string(out))
   625  	path, err = tools.TranslateCygwinPath(path)
   626  	if len(path) > 0 {
   627  		return filepath.Abs(path)
   628  	}
   629  	return "", nil
   630  
   631  }
   632  
   633  func GitDir() (string, error) {
   634  	cmd := subprocess.ExecCommand("git", "rev-parse", "--git-dir")
   635  	out, err := cmd.Output()
   636  	if err != nil {
   637  		return "", fmt.Errorf("Failed to call git rev-parse --git-dir: %v %v", err, string(out))
   638  	}
   639  	path := strings.TrimSpace(string(out))
   640  	if len(path) > 0 {
   641  		return filepath.Abs(path)
   642  	}
   643  	return "", nil
   644  }
   645  
   646  // GetAllWorkTreeHEADs returns the refs that all worktrees are using as HEADs
   647  // This returns all worktrees plus the master working copy, and works even if
   648  // working dir is actually in a worktree right now
   649  // Pass in the git storage dir (parent of 'objects') to work from
   650  func GetAllWorkTreeHEADs(storageDir string) ([]*Ref, error) {
   651  	worktreesdir := filepath.Join(storageDir, "worktrees")
   652  	dirf, err := os.Open(worktreesdir)
   653  	if err != nil && !os.IsNotExist(err) {
   654  		return nil, err
   655  	}
   656  
   657  	var worktrees []*Ref
   658  	if err == nil {
   659  		// There are some worktrees
   660  		defer dirf.Close()
   661  		direntries, err := dirf.Readdir(0)
   662  		if err != nil {
   663  			return nil, err
   664  		}
   665  		for _, dirfi := range direntries {
   666  			if dirfi.IsDir() {
   667  				// to avoid having to chdir and run git commands to identify the commit
   668  				// just read the HEAD file & git rev-parse if necessary
   669  				// Since the git repo is shared the same rev-parse will work from this location
   670  				headfile := filepath.Join(worktreesdir, dirfi.Name(), "HEAD")
   671  				ref, err := parseRefFile(headfile)
   672  				if err != nil {
   673  					tracerx.Printf("Error reading %v for worktree, skipping: %v", headfile, err)
   674  					continue
   675  				}
   676  				worktrees = append(worktrees, ref)
   677  			}
   678  		}
   679  	}
   680  
   681  	// This has only established the separate worktrees, not the original checkout
   682  	// If the storageDir contains a HEAD file then there is a main checkout
   683  	// as well; this mus tbe resolveable whether you're in the main checkout or
   684  	// a worktree
   685  	headfile := filepath.Join(storageDir, "HEAD")
   686  	ref, err := parseRefFile(headfile)
   687  	if err == nil {
   688  		worktrees = append(worktrees, ref)
   689  	} else if !os.IsNotExist(err) { // ok if not exists, probably bare repo
   690  		tracerx.Printf("Error reading %v for main checkout, skipping: %v", headfile, err)
   691  	}
   692  
   693  	return worktrees, nil
   694  }
   695  
   696  // Manually parse a reference file like HEAD and return the Ref it resolves to
   697  func parseRefFile(filename string) (*Ref, error) {
   698  	bytes, err := ioutil.ReadFile(filename)
   699  	if err != nil {
   700  		return nil, err
   701  	}
   702  	contents := strings.TrimSpace(string(bytes))
   703  	if strings.HasPrefix(contents, "ref:") {
   704  		contents = strings.TrimSpace(contents[4:])
   705  	}
   706  	return ResolveRef(contents)
   707  }
   708  
   709  // IsVersionAtLeast compares 2 version strings (ok to be prefixed with 'git version', ignores)
   710  func IsVersionAtLeast(actualVersion, desiredVersion string) bool {
   711  	// Capture 1-3 version digits, optionally prefixed with 'git version' and possibly
   712  	// with suffixes which we'll ignore (e.g. unstable builds, MinGW versions)
   713  	verregex := regexp.MustCompile(`(?:git version\s+)?(\d+)(?:.(\d+))?(?:.(\d+))?.*`)
   714  
   715  	var atleast uint64
   716  	// Support up to 1000 in major/minor/patch digits
   717  	const majorscale = 1000 * 1000
   718  	const minorscale = 1000
   719  
   720  	if match := verregex.FindStringSubmatch(desiredVersion); match != nil {
   721  		// Ignore errors as regex won't match anything other than digits
   722  		major, _ := strconv.Atoi(match[1])
   723  		atleast += uint64(major * majorscale)
   724  		if len(match) > 2 {
   725  			minor, _ := strconv.Atoi(match[2])
   726  			atleast += uint64(minor * minorscale)
   727  		}
   728  		if len(match) > 3 {
   729  			patch, _ := strconv.Atoi(match[3])
   730  			atleast += uint64(patch)
   731  		}
   732  	}
   733  
   734  	var actual uint64
   735  	if match := verregex.FindStringSubmatch(actualVersion); match != nil {
   736  		major, _ := strconv.Atoi(match[1])
   737  		actual += uint64(major * majorscale)
   738  		if len(match) > 2 {
   739  			minor, _ := strconv.Atoi(match[2])
   740  			actual += uint64(minor * minorscale)
   741  		}
   742  		if len(match) > 3 {
   743  			patch, _ := strconv.Atoi(match[3])
   744  			actual += uint64(patch)
   745  		}
   746  	}
   747  
   748  	return actual >= atleast
   749  }
   750  
   751  // For compatibility with git clone we must mirror all flags in CloneWithoutFilters
   752  type CloneFlags struct {
   753  	// --template <template_directory>
   754  	TemplateDirectory string
   755  	// -l --local
   756  	Local bool
   757  	// -s --shared
   758  	Shared bool
   759  	// --no-hardlinks
   760  	NoHardlinks bool
   761  	// -q --quiet
   762  	Quiet bool
   763  	// -n --no-checkout
   764  	NoCheckout bool
   765  	// --progress
   766  	Progress bool
   767  	// --bare
   768  	Bare bool
   769  	// --mirror
   770  	Mirror bool
   771  	// -o <name> --origin <name>
   772  	Origin string
   773  	// -b <name> --branch <name>
   774  	Branch string
   775  	// -u <upload-pack> --upload-pack <pack>
   776  	Upload string
   777  	// --reference <repository>
   778  	Reference string
   779  	// --dissociate
   780  	Dissociate bool
   781  	// --separate-git-dir <git dir>
   782  	SeparateGit string
   783  	// --depth <depth>
   784  	Depth string
   785  	// --recursive
   786  	Recursive bool
   787  	// --recurse-submodules
   788  	RecurseSubmodules bool
   789  	// -c <value> --config <value>
   790  	Config string
   791  	// --single-branch
   792  	SingleBranch bool
   793  	// --no-single-branch
   794  	NoSingleBranch bool
   795  	// --verbose
   796  	Verbose bool
   797  	// --ipv4
   798  	Ipv4 bool
   799  	// --ipv6
   800  	Ipv6 bool
   801  }
   802  
   803  // CloneWithoutFilters clones a git repo but without the smudge filter enabled
   804  // so that files in the working copy will be pointers and not real LFS data
   805  func CloneWithoutFilters(flags CloneFlags, args []string) error {
   806  
   807  	// Before git 2.8, setting filters to blank causes lots of warnings, so use cat instead (slightly slower)
   808  	// Also pre 2.2 it failed completely. We used to use it anyway in git 2.2-2.7 and
   809  	// suppress the messages in stderr, but doing that with standard StderrPipe suppresses
   810  	// the git clone output (git thinks it's not a terminal) and makes it look like it's
   811  	// not working. You can get around that with https://github.com/kr/pty but that
   812  	// causes difficult issues with passing through Stdin for login prompts
   813  	// This way is simpler & more practical.
   814  	filterOverride := ""
   815  	if !Config.IsGitVersionAtLeast("2.8.0") {
   816  		filterOverride = "cat"
   817  	}
   818  	// Disable the LFS filters while cloning to speed things up
   819  	// this is especially effective on Windows where even calling git-lfs at all
   820  	// with --skip-smudge is costly across many files in a checkout
   821  	cmdargs := []string{
   822  		"-c", fmt.Sprintf("filter.lfs.smudge=%v", filterOverride),
   823  		"-c", "filter.lfs.process=",
   824  		"-c", "filter.lfs.required=false",
   825  		"clone"}
   826  
   827  	// flags
   828  	if flags.Bare {
   829  		cmdargs = append(cmdargs, "--bare")
   830  	}
   831  	if len(flags.Branch) > 0 {
   832  		cmdargs = append(cmdargs, "--branch", flags.Branch)
   833  	}
   834  	if len(flags.Config) > 0 {
   835  		cmdargs = append(cmdargs, "--config", flags.Config)
   836  	}
   837  	if len(flags.Depth) > 0 {
   838  		cmdargs = append(cmdargs, "--depth", flags.Depth)
   839  	}
   840  	if flags.Dissociate {
   841  		cmdargs = append(cmdargs, "--dissociate")
   842  	}
   843  	if flags.Ipv4 {
   844  		cmdargs = append(cmdargs, "--ipv4")
   845  	}
   846  	if flags.Ipv6 {
   847  		cmdargs = append(cmdargs, "--ipv6")
   848  	}
   849  	if flags.Local {
   850  		cmdargs = append(cmdargs, "--local")
   851  	}
   852  	if flags.Mirror {
   853  		cmdargs = append(cmdargs, "--mirror")
   854  	}
   855  	if flags.NoCheckout {
   856  		cmdargs = append(cmdargs, "--no-checkout")
   857  	}
   858  	if flags.NoHardlinks {
   859  		cmdargs = append(cmdargs, "--no-hardlinks")
   860  	}
   861  	if flags.NoSingleBranch {
   862  		cmdargs = append(cmdargs, "--no-single-branch")
   863  	}
   864  	if len(flags.Origin) > 0 {
   865  		cmdargs = append(cmdargs, "--origin", flags.Origin)
   866  	}
   867  	if flags.Progress {
   868  		cmdargs = append(cmdargs, "--progress")
   869  	}
   870  	if flags.Quiet {
   871  		cmdargs = append(cmdargs, "--quiet")
   872  	}
   873  	if flags.Recursive {
   874  		cmdargs = append(cmdargs, "--recursive")
   875  	}
   876  	if flags.RecurseSubmodules {
   877  		cmdargs = append(cmdargs, "--recurse-submodules")
   878  	}
   879  	if len(flags.Reference) > 0 {
   880  		cmdargs = append(cmdargs, "--reference", flags.Reference)
   881  	}
   882  	if len(flags.SeparateGit) > 0 {
   883  		cmdargs = append(cmdargs, "--separate-git-dir", flags.SeparateGit)
   884  	}
   885  	if flags.Shared {
   886  		cmdargs = append(cmdargs, "--shared")
   887  	}
   888  	if flags.SingleBranch {
   889  		cmdargs = append(cmdargs, "--single-branch")
   890  	}
   891  	if len(flags.TemplateDirectory) > 0 {
   892  		cmdargs = append(cmdargs, "--template", flags.TemplateDirectory)
   893  	}
   894  	if len(flags.Upload) > 0 {
   895  		cmdargs = append(cmdargs, "--upload-pack", flags.Upload)
   896  	}
   897  	if flags.Verbose {
   898  		cmdargs = append(cmdargs, "--verbose")
   899  	}
   900  
   901  	// Now args
   902  	cmdargs = append(cmdargs, args...)
   903  	cmd := subprocess.ExecCommand("git", cmdargs...)
   904  
   905  	// Assign all streams direct
   906  	cmd.Stdout = os.Stdout
   907  	cmd.Stderr = os.Stderr
   908  	cmd.Stdin = os.Stdin
   909  
   910  	err := cmd.Start()
   911  	if err != nil {
   912  		return fmt.Errorf("Failed to start git clone: %v", err)
   913  	}
   914  
   915  	err = cmd.Wait()
   916  	if err != nil {
   917  		return fmt.Errorf("git clone failed: %v", err)
   918  	}
   919  
   920  	return nil
   921  }
   922  
   923  // CachedRemoteRefs returns the list of branches & tags for a remote which are
   924  // currently cached locally. No remote request is made to verify them.
   925  func CachedRemoteRefs(remoteName string) ([]*Ref, error) {
   926  	var ret []*Ref
   927  	cmd := subprocess.ExecCommand("git", "show-ref")
   928  
   929  	outp, err := cmd.StdoutPipe()
   930  	if err != nil {
   931  		return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
   932  	}
   933  	cmd.Start()
   934  	scanner := bufio.NewScanner(outp)
   935  
   936  	r := regexp.MustCompile(fmt.Sprintf(`([0-9a-fA-F]{40})\s+refs/remotes/%v/(.*)`, remoteName))
   937  	for scanner.Scan() {
   938  		if match := r.FindStringSubmatch(scanner.Text()); match != nil {
   939  			name := strings.TrimSpace(match[2])
   940  			// Don't match head
   941  			if name == "HEAD" {
   942  				continue
   943  			}
   944  
   945  			sha := match[1]
   946  			ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
   947  		}
   948  	}
   949  	return ret, cmd.Wait()
   950  }
   951  
   952  // RemoteRefs returns a list of branches & tags for a remote by actually
   953  // accessing the remote vir git ls-remote
   954  func RemoteRefs(remoteName string) ([]*Ref, error) {
   955  	var ret []*Ref
   956  	cmd := subprocess.ExecCommand("git", "ls-remote", "--heads", "--tags", "-q", remoteName)
   957  
   958  	outp, err := cmd.StdoutPipe()
   959  	if err != nil {
   960  		return nil, fmt.Errorf("Failed to call git ls-remote: %v", err)
   961  	}
   962  	cmd.Start()
   963  	scanner := bufio.NewScanner(outp)
   964  
   965  	r := regexp.MustCompile(`([0-9a-fA-F]{40})\s+refs/(heads|tags)/(.*)`)
   966  	for scanner.Scan() {
   967  		if match := r.FindStringSubmatch(scanner.Text()); match != nil {
   968  			name := strings.TrimSpace(match[3])
   969  			// Don't match head
   970  			if name == "HEAD" {
   971  				continue
   972  			}
   973  
   974  			sha := match[1]
   975  			if match[2] == "heads" {
   976  				ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
   977  			} else {
   978  				ret = append(ret, &Ref{name, RefTypeRemoteTag, sha})
   979  			}
   980  		}
   981  	}
   982  	return ret, cmd.Wait()
   983  }
   984  
   985  // GetTrackedFiles returns a list of files which are tracked in Git which match
   986  // the pattern specified (standard wildcard form)
   987  // Both pattern and the results are relative to the current working directory, not
   988  // the root of the repository
   989  func GetTrackedFiles(pattern string) ([]string, error) {
   990  	safePattern := sanitizePattern(pattern)
   991  	rootWildcard := len(safePattern) < len(pattern) && strings.ContainsRune(safePattern, '*')
   992  
   993  	var ret []string
   994  	cmd := subprocess.ExecCommand("git",
   995  		"-c", "core.quotepath=false", // handle special chars in filenames
   996  		"ls-files",
   997  		"--cached", // include things which are staged but not committed right now
   998  		"--",       // no ambiguous patterns
   999  		safePattern)
  1000  
  1001  	outp, err := cmd.StdoutPipe()
  1002  	if err != nil {
  1003  		return nil, fmt.Errorf("Failed to call git ls-files: %v", err)
  1004  	}
  1005  	cmd.Start()
  1006  	scanner := bufio.NewScanner(outp)
  1007  	for scanner.Scan() {
  1008  		line := scanner.Text()
  1009  
  1010  		// If the given pattern is a root wildcard, skip all files which
  1011  		// are not direct descendants of the repository's root.
  1012  		//
  1013  		// This matches the behavior of how .gitattributes performs
  1014  		// filename matches.
  1015  		if rootWildcard && filepath.Dir(line) != "." {
  1016  			continue
  1017  		}
  1018  
  1019  		ret = append(ret, strings.TrimSpace(line))
  1020  	}
  1021  	return ret, cmd.Wait()
  1022  }
  1023  
  1024  func sanitizePattern(pattern string) string {
  1025  	if strings.HasPrefix(pattern, "/") {
  1026  		return pattern[1:]
  1027  	}
  1028  
  1029  	return pattern
  1030  }
  1031  
  1032  // GetFilesChanged returns a list of files which were changed, either between 2
  1033  // commits, or at a single commit if you only supply one argument and a blank
  1034  // string for the other
  1035  func GetFilesChanged(from, to string) ([]string, error) {
  1036  	var files []string
  1037  	args := []string{
  1038  		"-c", "core.quotepath=false", // handle special chars in filenames
  1039  		"diff-tree",
  1040  		"--no-commit-id",
  1041  		"--name-only",
  1042  		"-r",
  1043  	}
  1044  
  1045  	if len(from) > 0 {
  1046  		args = append(args, from)
  1047  	}
  1048  	if len(to) > 0 {
  1049  		args = append(args, to)
  1050  	}
  1051  	args = append(args, "--") // no ambiguous patterns
  1052  
  1053  	cmd := subprocess.ExecCommand("git", args...)
  1054  	outp, err := cmd.StdoutPipe()
  1055  	if err != nil {
  1056  		return nil, fmt.Errorf("Failed to call git diff: %v", err)
  1057  	}
  1058  	if err := cmd.Start(); err != nil {
  1059  		return nil, fmt.Errorf("Failed to start git diff: %v", err)
  1060  	}
  1061  	scanner := bufio.NewScanner(outp)
  1062  	for scanner.Scan() {
  1063  		files = append(files, strings.TrimSpace(scanner.Text()))
  1064  	}
  1065  	if err := cmd.Wait(); err != nil {
  1066  		return nil, fmt.Errorf("Git diff failed: %v", err)
  1067  	}
  1068  
  1069  	return files, err
  1070  }
  1071  
  1072  // IsFileModified returns whether the filepath specified is modified according
  1073  // to `git status`. A file is modified if it has uncommitted changes in the
  1074  // working copy or the index. This includes being untracked.
  1075  func IsFileModified(filepath string) (bool, error) {
  1076  
  1077  	args := []string{
  1078  		"-c", "core.quotepath=false", // handle special chars in filenames
  1079  		"status",
  1080  		"--porcelain",
  1081  		"--", // separator in case filename ambiguous
  1082  		filepath,
  1083  	}
  1084  	cmd := subprocess.ExecCommand("git", args...)
  1085  	outp, err := cmd.StdoutPipe()
  1086  	if err != nil {
  1087  		return false, lfserrors.Wrap(err, "Failed to call git status")
  1088  	}
  1089  	if err := cmd.Start(); err != nil {
  1090  		return false, lfserrors.Wrap(err, "Failed to start git status")
  1091  	}
  1092  	matched := false
  1093  	for scanner := bufio.NewScanner(outp); scanner.Scan(); {
  1094  		line := scanner.Text()
  1095  		// Porcelain format is "<I><W> <filename>"
  1096  		// Where <I> = index status, <W> = working copy status
  1097  		if len(line) > 3 {
  1098  			// Double-check even though should be only match
  1099  			if strings.TrimSpace(line[3:]) == filepath {
  1100  				matched = true
  1101  				// keep consuming output to exit cleanly
  1102  				// will typically fall straight through anyway due to 1 line output
  1103  			}
  1104  		}
  1105  	}
  1106  	if err := cmd.Wait(); err != nil {
  1107  		return false, lfserrors.Wrap(err, "Git status failed")
  1108  	}
  1109  
  1110  	return matched, nil
  1111  }