Merge branch 'master' into release-ubuntu-artful
This commit is contained in:
commit
c6f8937168
@ -72,13 +72,9 @@ func cloneCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
requireInRepo()
|
||||
|
||||
// Now just call pull with default args
|
||||
// Support --origin option to clone
|
||||
var remote string
|
||||
if len(cloneFlags.Origin) > 0 {
|
||||
remote = cloneFlags.Origin
|
||||
} else {
|
||||
remote = "origin"
|
||||
cfg.SetRemote(cloneFlags.Origin)
|
||||
}
|
||||
|
||||
if ref, err := git.CurrentRef(); err == nil {
|
||||
@ -86,10 +82,9 @@ func cloneCommand(cmd *cobra.Command, args []string) {
|
||||
filter := buildFilepathFilter(cfg, includeArg, excludeArg)
|
||||
if cloneFlags.NoCheckout || cloneFlags.Bare {
|
||||
// If --no-checkout or --bare then we shouldn't check out, just fetch instead
|
||||
cfg.CurrentRemote = remote
|
||||
fetchRef(ref.Name, filter)
|
||||
} else {
|
||||
pull(remote, filter)
|
||||
pull(filter)
|
||||
err := postCloneSubmodules(args)
|
||||
if err != nil {
|
||||
Exit("Error performing 'git lfs pull' for submodules: %v", err)
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
func envCommand(cmd *cobra.Command, args []string) {
|
||||
config.ShowConfigWarnings = true
|
||||
endpoint := getAPIClient().Endpoints.Endpoint("download", cfg.CurrentRemote)
|
||||
|
||||
gitV, err := git.Version()
|
||||
if err != nil {
|
||||
@ -20,6 +19,8 @@ func envCommand(cmd *cobra.Command, args []string) {
|
||||
Print(gitV)
|
||||
Print("")
|
||||
|
||||
if cfg.IsDefaultRemote() {
|
||||
endpoint := getAPIClient().Endpoints.Endpoint("download", cfg.Remote())
|
||||
if len(endpoint.Url) > 0 {
|
||||
access := getAPIClient().Endpoints.AccessFor(endpoint.Url)
|
||||
Print("Endpoint=%s (auth=%s)", endpoint.Url, access)
|
||||
@ -27,6 +28,7 @@ func envCommand(cmd *cobra.Command, args []string) {
|
||||
Print(" SSH=%s:%s", endpoint.SshUserAndHost, endpoint.SshPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, remote := range cfg.Remotes() {
|
||||
remoteEndpoint := getAPIClient().Endpoints.RemoteEndpoint("download", remote)
|
||||
|
@ -39,12 +39,9 @@ func fetchCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(args) > 0 {
|
||||
// Remote is first arg
|
||||
if err := git.ValidateRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q", args[0])
|
||||
if err := cfg.SetValidRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q: %s", args[0], err)
|
||||
}
|
||||
cfg.CurrentRemote = args[0]
|
||||
} else {
|
||||
cfg.CurrentRemote = ""
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
@ -104,7 +101,7 @@ func fetchCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
if !success {
|
||||
c := getAPIClient()
|
||||
e := c.Endpoints.Endpoint("download", cfg.CurrentRemote)
|
||||
e := c.Endpoints.Endpoint("download", cfg.Remote())
|
||||
Exit("error: failed to fetch some objects from '%s'", e.Url)
|
||||
}
|
||||
}
|
||||
@ -184,7 +181,7 @@ func fetchRecent(fetchconf lfs.FetchPruneConfig, alreadyFetchedRefs []*git.Ref,
|
||||
if fetchconf.FetchRecentRefsDays > 0 {
|
||||
Print("Fetching recent branches within %v days", fetchconf.FetchRecentRefsDays)
|
||||
refsSince := time.Now().AddDate(0, 0, -fetchconf.FetchRecentRefsDays)
|
||||
refs, err := git.RecentBranches(refsSince, fetchconf.FetchRecentRefsIncludeRemotes, cfg.CurrentRemote)
|
||||
refs, err := git.RecentBranches(refsSince, fetchconf.FetchRecentRefsIncludeRemotes, cfg.Remote())
|
||||
if err != nil {
|
||||
Panic(err, "Could not scan for recent refs")
|
||||
}
|
||||
@ -268,20 +265,10 @@ func scanAll() []*lfs.WrappedPointer {
|
||||
// Fetch and report completion of each OID to a channel (optional, pass nil to skip)
|
||||
// Returns true if all completed with no errors, false if errors were written to stderr/log
|
||||
func fetchAndReportToChan(allpointers []*lfs.WrappedPointer, filter *filepathfilter.Filter, out chan<- *lfs.WrappedPointer) bool {
|
||||
// Lazily initialize the current remote.
|
||||
if len(cfg.CurrentRemote) == 0 {
|
||||
// Actively find the default remote, don't just assume origin
|
||||
defaultRemote, err := cfg.GitConfig().DefaultRemote()
|
||||
if err != nil {
|
||||
Exit("No default remote")
|
||||
}
|
||||
cfg.CurrentRemote = defaultRemote
|
||||
}
|
||||
|
||||
ready, pointers, meter := readyAndMissingPointers(allpointers, filter)
|
||||
q := newDownloadQueue(
|
||||
getTransferManifestOperationRemote("download", cfg.CurrentRemote),
|
||||
cfg.CurrentRemote, tq.WithProgress(meter),
|
||||
getTransferManifestOperationRemote("download", cfg.Remote()),
|
||||
cfg.Remote(), tq.WithProgress(meter),
|
||||
)
|
||||
|
||||
if out != nil {
|
||||
|
@ -69,8 +69,8 @@ func filterCommand(cmd *cobra.Command, args []string) {
|
||||
if supportsDelay {
|
||||
q = tq.NewTransferQueue(
|
||||
tq.Download,
|
||||
getTransferManifestOperationRemote("download", cfg.CurrentRemote),
|
||||
cfg.CurrentRemote,
|
||||
getTransferManifestOperationRemote("download", cfg.Remote()),
|
||||
cfg.Remote(),
|
||||
)
|
||||
go infiniteTransferBuffer(q, available)
|
||||
}
|
||||
|
@ -28,7 +28,11 @@ func lockCommand(cmd *cobra.Command, args []string) {
|
||||
Exit(err.Error())
|
||||
}
|
||||
|
||||
lockClient := newLockClient(lockRemote)
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.SetRemote(lockRemote)
|
||||
}
|
||||
|
||||
lockClient := newLockClient()
|
||||
defer lockClient.Close()
|
||||
|
||||
lock, err := lockClient.LockFile(path)
|
||||
@ -90,7 +94,7 @@ func lockPath(file string) (string, error) {
|
||||
|
||||
func init() {
|
||||
RegisterCommand("lock", lockCommand, func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", cfg.CurrentRemote, lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", "", lockRemoteHelp)
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
|
||||
})
|
||||
}
|
||||
|
@ -22,7 +22,11 @@ func locksCommand(cmd *cobra.Command, args []string) {
|
||||
Exit("Error building filters: %v", err)
|
||||
}
|
||||
|
||||
lockClient := newLockClient(lockRemote)
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.SetRemote(lockRemote)
|
||||
}
|
||||
|
||||
lockClient := newLockClient()
|
||||
defer lockClient.Close()
|
||||
|
||||
locks, err := lockClient.SearchLocks(filters, locksCmdFlags.Limit, locksCmdFlags.Local)
|
||||
@ -109,7 +113,7 @@ func (l *locksFlags) Filters() (map[string]string, error) {
|
||||
|
||||
func init() {
|
||||
RegisterCommand("locks", locksCommand, func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", cfg.CurrentRemote, lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", "", lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&locksCmdFlags.Path, "path", "p", "", "filter locks results matching a particular path")
|
||||
cmd.Flags().StringVarP(&locksCmdFlags.Id, "id", "i", "", "filter locks results matching a particular ID")
|
||||
cmd.Flags().IntVarP(&locksCmdFlags.Limit, "limit", "l", 0, "optional limit for number of results to return")
|
||||
|
@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -20,6 +21,11 @@ var (
|
||||
// in the migration.
|
||||
migrateExcludeRefs []string
|
||||
|
||||
// migrateSkipFetch assumes that the client has the latest copy of
|
||||
// remote references, and thus should not contact the remote for a set
|
||||
// of updated references.
|
||||
migrateSkipFetch bool
|
||||
|
||||
// migrateEverything indicates the presence of the --everything flag,
|
||||
// and instructs 'git lfs migrate' to migrate all local references.
|
||||
migrateEverything bool
|
||||
@ -122,7 +128,7 @@ func includeExcludeRefs(l *log.Logger, args []string) (include, exclude []string
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
include = append(include, ref.Name)
|
||||
include = append(include, ref.Refspec())
|
||||
}
|
||||
|
||||
if hardcore {
|
||||
@ -142,7 +148,7 @@ func includeExcludeRefs(l *log.Logger, args []string) (include, exclude []string
|
||||
}
|
||||
|
||||
for _, ref := range localRefs {
|
||||
include = append(include, ref.Name)
|
||||
include = append(include, ref.Refspec())
|
||||
}
|
||||
} else {
|
||||
// Otherwise, if neither --include-ref=<ref> or
|
||||
@ -154,7 +160,9 @@ func includeExcludeRefs(l *log.Logger, args []string) (include, exclude []string
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
exclude = append(exclude, remoteRefs...)
|
||||
for _, rr := range remoteRefs {
|
||||
exclude = append(exclude, rr.Refspec())
|
||||
}
|
||||
}
|
||||
|
||||
return include, exclude, nil
|
||||
@ -163,28 +171,40 @@ func includeExcludeRefs(l *log.Logger, args []string) (include, exclude []string
|
||||
// getRemoteRefs returns a fully qualified set of references belonging to all
|
||||
// remotes known by the currently checked-out repository, or an error if those
|
||||
// references could not be determined.
|
||||
func getRemoteRefs(l *log.Logger) ([]string, error) {
|
||||
var refs []string
|
||||
func getRemoteRefs(l *log.Logger) ([]*git.Ref, error) {
|
||||
var refs []*git.Ref
|
||||
|
||||
remotes, err := git.RemoteList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !migrateSkipFetch {
|
||||
w := l.Waiter("migrate: Fetching remote refs")
|
||||
if err := git.Fetch(remotes...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.Complete()
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
refsForRemote, err := git.RemoteRefs(remote)
|
||||
var refsForRemote []*git.Ref
|
||||
if migrateSkipFetch {
|
||||
refsForRemote, err = git.CachedRemoteRefs(remote)
|
||||
} else {
|
||||
refsForRemote, err = git.RemoteRefs(remote)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ref := range refsForRemote {
|
||||
refs = append(refs, formatRefName(ref, remote))
|
||||
for _, rr := range refsForRemote {
|
||||
// HACK(@ttaylorr): add remote name to fully-qualify
|
||||
// references:
|
||||
rr.Name = fmt.Sprintf("%s/%s", remote, rr.Name)
|
||||
|
||||
refs = append(refs, rr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,6 +272,7 @@ func init() {
|
||||
cmd.PersistentFlags().StringSliceVar(&migrateIncludeRefs, "include-ref", nil, "An explicit list of refs to include")
|
||||
cmd.PersistentFlags().StringSliceVar(&migrateExcludeRefs, "exclude-ref", nil, "An explicit list of refs to exclude")
|
||||
cmd.PersistentFlags().BoolVar(&migrateEverything, "everything", false, "Migrate all local references")
|
||||
cmd.PersistentFlags().BoolVar(&migrateSkipFetch, "skip-fetch", false, "Assume up-to-date remote references.")
|
||||
|
||||
cmd.AddCommand(importCmd, info)
|
||||
})
|
||||
|
@ -32,7 +32,7 @@ func postCheckoutCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
requireGitVersion()
|
||||
|
||||
lockClient := newLockClient(cfg.CurrentRemote)
|
||||
lockClient := newLockClient()
|
||||
|
||||
// Skip this hook if no lockable patterns have been configured
|
||||
if len(lockClient.GetLockablePatterns()) == 0 {
|
||||
|
@ -24,7 +24,7 @@ func postCommitCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
requireGitVersion()
|
||||
|
||||
lockClient := newLockClient(cfg.CurrentRemote)
|
||||
lockClient := newLockClient()
|
||||
|
||||
// Skip this hook if no lockable patterns have been configured
|
||||
if len(lockClient.GetLockablePatterns()) == 0 {
|
||||
|
@ -24,7 +24,7 @@ func postMergeCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
requireGitVersion()
|
||||
|
||||
lockClient := newLockClient(cfg.CurrentRemote)
|
||||
lockClient := newLockClient()
|
||||
|
||||
// Skip this hook if no lockable patterns have been configured
|
||||
if len(lockClient.GetLockablePatterns()) == 0 {
|
||||
|
@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@ -46,59 +47,59 @@ func prePushCommand(cmd *cobra.Command, args []string) {
|
||||
requireGitVersion()
|
||||
|
||||
// Remote is first arg
|
||||
if err := git.ValidateRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q", args[0])
|
||||
if err := cfg.SetValidRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q: %s", args[0], err)
|
||||
}
|
||||
|
||||
ctx := newUploadContext(args[0], prePushDryRun)
|
||||
|
||||
gitscanner, err := ctx.buildGitScanner()
|
||||
if err != nil {
|
||||
ctx := newUploadContext(prePushDryRun)
|
||||
updates := prePushRefs(os.Stdin)
|
||||
if err := uploadForRefUpdates(ctx, updates, false); err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
defer gitscanner.Close()
|
||||
}
|
||||
|
||||
// prePushRefs parses commit information that the pre-push git hook receives:
|
||||
//
|
||||
// <local ref> <local sha1> <remote ref> <remote sha1>
|
||||
//
|
||||
// Each line describes a proposed update of the remote ref at the remote sha to
|
||||
// the local sha. Multiple updates can be received on multiple lines (such as
|
||||
// from 'git push --all'). These updates are typically received over STDIN.
|
||||
func prePushRefs(r io.Reader) []*refUpdate {
|
||||
scanner := bufio.NewScanner(r)
|
||||
refs := make([]*refUpdate, 0, 1)
|
||||
|
||||
// We can be passed multiple lines of refs
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tracerx.Printf("pre-push: %s", line)
|
||||
|
||||
left, _ := decodeRefs(line)
|
||||
if left == prePushDeleteBranch {
|
||||
left, right := decodeRefs(line)
|
||||
if left.Sha == prePushDeleteBranch {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := uploadLeftOrAll(gitscanner, ctx, left); err != nil {
|
||||
Print("Error scanning for Git LFS files in %q", left)
|
||||
ExitWithError(err)
|
||||
}
|
||||
refs = append(refs, newRefUpdate(cfg.Git, cfg.PushRemote(), left, right))
|
||||
}
|
||||
|
||||
ctx.Await()
|
||||
return refs
|
||||
}
|
||||
|
||||
// decodeRefs pulls the sha1s out of the line read from the pre-push
|
||||
// hook's stdin.
|
||||
func decodeRefs(input string) (string, string) {
|
||||
func decodeRefs(input string) (*git.Ref, *git.Ref) {
|
||||
refs := strings.Split(strings.TrimSpace(input), " ")
|
||||
var left, right string
|
||||
|
||||
if len(refs) > 1 {
|
||||
left = refs[1]
|
||||
for len(refs) < 4 {
|
||||
refs = append(refs, "")
|
||||
}
|
||||
|
||||
if len(refs) > 3 {
|
||||
right = "^" + refs[3]
|
||||
}
|
||||
|
||||
return left, right
|
||||
leftRef := git.ParseRef(refs[0], refs[1])
|
||||
rightRef := git.ParseRef(refs[2], refs[3])
|
||||
return leftRef, rightRef
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -18,29 +18,19 @@ func pullCommand(cmd *cobra.Command, args []string) {
|
||||
requireGitVersion()
|
||||
requireInRepo()
|
||||
|
||||
var remote string
|
||||
if len(args) > 0 {
|
||||
// Remote is first arg
|
||||
if err := git.ValidateRemote(args[0]); err != nil {
|
||||
Panic(err, fmt.Sprintf("Invalid remote name '%v'", args[0]))
|
||||
if err := cfg.SetValidRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q: %s", args[0], err)
|
||||
}
|
||||
remote = args[0]
|
||||
} else {
|
||||
// Actively find the default remote, don't just assume origin
|
||||
defaultRemote, err := cfg.GitConfig().DefaultRemote()
|
||||
if err != nil {
|
||||
Panic(err, "No default remote")
|
||||
}
|
||||
remote = defaultRemote
|
||||
}
|
||||
|
||||
includeArg, excludeArg := getIncludeExcludeArgs(cmd)
|
||||
filter := buildFilepathFilter(cfg, includeArg, excludeArg)
|
||||
pull(remote, filter)
|
||||
pull(filter)
|
||||
}
|
||||
|
||||
func pull(remote string, filter *filepathfilter.Filter) {
|
||||
cfg.CurrentRemote = remote
|
||||
func pull(filter *filepathfilter.Filter) {
|
||||
ref, err := git.CurrentRef()
|
||||
if err != nil {
|
||||
Panic(err, "Could not pull")
|
||||
@ -48,6 +38,7 @@ func pull(remote string, filter *filepathfilter.Filter) {
|
||||
|
||||
pointers := newPointerMap()
|
||||
meter := progress.NewMeter(progress.WithOSEnv(cfg.Os))
|
||||
remote := cfg.Remote()
|
||||
singleCheckout := newSingleCheckout(cfg.Git, remote)
|
||||
q := newDownloadQueue(singleCheckout.Manifest(), remote, tq.WithProgress(meter))
|
||||
gitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
|
||||
|
@ -19,82 +19,6 @@ var (
|
||||
// shares some global vars and functions with command_pre_push.go
|
||||
)
|
||||
|
||||
func uploadsBetweenRefAndRemote(ctx *uploadContext, refnames []string) {
|
||||
tracerx.Printf("Upload refs %v to remote %v", refnames, ctx.Remote)
|
||||
|
||||
gitscanner, err := ctx.buildGitScanner()
|
||||
if err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
defer gitscanner.Close()
|
||||
|
||||
refs, err := refsByNames(refnames)
|
||||
if err != nil {
|
||||
Error(err.Error())
|
||||
Exit("Error getting local refs.")
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
if err = uploadLeftOrAll(gitscanner, ctx, ref.Name); err != nil {
|
||||
Print("Error scanning for Git LFS files in the %q ref", ref.Name)
|
||||
ExitWithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
func uploadsWithObjectIDs(ctx *uploadContext, oids []string) {
|
||||
for _, oid := range oids {
|
||||
mp, err := ctx.gitfilter.ObjectPath(oid)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to find local media path:"))
|
||||
}
|
||||
|
||||
stat, err := os.Stat(mp)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to stat local media path"))
|
||||
}
|
||||
|
||||
uploadPointers(ctx, &lfs.WrappedPointer{
|
||||
Name: mp,
|
||||
Pointer: &lfs.Pointer{
|
||||
Oid: oid,
|
||||
Size: stat.Size(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
func refsByNames(refnames []string) ([]*git.Ref, error) {
|
||||
localrefs, err := git.LocalRefs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pushAll && len(refnames) == 0 {
|
||||
return localrefs, nil
|
||||
}
|
||||
|
||||
reflookup := make(map[string]*git.Ref, len(localrefs))
|
||||
for _, ref := range localrefs {
|
||||
reflookup[ref.Name] = ref
|
||||
}
|
||||
|
||||
refs := make([]*git.Ref, len(refnames))
|
||||
for i, name := range refnames {
|
||||
if ref, ok := reflookup[name]; ok {
|
||||
refs[i] = ref
|
||||
} else {
|
||||
refs[i] = &git.Ref{Name: name, Type: git.RefTypeOther, Sha: name}
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
// pushCommand pushes local objects to a Git LFS server. It takes two
|
||||
// arguments:
|
||||
//
|
||||
@ -113,12 +37,11 @@ func pushCommand(cmd *cobra.Command, args []string) {
|
||||
requireGitVersion()
|
||||
|
||||
// Remote is first arg
|
||||
if err := git.ValidateRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q", args[0])
|
||||
if err := cfg.SetValidRemote(args[0]); err != nil {
|
||||
Exit("Invalid remote name %q: %s", args[0], err)
|
||||
}
|
||||
|
||||
ctx := newUploadContext(args[0], pushDryRun)
|
||||
|
||||
ctx := newUploadContext(pushDryRun)
|
||||
if pushObjectIDs {
|
||||
if len(args) < 2 {
|
||||
Print("Usage: git lfs push --object-id <remote> <lfs-object-id> [lfs-object-id] ...")
|
||||
@ -136,6 +59,81 @@ func pushCommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func uploadsBetweenRefAndRemote(ctx *uploadContext, refnames []string) {
|
||||
tracerx.Printf("Upload refs %v to remote %v", refnames, ctx.Remote)
|
||||
|
||||
updates, err := lfsPushRefs(refnames, pushAll)
|
||||
if err != nil {
|
||||
Error(err.Error())
|
||||
Exit("Error getting local refs.")
|
||||
}
|
||||
|
||||
if err := uploadForRefUpdates(ctx, updates, pushAll); err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func uploadsWithObjectIDs(ctx *uploadContext, oids []string) {
|
||||
pointers := make([]*lfs.WrappedPointer, len(oids))
|
||||
for i, oid := range oids {
|
||||
mp, err := ctx.gitfilter.ObjectPath(oid)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to find local media path:"))
|
||||
}
|
||||
|
||||
stat, err := os.Stat(mp)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to stat local media path"))
|
||||
}
|
||||
|
||||
pointers[i] = &lfs.WrappedPointer{
|
||||
Name: mp,
|
||||
Pointer: &lfs.Pointer{
|
||||
Oid: oid,
|
||||
Size: stat.Size(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
uploadPointers(ctx, pointers...)
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
// lfsPushRefs returns valid ref updates from the given ref and --all arguments.
|
||||
// Either one or more refs can be explicitly specified, or --all indicates all
|
||||
// local refs are pushed.
|
||||
func lfsPushRefs(refnames []string, pushAll bool) ([]*refUpdate, error) {
|
||||
localrefs, err := git.LocalRefs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pushAll && len(refnames) == 0 {
|
||||
refs := make([]*refUpdate, len(localrefs))
|
||||
for i, lr := range localrefs {
|
||||
refs[i] = newRefUpdate(cfg.Git, cfg.PushRemote(), lr, nil)
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
reflookup := make(map[string]*git.Ref, len(localrefs))
|
||||
for _, ref := range localrefs {
|
||||
reflookup[ref.Name] = ref
|
||||
}
|
||||
|
||||
refs := make([]*refUpdate, len(refnames))
|
||||
for i, name := range refnames {
|
||||
if left, ok := reflookup[name]; ok {
|
||||
refs[i] = newRefUpdate(cfg.Git, cfg.PushRemote(), left, nil)
|
||||
} else {
|
||||
left := &git.Ref{Name: name, Type: git.RefTypeOther, Sha: name}
|
||||
refs[i] = newRefUpdate(cfg.Git, cfg.PushRemote(), left, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterCommand("push", pushCommand, func(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVarP(&pushDryRun, "dry-run", "d", false, "Do everything except actually send the updates")
|
||||
|
@ -205,7 +205,7 @@ ArgsLoop:
|
||||
}
|
||||
|
||||
// now flip read-only mode based on lockable / not lockable changes
|
||||
lockClient := newLockClient(cfg.CurrentRemote)
|
||||
lockClient := newLockClient()
|
||||
err = lockClient.FixFileWriteFlagsInDir(relpath, readOnlyPatterns, writeablePatterns)
|
||||
if err != nil {
|
||||
LoggedError(err, "Error changing lockable file permissions: %s", err)
|
||||
|
@ -35,7 +35,11 @@ func unlockCommand(cmd *cobra.Command, args []string) {
|
||||
Exit(unlockUsage)
|
||||
}
|
||||
|
||||
lockClient := newLockClient(lockRemote)
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.SetRemote(lockRemote)
|
||||
}
|
||||
|
||||
lockClient := newLockClient()
|
||||
defer lockClient.Close()
|
||||
|
||||
if hasPath {
|
||||
@ -131,7 +135,7 @@ func unlockAbortIfFileModifiedById(id string, lockClient *locking.Client) {
|
||||
|
||||
func init() {
|
||||
RegisterCommand("unlock", unlockCommand, func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", cfg.CurrentRemote, lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", "", lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&unlockCmdFlags.Id, "id", "i", "", "unlock a lock by its ID")
|
||||
cmd.Flags().BoolVarP(&unlockCmdFlags.Force, "force", "f", false, "forcibly break another user's lock(s)")
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
|
||||
|
@ -90,8 +90,8 @@ func closeAPIClient() error {
|
||||
return apiClient.Close()
|
||||
}
|
||||
|
||||
func newLockClient(remote string) *locking.Client {
|
||||
lockClient, err := locking.NewClient(remote, getAPIClient())
|
||||
func newLockClient() *locking.Client {
|
||||
lockClient, err := locking.NewClient(cfg.PushRemote(), getAPIClient())
|
||||
if err == nil {
|
||||
os.MkdirAll(cfg.LFSStorageDir(), 0755)
|
||||
err = lockClient.SetupFileCache(cfg.LFSStorageDir())
|
||||
|
239
commands/lockverifier.go
Normal file
239
commands/lockverifier.go
Normal file
@ -0,0 +1,239 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/git-lfs/git-lfs/tq"
|
||||
)
|
||||
|
||||
type verifyState byte
|
||||
|
||||
const (
|
||||
verifyStateUnknown verifyState = iota
|
||||
verifyStateEnabled
|
||||
verifyStateDisabled
|
||||
)
|
||||
|
||||
func verifyLocksForUpdates(lv *lockVerifier, updates []*refUpdate) {
|
||||
for _, update := range updates {
|
||||
lv.Verify(update.Right())
|
||||
}
|
||||
}
|
||||
|
||||
// lockVerifier verifies locked files before updating one or more refs.
|
||||
type lockVerifier struct {
|
||||
endpoint lfsapi.Endpoint
|
||||
verifyState verifyState
|
||||
verifiedRefs map[string]bool
|
||||
|
||||
// all existing locks
|
||||
ourLocks map[string]*refLock
|
||||
theirLocks map[string]*refLock
|
||||
|
||||
// locks from ourLocks that have been modified
|
||||
ownedLocks []*refLock
|
||||
|
||||
// locks from theirLocks that have been modified
|
||||
unownedLocks []*refLock
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) Verify(ref *git.Ref) {
|
||||
if lv.verifyState == verifyStateDisabled || lv.verifiedRefs[ref.Refspec()] {
|
||||
return
|
||||
}
|
||||
|
||||
lockClient := newLockClient()
|
||||
ours, theirs, err := lockClient.VerifiableLocks(ref, 0)
|
||||
if err != nil {
|
||||
if errors.IsNotImplementedError(err) {
|
||||
disableFor(lv.endpoint.Url)
|
||||
} else if lv.verifyState == verifyStateUnknown || lv.verifyState == verifyStateEnabled {
|
||||
if errors.IsAuthError(err) {
|
||||
if lv.verifyState == verifyStateUnknown {
|
||||
Error("WARNING: Authentication error: %s", err)
|
||||
} else if lv.verifyState == verifyStateEnabled {
|
||||
Exit("ERROR: Authentication error: %s", err)
|
||||
}
|
||||
} else {
|
||||
Print("Remote %q does not support the LFS locking API. Consider disabling it with:", cfg.PushRemote())
|
||||
Print(" $ git config lfs.%s.locksverify false", lv.endpoint.Url)
|
||||
if lv.verifyState == verifyStateEnabled {
|
||||
ExitWithError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if lv.verifyState == verifyStateUnknown {
|
||||
Print("Locking support detected on remote %q. Consider enabling it with:", cfg.PushRemote())
|
||||
Print(" $ git config lfs.%s.locksverify true", lv.endpoint.Url)
|
||||
}
|
||||
|
||||
lv.addLocks(ref, ours, lv.ourLocks)
|
||||
lv.addLocks(ref, theirs, lv.theirLocks)
|
||||
lv.verifiedRefs[ref.Refspec()] = true
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) addLocks(ref *git.Ref, locks []locking.Lock, set map[string]*refLock) {
|
||||
for _, l := range locks {
|
||||
if rl, ok := set[l.Path]; ok {
|
||||
if err := rl.Add(ref, l); err != nil {
|
||||
Error("WARNING: error adding %q lock for ref %q: %+v", l.Path, ref, err)
|
||||
}
|
||||
} else {
|
||||
set[l.Path] = lv.newRefLocks(ref, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determines if a filename is lockable. Implements lfs.GitScannerSet
|
||||
func (lv *lockVerifier) Contains(name string) bool {
|
||||
if lv == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := lv.theirLocks[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) LockedByThem(name string) bool {
|
||||
if lock, ok := lv.theirLocks[name]; ok {
|
||||
lv.unownedLocks = append(lv.unownedLocks, lock)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) LockedByUs(name string) bool {
|
||||
if lock, ok := lv.ourLocks[name]; ok {
|
||||
lv.ownedLocks = append(lv.ownedLocks, lock)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) UnownedLocks() []*refLock {
|
||||
return lv.unownedLocks
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) HasUnownedLocks() bool {
|
||||
return len(lv.unownedLocks) > 0
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) OwnedLocks() []*refLock {
|
||||
return lv.ownedLocks
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) HasOwnedLocks() bool {
|
||||
return len(lv.ownedLocks) > 0
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) Enabled() bool {
|
||||
return lv.verifyState == verifyStateEnabled
|
||||
}
|
||||
|
||||
func (lv *lockVerifier) newRefLocks(ref *git.Ref, l locking.Lock) *refLock {
|
||||
return &refLock{
|
||||
allRefs: lv.verifiedRefs,
|
||||
path: l.Path,
|
||||
refs: map[*git.Ref]locking.Lock{ref: l},
|
||||
}
|
||||
}
|
||||
|
||||
func newLockVerifier(m *tq.Manifest) *lockVerifier {
|
||||
lv := &lockVerifier{
|
||||
endpoint: getAPIClient().Endpoints.Endpoint("upload", cfg.PushRemote()),
|
||||
verifiedRefs: make(map[string]bool),
|
||||
ourLocks: make(map[string]*refLock),
|
||||
theirLocks: make(map[string]*refLock),
|
||||
}
|
||||
|
||||
// Do not check locks for standalone transfer, because there is no LFS
|
||||
// server to ask.
|
||||
if m.IsStandaloneTransfer() {
|
||||
lv.verifyState = verifyStateDisabled
|
||||
} else {
|
||||
lv.verifyState = getVerifyStateFor(lv.endpoint.Url)
|
||||
}
|
||||
|
||||
return lv
|
||||
}
|
||||
|
||||
// refLock represents a unique locked file path, potentially across multiple
|
||||
// refs. It tracks each individual lock in case different users locked the
|
||||
// same path across multiple refs.
|
||||
type refLock struct {
|
||||
path string
|
||||
allRefs map[string]bool
|
||||
refs map[*git.Ref]locking.Lock
|
||||
}
|
||||
|
||||
// Path returns the locked path.
|
||||
func (r *refLock) Path() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
// Owners returns the list of owners that locked this file, including what
|
||||
// specific refs the files were locked in. If a user locked a file on all refs,
|
||||
// don't bother listing them.
|
||||
//
|
||||
// Example: technoweenie, bob (refs: foo)
|
||||
func (r *refLock) Owners() string {
|
||||
users := make(map[string][]string, len(r.refs))
|
||||
for ref, lock := range r.refs {
|
||||
u := lock.Owner.Name
|
||||
if _, ok := users[u]; !ok {
|
||||
users[u] = make([]string, 0, len(r.refs))
|
||||
}
|
||||
users[u] = append(users[u], ref.Name)
|
||||
}
|
||||
|
||||
owners := make([]string, 0, len(users))
|
||||
for name, refs := range users {
|
||||
seenRefCount := 0
|
||||
for _, ref := range refs {
|
||||
if r.allRefs[ref] {
|
||||
seenRefCount++
|
||||
}
|
||||
}
|
||||
if seenRefCount == len(r.allRefs) { // lock is included in all refs, so don't list them
|
||||
owners = append(owners, name)
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Strings(refs)
|
||||
owners = append(owners, fmt.Sprintf("%s (refs: %s)", name, strings.Join(refs, ", ")))
|
||||
}
|
||||
sort.Strings(owners)
|
||||
return strings.Join(owners, ", ")
|
||||
}
|
||||
|
||||
func (r *refLock) Add(ref *git.Ref, l locking.Lock) error {
|
||||
r.refs[ref] = l
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVerifyStateFor returns whether or not lock verification is enabled for the
|
||||
// given url. If no state has been explicitly set, an "unknown" state will be
|
||||
// returned instead.
|
||||
func getVerifyStateFor(rawurl string) verifyState {
|
||||
uc := config.NewURLConfig(cfg.Git)
|
||||
|
||||
v, ok := uc.Get("lfs", rawurl, "locksverify")
|
||||
if !ok {
|
||||
if supportsLockingAPI(rawurl) {
|
||||
return verifyStateEnabled
|
||||
}
|
||||
return verifyStateUnknown
|
||||
}
|
||||
|
||||
if enabled, _ := strconv.ParseBool(v); enabled {
|
||||
return verifyStateEnabled
|
||||
}
|
||||
return verifyStateDisabled
|
||||
}
|
91
commands/refs.go
Normal file
91
commands/refs.go
Normal file
@ -0,0 +1,91 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
type refUpdate struct {
|
||||
git config.Environment
|
||||
remote string
|
||||
left *git.Ref
|
||||
right *git.Ref
|
||||
}
|
||||
|
||||
func newRefUpdate(g config.Environment, remote string, l, r *git.Ref) *refUpdate {
|
||||
return &refUpdate{
|
||||
git: g,
|
||||
remote: remote,
|
||||
left: l,
|
||||
right: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *refUpdate) Left() *git.Ref {
|
||||
return u.left
|
||||
}
|
||||
|
||||
func (u *refUpdate) LeftCommitish() string {
|
||||
return refCommitish(u.Left())
|
||||
}
|
||||
|
||||
func (u *refUpdate) Right() *git.Ref {
|
||||
if u.right == nil {
|
||||
u.right = defaultRemoteRef(u.git, u.remote, u.Left())
|
||||
}
|
||||
return u.right
|
||||
}
|
||||
|
||||
// defaultRemoteRef returns the remote ref receiving a push based on the current
|
||||
// repository config and local ref being pushed.
|
||||
//
|
||||
// See push.default rules in https://git-scm.com/docs/git-config
|
||||
func defaultRemoteRef(g config.Environment, remote string, left *git.Ref) *git.Ref {
|
||||
pushMode, _ := g.Get("push.default")
|
||||
switch pushMode {
|
||||
case "", "simple":
|
||||
brRemote, _ := g.Get(fmt.Sprintf("branch.%s.remote", left.Name))
|
||||
if brRemote == remote {
|
||||
// in centralized workflow, work like 'upstream' with an added safety to
|
||||
// refuse to push if the upstream branch’s name is different from the
|
||||
// local one.
|
||||
return trackingRef(g, left)
|
||||
}
|
||||
|
||||
// When pushing to a remote that is different from the remote you normally
|
||||
// pull from, work as current.
|
||||
return left
|
||||
case "upstream", "tracking":
|
||||
// push the current branch back to the branch whose changes are usually
|
||||
// integrated into the current branch
|
||||
return trackingRef(g, left)
|
||||
case "current":
|
||||
// push the current branch to update a branch with the same name on the
|
||||
// receiving end.
|
||||
return left
|
||||
default:
|
||||
tracerx.Printf("WARNING: %q push mode not supported", pushMode)
|
||||
return left
|
||||
}
|
||||
}
|
||||
|
||||
func trackingRef(g config.Environment, left *git.Ref) *git.Ref {
|
||||
if merge, ok := g.Get(fmt.Sprintf("branch.%s.merge", left.Name)); ok {
|
||||
return git.ParseRef(merge, "")
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
func (u *refUpdate) RightCommitish() string {
|
||||
return refCommitish(u.Right())
|
||||
}
|
||||
|
||||
func refCommitish(r *git.Ref) string {
|
||||
if len(r.Sha) > 0 {
|
||||
return r.Sha
|
||||
}
|
||||
return r.Name
|
||||
}
|
75
commands/refs_test.go
Normal file
75
commands/refs_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRefUpdateDefault(t *testing.T) {
|
||||
pushModes := []string{"simple", ""}
|
||||
for _, pushMode := range pushModes {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string][]string{
|
||||
"push.default": []string{pushMode},
|
||||
"branch.left.remote": []string{"ignore"},
|
||||
"branch.left.merge": []string{"me"},
|
||||
},
|
||||
})
|
||||
|
||||
u := newRefUpdate(cfg.Git, "origin", git.ParseRef("refs/heads/left", ""), nil)
|
||||
assert.Equal(t, "left", u.Right().Name, "pushmode=%q", pushMode)
|
||||
assert.Equal(t, git.RefTypeLocalBranch, u.Right().Type, "pushmode=%q", pushMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefUpdateTrackedDefault(t *testing.T) {
|
||||
pushModes := []string{"simple", "upstream", "tracking", ""}
|
||||
for _, pushMode := range pushModes {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string][]string{
|
||||
"push.default": []string{pushMode},
|
||||
"branch.left.remote": []string{"origin"},
|
||||
"branch.left.merge": []string{"refs/heads/tracked"},
|
||||
},
|
||||
})
|
||||
|
||||
u := newRefUpdate(cfg.Git, "origin", git.ParseRef("refs/heads/left", ""), nil)
|
||||
assert.Equal(t, "tracked", u.Right().Name, "pushmode=%s", pushMode)
|
||||
assert.Equal(t, git.RefTypeLocalBranch, u.Right().Type, "pushmode=%q", pushMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefUpdateCurrentDefault(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string][]string{
|
||||
"push.default": []string{"current"},
|
||||
"branch.left.remote": []string{"origin"},
|
||||
"branch.left.merge": []string{"tracked"},
|
||||
},
|
||||
})
|
||||
|
||||
u := newRefUpdate(cfg.Git, "origin", git.ParseRef("refs/heads/left", ""), nil)
|
||||
assert.Equal(t, "left", u.Right().Name)
|
||||
assert.Equal(t, git.RefTypeLocalBranch, u.Right().Type)
|
||||
}
|
||||
|
||||
func TestRefUpdateExplicitLeftAndRight(t *testing.T) {
|
||||
u := newRefUpdate(nil, "", git.ParseRef("refs/heads/left", "abc123"), git.ParseRef("refs/heads/right", "def456"))
|
||||
assert.Equal(t, "left", u.Left().Name)
|
||||
assert.Equal(t, "abc123", u.Left().Sha)
|
||||
assert.Equal(t, "abc123", u.LeftCommitish())
|
||||
assert.Equal(t, "right", u.Right().Name)
|
||||
assert.Equal(t, "def456", u.Right().Sha)
|
||||
assert.Equal(t, "def456", u.RightCommitish())
|
||||
|
||||
u = newRefUpdate(nil, "", git.ParseRef("refs/heads/left", ""), git.ParseRef("refs/heads/right", ""))
|
||||
assert.Equal(t, "left", u.Left().Name)
|
||||
assert.Equal(t, "", u.Left().Sha)
|
||||
assert.Equal(t, "left", u.LeftCommitish())
|
||||
assert.Equal(t, "right", u.Right().Name)
|
||||
assert.Equal(t, "", u.Right().Sha)
|
||||
assert.Equal(t, "right", u.RightCommitish())
|
||||
}
|
@ -5,28 +5,42 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/git-lfs/git-lfs/progress"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
"github.com/git-lfs/git-lfs/tq"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
func uploadLeftOrAll(g *lfs.GitScanner, ctx *uploadContext, ref string) error {
|
||||
func uploadForRefUpdates(ctx *uploadContext, updates []*refUpdate, pushAll bool) error {
|
||||
gitscanner, err := ctx.buildGitScanner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gitscanner.Close()
|
||||
|
||||
verifyLocksForUpdates(ctx.lockVerifier, updates)
|
||||
for _, update := range updates {
|
||||
if err := uploadLeftOrAll(gitscanner, ctx, update, pushAll); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("ref %s:", update.Left().Name))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Await()
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadLeftOrAll(g *lfs.GitScanner, ctx *uploadContext, update *refUpdate, pushAll bool) error {
|
||||
if pushAll {
|
||||
if err := g.ScanRefWithDeleted(ref, nil); err != nil {
|
||||
if err := g.ScanRefWithDeleted(update.LeftCommitish(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := g.ScanLeftToRemote(ref, nil); err != nil {
|
||||
if err := g.ScanLeftToRemote(update.LeftCommitish(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -46,18 +60,7 @@ type uploadContext struct {
|
||||
committerName string
|
||||
committerEmail string
|
||||
|
||||
trackedLocksMu *sync.Mutex
|
||||
|
||||
// ALL verifiable locks
|
||||
lockVerifyState verifyState
|
||||
ourLocks map[string]locking.Lock
|
||||
theirLocks map[string]locking.Lock
|
||||
|
||||
// locks from ourLocks that were modified in this push
|
||||
ownedLocks []locking.Lock
|
||||
|
||||
// locks from theirLocks that were modified in this push
|
||||
unownedLocks []locking.Lock
|
||||
lockVerifier *lockVerifier
|
||||
|
||||
// allowMissing specifies whether pushes containing missing/corrupt
|
||||
// pointers should allow pushing Git blobs
|
||||
@ -68,100 +71,23 @@ type uploadContext struct {
|
||||
errMu sync.Mutex
|
||||
}
|
||||
|
||||
// Determines if a filename is lockable. Serves as a wrapper around theirLocks
|
||||
// that implements GitScannerSet.
|
||||
type gitScannerLockables struct {
|
||||
m map[string]locking.Lock
|
||||
}
|
||||
|
||||
func (l *gitScannerLockables) Contains(name string) bool {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := l.m[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
type verifyState byte
|
||||
|
||||
const (
|
||||
verifyStateUnknown verifyState = iota
|
||||
verifyStateEnabled
|
||||
verifyStateDisabled
|
||||
)
|
||||
|
||||
func newUploadContext(remote string, dryRun bool) *uploadContext {
|
||||
cfg.CurrentRemote = remote
|
||||
|
||||
func newUploadContext(dryRun bool) *uploadContext {
|
||||
remote := cfg.PushRemote()
|
||||
manifest := getTransferManifestOperationRemote("upload", remote)
|
||||
ctx := &uploadContext{
|
||||
Remote: remote,
|
||||
Manifest: getTransferManifestOperationRemote("upload", remote),
|
||||
Manifest: manifest,
|
||||
DryRun: dryRun,
|
||||
uploadedOids: tools.NewStringSet(),
|
||||
gitfilter: lfs.NewGitFilter(cfg),
|
||||
ourLocks: make(map[string]locking.Lock),
|
||||
theirLocks: make(map[string]locking.Lock),
|
||||
trackedLocksMu: new(sync.Mutex),
|
||||
lockVerifier: newLockVerifier(manifest),
|
||||
allowMissing: cfg.Git.Bool("lfs.allowincompletepush", true),
|
||||
}
|
||||
|
||||
ctx.meter = buildProgressMeter(ctx.DryRun)
|
||||
ctx.tq = newUploadQueue(ctx.Manifest, ctx.Remote, tq.WithProgress(ctx.meter), tq.DryRun(ctx.DryRun))
|
||||
ctx.committerName, ctx.committerEmail = cfg.CurrentCommitter()
|
||||
|
||||
// Do not check locks for standalone transfer, because there is no LFS
|
||||
// server to ask.
|
||||
if ctx.Manifest.IsStandaloneTransfer() {
|
||||
ctx.lockVerifyState = verifyStateDisabled
|
||||
return ctx
|
||||
}
|
||||
|
||||
ourLocks, theirLocks, verifyState := verifyLocks(remote)
|
||||
ctx.lockVerifyState = verifyState
|
||||
for _, l := range theirLocks {
|
||||
ctx.theirLocks[l.Path] = l
|
||||
}
|
||||
for _, l := range ourLocks {
|
||||
ctx.ourLocks[l.Path] = l
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func verifyLocks(remote string) (ours, theirs []locking.Lock, st verifyState) {
|
||||
endpoint := getAPIClient().Endpoints.Endpoint("upload", remote)
|
||||
state := getVerifyStateFor(endpoint)
|
||||
if state == verifyStateDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
lockClient := newLockClient(remote)
|
||||
|
||||
ours, theirs, err := lockClient.VerifiableLocks(0)
|
||||
if err != nil {
|
||||
if errors.IsNotImplementedError(err) {
|
||||
disableFor(endpoint)
|
||||
} else if state == verifyStateUnknown || state == verifyStateEnabled {
|
||||
if errors.IsAuthError(err) {
|
||||
if state == verifyStateUnknown {
|
||||
Error("WARNING: Authentication error: %s", err)
|
||||
} else if state == verifyStateEnabled {
|
||||
Exit("ERROR: Authentication error: %s", err)
|
||||
}
|
||||
} else {
|
||||
Print("Remote %q does not support the LFS locking API. Consider disabling it with:", remote)
|
||||
Print(" $ git config lfs.%s.locksverify false", endpoint.Url)
|
||||
if state == verifyStateEnabled {
|
||||
ExitWithError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if state == verifyStateUnknown {
|
||||
Print("Locking support detected on remote %q. Consider enabling it with:", remote)
|
||||
Print(" $ git config lfs.%s.locksverify true", endpoint.Url)
|
||||
}
|
||||
|
||||
return ours, theirs, state
|
||||
}
|
||||
|
||||
func (c *uploadContext) scannerError() error {
|
||||
@ -191,15 +117,8 @@ func (c *uploadContext) buildGitScanner() (*lfs.GitScanner, error) {
|
||||
}
|
||||
})
|
||||
|
||||
gitscanner.FoundLockable = func(name string) {
|
||||
if lock, ok := c.theirLocks[name]; ok {
|
||||
c.trackedLocksMu.Lock()
|
||||
c.unownedLocks = append(c.unownedLocks, lock)
|
||||
c.trackedLocksMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
gitscanner.PotentialLockables = &gitScannerLockables{m: c.theirLocks}
|
||||
gitscanner.FoundLockable = func(n string) { c.lockVerifier.LockedByThem(n) }
|
||||
gitscanner.PotentialLockables = c.lockVerifier
|
||||
return gitscanner, gitscanner.RemoteForPush(c.Remote)
|
||||
}
|
||||
|
||||
@ -239,11 +158,7 @@ func (c *uploadContext) prepareUpload(unfiltered ...*lfs.WrappedPointer) (*tq.Tr
|
||||
// current committer.
|
||||
var canUpload bool = true
|
||||
|
||||
if lock, ok := c.theirLocks[p.Name]; ok {
|
||||
c.trackedLocksMu.Lock()
|
||||
c.unownedLocks = append(c.unownedLocks, lock)
|
||||
c.trackedLocksMu.Unlock()
|
||||
|
||||
if c.lockVerifier.LockedByThem(p.Name) {
|
||||
// If the verification state is enabled, this failed
|
||||
// locks verification means that the push should fail.
|
||||
//
|
||||
@ -252,14 +167,10 @@ func (c *uploadContext) prepareUpload(unfiltered ...*lfs.WrappedPointer) (*tq.Tr
|
||||
//
|
||||
// If the state is undefined, the verification error is
|
||||
// sent as a warning and the user can upload.
|
||||
canUpload = c.lockVerifyState != verifyStateEnabled
|
||||
canUpload = !c.lockVerifier.Enabled()
|
||||
}
|
||||
|
||||
if lock, ok := c.ourLocks[p.Name]; ok {
|
||||
c.trackedLocksMu.Lock()
|
||||
c.ownedLocks = append(c.ownedLocks, lock)
|
||||
c.trackedLocksMu.Unlock()
|
||||
}
|
||||
c.lockVerifier.LockedByUs(p.Name)
|
||||
|
||||
if canUpload {
|
||||
// estimate in meter early (even if it's not going into
|
||||
@ -348,25 +259,23 @@ func (c *uploadContext) Await() {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
c.trackedLocksMu.Lock()
|
||||
if ul := len(c.unownedLocks); ul > 0 {
|
||||
Print("Unable to push %d locked file(s):", ul)
|
||||
for _, unowned := range c.unownedLocks {
|
||||
Print("* %s - %s", unowned.Path, unowned.Owner)
|
||||
if c.lockVerifier.HasUnownedLocks() {
|
||||
Print("Unable to push locked files:")
|
||||
for _, unowned := range c.lockVerifier.UnownedLocks() {
|
||||
Print("* %s - %s", unowned.Path(), unowned.Owners())
|
||||
}
|
||||
|
||||
if c.lockVerifyState == verifyStateEnabled {
|
||||
if c.lockVerifier.Enabled() {
|
||||
Exit("ERROR: Cannot update locked files.")
|
||||
} else {
|
||||
Error("WARNING: The above files would have halted this push.")
|
||||
}
|
||||
} else if len(c.ownedLocks) > 0 {
|
||||
Print("Consider unlocking your own locked file(s): (`git lfs unlock <path>`)")
|
||||
for _, owned := range c.ownedLocks {
|
||||
Print("* %s", owned.Path)
|
||||
} else if c.lockVerifier.HasOwnedLocks() {
|
||||
Print("Consider unlocking your own locked files: (`git lfs unlock <path>`)")
|
||||
for _, owned := range c.lockVerifier.OwnedLocks() {
|
||||
Print("* %s", owned.Path())
|
||||
}
|
||||
}
|
||||
c.trackedLocksMu.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
@ -440,33 +349,13 @@ func (c *uploadContext) ensureFile(smudgePath, cleanPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVerifyStateFor returns whether or not lock verification is enabled for the
|
||||
// given "endpoint". If no state has been explicitly set, an "unknown" state
|
||||
// will be returned instead.
|
||||
func getVerifyStateFor(endpoint lfsapi.Endpoint) verifyState {
|
||||
uc := config.NewURLConfig(cfg.Git)
|
||||
|
||||
v, ok := uc.Get("lfs", endpoint.Url, "locksverify")
|
||||
if !ok {
|
||||
if supportsLockingAPI(endpoint) {
|
||||
return verifyStateEnabled
|
||||
}
|
||||
return verifyStateUnknown
|
||||
}
|
||||
|
||||
if enabled, _ := strconv.ParseBool(v); enabled {
|
||||
return verifyStateEnabled
|
||||
}
|
||||
return verifyStateDisabled
|
||||
}
|
||||
|
||||
// supportsLockingAPI returns whether or not a given lfsapi.Endpoint "e"
|
||||
// is known to support the LFS locking API by whether or not its hostname is
|
||||
// included in the list above.
|
||||
func supportsLockingAPI(e lfsapi.Endpoint) bool {
|
||||
u, err := url.Parse(e.Url)
|
||||
// supportsLockingAPI returns whether or not a given url is known to support
|
||||
// the LFS locking API by whether or not its hostname is included in the list
|
||||
// above.
|
||||
func supportsLockingAPI(rawurl string) bool {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
tracerx.Printf("commands: unable to parse %q to determine locking support: %v", e.Url, err)
|
||||
tracerx.Printf("commands: unable to parse %q to determine locking support: %v", rawurl, err)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -482,10 +371,10 @@ func supportsLockingAPI(e lfsapi.Endpoint) bool {
|
||||
|
||||
// disableFor disables lock verification for the given lfsapi.Endpoint,
|
||||
// "endpoint".
|
||||
func disableFor(endpoint lfsapi.Endpoint) error {
|
||||
tracerx.Printf("commands: disabling lock verification for %q", endpoint.Url)
|
||||
func disableFor(rawurl string) error {
|
||||
tracerx.Printf("commands: disabling lock verification for %q", rawurl)
|
||||
|
||||
key := strings.Join([]string{"lfs", endpoint.Url, "locksverify"}, ".")
|
||||
key := strings.Join([]string{"lfs", rawurl, "locksverify"}, ".")
|
||||
|
||||
_, err := cfg.SetGitLocalKey(key, "false")
|
||||
return err
|
||||
|
@ -3,7 +3,6 @@ package commands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -13,11 +12,7 @@ type LockingSupportTestCase struct {
|
||||
}
|
||||
|
||||
func (l *LockingSupportTestCase) Assert(t *testing.T) {
|
||||
ep := lfsapi.Endpoint{
|
||||
Url: l.Given,
|
||||
}
|
||||
|
||||
assert.Equal(t, l.ExpectedToMatch, supportsLockingAPI(ep))
|
||||
assert.Equal(t, l.ExpectedToMatch, supportsLockingAPI(l.Given))
|
||||
}
|
||||
|
||||
func TestSupportedLockingHosts(t *testing.T) {
|
||||
|
@ -32,12 +32,15 @@ type Configuration struct {
|
||||
// configuration.
|
||||
Git Environment
|
||||
|
||||
CurrentRemote string
|
||||
currentRemote *string
|
||||
pushRemote *string
|
||||
|
||||
// gitConfig can fetch or modify the current Git config and track the Git
|
||||
// version.
|
||||
gitConfig *git.Configuration
|
||||
|
||||
ref *git.Ref
|
||||
remoteRef *git.Ref
|
||||
fs *fs.Filesystem
|
||||
gitDir *string
|
||||
workDir string
|
||||
@ -54,7 +57,6 @@ func New() *Configuration {
|
||||
func NewIn(workdir, gitdir string) *Configuration {
|
||||
gitConf := git.NewConfig(workdir, gitdir)
|
||||
c := &Configuration{
|
||||
CurrentRemote: defaultRemote,
|
||||
Os: EnvironmentOf(NewOsFetcher()),
|
||||
gitConfig: gitConf,
|
||||
}
|
||||
@ -106,7 +108,6 @@ type Values struct {
|
||||
// This method should only be used during testing.
|
||||
func NewFrom(v Values) *Configuration {
|
||||
c := &Configuration{
|
||||
CurrentRemote: defaultRemote,
|
||||
Os: EnvironmentOf(mapFetcher(v.Os)),
|
||||
gitConfig: git.NewConfig("", ""),
|
||||
}
|
||||
@ -151,6 +152,90 @@ func (c *Configuration) FetchExcludePaths() []string {
|
||||
return tools.CleanPaths(patterns, ",")
|
||||
}
|
||||
|
||||
func (c *Configuration) CurrentRef() *git.Ref {
|
||||
c.loading.Lock()
|
||||
defer c.loading.Unlock()
|
||||
if c.ref == nil {
|
||||
r, err := git.CurrentRef()
|
||||
if err != nil {
|
||||
tracerx.Printf("Error loading current ref: %s", err)
|
||||
c.ref = &git.Ref{}
|
||||
} else {
|
||||
c.ref = r
|
||||
}
|
||||
}
|
||||
return c.ref
|
||||
}
|
||||
|
||||
func (c *Configuration) IsDefaultRemote() bool {
|
||||
return c.Remote() == defaultRemote
|
||||
}
|
||||
|
||||
// Remote returns the default remote based on:
|
||||
// 1. The currently tracked remote branch, if present
|
||||
// 2. Any other SINGLE remote defined in .git/config
|
||||
// 3. Use "origin" as a fallback.
|
||||
// Results are cached after the first hit.
|
||||
func (c *Configuration) Remote() string {
|
||||
ref := c.CurrentRef()
|
||||
|
||||
c.loading.Lock()
|
||||
defer c.loading.Unlock()
|
||||
|
||||
if c.currentRemote == nil {
|
||||
if len(ref.Name) == 0 {
|
||||
c.currentRemote = &defaultRemote
|
||||
return defaultRemote
|
||||
}
|
||||
|
||||
if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.remote", ref.Name)); ok {
|
||||
// try tracking remote
|
||||
c.currentRemote = &remote
|
||||
} else if remotes := c.Remotes(); len(remotes) == 1 {
|
||||
// use only remote if there is only 1
|
||||
c.currentRemote = &remotes[0]
|
||||
} else {
|
||||
// fall back to default :(
|
||||
c.currentRemote = &defaultRemote
|
||||
}
|
||||
}
|
||||
return *c.currentRemote
|
||||
}
|
||||
|
||||
func (c *Configuration) PushRemote() string {
|
||||
ref := c.CurrentRef()
|
||||
c.loading.Lock()
|
||||
defer c.loading.Unlock()
|
||||
|
||||
if c.pushRemote == nil {
|
||||
if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.pushRemote", ref.Name)); ok {
|
||||
c.pushRemote = &remote
|
||||
} else if remote, ok := c.Git.Get("remote.pushDefault"); ok {
|
||||
c.pushRemote = &remote
|
||||
} else {
|
||||
c.loading.Unlock()
|
||||
remote := c.Remote()
|
||||
c.loading.Lock()
|
||||
|
||||
c.pushRemote = &remote
|
||||
}
|
||||
}
|
||||
|
||||
return *c.pushRemote
|
||||
}
|
||||
|
||||
func (c *Configuration) SetValidRemote(name string) error {
|
||||
if err := git.ValidateRemote(name); err != nil {
|
||||
return err
|
||||
}
|
||||
c.SetRemote(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Configuration) SetRemote(name string) {
|
||||
c.currentRemote = &name
|
||||
}
|
||||
|
||||
func (c *Configuration) Remotes() []string {
|
||||
c.loadGitConfig()
|
||||
return c.remotes
|
||||
|
@ -3,9 +3,62 @@ package config
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoteDefault(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string][]string{
|
||||
"branch.unused.remote": []string{"a"},
|
||||
"branch.unused.pushRemote": []string{"b"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "origin", cfg.Remote())
|
||||
assert.Equal(t, "origin", cfg.PushRemote())
|
||||
}
|
||||
|
||||
func TestRemoteBranchConfig(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string][]string{
|
||||
"branch.master.remote": []string{"a"},
|
||||
"branch.other.pushRemote": []string{"b"},
|
||||
},
|
||||
})
|
||||
cfg.ref = &git.Ref{Name: "master"}
|
||||
|
||||
assert.Equal(t, "a", cfg.Remote())
|
||||
assert.Equal(t, "a", cfg.PushRemote())
|
||||
}
|
||||
|
||||
func TestRemotePushDefault(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string][]string{
|
||||
"branch.master.remote": []string{"a"},
|
||||
"remote.pushDefault": []string{"b"},
|
||||
"branch.other.pushRemote": []string{"c"},
|
||||
},
|
||||
})
|
||||
cfg.ref = &git.Ref{Name: "master"}
|
||||
|
||||
assert.Equal(t, "a", cfg.Remote())
|
||||
assert.Equal(t, "b", cfg.PushRemote())
|
||||
}
|
||||
|
||||
func TestRemoteBranchPushDefault(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string][]string{
|
||||
"branch.master.remote": []string{"a"},
|
||||
"remote.pushDefault": []string{"b"},
|
||||
"branch.master.pushRemote": []string{"c"},
|
||||
},
|
||||
})
|
||||
cfg.ref = &git.Ref{Name: "master"}
|
||||
|
||||
assert.Equal(t, "a", cfg.Remote())
|
||||
assert.Equal(t, "c", cfg.PushRemote())
|
||||
}
|
||||
|
||||
func TestBasicTransfersOnlySetValue(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string][]string{
|
||||
|
@ -65,13 +65,58 @@ func (e *environment) GetAll(key string) []string {
|
||||
return e.Fetcher.GetAll(key)
|
||||
}
|
||||
|
||||
func (e *environment) Bool(key string, def bool) (val bool) {
|
||||
func (e *environment) Bool(key string, def bool) bool {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
if len(s) == 0 {
|
||||
return Bool(s, def)
|
||||
}
|
||||
|
||||
func (e *environment) Int(key string, def int) int {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
return Int(s, def)
|
||||
}
|
||||
|
||||
func (e *environment) All() map[string][]string {
|
||||
return e.Fetcher.All()
|
||||
}
|
||||
|
||||
// Int returns the int value associated with the given value, or the value
|
||||
// "def", if the value is blank.
|
||||
//
|
||||
// To convert from a the string value attached to a given key,
|
||||
// `strconv.Atoi(val)` is called. If `Atoi` returned a non-nil error,
|
||||
// then the value "def" will be returned instead.
|
||||
//
|
||||
// Otherwise, if the value was converted `string -> int` successfully,
|
||||
// then it will be returned wholesale.
|
||||
func Int(value string, def int) int {
|
||||
if len(value) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Bool returns the boolean state associated with the given value, or the
|
||||
// value "def", if the value is blank.
|
||||
//
|
||||
// The "boolean state associated with a given key" is defined as the
|
||||
// case-insensitive string comparison with the following:
|
||||
//
|
||||
// 1) true if...
|
||||
// "true", "1", "on", "yes", or "t"
|
||||
// 2) false if...
|
||||
// "false", "0", "off", "no", "f", or otherwise.
|
||||
func Bool(value string, def bool) bool {
|
||||
if len(value) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "1", "on", "yes", "t":
|
||||
return true
|
||||
case "false", "0", "off", "no", "f":
|
||||
@ -80,21 +125,3 @@ func (e *environment) Bool(key string, def bool) (val bool) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (e *environment) Int(key string, def int) (val int) {
|
||||
s, _ := e.Fetcher.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (e *environment) All() map[string][]string {
|
||||
return e.Fetcher.All()
|
||||
}
|
||||
|
@ -50,6 +50,11 @@ func (c *URLConfig) GetAll(prefix, rawurl, key string) []string {
|
||||
return c.git.GetAll(strings.Join([]string{prefix, key}, "."))
|
||||
}
|
||||
|
||||
func (c *URLConfig) Bool(prefix, rawurl, key string, def bool) bool {
|
||||
s, _ := c.Get(prefix, rawurl, key)
|
||||
return Bool(s, def)
|
||||
}
|
||||
|
||||
func (c *URLConfig) getAll(prefix, rawurl, key string) []string {
|
||||
hosts, paths := c.hostsAndPaths(rawurl)
|
||||
|
||||
|
@ -9,6 +9,7 @@ git-lfs-ls-files(1) -- Show information about Git LFS files in the index and wor
|
||||
|
||||
Display paths of Git LFS files that are found in the tree at the given
|
||||
reference. If no reference is given, scan the currently checked-out branch.
|
||||
An asterisk (*) after the OID indicates a LFS pointer, a minus (-) a full object.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
|
@ -27,6 +27,11 @@ git-lfs-migrate(1) - Migrate history to or from git-lfs
|
||||
* `--exclude-ref`=<refname>:
|
||||
See [INCLUDE AND EXCLUDE (REFS)].
|
||||
|
||||
* `--skip-fetch`:
|
||||
Assumes that the known set of remote references is complete, and should not
|
||||
be refreshed when determining the set of "un-pushed" commits to migrate. Has
|
||||
no effect when combined with `--include-ref` or `--exclude-ref`.
|
||||
|
||||
* `--everything`:
|
||||
See [INCLUDE AND EXCLUDE (REFS)].
|
||||
|
||||
|
@ -12,6 +12,12 @@ Perform the following actions to remove the Git LFS configuration:
|
||||
* Remove the "lfs" clean and smudge filters from the global Git config.
|
||||
* Uninstall the Git LFS pre-push hook if run from inside a Git repository.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* --local:
|
||||
Removes the "lfs" smudge and clean filters from the local repository's git
|
||||
config, instead of the global git config (~/.gitconfig).
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
git-lfs-install(1).
|
||||
|
@ -59,6 +59,8 @@ commands and low level ("plumbing") commands.
|
||||
Show the status of Git LFS files in the working tree.
|
||||
* git-lfs-track(1):
|
||||
View or add Git LFS paths to Git attributes.
|
||||
* git-lfs-uninstall(1):
|
||||
Uninstall Git LFS by removing hooks and smudge/clean filter configuration.
|
||||
* git-lfs-unlock(1):
|
||||
Remove "locked" setting for a file on the Git LFS server.
|
||||
* git-lfs-untrack(1):
|
||||
|
@ -36,7 +36,7 @@ func (s *AttributeSource) String() string {
|
||||
|
||||
// GetAttributePaths returns a list of entries in .gitattributes which are
|
||||
// configured with the filter=lfs attribute
|
||||
// workingDIr is the root of the working copy
|
||||
// workingDir is the root of the working copy
|
||||
// gitDir is the root of the git repo
|
||||
func GetAttributePaths(workingDir, gitDir string) []AttributePath {
|
||||
paths := make([]AttributePath, 0)
|
||||
@ -56,7 +56,11 @@ func GetAttributePaths(workingDir, gitDir string) []AttributePath {
|
||||
scanner.Split(le.ScanLines)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for filter=lfs (signifying that LFS is tracking
|
||||
// this file) or "lockable", which indicates that the
|
||||
|
@ -172,8 +172,6 @@ func (o *FilterProcessScanner) Err() error { return o.err }
|
||||
// will read the body of the request. Since the body is _not_ offset, one
|
||||
// request should be read in its entirety before consuming the next request.
|
||||
func (o *FilterProcessScanner) readRequest() (*Request, error) {
|
||||
tracerx.Printf("Read filter-process request.")
|
||||
|
||||
requestList, err := o.pl.readPacketList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
104
git/git.go
104
git/git.go
@ -54,15 +54,36 @@ func (t RefType) Prefix() (string, bool) {
|
||||
return "refs/tags", true
|
||||
case RefTypeRemoteTag:
|
||||
return "refs/remotes/tags", true
|
||||
case RefTypeHEAD:
|
||||
return "", false
|
||||
case RefTypeOther:
|
||||
return "", false
|
||||
default:
|
||||
panic(fmt.Sprintf("git: unknown RefType %d", t))
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func ParseRef(absRef, sha string) *Ref {
|
||||
r := &Ref{Sha: sha}
|
||||
if strings.HasPrefix(absRef, "refs/heads/") {
|
||||
r.Name = absRef[11:]
|
||||
r.Type = RefTypeLocalBranch
|
||||
} else if strings.HasPrefix(absRef, "refs/tags/") {
|
||||
r.Name = absRef[10:]
|
||||
r.Type = RefTypeLocalTag
|
||||
} else if strings.HasPrefix(absRef, "refs/remotes/tags/") {
|
||||
r.Name = absRef[18:]
|
||||
r.Type = RefTypeRemoteTag
|
||||
} else if strings.HasPrefix(absRef, "refs/remotes/") {
|
||||
r.Name = absRef[13:]
|
||||
r.Type = RefTypeRemoteBranch
|
||||
} else {
|
||||
r.Name = absRef
|
||||
if absRef == "HEAD" {
|
||||
r.Type = RefTypeHEAD
|
||||
} else {
|
||||
r.Type = RefTypeOther
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// A git reference (branch, tag etc)
|
||||
type Ref struct {
|
||||
Name string
|
||||
@ -70,6 +91,24 @@ type Ref struct {
|
||||
Sha string
|
||||
}
|
||||
|
||||
// Refspec returns the fully-qualified reference name (including remote), i.e.,
|
||||
// for a remote branch called 'my-feature' on remote 'origin', this function
|
||||
// will return:
|
||||
//
|
||||
// refs/remotes/origin/my-feature
|
||||
func (r *Ref) Refspec() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
prefix, ok := r.Type.Prefix()
|
||||
if ok {
|
||||
return fmt.Sprintf("%s/%s", prefix, r.Name)
|
||||
}
|
||||
|
||||
return r.Name
|
||||
}
|
||||
|
||||
// Some top level information about a commit (only first line of message)
|
||||
type CommitSummary struct {
|
||||
Sha string
|
||||
@ -242,19 +281,6 @@ func (c *Configuration) CurrentRemoteRef() (*Ref, error) {
|
||||
return ResolveRef(remoteref)
|
||||
}
|
||||
|
||||
// RemoteForCurrentBranch returns the name of the remote that the current branch is tracking
|
||||
func (c *Configuration) RemoteForCurrentBranch() (string, error) {
|
||||
ref, err := CurrentRef()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
remote := c.RemoteForBranch(ref.Name)
|
||||
if remote == "" {
|
||||
return "", fmt.Errorf("remote not found for branch %q", ref.Name)
|
||||
}
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
// RemoteRefForCurrentBranch returns the full remote ref (refs/remotes/{remote}/{remotebranch})
|
||||
// that the current branch is tracking.
|
||||
func (c *Configuration) RemoteRefNameForCurrentBranch() (string, error) {
|
||||
@ -361,14 +387,7 @@ func UpdateRef(ref *Ref, to []byte, reason string) error {
|
||||
// reflog entry, if a "reason" was provided). It operates within the given
|
||||
// working directory "wd". It returns an error if any were encountered.
|
||||
func UpdateRefIn(wd string, ref *Ref, to []byte, reason string) error {
|
||||
var refspec string
|
||||
if prefix, ok := ref.Type.Prefix(); ok {
|
||||
refspec = fmt.Sprintf("%s/%s", prefix, ref.Name)
|
||||
} else {
|
||||
refspec = ref.Name
|
||||
}
|
||||
|
||||
args := []string{"update-ref", refspec, hex.EncodeToString(to)}
|
||||
args := []string{"update-ref", ref.Refspec(), hex.EncodeToString(to)}
|
||||
if len(reason) > 0 {
|
||||
args = append(args, "-m", reason)
|
||||
}
|
||||
@ -423,39 +442,6 @@ func ValidateRemoteURL(remote string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRemote returns the default remote based on:
|
||||
// 1. The currently tracked remote branch, if present
|
||||
// 2. "origin", if defined
|
||||
// 3. Any other SINGLE remote defined in .git/config
|
||||
// Returns an error if all of these fail, i.e. no tracked remote branch, no
|
||||
// "origin", and either no remotes defined or 2+ non-"origin" remotes
|
||||
func (c *Configuration) DefaultRemote() (string, error) {
|
||||
tracked, err := c.RemoteForCurrentBranch()
|
||||
if err == nil {
|
||||
return tracked, nil
|
||||
}
|
||||
|
||||
// Otherwise, check what remotes are defined
|
||||
remotes, err := RemoteList()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch len(remotes) {
|
||||
case 0:
|
||||
return "", errors.New("No remotes defined")
|
||||
case 1: // always use a single remote whether it's origin or otherwise
|
||||
return remotes[0], nil
|
||||
default:
|
||||
for _, remote := range remotes {
|
||||
// Use origin if present
|
||||
if remote == "origin" {
|
||||
return remote, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("Unable to pick default remote, too ambiguous")
|
||||
}
|
||||
|
||||
func UpdateIndexFromStdin() *subprocess.Cmd {
|
||||
return git("update-index", "-q", "--refresh", "--stdin")
|
||||
}
|
||||
|
@ -13,6 +13,70 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRefString(t *testing.T) {
|
||||
const sha = "0000000000000000000000000000000000000000"
|
||||
for s, r := range map[string]*Ref{
|
||||
"refs/heads/master": &Ref{
|
||||
Name: "master",
|
||||
Type: RefTypeLocalBranch,
|
||||
Sha: sha,
|
||||
},
|
||||
"refs/remotes/origin/master": &Ref{
|
||||
Name: "origin/master",
|
||||
Type: RefTypeRemoteBranch,
|
||||
Sha: sha,
|
||||
},
|
||||
"refs/remotes/tags/v1.0.0": &Ref{
|
||||
Name: "v1.0.0",
|
||||
Type: RefTypeRemoteTag,
|
||||
Sha: sha,
|
||||
},
|
||||
"refs/tags/v1.0.0": &Ref{
|
||||
Name: "v1.0.0",
|
||||
Type: RefTypeLocalTag,
|
||||
Sha: sha,
|
||||
},
|
||||
"HEAD": &Ref{
|
||||
Name: "HEAD",
|
||||
Type: RefTypeHEAD,
|
||||
Sha: sha,
|
||||
},
|
||||
"other": &Ref{
|
||||
Name: "other",
|
||||
Type: RefTypeOther,
|
||||
Sha: sha,
|
||||
},
|
||||
} {
|
||||
assert.Equal(t, s, r.Refspec())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRefs(t *testing.T) {
|
||||
tests := map[string]RefType{
|
||||
"refs/heads": RefTypeLocalBranch,
|
||||
"refs/tags": RefTypeLocalTag,
|
||||
"refs/remotes/tags": RefTypeRemoteTag,
|
||||
"refs/remotes": RefTypeRemoteBranch,
|
||||
}
|
||||
|
||||
for prefix, expectedType := range tests {
|
||||
r := ParseRef(prefix+"/branch", "abc123")
|
||||
assert.Equal(t, "abc123", r.Sha, "prefix: "+prefix)
|
||||
assert.Equal(t, "branch", r.Name, "prefix: "+prefix)
|
||||
assert.Equal(t, expectedType, r.Type, "prefix: "+prefix)
|
||||
}
|
||||
|
||||
r := ParseRef("refs/foo/branch", "abc123")
|
||||
assert.Equal(t, "abc123", r.Sha, "prefix: refs/foo")
|
||||
assert.Equal(t, "refs/foo/branch", r.Name, "prefix: refs/foo")
|
||||
assert.Equal(t, RefTypeOther, r.Type, "prefix: refs/foo")
|
||||
|
||||
r = ParseRef("HEAD", "abc123")
|
||||
assert.Equal(t, "abc123", r.Sha, "prefix: HEAD")
|
||||
assert.Equal(t, "HEAD", r.Name, "prefix: HEAD")
|
||||
assert.Equal(t, RefTypeHEAD, r.Type, "prefix: HEAD")
|
||||
}
|
||||
|
||||
func TestCurrentRefAndCurrentRemoteRef(t *testing.T) {
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
@ -71,10 +135,6 @@ func TestCurrentRefAndCurrentRemoteRef(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "refs/remotes/origin/someremotebranch", refname)
|
||||
|
||||
remote, err := gitConf.RemoteForCurrentBranch()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "origin", remote)
|
||||
|
||||
ref, err = ResolveRef(outputs[2].Sha)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &Ref{outputs[2].Sha, RefTypeOther, outputs[2].Sha}, ref)
|
||||
@ -563,16 +623,3 @@ func TestRefTypeKnownPrefixes(t *testing.T) {
|
||||
assert.Equal(t, expected.Ok, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefTypeUnknownPrefix(t *testing.T) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
assert.Equal(t, "git: unknown RefType -1", err)
|
||||
} else {
|
||||
t.Fatal("git: expected panic() from RefType.Prefix()")
|
||||
}
|
||||
}()
|
||||
|
||||
unknown := RefType(-1)
|
||||
unknown.Prefix()
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ func NewRevListScanner(include, excluded []string, opt *ScanRefsOptions) (*RevLi
|
||||
// occurred.
|
||||
func revListArgs(include, exclude []string, opt *ScanRefsOptions) (io.Reader, []string, error) {
|
||||
var stdin io.Reader
|
||||
args := []string{"rev-list"}
|
||||
args := []string{"rev-list", "--stdin"}
|
||||
if !opt.CommitsOnly {
|
||||
args = append(args, "--objects")
|
||||
}
|
||||
@ -246,15 +246,16 @@ func revListArgs(include, exclude []string, opt *ScanRefsOptions) (io.Reader, []
|
||||
args = append(args, "--do-walk")
|
||||
}
|
||||
|
||||
args = append(args, includeExcludeShas(include, exclude)...)
|
||||
stdin = strings.NewReader(strings.Join(
|
||||
includeExcludeShas(include, exclude), "\n"))
|
||||
case ScanAllMode:
|
||||
args = append(args, "--all")
|
||||
case ScanLeftToRemoteMode:
|
||||
if len(opt.SkippedRefs) == 0 {
|
||||
args = append(args, includeExcludeShas(include, exclude)...)
|
||||
args = append(args, "--not", "--remotes="+opt.Remote)
|
||||
stdin = strings.NewReader(strings.Join(
|
||||
includeExcludeShas(include, exclude), "\n"))
|
||||
} else {
|
||||
args = append(args, "--stdin")
|
||||
stdin = strings.NewReader(strings.Join(
|
||||
append(includeExcludeShas(include, exclude), opt.SkippedRefs...), "\n"),
|
||||
)
|
||||
|
@ -4,13 +4,13 @@ import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type ArgsTestCase struct {
|
||||
@ -32,11 +32,7 @@ func (c *ArgsTestCase) Assert(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
require.Equal(t, len(c.ExpectedArgs), len(args))
|
||||
for i := 0; i < len(c.ExpectedArgs); i++ {
|
||||
assert.Equal(t, c.ExpectedArgs[i], args[i],
|
||||
"element #%d not equal: wanted %q, got %q", i, c.ExpectedArgs[i], args[i])
|
||||
}
|
||||
assert.EqualValues(t, c.ExpectedArgs, args)
|
||||
|
||||
if stdin != nil {
|
||||
b, err := ioutil.ReadAll(stdin)
|
||||
@ -60,34 +56,38 @@ func TestRevListArgs(t *testing.T) {
|
||||
Mode: ScanRefsMode,
|
||||
SkipDeletedBlobs: false,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--do-walk", "--"},
|
||||
},
|
||||
"scan refs not deleted, left and right": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
SkipDeletedBlobs: true,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--no-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--no-walk", "--"},
|
||||
},
|
||||
"scan refs deleted, left only": {
|
||||
Include: []string{s1}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
SkipDeletedBlobs: false,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--do-walk", s1, "--"},
|
||||
ExpectedStdin: s1,
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--do-walk", "--"},
|
||||
},
|
||||
"scan refs not deleted, left only": {
|
||||
Include: []string{s1}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
SkipDeletedBlobs: true,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--no-walk", s1, "--"},
|
||||
ExpectedStdin: s1,
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--no-walk", "--"},
|
||||
},
|
||||
"scan all": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanAllMode,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--all", "--"},
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--all", "--"},
|
||||
},
|
||||
"scan left to remote, no skipped refs": {
|
||||
Include: []string{s1}, Opt: &ScanRefsOptions{
|
||||
@ -95,7 +95,8 @@ func TestRevListArgs(t *testing.T) {
|
||||
Remote: "origin",
|
||||
SkippedRefs: []string{},
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", s1, "--not", "--remotes=origin", "--"},
|
||||
ExpectedStdin: s1,
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--not", "--remotes=origin", "--"},
|
||||
},
|
||||
"scan left to remote, skipped refs": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
@ -103,7 +104,7 @@ func TestRevListArgs(t *testing.T) {
|
||||
Remote: "origin",
|
||||
SkippedRefs: []string{"a", "b", "c"},
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--stdin", "--"},
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--"},
|
||||
ExpectedStdin: s1 + "\n^" + s2 + "\na\nb\nc",
|
||||
},
|
||||
"scan unknown type": {
|
||||
@ -117,35 +118,40 @@ func TestRevListArgs(t *testing.T) {
|
||||
Mode: ScanRefsMode,
|
||||
Order: DateRevListOrder,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--date-order", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--date-order", "--do-walk", "--"},
|
||||
},
|
||||
"scan author date order": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
Order: AuthorDateRevListOrder,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--author-date-order", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--author-date-order", "--do-walk", "--"},
|
||||
},
|
||||
"scan topo order": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
Order: TopoRevListOrder,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--topo-order", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--topo-order", "--do-walk", "--"},
|
||||
},
|
||||
"scan commits only": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
CommitsOnly: true,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--do-walk", "--"},
|
||||
},
|
||||
"scan reverse": {
|
||||
Include: []string{s1}, Exclude: []string{s2}, Opt: &ScanRefsOptions{
|
||||
Mode: ScanRefsMode,
|
||||
Reverse: true,
|
||||
},
|
||||
ExpectedArgs: []string{"rev-list", "--objects", "--reverse", "--do-walk", s1, "^" + s2, "--"},
|
||||
ExpectedStdin: fmt.Sprintf("%s\n^%s", s1, s2),
|
||||
ExpectedArgs: []string{"rev-list", "--stdin", "--objects", "--reverse", "--do-walk", "--"},
|
||||
},
|
||||
} {
|
||||
t.Run(desc, c.Assert)
|
||||
|
@ -81,7 +81,8 @@ func (f *GitFilter) downloadFile(writer io.Writer, ptr *Pointer, workingfile, me
|
||||
//
|
||||
// Either way, forward it into the *tq.TransferQueue so that updates are
|
||||
// sent over correctly.
|
||||
q := tq.NewTransferQueue(tq.Download, manifest, "", tq.WithProgressCallback(cb))
|
||||
|
||||
q := tq.NewTransferQueue(tq.Download, manifest, f.cfg.Remote(), tq.WithProgressCallback(cb))
|
||||
q.Add(filepath.Base(workingfile), mediafile, ptr.Oid, ptr.Size)
|
||||
q.Wait()
|
||||
|
||||
|
@ -25,8 +25,8 @@ func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
download := api.Endpoints.AccessFor(api.Endpoints.Endpoint("download", cfg.CurrentRemote).Url)
|
||||
upload := api.Endpoints.AccessFor(api.Endpoints.Endpoint("upload", cfg.CurrentRemote).Url)
|
||||
download := api.Endpoints.AccessFor(api.Endpoints.Endpoint("download", cfg.Remote()).Url)
|
||||
upload := api.Endpoints.AccessFor(api.Endpoints.Endpoint("upload", cfg.PushRemote()).Url)
|
||||
|
||||
dltransfers := manifest.GetDownloadAdapterNames()
|
||||
sort.Strings(dltransfers)
|
||||
|
@ -20,23 +20,13 @@ var (
|
||||
defaultEndpointFinder = NewEndpointFinder(nil)
|
||||
)
|
||||
|
||||
// DoWithAuth sends an HTTP request to get an HTTP response. It attempts to add
|
||||
// authentication from netrc or git's credential helpers if necessary,
|
||||
// supporting basic and ntlm authentication.
|
||||
func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
|
||||
credHelper := c.Credentials
|
||||
if credHelper == nil {
|
||||
credHelper = defaultCredentialHelper
|
||||
}
|
||||
req.Header = c.extraHeadersFor(req)
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
ef := c.Endpoints
|
||||
if ef == nil {
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
apiEndpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, remote, req)
|
||||
apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -71,7 +61,7 @@ func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, cre
|
||||
if access == NTLMAccess {
|
||||
return c.doWithNTLM(req, credHelper, creds, credsURL)
|
||||
}
|
||||
return c.Do(req)
|
||||
return c.do(req)
|
||||
}
|
||||
|
||||
// getCreds fills the authorization header for the given request if possible,
|
||||
@ -94,32 +84,48 @@ func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, cre
|
||||
// 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
|
||||
// This URL is used for the Git Credential Helper. This way existing https
|
||||
// Git remote credentials can be re-used for LFS.
|
||||
func getCreds(credHelper CredentialHelper, netrcFinder NetrcFinder, ef EndpointFinder, remote string, req *http.Request) (Endpoint, Access, Creds, *url.URL, error) {
|
||||
func (c *Client) getCreds(remote string, req *http.Request) (Endpoint, Access, CredentialHelper, *url.URL, Creds, error) {
|
||||
ef := c.Endpoints
|
||||
if ef == nil {
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
operation := getReqOperation(req)
|
||||
apiEndpoint := ef.Endpoint(operation, remote)
|
||||
access := ef.AccessFor(apiEndpoint.Url)
|
||||
|
||||
if access != NTLMAccess {
|
||||
if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
return apiEndpoint, access, nullCreds, nil, nil, nil
|
||||
}
|
||||
|
||||
credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if credsURL == nil {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
return apiEndpoint, access, nullCreds, nil, nil, nil
|
||||
}
|
||||
|
||||
creds, err := fillGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
|
||||
if err == nil {
|
||||
tracerx.Printf("Filled credentials for %s", credsURL)
|
||||
setRequestAuth(req, creds["username"], creds["password"])
|
||||
}
|
||||
return apiEndpoint, access, credHelper, credsURL, creds, err
|
||||
}
|
||||
|
||||
// NTLM ONLY
|
||||
|
||||
credsURL, err := url.Parse(apiEndpoint.Url)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
|
||||
@ -131,20 +137,16 @@ func getCreds(credHelper CredentialHelper, netrcFinder NetrcFinder, ef EndpointF
|
||||
"source": "netrc",
|
||||
}
|
||||
|
||||
return apiEndpoint, access, creds, credsURL, nil
|
||||
return apiEndpoint, access, nullCreds, credsURL, creds, nil
|
||||
}
|
||||
|
||||
creds, err := getGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
// NTLM uses creds to create the session
|
||||
credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
|
||||
return apiEndpoint, access, credHelper, credsURL, creds, err
|
||||
}
|
||||
|
||||
func getGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host, "path": path}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
|
||||
func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (CredentialHelper, Creds, error) {
|
||||
credHelper, input := c.getCredentialHelper(u)
|
||||
creds, err := credHelper.Fill(input)
|
||||
if creds == nil || len(creds) < 1 {
|
||||
errmsg := fmt.Sprintf("Git credentials for %s not found", u)
|
||||
@ -156,17 +158,7 @@ func getGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Reque
|
||||
err = errors.New(errmsg)
|
||||
}
|
||||
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func fillGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
creds, err := getGitCreds(credHelper, ef, req, u)
|
||||
if err == nil {
|
||||
tracerx.Printf("Filled credentials for %s", u)
|
||||
setRequestAuth(req, creds["username"], creds["password"])
|
||||
}
|
||||
|
||||
return creds, err
|
||||
return credHelper, creds, err
|
||||
}
|
||||
|
||||
func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -93,7 +94,6 @@ func TestDoWithAuthApprove(t *testing.T) {
|
||||
assert.True(t, creds.IsApproved(Creds(map[string]string{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"path": "repo/lfs",
|
||||
"protocol": "http",
|
||||
"host": srv.Listener.Addr().String(),
|
||||
})))
|
||||
@ -264,6 +264,51 @@ func TestGetCreds(t *testing.T) {
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic access with usehttppath": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
"credential.usehttppath": "true",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"basic access with url-specific usehttppath": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
"credential.https://git-server.com.usehttppath": "true",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
@ -295,7 +340,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -369,7 +413,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "user",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -392,7 +435,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -443,7 +485,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -465,7 +506,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "lfs-server.com",
|
||||
"username": "lfs-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -487,7 +527,6 @@ func TestGetCreds(t *testing.T) {
|
||||
"host": "git-server.com:8080",
|
||||
"username": "git-server.com:8080",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -509,7 +548,6 @@ func TestGetCreds(t *testing.T) {
|
||||
Creds: map[string]string{
|
||||
"host": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
"protocol": "https",
|
||||
"username": "git-server.com",
|
||||
},
|
||||
@ -517,8 +555,6 @@ func TestGetCreds(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
credHelper := &fakeCredentialFiller{}
|
||||
netrcFinder := &fakeNetrc{}
|
||||
for desc, test := range tests {
|
||||
t.Log(desc)
|
||||
req, err := http.NewRequest(test.Method, test.Href, nil)
|
||||
@ -531,8 +567,12 @@ func TestGetCreds(t *testing.T) {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
ef := NewEndpointFinder(NewContext(nil, nil, test.Config))
|
||||
endpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, test.Remote, req)
|
||||
ctx := NewContext(git.NewConfig("", ""), nil, test.Config)
|
||||
client, _ := NewClient(ctx)
|
||||
client.Credentials = &fakeCredentialFiller{}
|
||||
client.Netrc = &fakeNetrc{}
|
||||
client.Endpoints = NewEndpointFinder(ctx)
|
||||
endpoint, access, _, credsURL, creds, err := client.getCreds(test.Remote, req)
|
||||
if !assert.Nil(t, err) {
|
||||
continue
|
||||
}
|
||||
|
@ -79,8 +79,18 @@ func joinURL(prefix, suffix string) string {
|
||||
return prefix + slash + suffix
|
||||
}
|
||||
|
||||
// Do sends an HTTP request to get an HTTP response. It wraps net/http, adding
|
||||
// extra headers, redirection handling, and error reporting.
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
req.Header = c.extraHeadersFor(req)
|
||||
|
||||
return c.do(req)
|
||||
}
|
||||
|
||||
// do performs an *http.Request respecting redirects, and handles the response
|
||||
// as defined in c.handleResponse. Notably, it does not alter the headers for
|
||||
// the request argument in any way.
|
||||
func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
res, err := c.doWithRedirects(c.httpClient(req.Host), req, nil)
|
||||
|
435
lfsapi/creds.go
435
lfsapi/creds.go
@ -8,28 +8,34 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// credsConfig supplies configuration options pertaining to the authorization
|
||||
// process in package lfsapi.
|
||||
type credsConfig struct {
|
||||
// AskPass is a string containing an executable name as well as a
|
||||
// program arguments.
|
||||
//
|
||||
// See: https://git-scm.com/docs/gitcredentials#_requesting_credentials
|
||||
// for more.
|
||||
AskPass string `os:"GIT_ASKPASS" git:"core.askpass" os:"SSH_ASKPASS"`
|
||||
// Helper is a string defining the credential helper that Git should use.
|
||||
Helper string `git:"credential.helper"`
|
||||
// Cached is a boolean determining whether or not to enable the
|
||||
// credential cacher.
|
||||
Cached bool
|
||||
// SkipPrompt is a boolean determining whether or not to prompt the user
|
||||
// for a password.
|
||||
SkipPrompt bool `os:"GIT_TERMINAL_PROMPT"`
|
||||
// CredentialHelper is an interface used by the lfsapi Client to interact with
|
||||
// the 'git credential' command: https://git-scm.com/docs/gitcredentials
|
||||
// Other implementations include ASKPASS support, and an in-memory cache.
|
||||
type CredentialHelper interface {
|
||||
Fill(Creds) (Creds, error)
|
||||
Reject(Creds) error
|
||||
Approve(Creds) error
|
||||
}
|
||||
|
||||
// Creds represents a set of key/value pairs that are passed to 'git credential'
|
||||
// as input.
|
||||
type Creds map[string]string
|
||||
|
||||
func bufferCreds(c Creds) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// getCredentialHelper parses a 'credsConfig' from the git and OS environments,
|
||||
@ -37,110 +43,32 @@ type credsConfig struct {
|
||||
//
|
||||
// It returns an error if any configuration was invalid, or otherwise
|
||||
// un-useable.
|
||||
func getCredentialHelper(osEnv, gitEnv config.Environment) (CredentialHelper, error) {
|
||||
ccfg, err := getCredentialConfig(osEnv, gitEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *Client) getCredentialHelper(u *url.URL) (CredentialHelper, Creds) {
|
||||
rawurl := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
if c.uc.Bool("credential", rawurl, "usehttppath", false) {
|
||||
input["path"] = strings.TrimPrefix(u.Path, "/")
|
||||
}
|
||||
|
||||
var hs []CredentialHelper
|
||||
if len(ccfg.Helper) == 0 && len(ccfg.AskPass) > 0 {
|
||||
hs = append(hs, &AskPassCredentialHelper{
|
||||
Program: ccfg.AskPass,
|
||||
})
|
||||
if c.Credentials != nil {
|
||||
return c.Credentials, input
|
||||
}
|
||||
|
||||
var h CredentialHelper
|
||||
h = &commandCredentialHelper{
|
||||
SkipPrompt: ccfg.SkipPrompt,
|
||||
helpers := make([]CredentialHelper, 0, 3)
|
||||
if c.cachingCredHelper != nil {
|
||||
helpers = append(helpers, c.cachingCredHelper)
|
||||
}
|
||||
|
||||
if ccfg.Cached {
|
||||
h = withCredentialCache(h)
|
||||
}
|
||||
hs = append(hs, h)
|
||||
|
||||
switch len(hs) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return hs[0], nil
|
||||
}
|
||||
return CredentialHelpers(hs), nil
|
||||
}
|
||||
|
||||
// getCredentialConfig parses a *credsConfig given the OS and Git
|
||||
// configurations.
|
||||
func getCredentialConfig(o, g config.Environment) (*credsConfig, error) {
|
||||
askpass, ok := o.Get("GIT_ASKPASS")
|
||||
if !ok {
|
||||
askpass, ok = g.Get("core.askpass")
|
||||
}
|
||||
if !ok {
|
||||
askpass, ok = o.Get("SSH_ASKPASS")
|
||||
}
|
||||
helper, _ := g.Get("credential.helper")
|
||||
what := &credsConfig{
|
||||
AskPass: askpass,
|
||||
Helper: helper,
|
||||
Cached: g.Bool("lfs.cachecredentials", true),
|
||||
SkipPrompt: o.Bool("GIT_TERMINAL_PROMPT", false),
|
||||
}
|
||||
|
||||
return what, nil
|
||||
}
|
||||
|
||||
// CredentialHelpers is a []CredentialHelper that iterates through each
|
||||
// credential helper to fill, reject, or approve credentials.
|
||||
type CredentialHelpers []CredentialHelper
|
||||
|
||||
// Fill implements CredentialHelper.Fill by asking each CredentialHelper in
|
||||
// order to fill the credentials.
|
||||
//
|
||||
// If a fill was successful, it is returned immediately, and no other
|
||||
// `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
|
||||
// it is returned immediately.
|
||||
func (h CredentialHelpers) Fill(what Creds) (Creds, error) {
|
||||
for _, c := range h {
|
||||
creds, err := c.Fill(what)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if creds != nil {
|
||||
return creds, nil
|
||||
if c.askpassCredHelper != nil {
|
||||
helper, _ := c.uc.Get("credential", rawurl, "helper")
|
||||
if len(helper) == 0 {
|
||||
helpers = append(helpers, c.askpassCredHelper)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Reject implements CredentialHelper.Reject and rejects the given Creds "what"
|
||||
// amongst all knonw CredentialHelpers. If any `CredentialHelper`s returned a
|
||||
// non-nil error, no further `CredentialHelper`s are notified, so as to prevent
|
||||
// inconsistent state.
|
||||
func (h CredentialHelpers) Reject(what Creds) error {
|
||||
for _, c := range h {
|
||||
if err := c.Reject(what); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Approve implements CredentialHelper.Approve and approves the given Creds
|
||||
// "what" amongst all known CredentialHelpers. If any `CredentialHelper`s
|
||||
// returned a non-nil error, no further `CredentialHelper`s are notified, so as
|
||||
// to prevent inconsistent state.
|
||||
func (h CredentialHelpers) Approve(what Creds) error {
|
||||
for _, c := range h {
|
||||
if err := c.Approve(what); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return NewCredentialHelpers(append(helpers, c.commandCredHelper)), input
|
||||
}
|
||||
|
||||
// AskPassCredentialHelper implements the CredentialHelper type for GIT_ASKPASS
|
||||
@ -234,88 +162,6 @@ func (a *AskPassCredentialHelper) args(prompt string) []string {
|
||||
return []string{prompt}
|
||||
}
|
||||
|
||||
type CredentialHelper interface {
|
||||
Fill(Creds) (Creds, error)
|
||||
Reject(Creds) error
|
||||
Approve(Creds) error
|
||||
}
|
||||
|
||||
type Creds map[string]string
|
||||
|
||||
func bufferCreds(c Creds) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func withCredentialCache(helper CredentialHelper) CredentialHelper {
|
||||
return &credentialCacher{
|
||||
cmu: new(sync.Mutex),
|
||||
creds: make(map[string]Creds),
|
||||
helper: helper,
|
||||
}
|
||||
}
|
||||
|
||||
type credentialCacher struct {
|
||||
// cmu guards creds
|
||||
cmu *sync.Mutex
|
||||
creds map[string]Creds
|
||||
helper CredentialHelper
|
||||
}
|
||||
|
||||
func credCacheKey(creds Creds) string {
|
||||
parts := []string{
|
||||
creds["protocol"],
|
||||
creds["host"],
|
||||
creds["path"],
|
||||
}
|
||||
return strings.Join(parts, "//")
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Fill(creds Creds) (Creds, error) {
|
||||
key := credCacheKey(creds)
|
||||
|
||||
c.cmu.Lock()
|
||||
defer c.cmu.Unlock()
|
||||
|
||||
if cache, ok := c.creds[key]; ok {
|
||||
tracerx.Printf("creds: git credential cache (%q, %q, %q)",
|
||||
creds["protocol"], creds["host"], creds["path"])
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
creds, err := c.helper.Fill(creds)
|
||||
if err == nil && len(creds["username"]) > 0 && len(creds["password"]) > 0 {
|
||||
c.creds[key] = creds
|
||||
}
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Reject(creds Creds) error {
|
||||
c.cmu.Lock()
|
||||
defer c.cmu.Unlock()
|
||||
|
||||
delete(c.creds, credCacheKey(creds))
|
||||
return c.helper.Reject(creds)
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Approve(creds Creds) error {
|
||||
err := c.helper.Approve(creds)
|
||||
if err == nil {
|
||||
c.cmu.Lock()
|
||||
c.creds[credCacheKey(creds)] = creds
|
||||
c.cmu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type commandCredentialHelper struct {
|
||||
SkipPrompt bool
|
||||
}
|
||||
@ -332,6 +178,8 @@ func (h *commandCredentialHelper) Reject(creds Creds) error {
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) Approve(creds Creds) error {
|
||||
tracerx.Printf("creds: git credential approve (%q, %q, %q)",
|
||||
creds["protocol"], creds["host"], creds["path"])
|
||||
_, err := h.exec("approve", creds)
|
||||
return err
|
||||
}
|
||||
@ -383,3 +231,198 @@ func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, e
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
type credentialCacher struct {
|
||||
creds map[string]Creds
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newCredentialCacher() *credentialCacher {
|
||||
return &credentialCacher{creds: make(map[string]Creds)}
|
||||
}
|
||||
|
||||
func credCacheKey(creds Creds) string {
|
||||
parts := []string{
|
||||
creds["protocol"],
|
||||
creds["host"],
|
||||
creds["path"],
|
||||
}
|
||||
return strings.Join(parts, "//")
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Fill(what Creds) (Creds, error) {
|
||||
key := credCacheKey(what)
|
||||
c.mu.Lock()
|
||||
cached, ok := c.creds[key]
|
||||
c.mu.Unlock()
|
||||
|
||||
if ok {
|
||||
tracerx.Printf("creds: git credential cache (%q, %q, %q)",
|
||||
what["protocol"], what["host"], what["path"])
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
return nil, credHelperNoOp
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Approve(what Creds) error {
|
||||
key := credCacheKey(what)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, ok := c.creds[key]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.creds[key] = what
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
func (c *credentialCacher) Reject(what Creds) error {
|
||||
key := credCacheKey(what)
|
||||
c.mu.Lock()
|
||||
delete(c.creds, key)
|
||||
c.mu.Unlock()
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
// CredentialHelpers iterates through a slice of CredentialHelper objects
|
||||
// CredentialHelpers is a []CredentialHelper that iterates through each
|
||||
// credential helper to fill, reject, or approve credentials. Typically, the
|
||||
// first success returns immediately. Errors are reported to tracerx, unless
|
||||
// all credential helpers return errors. Any erroring credential helpers are
|
||||
// skipped for future calls.
|
||||
//
|
||||
// A CredentialHelper can return a credHelperNoOp error, signaling that the
|
||||
// CredentialHelpers should try the next one.
|
||||
type CredentialHelpers struct {
|
||||
helpers []CredentialHelper
|
||||
skippedHelpers map[int]bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewCredentialHelpers initializes a new CredentialHelpers from the given
|
||||
// slice of CredentialHelper instances.
|
||||
func NewCredentialHelpers(helpers []CredentialHelper) CredentialHelper {
|
||||
return &CredentialHelpers{
|
||||
helpers: helpers,
|
||||
skippedHelpers: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
var credHelperNoOp = errors.New("no-op!")
|
||||
|
||||
// Fill implements CredentialHelper.Fill by asking each CredentialHelper in
|
||||
// order to fill the credentials.
|
||||
//
|
||||
// If a fill was successful, it is returned immediately, and no other
|
||||
// `CredentialHelper`s are consulted. If any CredentialHelper returns an error,
|
||||
// it is reported to tracerx, and the next one is attempted. If they all error,
|
||||
// then a collection of all the error messages is returned. Erroring credential
|
||||
// helpers are added to the skip list, and never attempted again for the
|
||||
// lifetime of the current Git LFS command.
|
||||
func (s *CredentialHelpers) Fill(what Creds) (Creds, error) {
|
||||
errs := make([]string, 0, len(s.helpers))
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
creds, err := h.Fill(what)
|
||||
if err != nil {
|
||||
if err != credHelperNoOp {
|
||||
s.skip(i)
|
||||
tracerx.Printf("credential fill error: %s", err)
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if creds != nil {
|
||||
return creds, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.New("credential fill errors:\n" + strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Reject implements CredentialHelper.Reject and rejects the given Creds "what"
|
||||
// with the first successful attempt.
|
||||
func (s *CredentialHelpers) Reject(what Creds) error {
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.Reject(what); err != credHelperNoOp {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("no valid credential helpers to reject")
|
||||
}
|
||||
|
||||
// Approve implements CredentialHelper.Approve and approves the given Creds
|
||||
// "what" with the first successful CredentialHelper. If an error occurrs,
|
||||
// it calls Reject() with the same Creds and returns the error immediately. This
|
||||
// ensures a caching credential helper removes the cache, since the Erroring
|
||||
// CredentialHelper never successfully saved it.
|
||||
func (s *CredentialHelpers) Approve(what Creds) error {
|
||||
skipped := make(map[int]bool)
|
||||
for i, h := range s.helpers {
|
||||
if s.skipped(i) {
|
||||
skipped[i] = true
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.Approve(what); err != credHelperNoOp {
|
||||
if err != nil && i > 0 { // clear any cached approvals
|
||||
for j := 0; j < i; j++ {
|
||||
if !skipped[j] {
|
||||
s.helpers[j].Reject(what)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("no valid credential helpers to approve")
|
||||
}
|
||||
|
||||
func (s *CredentialHelpers) skip(i int) {
|
||||
s.mu.Lock()
|
||||
s.skippedHelpers[i] = true
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *CredentialHelpers) skipped(i int) bool {
|
||||
s.mu.Lock()
|
||||
skipped := s.skippedHelpers[i]
|
||||
s.mu.Unlock()
|
||||
return skipped
|
||||
}
|
||||
|
||||
type nullCredentialHelper struct{}
|
||||
|
||||
var (
|
||||
nullCredError = errors.New("No credential helper configured")
|
||||
nullCreds = &nullCredentialHelper{}
|
||||
)
|
||||
|
||||
func (h *nullCredentialHelper) Fill(input Creds) (Creds, error) {
|
||||
return nil, nullCredError
|
||||
}
|
||||
|
||||
func (h *nullCredentialHelper) Approve(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *nullCredentialHelper) Reject(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -5,264 +5,262 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// test that cache satisfies Fill() without looking at creds
|
||||
func TestCredsCacheFillFromCache(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
type testCredHelper struct {
|
||||
fillErr error
|
||||
approveErr error
|
||||
rejectErr error
|
||||
fill []Creds
|
||||
approve []Creds
|
||||
reject []Creds
|
||||
}
|
||||
|
||||
func newTestCredHelper() *testCredHelper {
|
||||
return &testCredHelper{
|
||||
fill: make([]Creds, 0),
|
||||
approve: make([]Creds, 0),
|
||||
reject: make([]Creds, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Fill(input Creds) (Creds, error) {
|
||||
h.fill = append(h.fill, input)
|
||||
return input, h.fillErr
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Approve(creds Creds) error {
|
||||
h.approve = append(h.approve, creds)
|
||||
return h.approveErr
|
||||
}
|
||||
|
||||
func (h *testCredHelper) Reject(creds Creds) error {
|
||||
h.reject = append(h.reject, creds)
|
||||
return h.rejectErr
|
||||
}
|
||||
|
||||
func TestCredHelperSetNoErrors(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
// calling Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
credsWithPass := Creds{"protocol": "https", "host": "example.com", "username": "foo", "password": "bar"}
|
||||
assert.Nil(t, helpers.Approve(credsWithPass))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// calling Approve() again is cached
|
||||
assert.Nil(t, helpers.Approve(credsWithPass))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// access cache
|
||||
for i := 0; i < 3; i++ {
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, credsWithPass, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "u", cached["username"])
|
||||
assert.Equal(t, "p", cached["password"])
|
||||
// Reject() is never cached
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 2, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
// calling Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 3, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
// test that cache caches Fill() value from creds
|
||||
func TestCredsCacheFillFromValidHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
func TestCredHelperSetFillError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
helper1.fillErr = errors.New("boom")
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "u", cached["username"])
|
||||
assert.Equal(t, "p", cached["password"])
|
||||
assert.Nil(t, helpers.Approve(creds))
|
||||
assert.Equal(t, 0, len(helper1.approve))
|
||||
assert.Equal(t, 1, len(helper2.approve))
|
||||
|
||||
creds.list = make([]Creds, 0)
|
||||
filled2, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
// Fill() with cache
|
||||
for i := 0; i < 3; i++ {
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled2)
|
||||
assert.Equal(t, "u", filled2["username"])
|
||||
assert.Equal(t, "p", filled2["password"])
|
||||
}
|
||||
|
||||
// test that cache ignores Fill() value from creds with missing username+password
|
||||
func TestCredsCacheFillFromInvalidHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "no-password",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "no-password", filled["username"])
|
||||
assert.Equal(t, "", filled["password"])
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
// test that cache ignores Fill() value from creds with error
|
||||
func TestCredsCacheFillFromErroringHelperFill(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
|
||||
creds.list = append(creds.list, Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
filled, err := cache.Fill(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
require.NotNil(t, filled)
|
||||
assert.Equal(t, "u", filled["username"])
|
||||
assert.Equal(t, "p", filled["password"])
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
func TestCredsCacheRejectWithoutError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
}
|
||||
|
||||
err := cache.Reject(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 0, len(helper1.reject))
|
||||
assert.Equal(t, 1, len(helper2.reject))
|
||||
|
||||
// Fill() with empty cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill)) // still skipped
|
||||
assert.Equal(t, 2, len(helper2.fill))
|
||||
}
|
||||
|
||||
func TestCredsCacheRejectWithError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
func TestCredHelperSetApproveError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
cache.creds["http//lfs.test//foo/bar"] = Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
}
|
||||
|
||||
err := cache.Reject(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
}
|
||||
|
||||
func TestCredsCacheApproveWithoutError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(creds).(*credentialCacher)
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
|
||||
err := cache.Approve(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "U",
|
||||
"password": "P",
|
||||
})
|
||||
approveErr := errors.New("boom")
|
||||
helper1.approveErr = approveErr
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(cache.creds))
|
||||
cached, ok := cache.creds["http//lfs.test//foo/bar"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "U", cached["username"])
|
||||
assert.Equal(t, "P", cached["password"])
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, approveErr, helpers.Approve(creds))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// cache is never set
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Nil(t, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
}
|
||||
|
||||
func TestCredsCacheApproveWithError(t *testing.T) {
|
||||
creds := newFakeCreds()
|
||||
cache := withCredentialCache(&erroringCreds{creds}).(*credentialCacher)
|
||||
func TestCredHelperSetFillAndApproveError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
credErr := errors.New("boom")
|
||||
helper1.fillErr = credErr
|
||||
helper2.approveErr = credErr
|
||||
|
||||
err := cache.Approve(Creds{
|
||||
"protocol": "http",
|
||||
"host": "lfs.test",
|
||||
"path": "foo/bar",
|
||||
"username": "u",
|
||||
"password": "p",
|
||||
})
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, len(cache.creds))
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, credErr, helpers.Approve(creds))
|
||||
assert.Equal(t, 0, len(helper1.approve)) // skipped
|
||||
assert.Equal(t, 0, len(helper1.reject)) // skipped
|
||||
assert.Equal(t, 1, len(helper2.approve))
|
||||
|
||||
// never approved, so cache is empty
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill)) // still skipped
|
||||
assert.Equal(t, 2, len(helper2.fill))
|
||||
}
|
||||
|
||||
func newFakeCreds() *fakeCreds {
|
||||
return &fakeCreds{list: make([]Creds, 0)}
|
||||
func TestCredHelperSetRejectError(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
rejectErr := errors.New("boom")
|
||||
helper1.rejectErr = rejectErr
|
||||
out, err := helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Nil(t, helpers.Approve(creds))
|
||||
assert.Equal(t, 1, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
// Fill() with cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
|
||||
assert.Equal(t, rejectErr, helpers.Reject(creds))
|
||||
assert.Equal(t, 1, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
|
||||
// failed Reject() still clears cache
|
||||
out, err = helpers.Fill(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, creds, out)
|
||||
assert.Equal(t, 2, len(helper1.fill))
|
||||
assert.Equal(t, 0, len(helper2.fill))
|
||||
}
|
||||
|
||||
type erroringCreds struct {
|
||||
helper CredentialHelper
|
||||
}
|
||||
func TestCredHelperSetAllFillErrors(t *testing.T) {
|
||||
cache := newCredentialCacher()
|
||||
helper1 := newTestCredHelper()
|
||||
helper2 := newTestCredHelper()
|
||||
helpers := NewCredentialHelpers([]CredentialHelper{cache, helper1, helper2})
|
||||
creds := Creds{"protocol": "https", "host": "example.com"}
|
||||
|
||||
func (e *erroringCreds) Fill(creds Creds) (Creds, error) {
|
||||
c, _ := e.helper.Fill(creds)
|
||||
return c, errors.New("fill error")
|
||||
}
|
||||
|
||||
func (e *erroringCreds) Reject(creds Creds) error {
|
||||
e.helper.Reject(creds)
|
||||
return errors.New("reject error")
|
||||
}
|
||||
|
||||
func (e *erroringCreds) Approve(creds Creds) error {
|
||||
e.helper.Approve(creds)
|
||||
return errors.New("approve error")
|
||||
}
|
||||
|
||||
type fakeCreds struct {
|
||||
list []Creds
|
||||
}
|
||||
|
||||
func credsMatch(c1, c2 Creds) bool {
|
||||
return c1["protocol"] == c2["protocol"] &&
|
||||
c1["host"] == c2["host"] &&
|
||||
c1["path"] == c2["path"]
|
||||
}
|
||||
|
||||
func (f *fakeCreds) Fill(creds Creds) (Creds, error) {
|
||||
for _, saved := range f.list {
|
||||
if credsMatch(creds, saved) {
|
||||
return saved, nil
|
||||
helper1.fillErr = errors.New("boom 1")
|
||||
helper2.fillErr = errors.New("boom 2")
|
||||
out, err := helpers.Fill(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "credential fill errors:\nboom 1\nboom 2", err.Error())
|
||||
}
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, 1, len(helper1.fill))
|
||||
assert.Equal(t, 1, len(helper2.fill))
|
||||
|
||||
err = helpers.Approve(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "no valid credential helpers to approve", err.Error())
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
assert.Equal(t, 0, len(helper1.approve))
|
||||
assert.Equal(t, 0, len(helper2.approve))
|
||||
|
||||
func (f *fakeCreds) Reject(creds Creds) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeCreds) Approve(creds Creds) error {
|
||||
return nil
|
||||
err = helpers.Reject(creds)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, "no valid credential helpers to reject", err.Error())
|
||||
}
|
||||
assert.Equal(t, 0, len(helper1.reject))
|
||||
assert.Equal(t, 0, len(helper2.reject))
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
@ -47,7 +45,9 @@ type Client struct {
|
||||
|
||||
LoggingStats bool // DEPRECATED
|
||||
|
||||
// only used for per-host ssl certs
|
||||
commandCredHelper *commandCredentialHelper
|
||||
askpassCredHelper *AskPassCredentialHelper
|
||||
cachingCredHelper *credentialCacher
|
||||
gitEnv config.Environment
|
||||
osEnv config.Environment
|
||||
uc *config.URLConfig
|
||||
@ -71,19 +71,14 @@ func NewClient(ctx Context) (*Client, error) {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("bad netrc file %s", netrcfile))
|
||||
}
|
||||
|
||||
creds, err := getCredentialHelper(osEnv, gitEnv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot find credential helper(s)")
|
||||
}
|
||||
|
||||
cacheCreds := gitEnv.Bool("lfs.cachecredentials", true)
|
||||
var sshResolver SSHResolver = &sshAuthClient{os: osEnv}
|
||||
if gitEnv.Bool("lfs.cachecredentials", true) {
|
||||
if cacheCreds {
|
||||
sshResolver = withSSHCache(sshResolver)
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
Endpoints: NewEndpointFinder(ctx),
|
||||
Credentials: creds,
|
||||
SSH: sshResolver,
|
||||
Netrc: netrc,
|
||||
DialTimeout: gitEnv.Int("lfs.dialtimeout", 0),
|
||||
@ -93,11 +88,31 @@ func NewClient(ctx Context) (*Client, error) {
|
||||
SkipSSLVerify: !gitEnv.Bool("http.sslverify", true) || osEnv.Bool("GIT_SSL_NO_VERIFY", false),
|
||||
Verbose: osEnv.Bool("GIT_CURL_VERBOSE", false),
|
||||
DebuggingVerbose: osEnv.Bool("LFS_DEBUG_HTTP", false),
|
||||
commandCredHelper: &commandCredentialHelper{
|
||||
SkipPrompt: osEnv.Bool("GIT_TERMINAL_PROMPT", false),
|
||||
},
|
||||
gitEnv: gitEnv,
|
||||
osEnv: osEnv,
|
||||
uc: config.NewURLConfig(gitEnv),
|
||||
}
|
||||
|
||||
askpass, ok := osEnv.Get("GIT_ASKPASS")
|
||||
if !ok {
|
||||
askpass, ok = gitEnv.Get("core.askpass")
|
||||
}
|
||||
if !ok {
|
||||
askpass, _ = osEnv.Get("SSH_ASKPASS")
|
||||
}
|
||||
if len(askpass) > 0 {
|
||||
c.askpassCredHelper = &AskPassCredentialHelper{
|
||||
Program: askpass,
|
||||
}
|
||||
}
|
||||
|
||||
if cacheCreds {
|
||||
c.cachingCredHelper = newCredentialCacher()
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@ -191,34 +206,14 @@ func (e testEnv) GetAll(key string) []string {
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
func (e testEnv) Int(key string, def int) (val int) {
|
||||
func (e testEnv) Int(key string, def int) int {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
return config.Int(s, def)
|
||||
}
|
||||
|
||||
func (e testEnv) Bool(key string, def bool) (val bool) {
|
||||
func (e testEnv) Bool(key string, def bool) bool {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "1", "on", "yes", "t":
|
||||
return true
|
||||
case "false", "0", "off", "no", "f":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return config.Bool(s, def)
|
||||
}
|
||||
|
||||
func (e testEnv) All() map[string][]string {
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func (c *Client) doWithNTLM(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL) (*http.Response, error) {
|
||||
res, err := c.Do(req)
|
||||
res, err := c.do(req)
|
||||
if err != nil && !errors.IsAuthError(err) {
|
||||
return res, err
|
||||
}
|
||||
@ -69,7 +69,7 @@ func (c *Client) ntlmReAuth(req *http.Request, credHelper CredentialHelper, cred
|
||||
|
||||
func (c *Client) ntlmNegotiate(req *http.Request, message string) (*http.Response, []byte, error) {
|
||||
req.Header.Add("Authorization", message)
|
||||
res, err := c.Do(req)
|
||||
res, err := c.do(req)
|
||||
if err != nil && !errors.IsAuthError(err) {
|
||||
return res, nil, err
|
||||
}
|
||||
@ -100,7 +100,7 @@ func (c *Client) ntlmChallenge(req *http.Request, challengeBytes []byte, creds C
|
||||
|
||||
authMsg := base64.StdEncoding.EncodeToString(authenticate.Bytes())
|
||||
req.Header.Set("Authorization", "NTLM "+authMsg)
|
||||
return c.Do(req)
|
||||
return c.do(req)
|
||||
}
|
||||
|
||||
func (c *Client) ntlmClientSession(creds Creds) (ntlm.ClientSession, error) {
|
||||
|
@ -90,7 +90,6 @@ func TestNTLMAuth(t *testing.T) {
|
||||
creds := Creds{
|
||||
"protocol": srvURL.Scheme,
|
||||
"host": srvURL.Host,
|
||||
"path": "ntlm",
|
||||
"username": "ntlmdomain\\ntlmuser",
|
||||
"password": "ntlmpass",
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ type lockClient struct {
|
||||
*lfsapi.Client
|
||||
}
|
||||
|
||||
type lockRef struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// LockRequest encapsulates the payload sent across the API when a client would
|
||||
// like to obtain a lock against a particular path on a given remote.
|
||||
type lockRequest struct {
|
||||
@ -192,6 +196,8 @@ func (c *lockClient) Search(remote string, searchReq *lockSearchRequest) (*lockL
|
||||
// lockVerifiableRequest encapsulates the request sent to the server when the
|
||||
// client would like a list of locks to verify a Git push.
|
||||
type lockVerifiableRequest struct {
|
||||
Ref *lockRef `json:"ref"`
|
||||
|
||||
// Cursor is an optional field used to tell the server which lock was
|
||||
// seen last, if scanning through multiple pages of results.
|
||||
//
|
||||
|
@ -206,13 +206,16 @@ func (c *Client) SearchLocks(filter map[string]string, limit int, localOnly bool
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) VerifiableLocks(limit int) (ourLocks, theirLocks []Lock, err error) {
|
||||
func (c *Client) VerifiableLocks(ref *git.Ref, limit int) (ourLocks, theirLocks []Lock, err error) {
|
||||
ourLocks = make([]Lock, 0, limit)
|
||||
theirLocks = make([]Lock, 0, limit)
|
||||
body := &lockVerifiableRequest{
|
||||
Ref: &lockRef{Name: ref.Refspec()},
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
c.cache.Clear()
|
||||
|
||||
for {
|
||||
list, res, err := c.client.SearchVerifiable(c.Remote, body)
|
||||
if res != nil {
|
||||
@ -236,6 +239,7 @@ func (c *Client) VerifiableLocks(limit int) (ourLocks, theirLocks []Lock, err er
|
||||
}
|
||||
|
||||
for _, l := range list.Ours {
|
||||
c.cache.Add(l)
|
||||
ourLocks = append(ourLocks, l)
|
||||
if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
|
||||
return ourLocks, theirLocks, nil
|
||||
@ -243,6 +247,7 @@ func (c *Client) VerifiableLocks(limit int) (ourLocks, theirLocks []Lock, err er
|
||||
}
|
||||
|
||||
for _, l := range list.Theirs {
|
||||
c.cache.Add(l)
|
||||
theirLocks = append(theirLocks, l)
|
||||
if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
|
||||
return ourLocks, theirLocks, nil
|
||||
@ -349,23 +354,6 @@ func (c *Client) lockIdFromPath(path string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch locked files for the current user and cache them locally
|
||||
// This can be used to sync up locked files when moving machines
|
||||
func (c *Client) refreshLockCache() error {
|
||||
ourLocks, _, err := c.VerifiableLocks(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We're going to overwrite the entire local cache
|
||||
c.cache.Clear()
|
||||
for _, l := range ourLocks {
|
||||
c.cache.Add(l)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFileLockedByCurrentCommitter returns whether a file is locked by the
|
||||
// current user, as cached locally
|
||||
func (c *Client) IsFileLockedByCurrentCommitter(path string) bool {
|
||||
|
@ -64,8 +64,7 @@ func TestRefreshCache(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Empty(t, locks)
|
||||
|
||||
// Should load from test data, just Fred's
|
||||
err = client.refreshLockCache()
|
||||
_, _, err = client.VerifiableLocks(nil, 100)
|
||||
assert.Nil(t, err)
|
||||
|
||||
locks, err = client.SearchLocks(nil, 0, true)
|
||||
@ -79,6 +78,8 @@ func TestRefreshCache(t *testing.T) {
|
||||
Lock{Path: "folder/test1.dat", Id: "101", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
Lock{Path: "folder/test2.dat", Id: "102", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
Lock{Path: "root.dat", Id: "103", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
Lock{Path: "other/test1.dat", Id: "199", Owner: &User{Name: "Charles"}, LockedAt: zeroTime},
|
||||
Lock{Path: "folder/test3.dat", Id: "99", Owner: &User{Name: "Alice"}, LockedAt: zeroTime},
|
||||
}, locks)
|
||||
}
|
||||
|
||||
@ -129,7 +130,7 @@ func TestGetVerifiableLocks(t *testing.T) {
|
||||
client, err := NewClient("", lfsclient)
|
||||
assert.Nil(t, err)
|
||||
|
||||
ourLocks, theirLocks, err := client.VerifiableLocks(0)
|
||||
ourLocks, theirLocks, err := client.VerifiableLocks(nil, 0)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Need to include zero time in structure for equal to work
|
||||
|
@ -50,17 +50,24 @@ func mainIntegration() {
|
||||
for _, file := range files {
|
||||
tests <- file
|
||||
}
|
||||
close(tests)
|
||||
|
||||
outputDone := make(chan bool)
|
||||
go func() {
|
||||
for out := range output {
|
||||
fmt.Println(out)
|
||||
}
|
||||
outputDone <- true
|
||||
}()
|
||||
|
||||
go printOutput(output)
|
||||
for i := 0; i < maxprocs; i++ {
|
||||
wg.Add(1)
|
||||
go worker(tests, output, &wg)
|
||||
}
|
||||
|
||||
close(tests)
|
||||
wg.Wait()
|
||||
close(output)
|
||||
printOutput(output)
|
||||
<-outputDone
|
||||
|
||||
if erroring {
|
||||
os.Exit(1)
|
||||
@ -116,19 +123,6 @@ func sendTestOutput(output chan string, testname string, buf *bytes.Buffer, err
|
||||
}
|
||||
}
|
||||
|
||||
func printOutput(output <-chan string) {
|
||||
for {
|
||||
select {
|
||||
case out, ok := <-output:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func worker(tests <-chan string, output chan string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
|
@ -881,11 +881,23 @@ type LockList struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type Ref struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type VerifiableLockRequest struct {
|
||||
Ref *Ref `json:"ref,omitempty"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
func (r *VerifiableLockRequest) RefName() string {
|
||||
if r.Ref == nil {
|
||||
return ""
|
||||
}
|
||||
return r.Ref.Name
|
||||
}
|
||||
|
||||
type VerifiableLockList struct {
|
||||
Ours []Lock `json:"ours"`
|
||||
Theirs []Lock `json:"theirs"`
|
||||
@ -1089,6 +1101,18 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(repo, "branch-required") {
|
||||
parts := strings.Split(repo, "-")
|
||||
lenParts := len(parts)
|
||||
if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != reqBody.RefName() {
|
||||
w.WriteHeader(403)
|
||||
enc.Encode(struct {
|
||||
Message string `json:"message"`
|
||||
}{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], reqBody.RefName())})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ll := &VerifiableLockList{}
|
||||
locks, nextCursor, err := getFilteredLocks(repo, "",
|
||||
reqBody.Cursor,
|
||||
|
@ -28,3 +28,25 @@ begin_test "attempt private access without credential helper"
|
||||
grep "Git credentials for $GITSERVER/$reponame not found" push.log
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "askpass: push with bad askpass"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="askpass-with-bad-askpass"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
git lfs track "*.dat"
|
||||
echo "hello" > a.dat
|
||||
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "initial commit"
|
||||
|
||||
git config "credential.helper" ""
|
||||
GIT_TERMINAL_PROMPT=0 GIT_ASKPASS="lfs-askpass-2" SSH_ASKPASS="dont-call-me" GIT_TRACE=1 git push origin master 2>&1 | tee push.log
|
||||
grep "filling with GIT_ASKPASS" push.log # attempt askpass
|
||||
grep 'credential fill error: exec: "lfs-askpass-2"' push.log # askpass fails
|
||||
grep "creds: git credential fill" push.log # attempt git credential
|
||||
)
|
||||
end_test
|
||||
|
@ -4,11 +4,36 @@
|
||||
|
||||
ensure_git_version_isnt $VERSION_LOWER "2.3.0"
|
||||
|
||||
begin_test "credentails with url-specific helper skips askpass"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="url-specific-helper"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
clone_repo "$reponame" "$reponame"
|
||||
git config credential.useHttpPath false
|
||||
git config credential.helper ""
|
||||
git config credential.$GITSERVER.helper "lfstest"
|
||||
|
||||
git lfs track "*.dat"
|
||||
echo "hello" > a.dat
|
||||
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "initial commit"
|
||||
|
||||
# askpass is skipped
|
||||
GIT_ASKPASS="lfs-bad-cmd" GIT_TRACE=1 git push origin master 2>&1 | tee push.log
|
||||
|
||||
[ "0" -eq "$(grep "filling with GIT_ASKPASS" push.log | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials without useHttpPath, with bad path password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="$(basename "$0" ".sh")"
|
||||
reponame="no-httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
@ -20,16 +45,55 @@ begin_test "credentials without useHttpPath, with bad path password"
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
contents="a"
|
||||
contents_oid=$(calc_oid "$contents")
|
||||
|
||||
printf "$contents" > a.dat
|
||||
printf "a" > a.dat
|
||||
git add a.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git push origin without-path 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin without-path 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
|
||||
echo "credential calls have no path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep "no-httppath-bad-password" | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials with url-specific useHttpPath, with bad path password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="url-specific-httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
|
||||
clone_repo "$reponame" with-url-specific-path
|
||||
git config credential.$GITSERVER.useHttpPath false
|
||||
git checkout -b without-path
|
||||
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
printf "a" > a.dat
|
||||
git add a.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
GIT_TRACE=1 git push origin without-path 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -37,7 +101,7 @@ begin_test "credentials with useHttpPath, with wrong password"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="$(basename "$0" ".sh")"
|
||||
reponame="httppath-bad-password"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:wrong" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
@ -56,8 +120,12 @@ begin_test "credentials with useHttpPath, with wrong password"
|
||||
git add .gitattributes
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git push origin with-path-wrong-pass 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin with-path-wrong-pass 2>&1 | tee push.log
|
||||
[ "0" = "$(grep -c "(1 of 1 files)" push.log)" ]
|
||||
echo "approvals:"
|
||||
[ "0" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "2" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -86,8 +154,17 @@ begin_test "credentials with useHttpPath, with correct password"
|
||||
git add .gitattributes
|
||||
git commit -m "add b.dat"
|
||||
|
||||
git push origin with-path-correct-pass 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git push origin with-path-correct-pass 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
echo "credential calls have path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "$credcalls" | grep "test-credentials" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -175,8 +252,10 @@ begin_test "credentials from netrc"
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "add a.dat"
|
||||
|
||||
git lfs push netrc master 2>&1 | tee push.log
|
||||
GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log
|
||||
grep "(1 of 1 files)" push.log
|
||||
echo "any git credential calls:"
|
||||
[ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
|
@ -47,7 +47,6 @@ for typ in "${expiration_types[@]}"; do
|
||||
|
||||
sshurl="${GITSERVER/http:\/\//ssh://git@}/$reponame"
|
||||
git config lfs.url "$sshurl"
|
||||
git config lfs.cachecredentials "true"
|
||||
|
||||
contents="contents"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
|
@ -25,3 +25,37 @@ begin_test "http.<url>.extraHeader"
|
||||
grep "> X-Foo: baz" curl.log
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "http.<url>.extraHeader with authorization"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="requirecreds"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
# See: test/cmd/lfstest-gitserver.go:1176.
|
||||
user="requirecreds"
|
||||
pass="pass"
|
||||
auth="Basic $(echo -n $user:$pass | base64)"
|
||||
|
||||
git config --add "http.extraHeader" "Authorization: $auth"
|
||||
|
||||
git lfs track "*.dat"
|
||||
printf "contents" > a.dat
|
||||
git add .gitattributes a.dat
|
||||
git commit -m "initial commit"
|
||||
|
||||
git push origin master 2>&1 | tee curl.log
|
||||
if [ "0" -ne "${PIPESTATUS[0]}" ]; then
|
||||
echo >&2 "expected \`git push origin master\` to succeed, didn't"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ "0" -eq "$(grep -c "creds: filling with GIT_ASKPASS" curl.log)" ]
|
||||
[ "0" -eq "$(grep -c "creds: git credential approve" curl.log)" ]
|
||||
[ "0" -eq "$(grep -c "creds: git credential cache" curl.log)" ]
|
||||
[ "0" -eq "$(grep -c "creds: git credential fill" curl.log)" ]
|
||||
[ "0" -eq "$(grep -c "creds: git credential reject" curl.log)" ]
|
||||
)
|
||||
end_test
|
||||
|
@ -440,12 +440,6 @@ begin_test "fetch with no origin remote"
|
||||
# and no origin, but only 1 remote, should pick the only one as default
|
||||
git lfs fetch
|
||||
assert_local_object "$contents_oid" 1
|
||||
|
||||
# delete again, now add a second remote, also non-origin
|
||||
rm -rf .git/lfs
|
||||
git remote add something2 "$GITSERVER/$reponame"
|
||||
git lfs fetch 2>&1 | grep "No default remote"
|
||||
refute_local_object "$contents_oid"
|
||||
)
|
||||
end_test
|
||||
|
||||
|
@ -64,6 +64,35 @@ begin_test "happy path"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "happy path on non-origin remote"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="happy-without-origin"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
clone_repo "$reponame" repo-without-origin
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes
|
||||
git commit -m "track"
|
||||
git push origin master
|
||||
|
||||
clone_repo "$reponame" clone-without-origin
|
||||
git remote rename origin happy-path
|
||||
|
||||
cd ../repo-without-origin
|
||||
echo "a" > a.dat
|
||||
git add a.dat
|
||||
git commit -m "boom"
|
||||
git push origin master
|
||||
|
||||
cd ../clone-without-origin
|
||||
echo "remotes:"
|
||||
git remote
|
||||
git pull happy-path master
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "clears local temp objects"
|
||||
(
|
||||
set -e
|
||||
|
@ -202,6 +202,47 @@ begin_test "migrate import (given branch, exclude remote refs)"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate import (given ref, --skip-fetch)"
|
||||
(
|
||||
set -e
|
||||
|
||||
setup_single_remote_branch
|
||||
|
||||
md_master_oid="$(calc_oid "$(git cat-file -p "refs/heads/master:a.md")")"
|
||||
md_remote_oid="$(calc_oid "$(git cat-file -p "refs/remotes/origin/master:a.md")")"
|
||||
txt_master_oid="$(calc_oid "$(git cat-file -p "refs/heads/master:a.txt")")"
|
||||
txt_remote_oid="$(calc_oid "$(git cat-file -p "refs/remotes/origin/master:a.txt")")"
|
||||
|
||||
git tag pseudo-remote "$(git rev-parse refs/remotes/origin/master)"
|
||||
# Remove the refs/remotes/origin/master ref, and instruct 'git lfs migrate' to
|
||||
# not fetch it.
|
||||
git update-ref -d refs/remotes/origin/master
|
||||
|
||||
git lfs migrate import --skip-fetch
|
||||
|
||||
assert_pointer "refs/heads/master" "a.md" "$md_master_oid" "50"
|
||||
assert_pointer "pseudo-remote" "a.md" "$md_remote_oid" "140"
|
||||
assert_pointer "refs/heads/master" "a.txt" "$txt_master_oid" "30"
|
||||
assert_pointer "pseudo-remote" "a.txt" "$txt_remote_oid" "120"
|
||||
|
||||
assert_local_object "$md_master_oid" "50"
|
||||
assert_local_object "$txt_master_oid" "30"
|
||||
assert_local_object "$md_remote_oid" "140"
|
||||
assert_local_object "$txt_remote_oid" "120"
|
||||
|
||||
master="$(git rev-parse refs/heads/master)"
|
||||
remote="$(git rev-parse pseudo-remote)"
|
||||
|
||||
master_attrs="$(git cat-file -p "$master:.gitattributes")"
|
||||
remote_attrs="$(git cat-file -p "$remote:.gitattributes")"
|
||||
|
||||
echo "$master_attrs" | grep -q "*.md filter=lfs diff=lfs merge=lfs"
|
||||
echo "$master_attrs" | grep -q "*.txt filter=lfs diff=lfs merge=lfs"
|
||||
echo "$remote_attrs" | grep -q "*.md filter=lfs diff=lfs merge=lfs"
|
||||
echo "$remote_attrs" | grep -q "*.txt filter=lfs diff=lfs merge=lfs"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate import (include/exclude ref)"
|
||||
(
|
||||
set -e
|
||||
@ -380,6 +421,20 @@ begin_test "migrate import (--everything)"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate import (ambiguous reference)"
|
||||
(
|
||||
set -e
|
||||
|
||||
setup_multiple_local_branches
|
||||
|
||||
# Create an ambiguously named reference sharing the name as the SHA-1 of
|
||||
# "HEAD".
|
||||
sha="$(git rev-parse HEAD)"
|
||||
git tag "$sha"
|
||||
|
||||
git lfs migrate import --everything
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate import (--everything with args)"
|
||||
(
|
||||
@ -403,8 +458,6 @@ begin_test "migrate import (--everything with --include-ref)"
|
||||
)
|
||||
end_test
|
||||
|
||||
exit 0
|
||||
|
||||
begin_test "migrate import (--everything with --exclude-ref)"
|
||||
(
|
||||
set -e
|
||||
|
@ -132,6 +132,33 @@ begin_test "migrate info (given branch, exclude remote refs)"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate info (given ref, --skip-fetch)"
|
||||
(
|
||||
set -e
|
||||
|
||||
setup_single_remote_branch
|
||||
|
||||
original_remote="$(git rev-parse refs/remotes/origin/master)"
|
||||
original_master="$(git rev-parse refs/heads/master)"
|
||||
|
||||
git tag pseudo-remote "$original_remote"
|
||||
# Remove the refs/remotes/origin/master ref, and instruct 'git lfs migrate' to
|
||||
# not fetch it.
|
||||
git update-ref -d refs/remotes/origin/master
|
||||
|
||||
diff -u <(git lfs migrate info --skip-fetch 2>&1 | tail -n 2) <(cat <<-EOF
|
||||
*.md 190 B 2/2 files(s) 100%
|
||||
*.txt 150 B 2/2 files(s) 100%
|
||||
EOF)
|
||||
|
||||
migrated_remote="$(git rev-parse pseudo-remote)"
|
||||
migrated_master="$(git rev-parse refs/heads/master)"
|
||||
|
||||
assert_ref_unmoved "refs/remotes/origin/master" "$original_remote" "$migrated_remote"
|
||||
assert_ref_unmoved "refs/heads/master" "$original_master" "$migrated_master"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate info (include/exclude ref)"
|
||||
(
|
||||
set -e
|
||||
@ -307,6 +334,21 @@ begin_test "migrate info (--everything)"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate info (ambiguous reference)"
|
||||
(
|
||||
set -e
|
||||
|
||||
setup_multiple_local_branches
|
||||
|
||||
# Create an ambiguously named reference sharing the name as the SHA-1 of
|
||||
# "HEAD".
|
||||
sha="$(git rev-parse HEAD)"
|
||||
git tag "$sha"
|
||||
|
||||
git lfs migrate info --everything
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "migrate info (--everything with args)"
|
||||
(
|
||||
set -e
|
||||
|
@ -583,7 +583,7 @@ begin_test "pre-push with our lock"
|
||||
git commit -m "add unauthorized changes"
|
||||
|
||||
GIT_CURL_VERBOSE=1 git push origin master 2>&1 | tee push.log
|
||||
grep "Consider unlocking your own locked file(s)" push.log
|
||||
grep "Consider unlocking your own locked files" push.log
|
||||
grep "* locked.dat" push.log
|
||||
|
||||
assert_server_lock "$id"
|
||||
@ -631,7 +631,7 @@ begin_test "pre-push with their lock on lfs file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep "Unable to push 1 locked file(s)" push.log
|
||||
grep "Unable to push locked files" push.log
|
||||
grep "* locked_theirs.dat - Git LFS Tests" push.log
|
||||
|
||||
grep "ERROR: Cannot update locked files." push.log
|
||||
@ -687,7 +687,7 @@ begin_test "pre-push with their lock on non-lfs lockable file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep "Unable to push 2 locked file(s)" push.log
|
||||
grep "Unable to push locked files" push.log
|
||||
grep "* large_locked_theirs.dat - Git LFS Tests" push.log
|
||||
grep "* tiny_locked_theirs.dat - Git LFS Tests" push.log
|
||||
grep "ERROR: Cannot update locked files." push.log
|
||||
@ -777,6 +777,96 @@ begin_test "pre-push disable locks verify on partial url"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "pre-push locks verify 403 with good ref"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="lock-verify-master-branch-required"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
contents="example"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
printf "$contents" > a.dat
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes a.dat
|
||||
git commit --message "initial commit"
|
||||
|
||||
git config "lfs.$GITSERVER/$reponame.git.locksverify" true
|
||||
git push origin master 2>&1 | tee push.log
|
||||
|
||||
assert_server_object "$reponame" "$contents_oid"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "pre-push locks verify 403 with good tracked ref"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="lock-verify-tracked-branch-required"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
contents="example"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
printf "$contents" > a.dat
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes a.dat
|
||||
git commit --message "initial commit"
|
||||
|
||||
git config push.default upstream
|
||||
git config branch.master.merge refs/heads/tracked
|
||||
git config "lfs.$GITSERVER/$reponame.git.locksverify" true
|
||||
git push 2>&1 | tee push.log
|
||||
|
||||
assert_server_object "$reponame" "$contents_oid"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "pre-push locks verify 403 with explicit ref"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="lock-verify-explicit-branch-required"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
contents="example"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
printf "$contents" > a.dat
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes a.dat
|
||||
git commit --message "initial commit"
|
||||
|
||||
git config "lfs.$GITSERVER/$reponame.git.locksverify" true
|
||||
git push origin master:explicit 2>&1 | tee push.log
|
||||
|
||||
assert_server_object "$reponame" "$contents_oid"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "pre-push locks verify 403 with bad ref"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="lock-verify-other-branch-required"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
contents="example"
|
||||
contents_oid="$(calc_oid "$contents")"
|
||||
printf "$contents" > a.dat
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes a.dat
|
||||
git commit --message "initial commit"
|
||||
|
||||
git config "lfs.$GITSERVER/$reponame.git.locksverify" true
|
||||
git push origin master 2>&1 | tee push.log
|
||||
grep "failed to push some refs" push.log
|
||||
refute_server_object "$reponame" "$contents_oid"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "pre-push locks verify 5xx with verification unset"
|
||||
(
|
||||
set -e
|
||||
|
@ -485,15 +485,8 @@ begin_test "push ambiguous branch name"
|
||||
# lfs push master, should work
|
||||
git lfs push origin master
|
||||
|
||||
# push ambiguous, should fail
|
||||
set +e
|
||||
# push ambiguous, does not fail since lfs scans git with sha, not ref name
|
||||
git lfs push origin ambiguous
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
|
||||
)
|
||||
end_test
|
||||
|
||||
|
@ -513,3 +513,30 @@ begin_test "track (\$GIT_LFS_TRACK_NO_INSTALL_HOOKS)"
|
||||
[ ! -f .git/hooks/post-merge ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "track (with comments)"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="track-with=comments"
|
||||
git init "$reponame"
|
||||
cd "$reponame"
|
||||
|
||||
echo "*.jpg filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
||||
echo "# *.png filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
||||
echo "*.pdf filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
||||
|
||||
git add .gitattributes
|
||||
git commit -m "initial commit"
|
||||
|
||||
git lfs track 2>&1 | tee track.log
|
||||
if [ "0" -ne "${PIPESTATUS[0]}" ]; then
|
||||
echo >&2 "expected \`git lfs track\` command to exit cleanly, didn't"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ "1" -eq "$(grep -c "\.jpg" track.log)" ]
|
||||
[ "1" -eq "$(grep -c "\.pdf" track.log)" ]
|
||||
[ "0" -eq "$(grep -c "\.png" track.log)" ]
|
||||
)
|
||||
end_test
|
||||
|
@ -266,15 +266,20 @@ size %s
|
||||
wait_for_file() {
|
||||
local filename="$1"
|
||||
n=0
|
||||
while [ $n -lt 10 ]; do
|
||||
wait_time=1
|
||||
while [ $n -lt 17 ]; do
|
||||
if [ -s $filename ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 0.5
|
||||
sleep $wait_time
|
||||
n=`expr $n + 1`
|
||||
if [ $wait_time -lt 4 ]; then
|
||||
wait_time=`expr $wait_time \* 2`
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$filename did not appear after 60 seconds."
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -92,6 +92,10 @@ begin_test () {
|
||||
mkdir "$HOME"
|
||||
cp "$TESTHOME/.gitconfig" "$HOME/.gitconfig"
|
||||
|
||||
# do not let Git use a different configuration file
|
||||
unset GIT_CONFIG
|
||||
unset XDG_CONFIG_HOME
|
||||
|
||||
# allow the subshell to exit non-zero without exiting this process
|
||||
set -x +e
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user