Skip to content

Commit

Permalink
Implemented 'git lfs clone'
Browse files Browse the repository at this point in the history
  • Loading branch information
sinbad committed Feb 5, 2016
1 parent 94d356c commit 463abf5
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
70 changes: 70 additions & 0 deletions 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)
}
9 changes: 7 additions & 2 deletions commands/command_pull.go
Expand Up @@ -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() {
Expand Down
26 changes: 26 additions & 0 deletions 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] <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.
58 changes: 58 additions & 0 deletions git/git.go
Expand Up @@ -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="
Expand Down

0 comments on commit 463abf5

Please sign in to comment.