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