Implemented 'git lfs clone'

This commit is contained in:
Steve Streeting 2016-02-05 17:45:12 +00:00
parent e801c47fb7
commit 894794bf3f
4 changed files with 161 additions and 2 deletions

70
commands/command_clone.go Normal file

@ -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)
}

@ -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() {

@ -0,0 +1,26 @@
git-lfs-clone(1) -- Efficiently clone a LFS-enabled repository
========================================================================
## SYNOPSIS
`git lfs clone` [git clone options] <repository> [<directory>]
## 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.

@ -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="