From 847a547f3468d18347c8d82bd3da65efcd4cc1bf Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Fri, 24 Apr 2015 10:41:11 -0600 Subject: [PATCH] split the push command code --- commands/command_push.go | 136 ++++---------------------- commands/commands_pre_push.go | 174 ++++++++++++++++++++++++++++++++++ commands/pre_push_test.go | 41 ++++++++ 3 files changed, 235 insertions(+), 116 deletions(-) create mode 100644 commands/commands_pre_push.go create mode 100644 commands/pre_push_test.go diff --git a/commands/command_push.go b/commands/command_push.go index fa8b1104..e0f8050e 100644 --- a/commands/command_push.go +++ b/commands/command_push.go @@ -1,56 +1,37 @@ package commands import ( - "fmt" "github.com/github/git-lfs/git" "github.com/github/git-lfs/lfs" - "github.com/github/git-lfs/pointer" "github.com/github/git-lfs/scanner" - "github.com/rubyist/tracerx" "github.com/spf13/cobra" "io/ioutil" "os" - "path/filepath" "strings" ) var ( pushCmd = &cobra.Command{ Use: "push", - Short: "Push files to the Git LFS endpoint", + Short: "Push files to the Git LFS server", Run: pushCommand, } - dryRun = false - useStdin = false - deleteBranch = "(delete)" + pushDryRun = false + pushDeleteBranch = "(delete)" + useStdin = false + + // shares some global vars and functions with commmands_pre_push.go ) -// pushCommand is the command that's run via `git lfs push`. It has two modes -// of operation. The primary mode is run via the git pre-push hook. The pre-push -// hook passes two arguments on the command line: -// 1. Name of the remote to which the push is being done -// 2. URL to which the push is being done +// pushCommand pushes local objects to a Git LFS server. It takes two +// arguments: // -// The hook receives commit information on stdin in the form: -// +// ` ` // -// In the typical case, pushCommand will get a list of git objects being pushed -// by using the following: -// git rev-list --objects ^ +// Both a remote name ("origin") or a remote URL are accepted. // -// If any of those git objects are associated with Git LFS objects, those -// objects will be pushed to the Git LFS API. -// -// In the case of pushing a new branch, the list of git objects will be all of -// the git objects in this branch. -// -// In the case of deleting a branch, no attempts to push Git LFS objects will be -// made. -// -// The other mode of operation is the dry run mode. In this mode, the repo -// and refspec are passed on the command line. pushCommand will calculate the -// git objects that would be pushed in a similar manner as above and will print -// out each file name. +// pushCommand calculates the git objects to send by looking comparing the range +// of commits between the local and remote git servers. func pushCommand(cmd *cobra.Command, args []string) { var left, right string @@ -72,20 +53,20 @@ func pushCommand(cmd *cobra.Command, args []string) { } left, right = decodeRefs(string(refsData)) - if left == deleteBranch { + if left == pushDeleteBranch { return } } else { - var repo, refspec string + var remoteArg, refArg string if len(args) < 1 { - Print("Usage: git lfs push --dry-run [refspec]") + Print("Usage: git lfs push --dry-run [ref]") return } - repo = args[0] + remoteArg = args[0] if len(args) == 2 { - refspec = args[1] + refArg = args[1] } localRef, err := git.CurrentRef() @@ -94,7 +75,7 @@ func pushCommand(cmd *cobra.Command, args []string) { } left = localRef - remoteRef, err := git.LsRemote(repo, refspec) + remoteRef, err := git.LsRemote(remoteArg, refArg) if err != nil { Panic(err, "Error getting remote ref") } @@ -111,7 +92,7 @@ func pushCommand(cmd *cobra.Command, args []string) { } for i, pointer := range pointers { - if dryRun { + if pushDryRun { Print("push %s", pointer.Name) continue } @@ -125,85 +106,8 @@ func pushCommand(cmd *cobra.Command, args []string) { } } -// pushAsset pushes the asset with the given oid to the Git LFS API. -func pushAsset(oid, filename string, index, totalFiles int) *lfs.WrappedError { - tracerx.Printf("checking_asset: %s %s %d/%d", oid, filename, index, totalFiles) - path, err := lfs.LocalMediaPath(oid) - if err != nil { - return lfs.Errorf(err, "Error uploading file %s (%s)", filename, oid) - } - - if err := ensureFile(filename, path); err != nil { - return lfs.Errorf(err, "Error uploading file %s (%s)", filename, oid) - } - - cb, file, cbErr := lfs.CopyCallbackFile("push", filename, index, totalFiles) - if cbErr != nil { - Error(cbErr.Error()) - } - - if file != nil { - defer file.Close() - } - - fmt.Fprintf(os.Stderr, "Uploading %s\n", filename) - return lfs.Upload(path, filename, cb) -} - -// ensureFile makes sure that the cleanPath exists before pushing it. If it -// does not exist, it attempts to clean it by reading the file at smudgePath. -func ensureFile(smudgePath, cleanPath string) error { - if _, err := os.Stat(cleanPath); err == nil { - return nil - } - - expectedOid := filepath.Base(cleanPath) - localPath := filepath.Join(lfs.LocalWorkingDir, smudgePath) - file, err := os.Open(localPath) - if err != nil { - return err - } - - defer file.Close() - - stat, err := file.Stat() - if err != nil { - return err - } - - cleaned, err := pointer.Clean(file, stat.Size(), nil) - if err != nil { - return err - } - - cleaned.Close() - - if expectedOid != cleaned.Oid { - return fmt.Errorf("Expected %s to have an OID of %s, got %s", smudgePath, expectedOid, cleaned.Oid) - } - - return nil -} - -// decodeRefs pulls the sha1s out of the line read from the pre-push -// hook's stdin. -func decodeRefs(input string) (string, string) { - refs := strings.Split(strings.TrimSpace(input), " ") - var left, right string - - if len(refs) > 1 { - left = refs[1] - } - - if len(refs) > 3 { - right = "^" + refs[3] - } - - return left, right -} - func init() { - pushCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "Do everything except actually send the updates") + pushCmd.Flags().BoolVarP(&pushDryRun, "dry-run", "d", false, "Do everything except actually send the updates") pushCmd.Flags().BoolVarP(&useStdin, "stdin", "s", false, "Take refs on stdin (for pre-push hook)") RootCmd.AddCommand(pushCmd) } diff --git a/commands/commands_pre_push.go b/commands/commands_pre_push.go new file mode 100644 index 00000000..4ab90cb3 --- /dev/null +++ b/commands/commands_pre_push.go @@ -0,0 +1,174 @@ +package commands + +import ( + "fmt" + "github.com/github/git-lfs/lfs" + "github.com/github/git-lfs/pointer" + "github.com/github/git-lfs/scanner" + "github.com/rubyist/tracerx" + "github.com/spf13/cobra" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +var ( + prePushCmd = &cobra.Command{ + Use: "pre-push", + Short: "Implements the Git pre-push hook", + Run: prePushCommand, + } + prePushDryRun = false + prePushDeleteBranch = "(delete)" +) + +// prePushCommand is run through Git's pre-push hook. The pre-push hook passes +// two arguments on the command line: +// +// 1. Name of the remote to which the push is being done +// 2. URL to which the push is being done +// +// The hook receives commit information on stdin in the form: +// +// +// In the typical case, prePushCommand will get a list of git objects being +// pushed by using the following: +// +// git rev-list --objects ^ +// +// If any of those git objects are associated with Git LFS objects, those +// objects will be pushed to the Git LFS API. +// +// In the case of pushing a new branch, the list of git objects will be all of +// the git objects in this branch. +// +// In the case of deleting a branch, no attempts to push Git LFS objects will be +// made. +func prePushCommand(cmd *cobra.Command, args []string) { + var left, right string + + if len(args) == 0 { + Print("This should be run through Git's pre-push hook. Run `git lfs update` to install it.") + os.Exit(1) + } + + lfs.Config.CurrentRemote = args[0] + + refsData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + Panic(err, "Error reading refs on stdin") + } + + if len(refsData) == 0 { + return + } + + left, right = decodeRefs(string(refsData)) + if left == prePushDeleteBranch { + return + } + + // Just use scanner here + pointers, err := scanner.Scan(left, right) + if err != nil { + Panic(err, "Error scanning for Git LFS files") + } + + for i, pointer := range pointers { + if prePushDryRun { + Print("push %s", pointer.Name) + continue + } + + if wErr := pushAsset(pointer.Oid, pointer.Name, i+1, len(pointers)); wErr != nil { + if Debugging || wErr.Panic { + Panic(wErr.Err, wErr.Error()) + } else { + Exit(wErr.Error()) + } + } + } +} + +// pushAsset pushes the asset with the given oid to the Git LFS API. +func pushAsset(oid, filename string, index, totalFiles int) *lfs.WrappedError { + tracerx.Printf("checking_asset: %s %s %d/%d", oid, filename, index, totalFiles) + path, err := lfs.LocalMediaPath(oid) + if err != nil { + return lfs.Errorf(err, "Error uploading file %s (%s)", filename, oid) + } + + if err := ensureFile(filename, path); err != nil { + return lfs.Errorf(err, "Error uploading file %s (%s)", filename, oid) + } + + cb, file, cbErr := lfs.CopyCallbackFile("push", filename, index, totalFiles) + if cbErr != nil { + Error(cbErr.Error()) + } + + if file != nil { + defer file.Close() + } + + fmt.Fprintf(os.Stderr, "Uploading %s\n", filename) + return lfs.Upload(path, filename, cb) +} + +// ensureFile makes sure that the cleanPath exists before pushing it. If it +// does not exist, it attempts to clean it by reading the file at smudgePath. +func ensureFile(smudgePath, cleanPath string) error { + if _, err := os.Stat(cleanPath); err == nil { + return nil + } + + expectedOid := filepath.Base(cleanPath) + localPath := filepath.Join(lfs.LocalWorkingDir, smudgePath) + file, err := os.Open(localPath) + if err != nil { + return err + } + + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return err + } + + cleaned, err := pointer.Clean(file, stat.Size(), nil) + if err != nil { + return err + } + + cleaned.Close() + + if expectedOid != cleaned.Oid { + return fmt.Errorf("Expected %s to have an OID of %s, got %s", smudgePath, expectedOid, cleaned.Oid) + } + + return nil +} + +// decodeRefs pulls the sha1s out of the line read from the pre-push +// hook's stdin. +func decodeRefs(input string) (string, string) { + refs := strings.Split(strings.TrimSpace(input), " ") + var left, right string + + if len(refs) > 1 { + left = refs[1] + } + + if len(refs) > 3 { + right = "^" + refs[3] + } + + return left, right +} + +func init() { + prePushCmd.Flags().BoolVarP(&prePushDryRun, "dry-run", "d", false, "Do everything except actually send the updates") + RootCmd.AddCommand(prePushCmd) +} diff --git a/commands/pre_push_test.go b/commands/pre_push_test.go new file mode 100644 index 00000000..9e2d62f5 --- /dev/null +++ b/commands/pre_push_test.go @@ -0,0 +1,41 @@ +package commands + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestPrePushWithEmptyQueue(t *testing.T) { + repo := NewRepository(t, "empty") + defer repo.Test() + + cmd := repo.Command("pre-push", "--dry-run", "origin", "https://git-remote.com") + cmd.Input = strings.NewReader("refs/heads/master master refs/heads/master 2206c37dddba83f58b1ada72709a6b60cf8b058e") + cmd.Output = "" +} + +func TestPrePushToMaster(t *testing.T) { + repo := NewRepository(t, "empty") + defer repo.Test() + + cmd := repo.Command("pre-push", "--dry-run", "origin", "https://git-remote.com") + cmd.Input = strings.NewReader("refs/heads/master master refs/heads/master 2206c37dddba83f58b1ada72709a6b60cf8b058e") + cmd.Output = "push a.dat" + + cmd.Before(func() { + repo.GitCmd("remote", "remove", "origin") + + originPath := filepath.Join(Root, "commands", "repos", "empty.git") + repo.GitCmd("remote", "add", "origin", originPath) + + repo.GitCmd("fetch") + + repo.WriteFile(filepath.Join(repo.Path, ".gitattributes"), "*.dat filter=lfs -crlf\n") + + // Add a Git LFS file + repo.WriteFile(filepath.Join(repo.Path, "a.dat"), "some data") + repo.GitCmd("add", "a.dat") + repo.GitCmd("commit", "-m", "a") + }) +}