From 463abf595fde8f627a170a352c2104e8a044a673 Mon Sep 17 00:00:00 2001 From: Steve Streeting Date: Fri, 5 Feb 2016 17:45:12 +0000 Subject: [PATCH] Implemented 'git lfs clone' --- commands/command_clone.go | 70 +++++++++++++++++++++++++++++++++++ commands/command_pull.go | 9 ++++- docs/man/git-lfs-clone.1.ronn | 26 +++++++++++++ git/git.go | 58 +++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 commands/command_clone.go create mode 100644 docs/man/git-lfs-clone.1.ronn diff --git a/commands/command_clone.go b/commands/command_clone.go new file mode 100644 index 0000000000..c225ec8656 --- /dev/null +++ b/commands/command_clone.go @@ -0,0 +1,70 @@ +package commands + +import ( + "os" + "path" + "path/filepath" + "strings" + + "github.com/github/git-lfs/git" + "github.com/github/git-lfs/lfs" + "github.com/github/git-lfs/vendor/_nuts/github.com/spf13/cobra" +) + +var ( + cloneCmd = &cobra.Command{ + Use: "clone", + Run: cloneCommand, + } +) + +func cloneCommand(cmd *cobra.Command, args []string) { + + // We pass all args to git clone + err := git.CloneWithoutFilters(args) + if err != nil { + Panic(err, "Error(s) during clone") + } + + // now execute pull (need to be inside dir) + cwd, err := os.Getwd() + if err != nil { + Panic(err, "Unable to derive current working dir") + } + + // Either the last argument was a relative or local dir, or we have to + // derive it from the clone URL + clonedir, err := filepath.Abs(args[len(args)-1]) + if err != nil || !lfs.DirExists(clonedir) { + // Derive from clone URL instead + base := path.Base(args[len(args)-1]) + if strings.HasSuffix(base, ".git") { + base = base[:len(base)-4] + } + clonedir, _ = filepath.Abs(base) + if !lfs.DirExists(clonedir) { + Exit("Unable to find clone dir at %q", clonedir) + } + } + + err = os.Chdir(clonedir) + if err != nil { + Panic(err, "Unable to change directory to clone dir %q", clonedir) + } + + // Make sure we pop back to dir we started in at the end + defer os.Chdir(cwd) + + // Also need to derive dirs now + lfs.ResolveDirs() + requireInRepo() + + // Now just call pull with default args + lfs.Config.CurrentRemote = "origin" // always origin after clone + pull(nil, nil) + +} + +func init() { + RootCmd.AddCommand(cloneCmd) +} diff --git a/commands/command_pull.go b/commands/command_pull.go index 92ccdd438c..fef9096c06 100644 --- a/commands/command_pull.go +++ b/commands/command_pull.go @@ -35,15 +35,20 @@ func pullCommand(cmd *cobra.Command, args []string) { lfs.Config.CurrentRemote = defaultRemote } + pull(determineIncludeExcludePaths(pullIncludeArg, pullExcludeArg)) + +} + +func pull(includePaths, excludePaths []string) { + ref, err := git.CurrentRef() if err != nil { Panic(err, "Could not pull") } - includePaths, excludePaths := determineIncludeExcludePaths(pullIncludeArg, pullExcludeArg) - c := fetchRefToChan(ref.Sha, includePaths, excludePaths) checkoutFromFetchChan(includePaths, excludePaths, c) + } func init() { diff --git a/docs/man/git-lfs-clone.1.ronn b/docs/man/git-lfs-clone.1.ronn new file mode 100644 index 0000000000..4b8ed09c84 --- /dev/null +++ b/docs/man/git-lfs-clone.1.ronn @@ -0,0 +1,26 @@ +git-lfs-clone(1) -- Efficiently clone a LFS-enabled repository +======================================================================== + +## SYNOPSIS + +`git lfs clone` [git clone options] [] + +## DESCRIPTION + +Clone an LFS enabled Git repository more efficiently by disabling LFS during the +git clone, then performing a 'git lfs pull' directly afterwards. + +This is faster than a regular 'git clone' because that will download LFS content +using the smudge filter, which is executed individually per file in the working +copy. This is relatively inefficient compared to the batch mode and parallel +downloads performed by 'git lfs pull'. + +## OPTIONS + +All options supported by 'git clone' + +## SEE ALSO + +git-clone(1), git-lfs-pull(1). + +Part of the git-lfs(1) suite. diff --git a/git/git.go b/git/git.go index 6d33eb4c02..5477249c64 100644 --- a/git/git.go +++ b/git/git.go @@ -622,6 +622,64 @@ func IsVersionAtLeast(actualVersion, desiredVersion string) bool { return actual >= atleast } +// CloneWithoutFilters clones a git repo but without the smudge filter enabled +// so that files in the working copy will be pointers and not real LFS data +func CloneWithoutFilters(args []string) error { + + // Disable the LFS filters while cloning to speed things up + // this is especially effective on Windows where even calling git-lfs at all + // with --skip-smudge is costly across many files in a checkout + cmdargs := []string{ + "-c", "filter.lfs.smudge=", + "-c", "filter.lfs.required=false", + "clone"} + cmdargs = append(cmdargs, args...) + cmd := execCommand("git", cmdargs...) + + // Spool stdout directly to our own + cmd.Stdout = os.Stdout + // stderr needs filtering + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("Failed to get stderr from git clone: %v", err) + } + err = cmd.Start() + if err != nil { + return fmt.Errorf("Failed to start git clone: %v", err) + } + + // Filter stderr to exclude messages caused by disabling the filters + // As of git 2.7 it still tries to call the blank filter but required=false + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + s := scanner.Text() + // Swallow all the known messages from intentionally breaking filter + if strings.Contains(s, "error: external filter") || + strings.Contains(s, "error: cannot fork") || + // Linux / Mac messages + strings.Contains(s, "error: cannot run : No such file or directory") || + strings.Contains(s, "warning: Clone succeeded, but checkout failed") || + strings.Contains(s, "You can inspect what was checked out with 'git status'") || + strings.Contains(s, "retry the checkout") || + strings.Contains(s, "substr") || + // Windows messages + strings.Contains(s, "error: cannot spawn : No such file or directory") || + // blank formatting + len(strings.TrimSpace(s)) == 0 { + continue + } + os.Stderr.WriteString(s) + os.Stderr.WriteString("\n") // stripped by scanner + } + + err = cmd.Wait() + if err != nil { + return fmt.Errorf("git clone failed: %v", err) + } + + return nil +} + // An env for an exec.Command without GIT_TRACE var env []string var traceEnv = "GIT_TRACE="