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