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
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"
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
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"
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.
46 // If the RefType is unrecognized, Prefix() will panic.
47 func (t RefType) Prefix() (string, bool) {
49 case RefTypeLocalBranch:
50 return "refs/heads", true
51 case RefTypeRemoteBranch:
52 return "refs/remotes", true
54 return "refs/tags", true
55 case RefTypeRemoteTag:
56 return "refs/remotes/tags", true
62 func ParseRef(absRef, sha string) *Ref {
64 if strings.HasPrefix(absRef, "refs/heads/") {
66 r.Type = RefTypeLocalBranch
67 } else if strings.HasPrefix(absRef, "refs/tags/") {
69 r.Type = RefTypeLocalTag
70 } else if strings.HasPrefix(absRef, "refs/remotes/tags/") {
72 r.Type = RefTypeRemoteTag
73 } else if strings.HasPrefix(absRef, "refs/remotes/") {
75 r.Type = RefTypeRemoteBranch
87 // A git reference (branch, tag etc)
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
98 // refs/remotes/origin/my-feature
99 func (r *Ref) Refspec() string {
104 prefix, ok := r.Type.Prefix()
106 return fmt.Sprintf("%s/%s", prefix, r.Name)
112 // Some top level information about a commit (only first line of message)
113 type CommitSummary struct {
122 CommitterEmail string
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.
136 if !IsGitVersionAtLeast("2.8.0") {
137 filterOverride = "cat"
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",
148 // Invoke Git with disabled LFS filters
149 func gitNoLFS(args ...string) *subprocess.Cmd {
150 return subprocess.ExecCommand("git", gitConfigNoLFS(args...)...)
153 func gitNoLFSSimple(args ...string) (string, error) {
154 return subprocess.SimpleExec("git", gitConfigNoLFS(args...)...)
157 func gitNoLFSBuffered(args ...string) (*subprocess.BufferedCmd, error) {
158 return subprocess.BufferedExec("git", gitConfigNoLFS(args...)...)
161 // Invoke Git with enabled LFS filters
162 func git(args ...string) *subprocess.Cmd {
163 return subprocess.ExecCommand("git", args...)
166 func gitSimple(args ...string) (string, error) {
167 return subprocess.SimpleExec("git", args...)
170 func gitBuffered(args ...string) (*subprocess.BufferedCmd, error) {
171 return subprocess.BufferedExec("git", args...)
174 func CatFile() (*subprocess.BufferedCmd, error) {
175 return gitNoLFSBuffered("cat-file", "--batch-check")
178 func DiffIndex(ref string, cached bool) (*bufio.Scanner, error) {
179 args := []string{"diff-index", "-M"}
181 args = append(args, "--cached")
183 args = append(args, ref)
185 cmd, err := gitBuffered(args...)
189 if err = cmd.Stdin.Close(); err != nil {
193 return bufio.NewScanner(cmd.Stdout), nil
196 func HashObject(r io.Reader) (string, error) {
197 cmd := gitNoLFS("hash-object", "--stdin")
199 out, err := cmd.Output()
201 return "", fmt.Errorf("Error building Git blob OID: %s", err)
204 return string(bytes.TrimSpace(out)), nil
207 func Log(args ...string) (*subprocess.BufferedCmd, error) {
208 logArgs := append([]string{"log"}, args...)
209 return gitNoLFSBuffered(logArgs...)
212 func LsRemote(remote, remoteRef string) (string, error) {
214 return "", errors.New("remote required")
217 return gitNoLFSSimple("ls-remote", remote)
220 return gitNoLFSSimple("ls-remote", remote, remoteRef)
223 func LsTree(ref string) (*subprocess.BufferedCmd, error) {
224 return gitNoLFSBuffered(
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
234 func ResolveRef(ref string) (*Ref, error) {
235 outp, err := gitNoLFSSimple("rev-parse", ref, "--symbolic-full-name", ref)
237 return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
240 return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
243 lines := strings.Split(outp, "\n")
244 fullref := &Ref{Sha: lines[0]}
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
254 // parse the symbolic-full-name
255 fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1])
259 func ResolveRefs(refnames []string) ([]*Ref, error) {
260 refs := make([]*Ref, len(refnames))
261 for i, name := range refnames {
262 ref, err := ResolveRef(name)
272 func CurrentRef() (*Ref, error) {
273 return ResolveRef("HEAD")
276 func (c *Configuration) CurrentRemoteRef() (*Ref, error) {
277 remoteref, err := c.RemoteRefNameForCurrentBranch()
282 return ResolveRef(remoteref)
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()
293 if ref.Type == RefTypeHEAD || ref.Type == RefTypeOther {
294 return "", errors.New("not on a branch")
297 remote := c.RemoteForBranch(ref.Name)
299 return "", fmt.Errorf("remote not found for branch %q", ref.Name)
302 remotebranch := c.RemoteBranchForLocalBranch(ref.Name)
304 return fmt.Sprintf("refs/remotes/%s/%s", remote, remotebranch), nil
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))
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/") {
324 func RemoteList() ([]string, error) {
325 cmd := gitNoLFS("remote")
327 outp, err := cmd.StdoutPipe()
329 return nil, fmt.Errorf("Failed to call git remote: %v", err)
334 scanner := bufio.NewScanner(outp)
338 ret = append(ret, strings.TrimSpace(scanner.Text()))
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")
349 outp, err := cmd.StdoutPipe()
351 return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
356 if err := cmd.Start(); err != nil {
360 scanner := bufio.NewScanner(outp)
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)
369 rtype, name := ParseRefToTypeAndName(parts[1])
370 if rtype != RefTypeLocalBranch && rtype != RefTypeLocalTag {
374 refs = append(refs, &Ref{name, rtype, parts[0]})
377 return refs, cmd.Wait()
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
383 func UpdateRef(ref *Ref, to []byte, reason string) error {
384 return UpdateRefIn("", ref, to, reason)
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)}
393 args = append(args, "-m", reason)
396 cmd := gitNoLFS(args...)
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()
409 for _, r := range remotes {
415 if err = ValidateRemoteURL(remote); err == nil {
419 return fmt.Errorf("Invalid remote name: %q", remote)
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
431 if strings.Contains(remote, ":") {
434 return fmt.Errorf("Invalid remote name: %q", remote)
439 case "ssh", "http", "https", "git":
442 return fmt.Errorf("Invalid remote url protocol %q in %q", u.Scheme, remote)
446 func UpdateIndexFromStdin() *subprocess.Cmd {
447 return git("update-index", "-q", "--refresh", "--stdin")
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)`,
460 outp, err := cmd.StdoutPipe()
462 return nil, fmt.Errorf("Failed to call git for-each-ref: %v", err)
467 scanner := bufio.NewScanner(outp)
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
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)
478 line := scanner.Text()
479 if match := regex.FindStringSubmatch(line); match != nil {
482 reftype, ref := ParseRefToTypeAndName(fullref)
483 if reftype == RefTypeRemoteBranch || reftype == RefTypeRemoteTag {
484 if !includeRemoteBranches {
487 if onlyRemote != "" && !strings.HasPrefix(ref, onlyRemote+"/") {
491 // This is a ref we might use
493 commitDate, err := ParseGitDate(match[3])
497 if commitDate.Before(since) {
501 tracerx.Printf("RECENT: %v (%v)", ref, commitDate)
502 ret = append(ret, &Ref{ref, reftype, sha})
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/"
517 if fullref == "HEAD" {
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):]
529 } else if strings.HasPrefix(fullref, localTagPrefix) {
530 name = fullref[len(localTagPrefix):]
539 // Parse a Git date formatted in ISO 8601 format (%ci/%ai)
540 func ParseGitDate(str string) (time.Time, error) {
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.
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)
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")
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)
562 out, err := cmd.CombinedOutput()
564 return nil, fmt.Errorf("Failed to call git show: %v %v", err, string(out))
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
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]
584 ret.Subject = strings.TrimRight(fields[9], "\n")
588 msg := fmt.Sprintf("Unexpected output from git show: %v", string(out))
589 return nil, errors.New(msg)
593 func GitAndRootDirs() (string, string, error) {
594 cmd := gitNoLFS("rev-parse", "--git-dir", "--show-toplevel")
595 buf := &bytes.Buffer{}
598 out, err := cmd.Output()
599 output := string(out)
601 return "", "", fmt.Errorf("Failed to call git rev-parse --git-dir --show-toplevel: %q", buf.String())
604 paths := strings.Split(output, "\n")
605 pathLen := len(paths)
607 for i := 0; i < pathLen; i++ {
608 paths[i], err = tools.TranslateCygwinPath(paths[i])
612 return "", "", fmt.Errorf("Bad git rev-parse output: %q", output)
615 absGitDir, err := filepath.Abs(paths[0])
617 return "", "", fmt.Errorf("Error converting %q to absolute: %s", paths[0], err)
620 if pathLen == 1 || len(paths[1]) == 0 {
621 return absGitDir, "", nil
624 absRootDir := paths[1]
625 return absGitDir, absRootDir, nil
628 func RootDir() (string, error) {
629 cmd := gitNoLFS("rev-parse", "--show-toplevel")
630 out, err := cmd.Output()
632 return "", fmt.Errorf("Failed to call git rev-parse --show-toplevel: %v %v", err, string(out))
635 path := strings.TrimSpace(string(out))
636 path, err = tools.TranslateCygwinPath(path)
638 return filepath.Abs(path)
644 func GitDir() (string, error) {
645 cmd := gitNoLFS("rev-parse", "--git-dir")
646 out, err := cmd.Output()
648 return "", fmt.Errorf("Failed to call git rev-parse --git-dir: %v %v", err, string(out))
650 path := strings.TrimSpace(string(out))
652 return filepath.Abs(path)
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) {
670 // There are some worktrees
672 direntries, err := dirf.Readdir(0)
676 for _, dirfi := range direntries {
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)
684 tracerx.Printf("Error reading %v for worktree, skipping: %v", headfile, err)
687 worktrees = append(worktrees, ref)
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
696 headfile := filepath.Join(storageDir, "HEAD")
697 ref, err := parseRefFile(headfile)
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)
704 return worktrees, nil
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)
713 contents := strings.TrimSpace(string(bytes))
714 if strings.HasPrefix(contents, "ref:") {
715 contents = strings.TrimSpace(contents[4:])
717 return ResolveRef(contents)
720 // IsBare returns whether or not a repository is bare. It requires that the
721 // current working directory is a repository.
723 // If there was an error determining whether or not the repository is bare, it
725 func IsBare() (bool, error) {
726 s, err := subprocess.SimpleExec(
727 "git", "rev-parse", "--is-bare-repository")
733 return strconv.ParseBool(s)
736 // For compatibility with git clone we must mirror all flags in CloneWithoutFilters
737 type CloneFlags struct {
738 // --template <template_directory>
739 TemplateDirectory string
756 // -o <name> --origin <name>
758 // -b <name> --branch <name>
760 // -u <upload-pack> --upload-pack <pack>
762 // --reference <repository>
764 // --reference-if-able <repository>
765 ReferenceIfAble string
768 // --separate-git-dir <git dir>
774 // --recurse-submodules
775 RecurseSubmodules bool
776 // -c <value> --config <value>
780 // --no-single-branch
788 // --shallow-since <date>
790 // --shallow-since <date>
791 ShallowExclude string
792 // --shallow-submodules
793 ShallowSubmodules bool
794 // --no-shallow-submodules
795 NoShallowSubmodules bool
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 {
804 cmdargs := []string{"clone"}
808 cmdargs = append(cmdargs, "--bare")
810 if len(flags.Branch) > 0 {
811 cmdargs = append(cmdargs, "--branch", flags.Branch)
813 if len(flags.Config) > 0 {
814 cmdargs = append(cmdargs, "--config", flags.Config)
816 if len(flags.Depth) > 0 {
817 cmdargs = append(cmdargs, "--depth", flags.Depth)
819 if flags.Dissociate {
820 cmdargs = append(cmdargs, "--dissociate")
823 cmdargs = append(cmdargs, "--ipv4")
826 cmdargs = append(cmdargs, "--ipv6")
829 cmdargs = append(cmdargs, "--local")
832 cmdargs = append(cmdargs, "--mirror")
834 if flags.NoCheckout {
835 cmdargs = append(cmdargs, "--no-checkout")
837 if flags.NoHardlinks {
838 cmdargs = append(cmdargs, "--no-hardlinks")
840 if flags.NoSingleBranch {
841 cmdargs = append(cmdargs, "--no-single-branch")
843 if len(flags.Origin) > 0 {
844 cmdargs = append(cmdargs, "--origin", flags.Origin)
847 cmdargs = append(cmdargs, "--progress")
850 cmdargs = append(cmdargs, "--quiet")
853 cmdargs = append(cmdargs, "--recursive")
855 if flags.RecurseSubmodules {
856 cmdargs = append(cmdargs, "--recurse-submodules")
858 if len(flags.Reference) > 0 {
859 cmdargs = append(cmdargs, "--reference", flags.Reference)
861 if len(flags.ReferenceIfAble) > 0 {
862 cmdargs = append(cmdargs, "--reference-if-able", flags.ReferenceIfAble)
864 if len(flags.SeparateGit) > 0 {
865 cmdargs = append(cmdargs, "--separate-git-dir", flags.SeparateGit)
868 cmdargs = append(cmdargs, "--shared")
870 if flags.SingleBranch {
871 cmdargs = append(cmdargs, "--single-branch")
873 if len(flags.TemplateDirectory) > 0 {
874 cmdargs = append(cmdargs, "--template", flags.TemplateDirectory)
876 if len(flags.Upload) > 0 {
877 cmdargs = append(cmdargs, "--upload-pack", flags.Upload)
880 cmdargs = append(cmdargs, "--verbose")
882 if len(flags.ShallowSince) > 0 {
883 cmdargs = append(cmdargs, "--shallow-since", flags.ShallowSince)
885 if len(flags.ShallowExclude) > 0 {
886 cmdargs = append(cmdargs, "--shallow-exclude", flags.ShallowExclude)
888 if flags.ShallowSubmodules {
889 cmdargs = append(cmdargs, "--shallow-submodules")
891 if flags.NoShallowSubmodules {
892 cmdargs = append(cmdargs, "--no-shallow-submodules")
895 cmdargs = append(cmdargs, "--jobs", strconv.FormatInt(flags.Jobs, 10))
899 cmdargs = append(cmdargs, args...)
900 cmd := gitNoLFS(cmdargs...)
902 // Assign all streams direct
903 cmd.Stdout = os.Stdout
904 cmd.Stderr = os.Stderr
909 return fmt.Errorf("Failed to start git clone: %v", err)
914 return fmt.Errorf("git clone failed: %v", err)
920 // Checkout performs an invocation of `git-checkout(1)` applying the given
921 // treeish, paths, and force option, if given.
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"}
928 args = append(args, "--force")
931 if len(treeish) > 0 {
932 args = append(args, treeish)
936 args = append(args, append([]string{"--"}, paths...)...)
939 _, err := gitNoLFSSimple(args...)
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) {
947 cmd := gitNoLFS("show-ref")
949 outp, err := cmd.StdoutPipe()
951 return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
954 scanner := bufio.NewScanner(outp)
956 r := regexp.MustCompile(fmt.Sprintf(`([0-9a-fA-F]{40})\s+refs/remotes/%v/(.*)`, remoteName))
958 if match := r.FindStringSubmatch(scanner.Text()); match != nil {
959 name := strings.TrimSpace(match[2])
966 ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
969 return ret, cmd.Wait()
972 // Fetch performs a fetch with no arguments against the given remotes.
973 func Fetch(remotes ...string) error {
974 if len(remotes) == 0 {
979 if len(remotes) > 1 {
980 args = []string{"--multiple", "--"}
982 args = append(args, remotes...)
984 _, err := gitNoLFSSimple(append([]string{"fetch"}, args...)...)
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) {
992 cmd := gitNoLFS("ls-remote", "--heads", "--tags", "-q", remoteName)
994 outp, err := cmd.StdoutPipe()
996 return nil, fmt.Errorf("Failed to call git ls-remote: %v", err)
999 scanner := bufio.NewScanner(outp)
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])
1011 if match[2] == "heads" {
1012 ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
1014 ret = append(ret, &Ref{name, RefTypeRemoteTag, sha})
1018 return ret, cmd.Wait()
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("")
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
1030 func AllRefsIn(wd string) ([]*Ref, error) {
1032 "for-each-ref", "--format=%(objectname)%00%(refname)")
1035 outp, err := cmd.StdoutPipe()
1037 return nil, lfserrors.Wrap(err, "cannot open pipe")
1041 refs := make([]*Ref, 0)
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())
1052 typ, name := ParseRefToTypeAndName(parts[1])
1054 refs = append(refs, &Ref{
1061 if err := scanner.Err(); err != nil {
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, '*')
1078 "-c", "core.quotepath=false", // handle special chars in filenames
1080 "--cached", // include things which are staged but not committed right now
1081 "--", // no ambiguous patterns
1084 outp, err := cmd.StdoutPipe()
1086 return nil, fmt.Errorf("Failed to call git ls-files: %v", err)
1089 scanner := bufio.NewScanner(outp)
1090 for scanner.Scan() {
1091 line := scanner.Text()
1093 // If the given pattern is a root wildcard, skip all files which
1094 // are not direct descendants of the repository's root.
1096 // This matches the behavior of how .gitattributes performs
1097 // filename matches.
1098 if rootWildcard && filepath.Dir(line) != "." {
1102 ret = append(ret, strings.TrimSpace(line))
1104 return ret, cmd.Wait()
1107 func sanitizePattern(pattern string) string {
1108 if strings.HasPrefix(pattern, "/") {
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) {
1121 "-c", "core.quotepath=false", // handle special chars in filenames
1129 args = append(args, from)
1132 args = append(args, to)
1134 args = append(args, "--") // no ambiguous patterns
1136 cmd := gitNoLFS(args...)
1137 outp, err := cmd.StdoutPipe()
1139 return nil, fmt.Errorf("Failed to call git diff: %v", err)
1141 if err := cmd.Start(); err != nil {
1142 return nil, fmt.Errorf("Failed to start git diff: %v", err)
1144 scanner := bufio.NewScanner(outp)
1145 for scanner.Scan() {
1146 files = append(files, strings.TrimSpace(scanner.Text()))
1148 if err := cmd.Wait(); err != nil {
1149 return nil, fmt.Errorf("Git diff failed: %v", err)
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) {
1161 "-c", "core.quotepath=false", // handle special chars in filenames
1164 "--", // separator in case filename ambiguous
1168 outp, err := cmd.StdoutPipe()
1170 return false, lfserrors.Wrap(err, "Failed to call git status")
1172 if err := cmd.Start(); err != nil {
1173 return false, lfserrors.Wrap(err, "Failed to start git status")
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
1181 // Double-check even though should be only match
1182 if strings.TrimSpace(line[3:]) == filepath {
1184 // keep consuming output to exit cleanly
1185 // will typically fall straight through anyway due to 1 line output
1189 if err := cmd.Wait(); err != nil {
1190 return false, lfserrors.Wrap(err, "Git status failed")
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.
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 {
1207 out, err := gitSimple("status", "--porcelain")
1211 return len(out) != 0, nil