f3bf4ea90b
this change improves drastically pre-push behaviour, by not sending lfs objects which are already on a remote. Works perfectly with pushing new branches and tags. currently pre-push command analyse "local sha1" vs "remote sha1" of the ref being pushed and if "remote sha1" is available locally tries to send only lfs objects introduced with new commits. why this is broken: - remote branch might have moved forward (local repo is not up to date). In this case you have no chance to isolate new lfs objects ("remote sha1" does not exist locally) and git-lfs sends everything from the local branch history. - remote branch does not exist (or new tag is pushed). Same consequences. But what is important - local repository always have remote references, from which user created his local branch and started making some local changes. So, all we have to do is to identify new lfs objects which do not exist on remote references. And all this can be easily achieved with the same all mighty git rev-list command. This change makes git-lfs usable with gerrit, where changes are uploaded by using magic gerrit branches which does not really exist. i.e. git push origin master:refs/for/master in this case "refs/for/master" does not exist and git feeds all 0-s as "remote sha1".
184 lines
4.8 KiB
Go
184 lines
4.8 KiB
Go
package commands
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/github/git-lfs/lfs"
|
|
"github.com/github/git-lfs/vendor/_nuts/github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
prePushCmd = &cobra.Command{
|
|
Use: "pre-push",
|
|
Short: "Implements the Git pre-push hook",
|
|
Run: prePushCommand,
|
|
}
|
|
prePushDryRun = false
|
|
prePushDeleteBranch = "(delete)"
|
|
prePushMissingErrMsg = "%s is an LFS pointer to %s, which does not exist in .git/lfs/objects.\n\nRun 'git lfs fsck' to verify Git LFS objects."
|
|
)
|
|
|
|
// 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:
|
|
// <local ref> <local sha1> <remote ref> <remote sha1>
|
|
//
|
|
// In the typical case, prePushCommand will get a list of git objects being
|
|
// pushed by using the following:
|
|
//
|
|
// git rev-list --objects <local sha1> ^<remote sha1>
|
|
//
|
|
// 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
|
|
scanOpt := &lfs.ScanRefsOptions{ScanMode: lfs.ScanLeftToRemoteMode, RemoteName: lfs.Config.CurrentRemote}
|
|
pointers, err := lfs.ScanRefs(left, right, scanOpt)
|
|
if err != nil {
|
|
Panic(err, "Error scanning for Git LFS files")
|
|
}
|
|
|
|
totalSize := int64(0)
|
|
for _, p := range pointers {
|
|
totalSize += p.Size
|
|
}
|
|
|
|
// Objects to skip because they're missing locally but on server
|
|
var skipObjects map[string]struct{}
|
|
|
|
if !prePushDryRun {
|
|
// Do this as a pre-flight check since upload queue starts immediately
|
|
skipObjects = prePushCheckForMissingObjects(pointers)
|
|
}
|
|
|
|
uploadQueue := lfs.NewUploadQueue(len(pointers), totalSize, prePushDryRun)
|
|
|
|
for _, pointer := range pointers {
|
|
if prePushDryRun {
|
|
Print("push %s", pointer.Name)
|
|
continue
|
|
}
|
|
|
|
if _, skip := skipObjects[pointer.Oid]; skip {
|
|
// object missing locally but on server, don't bother
|
|
continue
|
|
}
|
|
|
|
u, wErr := lfs.NewUploadable(pointer.Oid, pointer.Name)
|
|
if wErr != nil {
|
|
if cleanPointerErr, ok := wErr.Err.(*lfs.CleanedPointerError); ok {
|
|
Exit(prePushMissingErrMsg, pointer.Name, cleanPointerErr.Pointer.Oid)
|
|
} else if Debugging || wErr.Panic {
|
|
Panic(wErr.Err, wErr.Error())
|
|
} else {
|
|
Exit(wErr.Error())
|
|
}
|
|
}
|
|
|
|
uploadQueue.Add(u)
|
|
}
|
|
|
|
if !prePushDryRun {
|
|
uploadQueue.Wait()
|
|
for _, err := range uploadQueue.Errors() {
|
|
if Debugging || err.Panic {
|
|
LoggedError(err.Err, err.Error())
|
|
} else {
|
|
Error(err.Error())
|
|
}
|
|
}
|
|
|
|
if len(uploadQueue.Errors()) > 0 {
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
func prePushCheckForMissingObjects(pointers []*lfs.WrappedPointer) (objectsOnServer map[string]struct{}) {
|
|
var missingLocalObjects []*lfs.WrappedPointer
|
|
var missingSize int64
|
|
var skipObjects = make(map[string]struct{}, len(pointers))
|
|
for _, pointer := range pointers {
|
|
if !lfs.ObjectExistsOfSize(pointer.Oid, pointer.Size) {
|
|
// We think we need to push this but we don't have it
|
|
// Store for server checking later
|
|
missingLocalObjects = append(missingLocalObjects, pointer)
|
|
missingSize += pointer.Size
|
|
}
|
|
}
|
|
if len(missingLocalObjects) == 0 {
|
|
return nil
|
|
}
|
|
|
|
checkQueue := lfs.NewDownloadCheckQueue(len(missingLocalObjects), missingSize, false)
|
|
for _, p := range missingLocalObjects {
|
|
checkQueue.Add(lfs.NewDownloadCheckable(p))
|
|
}
|
|
// this channel is filled with oids for which Check() succeeded & Transfer() was called
|
|
transferc := checkQueue.Watch()
|
|
go func() {
|
|
for oid := range transferc {
|
|
skipObjects[oid] = struct{}{}
|
|
}
|
|
}()
|
|
checkQueue.Wait()
|
|
return skipObjects
|
|
}
|
|
|
|
// 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)
|
|
}
|