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