diff --git a/commands/command_checkout.go b/commands/command_checkout.go index a4b95e96..f7af2fd6 100644 --- a/commands/command_checkout.go +++ b/commands/command_checkout.go @@ -63,7 +63,7 @@ func checkoutCommand(cmd *cobra.Command, args []string) { // firstly convert any pathspecs to the root of the repo, in case this is being // executed in a sub-folder func rootedPaths(args []string) []string { - pathConverter, err := lfs.NewCurrentToRepoPathConverter() + pathConverter, err := lfs.NewCurrentToRepoPathConverter(cfg) if err != nil { Panic(err, "Could not checkout") } diff --git a/commands/command_clean.go b/commands/command_clean.go index 314beee8..e9264c2d 100644 --- a/commands/command_clean.go +++ b/commands/command_clean.go @@ -90,7 +90,7 @@ func clean(to io.Writer, from io.Reader, fileName string, fileSize int64) (*lfs. func cleanCommand(cmd *cobra.Command, args []string) { requireStdin("This command should be run by the Git 'clean' filter") - lfs.InstallHooks(false) + installHooks(false) var fileName string if len(args) > 0 { diff --git a/commands/command_clone.go b/commands/command_clone.go index dd5ecdbf..e690453a 100644 --- a/commands/command_clone.go +++ b/commands/command_clone.go @@ -7,7 +7,6 @@ import ( "path/filepath" "strings" - "github.com/git-lfs/git-lfs/lfs" "github.com/git-lfs/git-lfs/localstorage" "github.com/git-lfs/git-lfs/subprocess" @@ -25,7 +24,7 @@ var ( func cloneCommand(cmd *cobra.Command, args []string) { requireGitVersion() - if git.Config.IsGitVersionAtLeast("2.15.0") { + if cfg.IsGitVersionAtLeast("2.15.0") { msg := []string{ "WARNING: 'git lfs clone' is deprecated and will not be updated", " with new flags from 'git clone'", @@ -73,7 +72,7 @@ func cloneCommand(cmd *cobra.Command, args []string) { defer os.Chdir(cwd) // Also need to derive dirs now - localstorage.ResolveDirs() + localstorage.ResolveDirs(cfg) requireInRepo() // Now just call pull with default args @@ -105,7 +104,7 @@ func cloneCommand(cmd *cobra.Command, args []string) { // If --skip-repo wasn't given, install repo-level hooks while // we're still in the checkout directory. - if err := lfs.InstallHooks(false); err != nil { + if err := installHooks(false); err != nil { ExitWithError(err) } } @@ -114,7 +113,7 @@ func cloneCommand(cmd *cobra.Command, args []string) { func postCloneSubmodules(args []string) error { // In git 2.9+ the filter option will have been passed through to submodules // So we need to lfs pull inside each - if !git.Config.IsGitVersionAtLeast("2.9.0") { + if !cfg.IsGitVersionAtLeast("2.9.0") { // In earlier versions submodules would have used smudge filter return nil } diff --git a/commands/command_env.go b/commands/command_env.go index 60651817..62ddf64c 100644 --- a/commands/command_env.go +++ b/commands/command_env.go @@ -2,7 +2,6 @@ package commands import ( "github.com/git-lfs/git-lfs/config" - "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfs" "github.com/spf13/cobra" ) @@ -11,7 +10,7 @@ func envCommand(cmd *cobra.Command, args []string) { config.ShowConfigWarnings = true endpoint := getAPIClient().Endpoints.Endpoint("download", cfg.CurrentRemote) - gitV, err := git.Config.Version() + gitV, err := cfg.GitVersion() if err != nil { gitV = "Error getting git version: " + err.Error() } diff --git a/commands/command_fetch.go b/commands/command_fetch.go index 8de9b000..d47b431f 100644 --- a/commands/command_fetch.go +++ b/commands/command_fetch.go @@ -66,6 +66,7 @@ func fetchCommand(cmd *cobra.Command, args []string) { defer gitscanner.Close() include, exclude := getIncludeExcludeArgs(cmd) + fetchPruneCfg := lfs.NewFetchPruneConfig(cfg.Git) if fetchAllArg { if fetchRecentArg || len(args) > 1 { @@ -89,17 +90,16 @@ func fetchCommand(cmd *cobra.Command, args []string) { success = success && s } - if fetchRecentArg || cfg.FetchPruneConfig().FetchRecentAlways { - s := fetchRecent(refs, filter) + if fetchRecentArg || fetchPruneCfg.FetchRecentAlways { + s := fetchRecent(fetchPruneCfg, refs, filter) success = success && s } } if fetchPruneArg { - fetchconf := cfg.FetchPruneConfig() - verify := fetchconf.PruneVerifyRemoteAlways + verify := fetchPruneCfg.PruneVerifyRemoteAlways // no dry-run or verbose options in fetch, assume false - prune(fetchconf, verify, false, false) + prune(fetchPruneCfg, verify, false, false) } if !success { @@ -169,9 +169,7 @@ func fetchPreviousVersions(ref string, since time.Time, filter *filepathfilter.F } // Fetch recent objects based on config -func fetchRecent(alreadyFetchedRefs []*git.Ref, filter *filepathfilter.Filter) bool { - fetchconf := cfg.FetchPruneConfig() - +func fetchRecent(fetchconf lfs.FetchPruneConfig, alreadyFetchedRefs []*git.Ref, filter *filepathfilter.Filter) bool { if fetchconf.FetchRecentRefsDays == 0 && fetchconf.FetchRecentCommitsDays == 0 { return true } diff --git a/commands/command_filter_process.go b/commands/command_filter_process.go index d21dad1c..90b36c01 100644 --- a/commands/command_filter_process.go +++ b/commands/command_filter_process.go @@ -36,7 +36,7 @@ var filterSmudgeSkip bool func filterCommand(cmd *cobra.Command, args []string) { requireStdin("This command should be run by the Git filter process") - lfs.InstallHooks(false) + installHooks(false) s := git.NewFilterProcessScanner(os.Stdin, os.Stdout) diff --git a/commands/command_fsck.go b/commands/command_fsck.go index 24b4697f..75daf4a8 100644 --- a/commands/command_fsck.go +++ b/commands/command_fsck.go @@ -7,9 +7,9 @@ import ( "os" "path/filepath" - "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfs" + "github.com/git-lfs/git-lfs/localstorage" "github.com/spf13/cobra" ) @@ -24,7 +24,7 @@ var ( // NOTE(zeroshirts): Ideally git would have hooks for fsck such that we could // chain a lfs-fsck, but I don't think it does. func fsckCommand(cmd *cobra.Command, args []string) { - lfs.InstallHooks(false) + installHooks(false) requireInRepo() ref, err := git.CurrentRef() @@ -66,7 +66,7 @@ func fsckCommand(cmd *cobra.Command, args []string) { return } - storageConfig := config.Config.StorageConfig() + storageConfig := localstorage.NewConfig(cfg) badDir := filepath.Join(storageConfig.LfsStorageDir, "bad") Print("Moving corrupt objects to %s", badDir) diff --git a/commands/command_install.go b/commands/command_install.go index bc3bf997..1a466eff 100644 --- a/commands/command_install.go +++ b/commands/command_install.go @@ -18,27 +18,21 @@ var ( ) func installCommand(cmd *cobra.Command, args []string) { - opt := cmdInstallOptions() - if skipSmudgeInstall { - // assume the user is changing their smudge mode, so enable force implicitly - opt.Force = true - } - - if err := lfs.InstallFilters(opt, skipSmudgeInstall); err != nil { + if err := cmdInstallOptions().Install(); err != nil { Print("WARNING: %s", err.Error()) Print("Run `git lfs install --force` to reset git config.") return } - if !skipRepoInstall && (localInstall || lfs.InRepo()) { - localstorage.InitStorageOrFail() + if !skipRepoInstall && (localInstall || cfg.InRepo()) { + localstorage.InitStorageOrFail(cfg) installHooksCommand(cmd, args) } Print("Git LFS initialized.") } -func cmdInstallOptions() lfs.InstallOptions { +func cmdInstallOptions() *lfs.FilterOptions { requireGitVersion() if localInstall { @@ -52,10 +46,12 @@ func cmdInstallOptions() lfs.InstallOptions { if systemInstall && os.Geteuid() != 0 { Print("WARNING: current user is not root/admin, system install is likely to fail.") } - return lfs.InstallOptions{ - Force: forceInstall, - Local: localInstall, - System: systemInstall, + + return &lfs.FilterOptions{ + Force: forceInstall, + Local: localInstall, + System: systemInstall, + SkipSmudge: skipSmudgeInstall, } } diff --git a/commands/command_logs.go b/commands/command_logs.go index 1ca649e2..ac5c8694 100644 --- a/commands/command_logs.go +++ b/commands/command_logs.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" - "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/errors" "github.com/spf13/cobra" ) @@ -33,7 +32,7 @@ func logsShowCommand(cmd *cobra.Command, args []string) { } name := args[0] - by, err := ioutil.ReadFile(filepath.Join(config.LocalLogDir, name)) + by, err := ioutil.ReadFile(filepath.Join(cfg.LocalLogDir(), name)) if err != nil { Exit("Error reading log: %s", name) } @@ -43,12 +42,12 @@ func logsShowCommand(cmd *cobra.Command, args []string) { } func logsClearCommand(cmd *cobra.Command, args []string) { - err := os.RemoveAll(config.LocalLogDir) + err := os.RemoveAll(cfg.LocalLogDir()) if err != nil { - Panic(err, "Error clearing %s", config.LocalLogDir) + Panic(err, "Error clearing %s", cfg.LocalLogDir()) } - Print("Cleared %s", config.LocalLogDir) + Print("Cleared %s", cfg.LocalLogDir()) } func logsBoomtownCommand(cmd *cobra.Command, args []string) { @@ -59,7 +58,7 @@ func logsBoomtownCommand(cmd *cobra.Command, args []string) { } func sortedLogs() []string { - fileinfos, err := ioutil.ReadDir(config.LocalLogDir) + fileinfos, err := ioutil.ReadDir(cfg.LocalLogDir()) if err != nil { return []string{} } diff --git a/commands/command_migrate.go b/commands/command_migrate.go index dde1d049..4d825b83 100644 --- a/commands/command_migrate.go +++ b/commands/command_migrate.go @@ -262,7 +262,7 @@ func init() { // When lfs.TempDir is initialized to "/tmp", // hard-linking can fail when another filesystem is // mounted at "/tmp" (such as tmpfs). - localstorage.InitStorageOrFail() + localstorage.InitStorageOrFail(cfg) } cmd.AddCommand(importCmd, info) diff --git a/commands/command_prune.go b/commands/command_prune.go index f71ee7aa..f45b3264 100644 --- a/commands/command_prune.go +++ b/commands/command_prune.go @@ -7,7 +7,6 @@ import ( "sync" "time" - "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfs" "github.com/git-lfs/git-lfs/localstorage" @@ -32,7 +31,7 @@ func pruneCommand(cmd *cobra.Command, args []string) { Exit("Cannot specify both --verify-remote and --no-verify-remote") } - fetchPruneConfig := cfg.FetchPruneConfig() + fetchPruneConfig := lfs.NewFetchPruneConfig(cfg.Git) verify := !pruneDoNotVerifyArg && (fetchPruneConfig.PruneVerifyRemoteAlways || pruneVerifyArg) prune(fetchPruneConfig, verify, pruneDryRunArg, pruneVerboseArg) @@ -53,7 +52,7 @@ type PruneProgress struct { } type PruneProgressChan chan PruneProgress -func prune(fetchPruneConfig config.FetchPruneConfig, verifyRemote, dryRun, verbose bool) { +func prune(fetchPruneConfig lfs.FetchPruneConfig, verifyRemote, dryRun, verbose bool) { localObjects := make([]localstorage.Object, 0, 100) retainedObjects := tools.NewStringSetWithCapacity(100) var reachableObjects tools.StringSet @@ -345,7 +344,7 @@ func pruneTaskGetPreviousVersionsOfRef(gitscanner *lfs.GitScanner, ref string, s } // Background task, must call waitg.Done() once at end -func pruneTaskGetRetainedCurrentAndRecentRefs(gitscanner *lfs.GitScanner, fetchconf config.FetchPruneConfig, retainChan chan string, errorChan chan error, waitg *sync.WaitGroup) { +func pruneTaskGetRetainedCurrentAndRecentRefs(gitscanner *lfs.GitScanner, fetchconf lfs.FetchPruneConfig, retainChan chan string, errorChan chan error, waitg *sync.WaitGroup) { defer waitg.Done() // We actually increment the waitg in this func since we kick off sub-goroutines @@ -399,7 +398,7 @@ func pruneTaskGetRetainedCurrentAndRecentRefs(gitscanner *lfs.GitScanner, fetchc } // Background task, must call waitg.Done() once at end -func pruneTaskGetRetainedUnpushed(gitscanner *lfs.GitScanner, fetchconf config.FetchPruneConfig, retainChan chan string, errorChan chan error, waitg *sync.WaitGroup) { +func pruneTaskGetRetainedUnpushed(gitscanner *lfs.GitScanner, fetchconf lfs.FetchPruneConfig, retainChan chan string, errorChan chan error, waitg *sync.WaitGroup) { defer waitg.Done() err := gitscanner.ScanUnpushed(fetchconf.PruneRemoteName, func(p *lfs.WrappedPointer, err error) { @@ -423,7 +422,7 @@ func pruneTaskGetRetainedWorktree(gitscanner *lfs.GitScanner, retainChan chan st // Retain other worktree HEADs too // Working copy, branch & maybe commit is different but repo is shared - allWorktreeRefs, err := git.GetAllWorkTreeHEADs(config.LocalGitStorageDir) + allWorktreeRefs, err := git.GetAllWorkTreeHEADs(cfg.LocalGitStorageDir()) if err != nil { errorChan <- err return diff --git a/commands/command_smudge.go b/commands/command_smudge.go index ca341806..475bcb22 100644 --- a/commands/command_smudge.go +++ b/commands/command_smudge.go @@ -151,7 +151,7 @@ func smudge(to io.Writer, from io.Reader, filename string, skip bool, filter *fi func smudgeCommand(cmd *cobra.Command, args []string) { requireStdin("This command should be run by the Git 'smudge' filter") - lfs.InstallHooks(false) + installHooks(false) if !smudgeSkip && cfg.Os.Bool("GIT_LFS_SKIP_SMUDGE", false) { smudgeSkip = true diff --git a/commands/command_track.go b/commands/command_track.go index 0986bbf5..057fb635 100644 --- a/commands/command_track.go +++ b/commands/command_track.go @@ -10,9 +10,7 @@ import ( "strings" "time" - "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/git" - "github.com/git-lfs/git-lfs/lfs" "github.com/git-lfs/git-lfs/tools" "github.com/spf13/cobra" ) @@ -32,18 +30,18 @@ var ( func trackCommand(cmd *cobra.Command, args []string) { requireGitVersion() - if config.LocalGitDir == "" { + if cfg.LocalGitDir() == "" { Print("Not a git repository.") os.Exit(128) } - if config.LocalWorkingDir == "" { + if cfg.LocalWorkingDir() == "" { Print("This operation must be run in a work tree.") os.Exit(128) } if !cfg.Os.Bool("GIT_LFS_TRACK_NO_INSTALL_HOOKS", false) { - lfs.InstallHooks(false) + installHooks(false) } if len(args) == 0 { @@ -51,7 +49,7 @@ func trackCommand(cmd *cobra.Command, args []string) { return } - knownPatterns := git.GetAttributePaths(config.LocalWorkingDir, config.LocalGitDir) + knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir()) lineEnd := getAttributeLineEnding(knownPatterns) if len(lineEnd) == 0 { lineEnd = gitLineEnding(cfg.Git) @@ -59,9 +57,9 @@ func trackCommand(cmd *cobra.Command, args []string) { wd, _ := tools.Getwd() wd = tools.ResolveSymlinks(wd) - relpath, err := filepath.Rel(config.LocalWorkingDir, wd) + relpath, err := filepath.Rel(cfg.LocalWorkingDir(), wd) if err != nil { - Exit("Current directory %q outside of git working directory %q.", wd, config.LocalWorkingDir) + Exit("Current directory %q outside of git working directory %q.", wd, cfg.LocalWorkingDir()) } changedAttribLines := make(map[string]string) @@ -215,7 +213,7 @@ ArgsLoop: } func listPatterns() { - knownPatterns := git.GetAttributePaths(config.LocalWorkingDir, config.LocalGitDir) + knownPatterns := git.GetAttributePaths(cfg.LocalWorkingDir(), cfg.LocalGitDir()) if len(knownPatterns) < 1 { return } diff --git a/commands/command_uninstall.go b/commands/command_uninstall.go index a578e047..56659d74 100644 --- a/commands/command_uninstall.go +++ b/commands/command_uninstall.go @@ -1,20 +1,18 @@ package commands import ( - "github.com/git-lfs/git-lfs/lfs" "github.com/git-lfs/git-lfs/localstorage" "github.com/spf13/cobra" ) // uninstallCmd removes any configuration and hooks set by Git LFS. func uninstallCommand(cmd *cobra.Command, args []string) { - opt := cmdInstallOptions() - if err := lfs.UninstallFilters(opt); err != nil { + if err := cmdInstallOptions().Uninstall(); err != nil { Error(err.Error()) } - if localInstall || lfs.InRepo() { - localstorage.InitStorageOrFail() + if localInstall || cfg.InRepo() { + localstorage.InitStorageOrFail(cfg) uninstallHooksCommand(cmd, args) } @@ -23,7 +21,7 @@ func uninstallCommand(cmd *cobra.Command, args []string) { // uninstallHooksCmd removes any hooks created by Git LFS. func uninstallHooksCommand(cmd *cobra.Command, args []string) { - if err := lfs.UninstallHooks(); err != nil { + if err := uninstallHooks(); err != nil { Error(err.Error()) } diff --git a/commands/command_untrack.go b/commands/command_untrack.go index e00a9b97..0b51fca8 100644 --- a/commands/command_untrack.go +++ b/commands/command_untrack.go @@ -6,24 +6,22 @@ import ( "os" "strings" - "github.com/git-lfs/git-lfs/config" - "github.com/git-lfs/git-lfs/lfs" "github.com/spf13/cobra" ) // untrackCommand takes a list of paths as an argument, and removes each path from the // default attributes file (.gitattributes), if it exists. func untrackCommand(cmd *cobra.Command, args []string) { - if config.LocalGitDir == "" { + if cfg.LocalGitDir() == "" { Print("Not a git repository.") os.Exit(128) } - if config.LocalWorkingDir == "" { + if cfg.LocalWorkingDir() == "" { Print("This operation must be run in a work tree.") os.Exit(128) } - lfs.InstallHooks(false) + installHooks(false) if len(args) < 1 { Print("git lfs untrack [path]*") diff --git a/commands/command_update.go b/commands/command_update.go index eaa4e14f..a9aa4d19 100644 --- a/commands/command_update.go +++ b/commands/command_update.go @@ -3,8 +3,6 @@ package commands import ( "regexp" - "github.com/git-lfs/git-lfs/git" - "github.com/git-lfs/git-lfs/lfs" "github.com/spf13/cobra" ) @@ -31,10 +29,10 @@ func updateCommand(cmd *cobra.Command, args []string) { switch value { case "basic": case "private": - git.Config.SetLocal("", key, "basic") + cfg.SetGitLocalKey("", key, "basic") Print("Updated %s access from %s to %s.", matches[1], value, "basic") default: - git.Config.UnsetLocalKey("", key) + cfg.UnsetGitLocalKey("", key) Print("Removed invalid %s access of %s.", matches[1], value) } } @@ -44,9 +42,9 @@ func updateCommand(cmd *cobra.Command, args []string) { } if updateManual { - Print(lfs.GetHookInstallSteps()) + Print(getHookInstallSteps()) } else { - if err := lfs.InstallHooks(updateForce); err != nil { + if err := installHooks(updateForce); err != nil { Error(err.Error()) Exit("To resolve this, either:\n 1: run `git lfs update --manual` for instructions on how to merge hooks.\n 2: run `git lfs update --force` to overwrite your hook.") } else { diff --git a/commands/commands.go b/commands/commands.go index 3f8b354d..ef7e15a1 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -16,9 +16,9 @@ import ( "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/errors" "github.com/git-lfs/git-lfs/filepathfilter" - "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfs" "github.com/git-lfs/git-lfs/lfsapi" + "github.com/git-lfs/git-lfs/localstorage" "github.com/git-lfs/git-lfs/locking" "github.com/git-lfs/git-lfs/progress" "github.com/git-lfs/git-lfs/tools" @@ -34,11 +34,11 @@ var ( ErrorWriter = io.MultiWriter(os.Stderr, ErrorBuffer) OutputWriter = io.MultiWriter(os.Stdout, ErrorBuffer) ManPages = make(map[string]string, 20) - cfg = config.Config + tqManifest = make(map[string]*tq.Manifest) - tqManifest = make(map[string]*tq.Manifest) - apiClient *lfsapi.Client - global sync.Mutex + cfg *config.Configuration + apiClient *lfsapi.Client + global sync.Mutex includeArg string excludeArg string @@ -91,7 +91,7 @@ func closeAPIClient() error { } func newLockClient(remote string) *locking.Client { - storageConfig := config.Config.StorageConfig() + storageConfig := localstorage.NewConfig(cfg) lockClient, err := locking.NewClient(remote, getAPIClient()) if err == nil { err = lockClient.SetupFileCache(storageConfig.LfsStorageDir) @@ -102,8 +102,8 @@ func newLockClient(remote string) *locking.Client { } // Configure dirs - lockClient.LocalWorkingDir = config.LocalWorkingDir - lockClient.LocalGitDir = config.LocalGitDir + lockClient.LocalWorkingDir = cfg.LocalWorkingDir() + lockClient.LocalGitDir = cfg.LocalGitDir() lockClient.SetLockableFilesReadOnly = cfg.SetLockableFilesReadOnly() return lockClient @@ -138,6 +138,46 @@ func downloadTransfer(p *lfs.WrappedPointer) (name, path, oid string, size int64 return p.Name, path, p.Oid, p.Size } +// Get user-readable manual install steps for hooks +func getHookInstallSteps() string { + hooks := lfs.LoadHooks(cfg.HookDir()) + steps := make([]string, 0, len(hooks)) + for _, h := range hooks { + steps = append(steps, fmt.Sprintf( + "Add the following to .git/hooks/%s:\n\n%s", + h.Type, tools.Indent(h.Contents))) + } + + return strings.Join(steps, "\n\n") +} + +func installHooks(force bool) error { + hooks := lfs.LoadHooks(cfg.HookDir()) + for _, h := range hooks { + if err := h.Install(force); err != nil { + return err + } + } + + return nil +} + +// uninstallHooks removes all hooks in range of the `hooks` var. +func uninstallHooks() error { + if !cfg.InRepo() { + return errors.New("Not in a git repository") + } + + hooks := lfs.LoadHooks(cfg.HookDir()) + for _, h := range hooks { + if err := h.Uninstall(); err != nil { + return err + } + } + + return nil +} + // Error prints a formatted message to Stderr. It also gets printed to the // panic log if one is created for this command. func Error(format string, args ...interface{}) { @@ -253,7 +293,7 @@ func requireStdin(msg string) { } func requireInRepo() { - if !lfs.InRepo() { + if !cfg.InRepo() { Print("Not in a git repository.") os.Exit(128) } @@ -275,11 +315,11 @@ func logPanic(loggedError error) string { now := time.Now() name := now.Format("20060102T150405.999999999") - full := filepath.Join(config.LocalLogDir, name+".log") + full := filepath.Join(cfg.LocalLogDir(), name+".log") - if err := os.MkdirAll(config.LocalLogDir, 0755); err != nil { + if err := os.MkdirAll(cfg.LocalLogDir(), 0755); err != nil { full = "" - fmt.Fprintf(fmtWriter, "Unable to log panic to %s: %s\n\n", config.LocalLogDir, err.Error()) + fmt.Fprintf(fmtWriter, "Unable to log panic to %s: %s\n\n", cfg.LocalLogDir(), err.Error()) } else if file, err := os.Create(full); err != nil { filename := full full = "" @@ -340,7 +380,7 @@ func ipAddresses() []string { func logPanicToWriter(w io.Writer, loggedError error, le string) { // log the version - gitV, err := git.Config.Version() + gitV, err := cfg.GitVersion() if err != nil { gitV = "Error getting git version: " + err.Error() } @@ -407,15 +447,11 @@ func buildProgressMeter(dryRun bool) *progress.ProgressMeter { func requireGitVersion() { minimumGit := "1.8.2" - if !git.Config.IsGitVersionAtLeast(minimumGit) { - gitver, err := git.Config.Version() + if !cfg.IsGitVersionAtLeast(minimumGit) { + gitver, err := cfg.GitVersion() if err != nil { Exit("Error getting git version: %s", err) } Exit("git version >= %s is required for Git LFS, your version: %s", minimumGit, gitver) } } - -func init() { - log.SetOutput(ErrorWriter) -} diff --git a/commands/pull.go b/commands/pull.go index cecdd2ac..0ac26746 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -27,7 +27,7 @@ func newSingleCheckout(gitEnv config.Environment, remote string) abstractCheckou // Get a converter from repo-relative to cwd-relative // Since writing data & calling git update-index must be relative to cwd - pathConverter, err := lfs.NewRepoToCurrentPathConverter() + pathConverter, err := lfs.NewRepoToCurrentPathConverter(cfg) if err != nil { Panic(err, "Could not convert file paths") } diff --git a/commands/run.go b/commands/run.go index dbac77d5..d55b6eb0 100644 --- a/commands/run.go +++ b/commands/run.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "log" "os" "path/filepath" "strings" @@ -51,6 +52,8 @@ func RegisterCommand(name string, runFn func(cmd *cobra.Command, args []string), // Run initializes the 'git-lfs' command and runs it with the given stdin and // command line args. func Run() { + log.SetOutput(ErrorWriter) + root := NewCommand("git-lfs", gitlfsCommand) root.PreRun = nil @@ -59,6 +62,8 @@ func Run() { root.SetHelpFunc(helpCommand) root.SetUsageFunc(usageCommand) + cfg = config.Config + for _, f := range commandFuncs { if cmd := f(); cmd != nil { root.AddCommand(cmd) @@ -78,12 +83,12 @@ func gitlfsCommand(cmd *cobra.Command, args []string) { // necessary to wire it up via `cobra.Command.PreRun`. When run, this function // will resolve the localstorage directories. func resolveLocalStorage(cmd *cobra.Command, args []string) { - localstorage.ResolveDirs() + localstorage.ResolveDirs(cfg) setupHTTPLogger(getAPIClient()) } func setupLocalStorage(cmd *cobra.Command, args []string) { - config.ResolveGitBasicDirs() + cfg.ResolveGitBasicDirs() setupHTTPLogger(getAPIClient()) } @@ -113,7 +118,7 @@ func setupHTTPLogger(c *lfsapi.Client) { return } - logBase := filepath.Join(config.LocalLogDir, "http") + logBase := filepath.Join(cfg.LocalLogDir(), "http") if err := os.MkdirAll(logBase, 0755); err != nil { fmt.Fprintf(os.Stderr, "Error logging http stats: %s\n", err) return diff --git a/commands/uploader.go b/commands/uploader.go index 6840aea0..475a7435 100644 --- a/commands/uploader.go +++ b/commands/uploader.go @@ -11,7 +11,6 @@ import ( "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/lfs" "github.com/git-lfs/git-lfs/lfsapi" "github.com/git-lfs/git-lfs/locking" @@ -412,7 +411,7 @@ func ensureFile(smudgePath, cleanPath string, allowMissing bool) error { return nil } - localPath := filepath.Join(config.LocalWorkingDir, smudgePath) + localPath := filepath.Join(cfg.LocalWorkingDir(), smudgePath) file, err := os.Open(localPath) if err != nil { if allowMissing { @@ -486,6 +485,6 @@ func disableFor(endpoint lfsapi.Endpoint) error { key := strings.Join([]string{"lfs", endpoint.Url, "locksverify"}, ".") - _, err := git.Config.SetLocal("", key, "false") + _, err := cfg.SetGitLocalKey("", key, "false") return err } diff --git a/config/config.go b/config/config.go index b4354449..212e5d56 100644 --- a/config/config.go +++ b/config/config.go @@ -4,15 +4,11 @@ package config import ( "fmt" - "reflect" - "regexp" - "strconv" - "strings" + "os" + "path/filepath" "sync" - "path/filepath" - - "github.com/git-lfs/git-lfs/errors" + "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/tools" ) @@ -23,32 +19,6 @@ var ( gitConfigWarningPrefix = "lfs." ) -// FetchPruneConfig collects together the config options that control fetching and pruning -type FetchPruneConfig struct { - // The number of days prior to current date for which (local) refs other than HEAD - // will be fetched with --recent (default 7, 0 = only fetch HEAD) - FetchRecentRefsDays int `git:"lfs.fetchrecentrefsdays"` - // Makes the FetchRecentRefsDays option apply to remote refs from fetch source as well (default true) - FetchRecentRefsIncludeRemotes bool `git:"lfs.fetchrecentremoterefs"` - // number of days prior to latest commit on a ref that we'll fetch previous - // LFS changes too (default 0 = only fetch at ref) - FetchRecentCommitsDays int `git:"lfs.fetchrecentcommitsdays"` - // Whether to always fetch recent even without --recent - FetchRecentAlways bool `git:"lfs.fetchrecentalways"` - // Number of days added to FetchRecent*; data outside combined window will be - // deleted when prune is run. (default 3) - PruneOffsetDays int `git:"lfs.pruneoffsetdays"` - // Always verify with remote before pruning - PruneVerifyRemoteAlways bool `git:"lfs.pruneverifyremotealways"` - // Name of remote to check for unpushed and verify checks - PruneRemoteName string `git:"lfs.pruneremotetocheck"` -} - -// Storage configuration -type StorageConfig struct { - LfsStorageDir string `git:"lfs.storage"` -} - type Configuration struct { // Os provides a `*Environment` used to access to the system's // environment through os.Getenv. It is the point of entry for all @@ -60,6 +30,12 @@ type Configuration struct { // configuration. Git Environment + // gitConfig can fetch or modify the current Git config and track the Git + // version. + gitConfig *git.Configuration + + fs *fs + CurrentRemote string loading sync.Mutex // guards initialization of gitConfig and remotes @@ -68,12 +44,38 @@ type Configuration struct { } func New() *Configuration { - c := &Configuration{Os: EnvironmentOf(NewOsFetcher())} - c.Git = &gitEnvironment{config: c} - initConfig(c) + gitConf := git.Config + c := &Configuration{ + CurrentRemote: defaultRemote, + Os: EnvironmentOf(NewOsFetcher()), + gitConfig: gitConf, + } + c.Git = &delayedEnvironment{ + callback: func() Environment { + sources, err := gitConf.Sources(filepath.Join(c.LocalWorkingDir(), ".lfsconfig")) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading git config: %s\n", err) + } + return c.readGitConfig(sources...) + }, + } return c } +func (c *Configuration) readGitConfig(gitconfigs ...*git.ConfigurationSource) Environment { + gf, extensions, uniqRemotes := readGitConfig(gitconfigs...) + c.extensions = extensions + c.remotes = make([]string, 0, len(uniqRemotes)) + for remote, isOrigin := range uniqRemotes { + if isOrigin { + continue + } + c.remotes = append(c.remotes, remote) + } + + return EnvironmentOf(gf) +} + // Values is a convenience type used to call the NewFromValues function. It // specifies `Git` and `Env` maps to use as mock values, instead of calling out // to real `.gitconfig`s and the `os.Getenv` function. @@ -90,137 +92,29 @@ type Values struct { // This method should only be used during testing. func NewFrom(v Values) *Configuration { c := &Configuration{ - Os: EnvironmentOf(mapFetcher(v.Os)), - Git: EnvironmentOf(mapFetcher(v.Git)), + CurrentRemote: defaultRemote, + Os: EnvironmentOf(mapFetcher(v.Os)), + gitConfig: git.Config, + } + c.Git = &delayedEnvironment{ + callback: func() Environment { + source := &git.ConfigurationSource{ + Lines: make([]string, 0, len(v.Git)), + } + + for key, values := range v.Git { + for _, value := range values { + fmt.Printf("Config: %s=%s\n", key, value) + source.Lines = append(source.Lines, fmt.Sprintf("%s=%s", key, value)) + } + } + + return c.readGitConfig(source) + }, } - initConfig(c) return c } -func initConfig(c *Configuration) { - c.CurrentRemote = defaultRemote -} - -// Unmarshal unmarshals the *Configuration in context into all of `v`'s fields, -// according to the following rules: -// -// Values are marshaled according to the given key and environment, as follows: -// type T struct { -// Field string `git:"key"` -// Other string `os:"key"` -// } -// -// If an unknown environment is given, an error will be returned. If there is no -// method supporting conversion into a field's type, an error will be returned. -// If no value is associated with the given key and environment, the field will -// // only be modified if there is a config value present matching the given -// key. If the field is already set to a non-zero value of that field's type, -// then it will be left alone. -// -// Otherwise, the field will be set to the value of calling the -// appropriately-typed method on the specified environment. -func (c *Configuration) Unmarshal(v interface{}) error { - into := reflect.ValueOf(v) - if into.Kind() != reflect.Ptr { - return fmt.Errorf("lfs/config: unable to parse non-pointer type of %T", v) - } - into = into.Elem() - - for i := 0; i < into.Type().NumField(); i++ { - field := into.Field(i) - sfield := into.Type().Field(i) - - lookups, err := c.parseTag(sfield.Tag) - if err != nil { - return err - } - - var val interface{} - for _, lookup := range lookups { - if _, ok := lookup.Get(); !ok { - continue - } - - switch sfield.Type.Kind() { - case reflect.String: - val, _ = lookup.Get() - case reflect.Int: - val = lookup.Int(int(field.Int())) - case reflect.Bool: - val = lookup.Bool(field.Bool()) - default: - return fmt.Errorf("lfs/config: unsupported target type for field %q: %v", - sfield.Name, sfield.Type.String()) - } - - if val != nil { - break - } - } - - if val != nil { - into.Field(i).Set(reflect.ValueOf(val)) - } - } - - return nil -} - -var ( - tagRe = regexp.MustCompile("((\\w+:\"[^\"]*\")\\b?)+") - emptyEnv = EnvironmentOf(MapFetcher(nil)) -) - -type lookup struct { - key string - env Environment -} - -func (l *lookup) Get() (interface{}, bool) { return l.env.Get(l.key) } -func (l *lookup) Int(or int) int { return l.env.Int(l.key, or) } -func (l *lookup) Bool(or bool) bool { return l.env.Bool(l.key, or) } - -// parseTag returns the key, environment, and optional error assosciated with a -// given tag. It will return the XOR of either the `git` or `os` tag. That is to -// say, a field tagged with EITHER `git` OR `os` is valid, but pone tagged with -// both is not. -// -// If neither field was found, then a nil environment will be returned. -func (c *Configuration) parseTag(tag reflect.StructTag) ([]*lookup, error) { - var lookups []*lookup - - parts := tagRe.FindAllString(string(tag), -1) - for _, part := range parts { - sep := strings.SplitN(part, ":", 2) - if len(sep) != 2 { - return nil, errors.Errorf("config: invalid struct tag %q", tag) - } - - var env Environment - switch strings.ToLower(sep[0]) { - case "git": - env = c.Git - case "os": - env = c.Os - default: - // ignore other struct tags, like `json:""`, etc. - env = emptyEnv - } - - uq, err := strconv.Unquote(sep[1]) - if err != nil { - return nil, err - } - - lookups = append(lookups, &lookup{ - key: uq, - env: env, - }) - } - - return lookups, nil -} - // BasicTransfersOnly returns whether to only allow "basic" HTTP transfers. // Default is false, including if the lfs.basictransfersonly is invalid func (c *Configuration) BasicTransfersOnly() bool { @@ -245,7 +139,6 @@ func (c *Configuration) FetchExcludePaths() []string { func (c *Configuration) Remotes() []string { c.loadGitConfig() - return c.remotes } @@ -259,34 +152,6 @@ func (c *Configuration) SortedExtensions() ([]Extension, error) { return SortExtensions(c.Extensions()) } -func (c *Configuration) FetchPruneConfig() FetchPruneConfig { - f := &FetchPruneConfig{ - FetchRecentRefsDays: 7, - FetchRecentRefsIncludeRemotes: true, - PruneOffsetDays: 3, - PruneRemoteName: "origin", - } - - if err := c.Unmarshal(f); err != nil { - panic(err.Error()) - } - return *f -} - -func (c *Configuration) StorageConfig() StorageConfig { - s := &StorageConfig{ - LfsStorageDir: "lfs", - } - - if err := c.Unmarshal(s); err != nil { - panic(err.Error()) - } - if !filepath.IsAbs(s.LfsStorageDir) { - s.LfsStorageDir = filepath.Join(LocalGitStorageDir, s.LfsStorageDir) - } - return *s -} - func (c *Configuration) SkipDownloadErrors() bool { return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false) } @@ -295,6 +160,110 @@ func (c *Configuration) SetLockableFilesReadOnly() bool { return c.Os.Bool("GIT_LFS_SET_LOCKABLE_READONLY", true) && c.Git.Bool("lfs.setlockablereadonly", true) } +func (c *Configuration) HookDir() string { + if c.gitConfig.IsGitVersionAtLeast("2.9.0") { + hp, ok := c.Git.Get("core.hooksPath") + if ok { + return hp + } + } + return filepath.Join(c.LocalGitDir(), "hooks") +} + +func (c *Configuration) InRepo() bool { + return c.fs.InRepo() +} + +func (c *Configuration) LocalWorkingDir() string { + c.ResolveGitBasicDirs() + return c.fs.WorkingDir +} + +func (c *Configuration) LocalGitDir() string { + c.ResolveGitBasicDirs() + return c.fs.GitDir +} + +func (c *Configuration) LocalGitStorageDir() string { + c.ResolveGitBasicDirs() + return c.fs.GitStorageDir +} + +func (c *Configuration) LocalReferenceDir() string { + return LocalReferenceDir +} + +func (c *Configuration) LocalLogDir() string { + c.ResolveGitBasicDirs() + return c.fs.LogDir +} + +func (c *Configuration) SetLocalLogDir(s string) { + c.ResolveGitBasicDirs() + c.fs.LogDir = s +} + +func (c *Configuration) GitConfig() *git.Configuration { + return c.gitConfig +} + +func (c *Configuration) GitVersion() (string, error) { + return c.gitConfig.Version() +} + +func (c *Configuration) IsGitVersionAtLeast(ver string) bool { + return c.gitConfig.IsGitVersionAtLeast(ver) +} + +func (c *Configuration) FindGitGlobalKey(key string) string { + return c.gitConfig.FindGlobal(key) +} + +func (c *Configuration) FindGitSystemKey(key string) string { + return c.gitConfig.FindSystem(key) +} + +func (c *Configuration) FindGitLocalKey(key string) string { + return c.gitConfig.FindLocal(key) +} + +func (c *Configuration) SetGitGlobalKey(key, val string) (string, error) { + return c.gitConfig.SetGlobal(key, val) +} + +func (c *Configuration) SetGitSystemKey(key, val string) (string, error) { + return c.gitConfig.SetSystem(key, val) +} + +func (c *Configuration) SetGitLocalKey(file, key, val string) (string, error) { + return c.gitConfig.SetLocal(file, key, val) +} + +func (c *Configuration) UnsetGitGlobalSection(key string) (string, error) { + return c.gitConfig.UnsetGlobalSection(key) +} + +func (c *Configuration) UnsetGitSystemSection(key string) (string, error) { + return c.gitConfig.UnsetSystemSection(key) +} + +func (c *Configuration) UnsetGitLocalSection(key string) (string, error) { + return c.gitConfig.UnsetLocalSection(key) +} + +func (c *Configuration) UnsetGitLocalKey(file, key string) (string, error) { + return c.gitConfig.UnsetLocalKey(file, key) +} + +func (c *Configuration) ResolveGitBasicDirs() { + c.loading.Lock() + defer c.loading.Unlock() + + if c.fs == nil { + c.fs = resolveGitBasicDirs() + } +} + // loadGitConfig is a temporary measure to support legacy behavior dependent on // accessing properties set by ReadGitConfig, namely: // - `c.extensions` @@ -306,12 +275,10 @@ func (c *Configuration) SetLockableFilesReadOnly() bool { // // loadGitConfig returns a bool returning whether or not `loadGitConfig` was // called AND the method did not return early. -func (c *Configuration) loadGitConfig() bool { - if g, ok := c.Git.(*gitEnvironment); ok { - return g.loadGitConfig() +func (c *Configuration) loadGitConfig() { + if g, ok := c.Git.(*delayedEnvironment); ok { + g.Load() } - - return false } // CurrentCommitter returns the name/email that would be used to author a commit diff --git a/config/config_test.go b/config/config_test.go index 390f5b48..c45083ba 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,7 +2,6 @@ package config import ( "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -67,17 +66,12 @@ func TestTusTransfersAllowedInvalidValue(t *testing.T) { func TestLoadValidExtension(t *testing.T) { cfg := NewFrom(Values{ - Git: map[string][]string{}, - }) - - cfg.extensions = map[string]Extension{ - "foo": Extension{ - "foo", - "foo-clean %f", - "foo-smudge %f", - 2, + Git: map[string][]string{ + "lfs.extension.foo.clean": []string{"foo-clean %f"}, + "lfs.extension.foo.smudge": []string{"foo-smudge %f"}, + "lfs.extension.foo.priority": []string{"2"}, }, - } + }) ext := cfg.Extensions()["foo"] @@ -97,40 +91,6 @@ func TestLoadInvalidExtension(t *testing.T) { assert.Equal(t, 0, ext.Priority) } -func TestFetchPruneConfigDefault(t *testing.T) { - cfg := NewFrom(Values{}) - fp := cfg.FetchPruneConfig() - - assert.Equal(t, 7, fp.FetchRecentRefsDays) - assert.Equal(t, 0, fp.FetchRecentCommitsDays) - assert.Equal(t, 3, fp.PruneOffsetDays) - assert.True(t, fp.FetchRecentRefsIncludeRemotes) - assert.Equal(t, 3, fp.PruneOffsetDays) - assert.Equal(t, "origin", fp.PruneRemoteName) - assert.False(t, fp.PruneVerifyRemoteAlways) - -} -func TestFetchPruneConfigCustom(t *testing.T) { - cfg := NewFrom(Values{ - Git: map[string][]string{ - "lfs.fetchrecentrefsdays": []string{"12"}, - "lfs.fetchrecentremoterefs": []string{"false"}, - "lfs.fetchrecentcommitsdays": []string{"9"}, - "lfs.pruneoffsetdays": []string{"30"}, - "lfs.pruneverifyremotealways": []string{"true"}, - "lfs.pruneremotetocheck": []string{"upstream"}, - }, - }) - fp := cfg.FetchPruneConfig() - - assert.Equal(t, 12, fp.FetchRecentRefsDays) - assert.Equal(t, 9, fp.FetchRecentCommitsDays) - assert.False(t, fp.FetchRecentRefsIncludeRemotes) - assert.Equal(t, 30, fp.PruneOffsetDays) - assert.Equal(t, "upstream", fp.PruneRemoteName) - assert.True(t, fp.PruneVerifyRemoteAlways) -} - func TestFetchIncludeExcludesAreCleaned(t *testing.T) { cfg := NewFrom(Values{ Git: map[string][]string{ @@ -142,157 +102,3 @@ func TestFetchIncludeExcludesAreCleaned(t *testing.T) { assert.Equal(t, []string{"/path/to/clean"}, cfg.FetchIncludePaths()) assert.Equal(t, []string{"/other/path/to/clean"}, cfg.FetchExcludePaths()) } - -func TestUnmarshalMultipleTypes(t *testing.T) { - cfg := NewFrom(Values{ - Git: map[string][]string{ - "string": []string{"string"}, - "int": []string{"1"}, - "bool": []string{"true"}, - }, - Os: map[string][]string{ - "string": []string{"string"}, - "int": []string{"1"}, - "bool": []string{"true"}, - }, - }) - - v := &struct { - GitString string `git:"string"` - GitInt int `git:"int"` - GitBool bool `git:"bool"` - OsString string `os:"string"` - OsInt int `os:"int"` - OsBool bool `os:"bool"` - }{} - - assert.Nil(t, cfg.Unmarshal(v)) - - assert.Equal(t, "string", v.GitString) - assert.Equal(t, 1, v.GitInt) - assert.Equal(t, true, v.GitBool) - assert.Equal(t, "string", v.OsString) - assert.Equal(t, 1, v.OsInt) - assert.Equal(t, true, v.OsBool) -} - -func TestUnmarshalErrsOnNonPointerType(t *testing.T) { - type T struct { - Foo string `git:"foo"` - } - - cfg := NewFrom(Values{}) - - err := cfg.Unmarshal(T{}) - - assert.Equal(t, "lfs/config: unable to parse non-pointer type of config.T", err.Error()) -} - -func TestUnmarshalLeavesNonZeroValuesWhenKeysEmpty(t *testing.T) { - v := &struct { - String string `git:"string"` - Int int `git:"int"` - Bool bool `git:"bool"` - }{"foo", 1, true} - - cfg := NewFrom(Values{}) - - err := cfg.Unmarshal(v) - - assert.Nil(t, err) - assert.Equal(t, "foo", v.String) - assert.Equal(t, 1, v.Int) - assert.Equal(t, true, v.Bool) -} - -func TestUnmarshalOverridesNonZeroValuesWhenValuesPresent(t *testing.T) { - v := &struct { - String string `git:"string"` - Int int `git:"int"` - Bool bool `git:"bool"` - }{"foo", 1, true} - - cfg := NewFrom(Values{ - Git: map[string][]string{ - "string": []string{"bar"}, - "int": []string{"2"}, - "bool": []string{"false"}, - }, - }) - - err := cfg.Unmarshal(v) - - assert.Nil(t, err) - assert.Equal(t, "bar", v.String) - assert.Equal(t, 2, v.Int) - assert.Equal(t, false, v.Bool) -} - -func TestUnmarshalAllowsBothOsAndGitTags(t *testing.T) { - v := &struct { - String string `git:"string" os:"STRING"` - }{} - - cfg := NewFrom(Values{ - Git: map[string][]string{"string": []string{"foo"}}, - Os: map[string][]string{"STRING": []string{"bar"}}, - }) - - err := cfg.Unmarshal(v) - - assert.Nil(t, err) - assert.Equal(t, "foo", v.String) -} - -func TestUnmarshalYieldsToDefaultIfBothEnvsMissing(t *testing.T) { - v := &struct { - String string `git:"string" os:"STRING"` - }{"foo"} - - cfg := NewFrom(Values{}) - - err := cfg.Unmarshal(v) - - assert.Nil(t, err) - assert.Equal(t, "foo", v.String) -} - -func TestUnmarshalOverridesDefaultIfAnyEnvPresent(t *testing.T) { - v := &struct { - String string `git:"string" os:"STRING"` - }{"foo"} - - cfg := NewFrom(Values{ - Git: map[string][]string{"string": []string{"bar"}}, - Os: map[string][]string{"STRING": []string{"baz"}}, - }) - - err := cfg.Unmarshal(v) - - assert.Nil(t, err) - assert.Equal(t, "bar", v.String) -} - -func TestUnmarshalIgnoresUnknownEnvironments(t *testing.T) { - v := &struct { - String string `unknown:"string"` - }{} - - cfg := NewFrom(Values{}) - - assert.Nil(t, cfg.Unmarshal(v)) -} - -func TestUnmarshalErrsOnUnsupportedTypes(t *testing.T) { - v := &struct { - Unsupported time.Duration `git:"duration"` - }{} - - cfg := NewFrom(Values{ - Git: map[string][]string{"duration": []string{"foo"}}, - }) - - err := cfg.Unmarshal(v) - - assert.Equal(t, "lfs/config: unsupported target type for field \"Unsupported\": time.Duration", err.Error()) -} diff --git a/config/delayed_environment.go b/config/delayed_environment.go new file mode 100644 index 00000000..b80caf48 --- /dev/null +++ b/config/delayed_environment.go @@ -0,0 +1,68 @@ +package config + +import ( + "sync" +) + +// delayedEnvironment is an implementation of the Environment which wraps the legacy +// behavior of `*config.Configuration.loadGitConfig()`. +// +// It is functionally equivelant to call `cfg.loadGitConfig()` before calling +// methods on the Environment type. +type delayedEnvironment struct { + env Environment + loading sync.Mutex + callback func() Environment +} + +// Get is shorthand for calling the e.Load(), and then returning +// `e.env.Get(key)`. +func (e *delayedEnvironment) Get(key string) (string, bool) { + e.Load() + return e.env.Get(key) +} + +// Get is shorthand for calling the e.Load(), and then returning +// `e.env.GetAll(key)`. +func (e *delayedEnvironment) GetAll(key string) []string { + e.Load() + return e.env.GetAll(key) +} + +// Get is shorthand for calling the e.Load(), and then returning +// `e.env.Bool(key, def)`. +func (e *delayedEnvironment) Bool(key string, def bool) bool { + e.Load() + return e.env.Bool(key, def) +} + +// Get is shorthand for calling the e.Load(), and then returning +// `e.env.Int(key, def)`. +func (e *delayedEnvironment) Int(key string, def int) int { + e.Load() + return e.env.Int(key, def) +} + +// All returns a copy of all the key/value pairs for the current git config. +func (e *delayedEnvironment) All() map[string][]string { + e.Load() + return e.env.All() +} + +// Load reads and parses the .gitconfig by calling ReadGitConfig. It +// also sets values on the configuration instance `g.config`. +// +// If Load has already been called, this method will bail out early, +// and return false. Otherwise it will preform the entire parse and return true. +// +// Load is safe to call across multiple goroutines. +func (e *delayedEnvironment) Load() { + e.loading.Lock() + defer e.loading.Unlock() + + if e.env != nil { + return + } + + e.env = e.callback() +} diff --git a/config/filesystem.go b/config/filesystem.go index 2b767338..39687f91 100644 --- a/config/filesystem.go +++ b/config/filesystem.go @@ -13,32 +13,48 @@ import ( ) var ( - LocalWorkingDir string - LocalGitDir string // parent of index / config / hooks etc - LocalGitStorageDir string // parent of objects/lfs (may be same as LocalGitDir but may not) - LocalReferenceDir string // alternative local media dir (relative to clone reference repo) - LocalLogDir string + LocalReferenceDir string // alternative local media dir (relative to clone reference repo) ) +type fs struct { + WorkingDir string + GitDir string // parent of index / config / hooks etc + GitStorageDir string // parent of objects/lfs (may be same as LocalGitDir but may not) + ReferenceDir string // alternative local media dir (relative to clone reference repo) + LogDir string +} + +func (f *fs) InRepo() bool { + if f == nil { + return false + } + return len(f.GitDir) > 0 +} + // Determins the LocalWorkingDir, LocalGitDir etc -func ResolveGitBasicDirs() { - var err error - LocalGitDir, LocalWorkingDir, err = git.GitAndRootDirs() - if err == nil { - // Make sure we've fully evaluated symlinks, failure to do consistently - // can cause discrepancies - LocalGitDir = tools.ResolveSymlinks(LocalGitDir) - LocalWorkingDir = tools.ResolveSymlinks(LocalWorkingDir) - - LocalGitStorageDir = resolveGitStorageDir(LocalGitDir) - LocalReferenceDir = resolveReferenceDir(LocalGitStorageDir) - - } else { +func resolveGitBasicDirs() *fs { + localGitDir, localWorkingDir, err := git.GitAndRootDirs() + if err != nil { errMsg := err.Error() tracerx.Printf("Error running 'git rev-parse': %s", errMsg) if !strings.Contains(errMsg, "Not a git repository") { fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg) } + return &fs{} + } + + // Make sure we've fully evaluated symlinks, failure to do consistently + // can cause discrepancies + localGitDir = tools.ResolveSymlinks(localGitDir) + localWorkingDir = tools.ResolveSymlinks(localWorkingDir) + localGitStorageDir := resolveGitStorageDir(localGitDir) + LocalReferenceDir = resolveReferenceDir(localGitStorageDir) + + return &fs{ + GitDir: localGitDir, + WorkingDir: localWorkingDir, + GitStorageDir: localGitStorageDir, + ReferenceDir: LocalReferenceDir, } } diff --git a/config/git_environment.go b/config/git_environment.go deleted file mode 100644 index 4c3de4fd..00000000 --- a/config/git_environment.go +++ /dev/null @@ -1,85 +0,0 @@ -package config - -// gitEnvironment is an implementation of the Environment which wraps the legacy -// behavior or `*config.Configuration.loadGitConfig()`. -// -// It is functionally equivelant to call `cfg.loadGitConfig()` before calling -// methods on the Environment type. -type gitEnvironment struct { - // git is the Environment which gitEnvironment wraps. - git Environment - // config is the *Configuration instance which is mutated by - // `loadGitConfig`. - config *Configuration -} - -// Get is shorthand for calling the loadGitConfig, and then returning -// `g.git.Get(key)`. -func (g *gitEnvironment) Get(key string) (val string, ok bool) { - g.loadGitConfig() - - return g.git.Get(key) -} - -// Get is shorthand for calling the loadGitConfig, and then returning -// `g.git.GetAll(key)`. -func (g *gitEnvironment) GetAll(key string) []string { - g.loadGitConfig() - - return g.git.GetAll(key) -} - -// Get is shorthand for calling the loadGitConfig, and then returning -// `g.git.Bool(key, def)`. -func (g *gitEnvironment) Bool(key string, def bool) (val bool) { - g.loadGitConfig() - - return g.git.Bool(key, def) -} - -// Get is shorthand for calling the loadGitConfig, and then returning -// `g.git.Int(key, def)`. -func (g *gitEnvironment) Int(key string, def int) (val int) { - g.loadGitConfig() - - return g.git.Int(key, def) -} - -// All returns a copy of all the key/value pairs for the current git config. -func (g *gitEnvironment) All() map[string][]string { - g.loadGitConfig() - - return g.git.All() -} - -// loadGitConfig reads and parses the .gitconfig by calling ReadGitConfig. It -// also sets values on the configuration instance `g.config`. -// -// If loadGitConfig has already been called, this method will bail out early, -// and return false. Otherwise it will preform the entire parse and return true. -// -// loadGitConfig is safe to call across multiple goroutines. -func (g *gitEnvironment) loadGitConfig() bool { - g.config.loading.Lock() - defer g.config.loading.Unlock() - - if g.git != nil { - return false - } - - gf, extensions, uniqRemotes := ReadGitConfig(getGitConfigs()...) - - g.git = EnvironmentOf(gf) - - g.config.extensions = extensions - - g.config.remotes = make([]string, 0, len(uniqRemotes)) - for remote, isOrigin := range uniqRemotes { - if isOrigin { - continue - } - g.config.remotes = append(g.config.remotes, remote) - } - - return true -} diff --git a/config/git_fetcher.go b/config/git_fetcher.go index aa2071c1..01c2376f 100644 --- a/config/git_fetcher.go +++ b/config/git_fetcher.go @@ -3,7 +3,6 @@ package config import ( "fmt" "os" - "path/filepath" "strconv" "strings" "sync" @@ -16,19 +15,7 @@ type GitFetcher struct { vals map[string][]string } -type GitConfig struct { - Lines []string - OnlySafeKeys bool -} - -func NewGitConfig(gitconfiglines string, onlysafe bool) *GitConfig { - return &GitConfig{ - Lines: strings.Split(gitconfiglines, "\n"), - OnlySafeKeys: onlysafe, - } -} - -func ReadGitConfig(configs ...*GitConfig) (gf *GitFetcher, extensions map[string]Extension, uniqRemotes map[string]bool) { +func readGitConfig(configs ...*git.ConfigurationSource) (gf *GitFetcher, extensions map[string]Extension, uniqRemotes map[string]bool) { vals := make(map[string][]string) ignored := make([]string, 0) @@ -161,39 +148,6 @@ func (g *GitFetcher) All() map[string][]string { return newmap } -func getGitConfigs() (sources []*GitConfig) { - if lfsconfig := getFileGitConfig(".lfsconfig"); lfsconfig != nil { - sources = append(sources, lfsconfig) - } - - globalList, err := git.Config.List() - if err == nil { - sources = append(sources, NewGitConfig(globalList, false)) - } else { - fmt.Fprintf(os.Stderr, "Error reading git config: %s\n", err) - } - - return -} - -func getFileGitConfig(basename string) *GitConfig { - fullname := filepath.Join(LocalWorkingDir, basename) - if _, err := os.Stat(fullname); err != nil { - if !os.IsNotExist(err) { - fmt.Fprintf(os.Stderr, "Error reading %s: %s\n", basename, err) - } - return nil - } - - lines, err := git.Config.ListFromFile(fullname) - if err == nil { - return NewGitConfig(lines, true) - } - - fmt.Fprintf(os.Stderr, "Error reading %s: %s\n", basename, err) - return nil -} - func keyIsUnsafe(key string) bool { for _, safe := range safeKeys { if safe == key { diff --git a/config/config_netrc.go b/config/netrc.go similarity index 100% rename from config/config_netrc.go rename to config/netrc.go diff --git a/config/config_nix.go b/config/netrc_nix.go similarity index 100% rename from config/config_nix.go rename to config/netrc_nix.go diff --git a/config/config_windows.go b/config/netrc_windows.go similarity index 100% rename from config/config_windows.go rename to config/netrc_windows.go diff --git a/git/config.go b/git/config.go new file mode 100644 index 00000000..8071c4bc --- /dev/null +++ b/git/config.go @@ -0,0 +1,167 @@ +package git + +import ( + "os" + "strings" + "sync" + + "github.com/rubyist/tracerx" +) + +var Config = &Configuration{} + +// Configuration can fetch or modify the current Git config and track the Git +// version. +type Configuration struct { + version string + mu sync.Mutex +} + +func ParseConfigLines(lines string, onlySafeKeys bool) *ConfigurationSource { + return &ConfigurationSource{ + Lines: strings.Split(lines, "\n"), + OnlySafeKeys: onlySafeKeys, + } +} + +type ConfigurationSource struct { + Lines []string + OnlySafeKeys bool +} + +// Find returns the git config value for the key +func (c *Configuration) Find(val string) string { + output, _ := gitSimple("config", val) + return output +} + +// FindGlobal returns the git config value global scope for the key +func (c *Configuration) FindGlobal(key string) string { + output, _ := gitSimple("config", "--global", key) + return output +} + +// FindSystem returns the git config value in system scope for the key +func (c *Configuration) FindSystem(key string) string { + output, _ := gitSimple("config", "--system", key) + return output +} + +// Find returns the git config value for the key +func (c *Configuration) FindLocal(key string) string { + output, _ := gitSimple("config", "--local", key) + return output +} + +// SetGlobal sets the git config value for the key in the global config +func (c *Configuration) SetGlobal(key, val string) (string, error) { + return gitSimple("config", "--global", "--replace-all", key, val) +} + +// SetSystem sets the git config value for the key in the system config +func (c *Configuration) SetSystem(key, val string) (string, error) { + return gitSimple("config", "--system", "--replace-all", key, val) +} + +// UnsetGlobalSection removes the entire named section from the global config +func (c *Configuration) UnsetGlobalSection(key string) (string, error) { + return gitSimple("config", "--global", "--remove-section", key) +} + +// UnsetSystemSection removes the entire named section from the system config +func (c *Configuration) UnsetSystemSection(key string) (string, error) { + return gitSimple("config", "--system", "--remove-section", key) +} + +// UnsetLocalSection removes the entire named section from the system config +func (c *Configuration) UnsetLocalSection(key string) (string, error) { + return gitSimple("config", "--local", "--remove-section", key) +} + +// SetLocal sets the git config value for the key in the specified config file +func (c *Configuration) SetLocal(file, key, val string) (string, error) { + args := make([]string, 1, 6) + args[0] = "config" + if len(file) > 0 { + args = append(args, "--file", file) + } + args = append(args, "--replace-all", key, val) + return gitSimple(args...) +} + +// UnsetLocalKey removes the git config value for the key from the specified config file +func (c *Configuration) UnsetLocalKey(file, key string) (string, error) { + args := make([]string, 1, 5) + args[0] = "config" + if len(file) > 0 { + args = append(args, "--file", file) + } + args = append(args, "--unset", key) + return gitSimple(args...) +} + +func (c *Configuration) Sources(optionalFilename string) ([]*ConfigurationSource, error) { + gitconfig, err := c.Source() + if err != nil { + return nil, err + } + + fileconfig, err := c.FileSource(optionalFilename) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + + configs := make([]*ConfigurationSource, 0, 2) + if fileconfig != nil { + configs = append(configs, fileconfig) + } + + return append(configs, gitconfig), nil +} + +func (c *Configuration) FileSource(filename string) (*ConfigurationSource, error) { + if _, err := os.Stat(filename); err != nil { + return nil, err + } + + out, err := gitSimple("config", "-l", "-f", filename) + if err != nil { + return nil, err + } + return ParseConfigLines(out, true), nil +} + +func (c *Configuration) Source() (*ConfigurationSource, error) { + out, err := gitSimple("config", "-l") + if err != nil { + return nil, err + } + return ParseConfigLines(out, false), nil +} + +// Version returns the git version +func (c *Configuration) Version() (string, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if len(c.version) == 0 { + v, err := gitSimple("version") + if err != nil { + return v, err + } + c.version = v + } + + return c.version, nil +} + +// IsVersionAtLeast returns whether the git version is the one specified or higher +// argument is plain version string separated by '.' e.g. "2.3.1" but can omit minor/patch +func (c *Configuration) IsGitVersionAtLeast(ver string) bool { + gitver, err := c.Version() + if err != nil { + tracerx.Printf("Error getting git version: %v", err) + return false + } + return IsVersionAtLeast(gitver, ver) +} diff --git a/git/git.go b/git/git.go index 86e7a77c..a92dad09 100644 --- a/git/git.go +++ b/git/git.go @@ -16,7 +16,6 @@ import ( "regexp" "strconv" "strings" - "sync" "time" lfserrors "github.com/git-lfs/git-lfs/errors" @@ -293,7 +292,6 @@ func RemoteBranchForLocalBranch(localBranch string) string { } else { return localBranch } - } func RemoteList() ([]string, error) { @@ -462,131 +460,6 @@ func UpdateIndexFromStdin() *subprocess.Cmd { return git("update-index", "-q", "--refresh", "--stdin") } -type gitConfig struct { - gitVersion string - mu sync.Mutex -} - -var Config = &gitConfig{} - -// Find returns the git config value for the key -func (c *gitConfig) Find(val string) string { - output, _ := gitSimple("config", val) - return output -} - -// FindGlobal returns the git config value global scope for the key -func (c *gitConfig) FindGlobal(val string) string { - output, _ := gitSimple("config", "--global", val) - return output -} - -// FindSystem returns the git config value in system scope for the key -func (c *gitConfig) FindSystem(val string) string { - output, _ := gitSimple("config", "--system", val) - return output -} - -// Find returns the git config value for the key -func (c *gitConfig) FindLocal(val string) string { - output, _ := gitSimple("config", "--local", val) - return output -} - -// SetGlobal sets the git config value for the key in the global config -func (c *gitConfig) SetGlobal(key, val string) (string, error) { - return gitSimple("config", "--global", "--replace-all", key, val) -} - -// SetSystem sets the git config value for the key in the system config -func (c *gitConfig) SetSystem(key, val string) (string, error) { - return gitSimple("config", "--system", "--replace-all", key, val) -} - -// UnsetGlobal removes the git config value for the key from the global config -func (c *gitConfig) UnsetGlobal(key string) (string, error) { - return gitSimple("config", "--global", "--unset", key) -} - -// UnsetSystem removes the git config value for the key from the system config -func (c *gitConfig) UnsetSystem(key string) (string, error) { - return gitSimple("config", "--system", "--unset", key) -} - -// UnsetGlobalSection removes the entire named section from the global config -func (c *gitConfig) UnsetGlobalSection(key string) (string, error) { - return gitSimple("config", "--global", "--remove-section", key) -} - -// UnsetSystemSection removes the entire named section from the system config -func (c *gitConfig) UnsetSystemSection(key string) (string, error) { - return gitSimple("config", "--system", "--remove-section", key) -} - -// UnsetLocalSection removes the entire named section from the system config -func (c *gitConfig) UnsetLocalSection(key string) (string, error) { - return gitSimple("config", "--local", "--remove-section", key) -} - -// SetLocal sets the git config value for the key in the specified config file -func (c *gitConfig) SetLocal(file, key, val string) (string, error) { - args := make([]string, 1, 6) - args[0] = "config" - if len(file) > 0 { - args = append(args, "--file", file) - } - args = append(args, "--replace-all", key, val) - return gitSimple(args...) -} - -// UnsetLocalKey removes the git config value for the key from the specified config file -func (c *gitConfig) UnsetLocalKey(file, key string) (string, error) { - args := make([]string, 1, 5) - args[0] = "config" - if len(file) > 0 { - args = append(args, "--file", file) - } - args = append(args, "--unset", key) - return gitSimple(args...) -} - -// List lists all of the git config values -func (c *gitConfig) List() (string, error) { - return gitSimple("config", "-l") -} - -// ListFromFile lists all of the git config values in the given config file -func (c *gitConfig) ListFromFile(f string) (string, error) { - return gitSimple("config", "-l", "-f", f) -} - -// Version returns the git version -func (c *gitConfig) Version() (string, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if len(c.gitVersion) == 0 { - v, err := gitSimple("version") - if err != nil { - return v, err - } - c.gitVersion = v - } - - return c.gitVersion, nil -} - -// IsVersionAtLeast returns whether the git version is the one specified or higher -// argument is plain version string separated by '.' e.g. "2.3.1" but can omit minor/patch -func (c *gitConfig) IsGitVersionAtLeast(ver string) bool { - gitver, err := c.Version() - if err != nil { - tracerx.Printf("Error getting git version: %v", err) - return false - } - return IsVersionAtLeast(gitver, ver) -} - // RecentBranches returns branches with commit dates on or after the given date/time // Return full Ref type for easier detection of duplicate SHAs etc // since: refs with commits on or after this date will be included diff --git a/lfs/attribute.go b/lfs/attribute.go index ac8b1c5e..0c83420f 100644 --- a/lfs/attribute.go +++ b/lfs/attribute.go @@ -26,11 +26,68 @@ type Attribute struct { Upgradeables map[string][]string } -// InstallOptions serves as an argument to Install(). -type InstallOptions struct { - Force bool - Local bool - System bool +// FilterOptions serves as an argument to Install(). +type FilterOptions struct { + Force bool + Local bool + System bool + SkipSmudge bool +} + +func (o *FilterOptions) Install() error { + if o.SkipSmudge { + return skipSmudgeFilterAttribute().Install(o) + } + return filterAttribute().Install(o) +} + +func (o *FilterOptions) Uninstall() error { + filterAttribute().Uninstall(o) + return nil +} + +func filterAttribute() *Attribute { + return &Attribute{ + Section: "filter.lfs", + Properties: map[string]string{ + "clean": "git-lfs clean -- %f", + "smudge": "git-lfs smudge -- %f", + "process": "git-lfs filter-process", + "required": "true", + }, + Upgradeables: upgradeables(), + } +} + +func skipSmudgeFilterAttribute() *Attribute { + return &Attribute{ + Section: "filter.lfs", + Properties: map[string]string{ + "clean": "git-lfs clean -- %f", + "smudge": "git-lfs smudge --skip -- %f", + "process": "git-lfs filter-process --skip", + "required": "true", + }, + Upgradeables: upgradeables(), + } +} + +func upgradeables() map[string][]string { + return map[string][]string{ + "clean": []string{"git-lfs clean %f"}, + "smudge": []string{ + "git-lfs smudge %f", + "git-lfs smudge --skip %f", + "git-lfs smudge -- %f", + "git-lfs smudge --skip -- %f", + }, + "process": []string{ + "git-lfs filter", + "git-lfs filter --skip", + "git-lfs filter-process", + "git-lfs filter-process --skip", + }, + } } // Install instructs Git to set all keys and values relative to the root @@ -39,7 +96,7 @@ type InstallOptions struct { // `force` argument is passed as true. If an attribute is already set to a // different value than what is given, and force is false, an error will be // returned immediately, and the rest of the attributes will not be set. -func (a *Attribute) Install(opt InstallOptions) error { +func (a *Attribute) Install(opt *FilterOptions) error { for k, v := range a.Properties { var upgradeables []string if a.Upgradeables != nil { @@ -65,7 +122,7 @@ func (a *Attribute) normalizeKey(relative string) string { // matching key already exists and the value is not equal to the desired value, // an error will be thrown if force is set to false. If force is true, the value // will be overridden. -func (a *Attribute) set(key, value string, upgradeables []string, opt InstallOptions) error { +func (a *Attribute) set(key, value string, upgradeables []string, opt *FilterOptions) error { var currentValue string if opt.Local { currentValue = git.Config.FindLocal(key) @@ -94,7 +151,7 @@ func (a *Attribute) set(key, value string, upgradeables []string, opt InstallOpt } // Uninstall removes all properties in the path of this property. -func (a *Attribute) Uninstall(opt InstallOptions) { +func (a *Attribute) Uninstall(opt *FilterOptions) { if opt.Local { git.Config.UnsetLocalSection(a.Section) } else if opt.System { diff --git a/lfs/config.go b/lfs/config.go new file mode 100644 index 00000000..62adc421 --- /dev/null +++ b/lfs/config.go @@ -0,0 +1,41 @@ +package lfs + +import "github.com/git-lfs/git-lfs/config" + +// FetchPruneConfig collects together the config options that control fetching and pruning +type FetchPruneConfig struct { + // The number of days prior to current date for which (local) refs other than HEAD + // will be fetched with --recent (default 7, 0 = only fetch HEAD) + FetchRecentRefsDays int + // Makes the FetchRecentRefsDays option apply to remote refs from fetch source as well (default true) + FetchRecentRefsIncludeRemotes bool + // number of days prior to latest commit on a ref that we'll fetch previous + // LFS changes too (default 0 = only fetch at ref) + FetchRecentCommitsDays int + // Whether to always fetch recent even without --recent + FetchRecentAlways bool + // Number of days added to FetchRecent*; data outside combined window will be + // deleted when prune is run. (default 3) + PruneOffsetDays int + // Always verify with remote before pruning + PruneVerifyRemoteAlways bool + // Name of remote to check for unpushed and verify checks + PruneRemoteName string +} + +func NewFetchPruneConfig(git config.Environment) FetchPruneConfig { + pruneRemote, _ := git.Get("lfs.pruneremotetocheck") + if len(pruneRemote) == 0 { + pruneRemote = "origin" + } + + return FetchPruneConfig{ + FetchRecentRefsDays: git.Int("lfs.fetchrecentrefsdays", 7), + FetchRecentRefsIncludeRemotes: git.Bool("lfs.fetchrecentremoterefs", true), + FetchRecentCommitsDays: git.Int("lfs.fetchrecentcommitsdays", 0), + FetchRecentAlways: git.Bool("lfs.fetchrecentalways", false), + PruneOffsetDays: git.Int("lfs.pruneoffsetdays", 3), + PruneVerifyRemoteAlways: git.Bool("lfs.pruneverifyremotealways", false), + PruneRemoteName: pruneRemote, + } +} diff --git a/lfs/config_test.go b/lfs/config_test.go new file mode 100644 index 00000000..6cf2e871 --- /dev/null +++ b/lfs/config_test.go @@ -0,0 +1,42 @@ +package lfs + +import ( + "testing" + + "github.com/git-lfs/git-lfs/config" + "github.com/stretchr/testify/assert" +) + +func TestFetchPruneConfigDefault(t *testing.T) { + cfg := config.NewFrom(config.Values{}) + fp := NewFetchPruneConfig(cfg.Git) + + assert.Equal(t, 7, fp.FetchRecentRefsDays) + assert.Equal(t, 0, fp.FetchRecentCommitsDays) + assert.Equal(t, 3, fp.PruneOffsetDays) + assert.True(t, fp.FetchRecentRefsIncludeRemotes) + assert.Equal(t, 3, fp.PruneOffsetDays) + assert.Equal(t, "origin", fp.PruneRemoteName) + assert.False(t, fp.PruneVerifyRemoteAlways) +} + +func TestFetchPruneConfigCustom(t *testing.T) { + cfg := config.NewFrom(config.Values{ + Git: map[string][]string{ + "lfs.fetchrecentrefsdays": []string{"12"}, + "lfs.fetchrecentremoterefs": []string{"false"}, + "lfs.fetchrecentcommitsdays": []string{"9"}, + "lfs.pruneoffsetdays": []string{"30"}, + "lfs.pruneverifyremotealways": []string{"true"}, + "lfs.pruneremotetocheck": []string{"upstream"}, + }, + }) + fp := NewFetchPruneConfig(cfg.Git) + + assert.Equal(t, 12, fp.FetchRecentRefsDays) + assert.Equal(t, 9, fp.FetchRecentCommitsDays) + assert.False(t, fp.FetchRecentRefsIncludeRemotes) + assert.Equal(t, 30, fp.PruneOffsetDays) + assert.Equal(t, "upstream", fp.PruneRemoteName) + assert.True(t, fp.PruneVerifyRemoteAlways) +} diff --git a/lfs/hook.go b/lfs/hook.go index 3ac3dc57..83b28dc3 100644 --- a/lfs/hook.go +++ b/lfs/hook.go @@ -8,9 +8,6 @@ import ( "path/filepath" "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/tools" "github.com/rubyist/tracerx" ) @@ -26,15 +23,32 @@ var ( type Hook struct { Type string Contents string - Upgradeables []string + Dir string + upgradeables []string +} + +func LoadHooks(hookDir string) []*Hook { + return []*Hook{ + NewStandardHook("pre-push", hookDir, []string{ + "#!/bin/sh\ngit lfs push --stdin $*", + "#!/bin/sh\ngit lfs push --stdin \"$@\"", + "#!/bin/sh\ngit lfs pre-push \"$@\"", + "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 0; }\ngit lfs pre-push \"$@\"", + "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 2; }\ngit lfs pre-push \"$@\"", + }), + NewStandardHook("post-checkout", hookDir, []string{}), + NewStandardHook("post-commit", hookDir, []string{}), + NewStandardHook("post-merge", hookDir, []string{}), + } } // NewStandardHook creates a new hook using the template script calling 'git lfs theType' -func NewStandardHook(theType string, upgradeables []string) *Hook { +func NewStandardHook(theType, hookDir string, upgradeables []string) *Hook { return &Hook{ Type: theType, Contents: strings.Replace(hookBaseContent, "{{Command}}", theType, -1), - Upgradeables: upgradeables, + Dir: hookDir, + upgradeables: upgradeables, } } @@ -47,20 +61,7 @@ func (h *Hook) Exists() bool { // Path returns the desired (or actual, if installed) location where this hook // should be installed. It returns an absolute path in all cases. func (h *Hook) Path() string { - return filepath.Join(h.Dir(), h.Type) -} - -// Dir returns the directory used by LFS for storing Git hooks. By default, it -// will return the hooks/ sub-directory of the local repository's .git -// directory. If `core.hooksPath` is configured and supported (Git verison is -// greater than "2.9.0"), it will return that instead. -func (h *Hook) Dir() string { - customHooksSupported := git.Config.IsGitVersionAtLeast("2.9.0") - if hp, ok := config.Config.Git.Get("core.hooksPath"); ok && customHooksSupported { - return hp - } - - return filepath.Join(config.LocalGitDir, "hooks") + return filepath.Join(h.Dir, h.Type) } // Install installs this Git hook on disk, or upgrades it if it does exist, and @@ -70,7 +71,7 @@ func (h *Hook) Dir() string { func (h *Hook) Install(force bool) error { msg := fmt.Sprintf("Install hook: %s, force=%t, path=%s", h.Type, force, h.Path()) - if err := os.MkdirAll(h.Dir(), 0755); err != nil { + if err := os.MkdirAll(h.Dir, 0755); err != nil { return err } @@ -110,10 +111,6 @@ func (h *Hook) Upgrade() error { // Uninstall removes the hook on disk so long as it matches the current version, // or any of the past versions of this hook. func (h *Hook) Uninstall() error { - if !InRepo() { - return errors.New("Not in a git repository") - } - msg := fmt.Sprintf("Uninstall hook: %s, path=%s", h.Type, h.Path()) match, err := h.matchesCurrent() @@ -151,7 +148,7 @@ func (h *Hook) matchesCurrent() (bool, error) { return true, nil } - for _, u := range h.Upgradeables { + for _, u := range h.upgradeables { if u == contents { return true, nil } diff --git a/lfs/lfs.go b/lfs/lfs.go index a15ccc72..95d036dd 100644 --- a/lfs/lfs.go +++ b/lfs/lfs.go @@ -78,15 +78,15 @@ func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string { ultransfers := manifest.GetUploadAdapterNames() sort.Strings(ultransfers) - fetchPruneConfig := cfg.FetchPruneConfig() - storageConfig := cfg.StorageConfig() + fetchPruneConfig := NewFetchPruneConfig(cfg.Git) + storageConfig := localstorage.NewConfig(cfg) env = append(env, - fmt.Sprintf("LocalWorkingDir=%s", config.LocalWorkingDir), - fmt.Sprintf("LocalGitDir=%s", config.LocalGitDir), - fmt.Sprintf("LocalGitStorageDir=%s", config.LocalGitStorageDir), + fmt.Sprintf("LocalWorkingDir=%s", cfg.LocalWorkingDir()), + fmt.Sprintf("LocalGitDir=%s", cfg.LocalGitDir()), + fmt.Sprintf("LocalGitStorageDir=%s", cfg.LocalGitStorageDir()), fmt.Sprintf("LocalMediaDir=%s", LocalMediaDir()), - fmt.Sprintf("LocalReferenceDir=%s", config.LocalReferenceDir), + fmt.Sprintf("LocalReferenceDir=%s", cfg.LocalReferenceDir()), fmt.Sprintf("TempDir=%s", TempDir()), fmt.Sprintf("ConcurrentTransfers=%d", api.ConcurrentTransfers), fmt.Sprintf("TusTransfers=%v", cfg.TusTransfersAllowed()), @@ -125,10 +125,6 @@ func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string { return env } -func InRepo() bool { - return config.LocalGitDir != "" -} - func ClearTempObjects() error { if localstorage.Objects() == nil { return nil diff --git a/lfs/setup.go b/lfs/setup.go deleted file mode 100644 index b6c2e30f..00000000 --- a/lfs/setup.go +++ /dev/null @@ -1,123 +0,0 @@ -package lfs - -import ( - "fmt" - "strings" - - "github.com/git-lfs/git-lfs/tools" -) - -var ( - // prePushHook invokes `git lfs pre-push` at the pre-push phase. - prePushHook = NewStandardHook("pre-push", []string{ - "#!/bin/sh\ngit lfs push --stdin $*", - "#!/bin/sh\ngit lfs push --stdin \"$@\"", - "#!/bin/sh\ngit lfs pre-push \"$@\"", - "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 0; }\ngit lfs pre-push \"$@\"", - "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 2; }\ngit lfs pre-push \"$@\"", - }) - // postCheckoutHook invokes `git lfs post-checkout` - postCheckoutHook = NewStandardHook("post-checkout", []string{}) - postCommitHook = NewStandardHook("post-commit", []string{}) - postMergeHook = NewStandardHook("post-merge", []string{}) - - hooks = []*Hook{ - prePushHook, - postCheckoutHook, - postCommitHook, - postMergeHook, - } - - upgradeables = map[string][]string{ - "clean": []string{"git-lfs clean %f"}, - "smudge": []string{ - "git-lfs smudge %f", - "git-lfs smudge --skip %f", - "git-lfs smudge -- %f", - "git-lfs smudge --skip -- %f", - }, - "process": []string{ - "git-lfs filter", - "git-lfs filter --skip", - "git-lfs filter-process", - "git-lfs filter-process --skip", - }, - } - - filters = &Attribute{ - Section: "filter.lfs", - Properties: map[string]string{ - "clean": "git-lfs clean -- %f", - "smudge": "git-lfs smudge -- %f", - "process": "git-lfs filter-process", - "required": "true", - }, - Upgradeables: upgradeables, - } - - passFilters = &Attribute{ - Section: "filter.lfs", - Properties: map[string]string{ - "clean": "git-lfs clean -- %f", - "smudge": "git-lfs smudge --skip -- %f", - "process": "git-lfs filter-process --skip", - "required": "true", - }, - Upgradeables: upgradeables, - } -) - -// Get user-readable manual install steps for hooks -func GetHookInstallSteps() string { - steps := make([]string, 0, len(hooks)) - for _, h := range hooks { - steps = append(steps, fmt.Sprintf( - "Add the following to .git/hooks/%s:\n\n%s", - h.Type, tools.Indent(h.Contents))) - } - - return strings.Join(steps, "\n\n") -} - -// InstallHooks installs all hooks in the `hooks` var. -func InstallHooks(force bool) error { - for _, h := range hooks { - if err := h.Install(force); err != nil { - return err - } - } - - return nil -} - -// UninstallHooks removes all hooks in range of the `hooks` var. -func UninstallHooks() error { - for _, h := range hooks { - if err := h.Uninstall(); err != nil { - return err - } - } - - return nil -} - -// InstallFilters installs filters necessary for git-lfs to process normal git -// operations. Currently, that list includes: -// - smudge filter -// - clean filter -// -// An error will be returned if a filter is unable to be set, or if the required -// filters were not present. -func InstallFilters(opt InstallOptions, passThrough bool) error { - if passThrough { - return passFilters.Install(opt) - } - return filters.Install(opt) -} - -// UninstallFilters proxies into the Uninstall method on the Filters type to -// remove all installed filters. -func UninstallFilters(opt InstallOptions) error { - filters.Uninstall(opt) - return nil -} diff --git a/lfs/util.go b/lfs/util.go index 06b67e7d..7745ff00 100644 --- a/lfs/util.go +++ b/lfs/util.go @@ -95,8 +95,8 @@ type PathConverter interface { // current working dir. Useful when needing to calling git with results from a rooted command, // but the user is in a subdir of their repo // Pass in a channel which you will fill with relative files & receive a channel which will get results -func NewRepoToCurrentPathConverter() (PathConverter, error) { - r, c, p, err := pathConverterArgs() +func NewRepoToCurrentPathConverter(cfg *config.Configuration) (PathConverter, error) { + r, c, p, err := pathConverterArgs(cfg) if err != nil { return nil, err } @@ -133,8 +133,8 @@ func (p *repoToCurrentPathConverter) Convert(filename string) string { // relative to the repo root. Useful when calling git with arguments that requires them // to be rooted but the user is in a subdir of their repo & expects to use relative args // Pass in a channel which you will fill with relative files & receive a channel which will get results -func NewCurrentToRepoPathConverter() (PathConverter, error) { - r, c, p, err := pathConverterArgs() +func NewCurrentToRepoPathConverter(cfg *config.Configuration) (PathConverter, error) { + r, c, p, err := pathConverterArgs(cfg) if err != nil { return nil, err } @@ -172,13 +172,13 @@ func (p *currentToRepoPathConverter) Convert(filename string) string { } } -func pathConverterArgs() (string, string, bool, error) { +func pathConverterArgs(cfg *config.Configuration) (string, string, bool, error) { currDir, err := os.Getwd() if err != nil { return "", "", false, fmt.Errorf("Unable to get working dir: %v", err) } currDir = tools.ResolveSymlinks(currDir) - return config.LocalWorkingDir, currDir, config.LocalWorkingDir == currDir, nil + return cfg.LocalWorkingDir(), currDir, cfg.LocalWorkingDir() == currDir, nil } // Are we running on Windows? Need to handle some extra path shenanigans diff --git a/lfsapi/creds.go b/lfsapi/creds.go index fa06e1e9..187043b1 100644 --- a/lfsapi/creds.go +++ b/lfsapi/creds.go @@ -37,8 +37,8 @@ type credsConfig struct { // // It returns an error if any configuration was invalid, or otherwise // un-useable. -func getCredentialHelper(cfg *config.Configuration) (CredentialHelper, error) { - ccfg, err := getCredentialConfig(cfg) +func getCredentialHelper(osEnv, gitEnv config.Environment) (CredentialHelper, error) { + ccfg, err := getCredentialConfig(osEnv, gitEnv) if err != nil { return nil, err } @@ -71,13 +71,20 @@ func getCredentialHelper(cfg *config.Configuration) (CredentialHelper, error) { // getCredentialConfig parses a *credsConfig given the OS and Git // configurations. -func getCredentialConfig(cfg *config.Configuration) (*credsConfig, error) { - what := &credsConfig{ - Cached: cfg.Git.Bool("lfs.cachecredentials", true), +func getCredentialConfig(o, g config.Environment) (*credsConfig, error) { + askpass, ok := o.Get("GIT_ASKPASS") + if !ok { + askpass, ok = g.Get("core.askpass") } - - if err := cfg.Unmarshal(what); err != nil { - return nil, err + 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 diff --git a/lfsapi/lfsapi.go b/lfsapi/lfsapi.go index d2c5392d..91bf3472 100644 --- a/lfsapi/lfsapi.go +++ b/lfsapi/lfsapi.go @@ -66,8 +66,7 @@ func NewClient(osEnv Env, gitEnv Env) (*Client, error) { return nil, errors.Wrap(err, fmt.Sprintf("bad netrc file %s", netrcfile)) } - creds, err := getCredentialHelper(&config.Configuration{ - Os: osEnv, Git: gitEnv}) + creds, err := getCredentialHelper(osEnv, gitEnv) if err != nil { return nil, errors.Wrap(err, "cannot find credential helper(s)") } diff --git a/localstorage/currentstore.go b/localstorage/currentstore.go index e138da30..6b2b2905 100644 --- a/localstorage/currentstore.go +++ b/localstorage/currentstore.go @@ -27,16 +27,15 @@ func Objects() *LocalStorage { return objects } -func InitStorage() error { - if len(config.LocalGitStorageDir) == 0 || len(config.LocalGitDir) == 0 { +func InitStorage(cfg *config.Configuration) error { + if len(cfg.LocalGitStorageDir()) == 0 || len(cfg.LocalGitDir()) == 0 { return notInRepoErr } - cfg := config.Config.StorageConfig() - - TempDir = filepath.Join(cfg.LfsStorageDir, "tmp") // temp files per worktree + storCfg := NewConfig(cfg) + TempDir = filepath.Join(storCfg.LfsStorageDir, "tmp") // temp files per worktree objs, err := NewStorage( - filepath.Join(cfg.LfsStorageDir, "objects"), + filepath.Join(storCfg.LfsStorageDir, "objects"), filepath.Join(TempDir, "objects"), ) @@ -45,16 +44,16 @@ func InitStorage() error { } objects = objs - config.LocalLogDir = filepath.Join(objs.RootDir, "logs") - if err := os.MkdirAll(config.LocalLogDir, localLogDirPerms); err != nil { + cfg.SetLocalLogDir(filepath.Join(objs.RootDir, "logs")) + if err := os.MkdirAll(cfg.LocalLogDir(), localLogDirPerms); err != nil { return errors.Wrap(err, "create log dir") } return nil } -func InitStorageOrFail() { - if err := InitStorage(); err != nil { +func InitStorageOrFail(cfg *config.Configuration) { + if err := InitStorage(cfg); err != nil { if err == notInRepoErr { return } @@ -64,9 +63,9 @@ func InitStorageOrFail() { } } -func ResolveDirs() { - config.ResolveGitBasicDirs() - InitStorageOrFail() +func ResolveDirs(cfg *config.Configuration) { + cfg.ResolveGitBasicDirs() + InitStorageOrFail(cfg) } func TempFile(prefix string) (*os.File, error) { diff --git a/localstorage/localstorage.go b/localstorage/localstorage.go index aec93eb0..e2216a8c 100644 --- a/localstorage/localstorage.go +++ b/localstorage/localstorage.go @@ -7,6 +7,8 @@ import ( "os" "path/filepath" "regexp" + + "github.com/git-lfs/git-lfs/config" ) const ( @@ -55,6 +57,25 @@ func (s *LocalStorage) BuildObjectPath(oid string) (string, error) { return filepath.Join(dir, oid), nil } +// Storage configuration +type Configuration struct { + LfsStorageDir string +} + +func NewConfig(cfg *config.Configuration) (c Configuration) { + dir, _ := cfg.Git.Get("lfs.storage") + if len(dir) == 0 { + dir = "lfs" + } + + if filepath.IsAbs(dir) { + c.LfsStorageDir = dir + } else { + c.LfsStorageDir = filepath.Join(cfg.LocalGitStorageDir(), dir) + } + return +} + func localObjectDir(s *LocalStorage, oid string) string { return filepath.Join(s.RootDir, oid[0:2], oid[2:4]) } diff --git a/test/testutils.go b/test/testutils.go index 06d691dc..c3204bfe 100644 --- a/test/testutils.go +++ b/test/testutils.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "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/lfs" @@ -83,7 +84,7 @@ func (r *Repo) Pushd() { r.callback.Fatalf("Can't chdir %v", err) } r.popDir = oldwd - localstorage.ResolveDirs() + localstorage.ResolveDirs(config.Config) } func (r *Repo) Popd() { @@ -220,7 +221,7 @@ func (infile *FileInput) writeLFSPointer(inputData io.Reader) (*lfs.Pointer, err // this only created the temp file, move to final location tmpfile := cleaned.Filename - storageOnce.Do(localstorage.ResolveDirs) + storageOnce.Do(func() { localstorage.ResolveDirs(config.Config) }) mediafile, err := lfs.LocalMediaPath(cleaned.Oid) if err != nil { return nil, errors.Wrap(err, "local media path")