import (
"fmt"
- "reflect"
- "regexp"
- "strconv"
+ "os"
+ "path/filepath"
"strings"
"sync"
- "path/filepath"
-
- "github.com/git-lfs/git-lfs/errors"
+ "github.com/git-lfs/git-lfs/fs"
+ "github.com/git-lfs/git-lfs/git"
"github.com/git-lfs/git-lfs/tools"
+ "github.com/rubyist/tracerx"
)
var (
- Config = New()
ShowConfigWarnings = false
defaultRemote = "origin"
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
// 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
loading sync.Mutex // guards initialization of gitConfig and remotes
+ loadingGit sync.Mutex // guards initialization of local git and working dirs
remotes []string
extensions map[string]Extension
}
func New() *Configuration {
- c := &Configuration{Os: EnvironmentOf(NewOsFetcher())}
- c.Git = &gitEnvironment{config: c}
- initConfig(c)
+ return NewIn("", "")
+}
+
+func NewIn(workdir, gitdir string) *Configuration {
+ gitConf := git.NewConfig(workdir, gitdir)
+ c := &Configuration{
+ Os: EnvironmentOf(NewOsFetcher()),
+ gitConfig: gitConf,
+ }
+
+ if len(gitConf.WorkDir) > 0 {
+ c.gitDir = &gitConf.GitDir
+ c.workDir = gitConf.WorkDir
+ }
+
+ 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.
// 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)),
+ Os: EnvironmentOf(mapFetcher(v.Os)),
+ gitConfig: git.NewConfig("", ""),
}
- initConfig(c)
- return c
-}
+ c.Git = &delayedEnvironment{
+ callback: func() Environment {
+ source := &git.ConfigurationSource{
+ Lines: make([]string, 0, len(v.Git)),
+ }
-func initConfig(c *Configuration) {
- c.CurrentRemote = defaultRemote
-}
+ 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))
+ }
+ }
-// 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)
+ return c.readGitConfig(source)
+ },
}
- into = into.Elem()
-
- for i := 0; i < into.Type().NumField(); i++ {
- field := into.Field(i)
- sfield := into.Type().Field(i)
+ return c
+}
- lookups, err := c.parseTag(sfield.Tag)
- if err != nil {
- return err
- }
+// BasicTransfersOnly returns whether to only allow "basic" HTTP transfers.
+// Default is false, including if the lfs.basictransfersonly is invalid
+func (c *Configuration) BasicTransfersOnly() bool {
+ return c.Git.Bool("lfs.basictransfersonly", false)
+}
- var val interface{}
- for _, lookup := range lookups {
- if _, ok := lookup.Get(); !ok {
- continue
- }
+// TusTransfersAllowed returns whether to only use "tus.io" HTTP transfers.
+// Default is false, including if the lfs.tustransfers is invalid
+func (c *Configuration) TusTransfersAllowed() bool {
+ return c.Git.Bool("lfs.tustransfers", false)
+}
- 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())
- }
+func (c *Configuration) FetchIncludePaths() []string {
+ patterns, _ := c.Git.Get("lfs.fetchinclude")
+ return tools.CleanPaths(patterns, ",")
+}
- if val != nil {
- break
- }
- }
+func (c *Configuration) FetchExcludePaths() []string {
+ patterns, _ := c.Git.Get("lfs.fetchexclude")
+ return tools.CleanPaths(patterns, ",")
+}
- if val != nil {
- into.Field(i).Set(reflect.ValueOf(val))
+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 nil
+ return c.ref
}
-var (
- tagRe = regexp.MustCompile("((\\w+:\"[^\"]*\")\\b?)+")
- emptyEnv = EnvironmentOf(MapFetcher(nil))
-)
-
-type lookup struct {
- key string
- env Environment
+func (c *Configuration) IsDefaultRemote() bool {
+ return c.Remote() == defaultRemote
}
-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)
+// 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
}
- 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
+ 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
+}
- uq, err := strconv.Unquote(sep[1])
- if err != nil {
- return nil, err
+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
}
-
- lookups = append(lookups, &lookup{
- key: uq,
- env: env,
- })
}
- return lookups, nil
+ return *c.pushRemote
}
-// BasicTransfersOnly returns whether to only allow "basic" HTTP transfers.
-// Default is false, including if the lfs.basictransfersonly is invalid
-func (c *Configuration) BasicTransfersOnly() bool {
- return c.Git.Bool("lfs.basictransfersonly", false)
+func (c *Configuration) SetValidRemote(name string) error {
+ if err := git.ValidateRemote(name); err != nil {
+ return err
+ }
+ c.SetRemote(name)
+ return nil
}
-// TusTransfersAllowed returns whether to only use "tus.io" HTTP transfers.
-// Default is false, including if the lfs.tustransfers is invalid
-func (c *Configuration) TusTransfersAllowed() bool {
- return c.Git.Bool("lfs.tustransfers", false)
+func (c *Configuration) SetValidPushRemote(name string) error {
+ if err := git.ValidateRemote(name); err != nil {
+ return err
+ }
+ c.SetPushRemote(name)
+ return nil
}
-func (c *Configuration) FetchIncludePaths() []string {
- patterns, _ := c.Git.Get("lfs.fetchinclude")
- return tools.CleanPaths(patterns, ",")
+func (c *Configuration) SetRemote(name string) {
+ c.currentRemote = &name
}
-func (c *Configuration) FetchExcludePaths() []string {
- patterns, _ := c.Git.Get("lfs.fetchexclude")
- return tools.CleanPaths(patterns, ",")
+func (c *Configuration) SetPushRemote(name string) {
+ c.pushRemote = &name
}
func (c *Configuration) Remotes() []string {
c.loadGitConfig()
-
return c.remotes
}
return SortExtensions(c.Extensions())
}
-func (c *Configuration) FetchPruneConfig() FetchPruneConfig {
- f := &FetchPruneConfig{
- FetchRecentRefsDays: 7,
- FetchRecentRefsIncludeRemotes: true,
- PruneOffsetDays: 3,
- PruneRemoteName: "origin",
- }
+func (c *Configuration) SkipDownloadErrors() bool {
+ return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false)
+}
- if err := c.Unmarshal(f); err != nil {
- panic(err.Error())
+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 git.IsGitVersionAtLeast("2.9.0") {
+ hp, ok := c.Git.Get("core.hooksPath")
+ if ok {
+ return hp
+ }
}
- return *f
+ return filepath.Join(c.LocalGitDir(), "hooks")
}
-func (c *Configuration) StorageConfig() StorageConfig {
- s := &StorageConfig{
- LfsStorageDir: "lfs",
+func (c *Configuration) InRepo() bool {
+ return len(c.LocalGitDir()) > 0
+}
+
+func (c *Configuration) LocalWorkingDir() string {
+ c.loadGitDirs()
+ return c.workDir
+}
+
+func (c *Configuration) LocalGitDir() string {
+ c.loadGitDirs()
+ return *c.gitDir
+}
+
+func (c *Configuration) loadGitDirs() {
+ c.loadingGit.Lock()
+ defer c.loadingGit.Unlock()
+
+ if c.gitDir != nil {
+ return
}
- if err := c.Unmarshal(s); err != nil {
- panic(err.Error())
+ gitdir, workdir, err := git.GitAndRootDirs()
+ if err != nil {
+ errMsg := err.Error()
+ tracerx.Printf("Error running 'git rev-parse': %s", errMsg)
+ if !strings.Contains(strings.ToLower(errMsg),
+ "not a git repository") {
+ fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
+ }
+ c.gitDir = &gitdir
}
- if !filepath.IsAbs(s.LfsStorageDir) {
- s.LfsStorageDir = filepath.Join(LocalGitStorageDir, s.LfsStorageDir)
+
+ gitdir = tools.ResolveSymlinks(gitdir)
+ c.gitDir = &gitdir
+ c.workDir = tools.ResolveSymlinks(workdir)
+}
+
+func (c *Configuration) LocalGitStorageDir() string {
+ return c.Filesystem().GitStorageDir
+}
+
+func (c *Configuration) LocalReferenceDirs() []string {
+ return c.Filesystem().ReferenceDirs
+}
+
+func (c *Configuration) LFSStorageDir() string {
+ return c.Filesystem().LFSStorageDir
+}
+
+func (c *Configuration) LFSObjectDir() string {
+ return c.Filesystem().LFSObjectDir()
+}
+
+func (c *Configuration) LFSObjectExists(oid string, size int64) bool {
+ return c.Filesystem().ObjectExists(oid, size)
+}
+
+func (c *Configuration) EachLFSObject(fn func(fs.Object) error) error {
+ return c.Filesystem().EachObject(fn)
+}
+
+func (c *Configuration) LocalLogDir() string {
+ return c.Filesystem().LogDir()
+}
+
+func (c *Configuration) TempDir() string {
+ return c.Filesystem().TempDir()
+}
+
+func (c *Configuration) Filesystem() *fs.Filesystem {
+ c.loadGitDirs()
+ c.loading.Lock()
+ defer c.loading.Unlock()
+
+ if c.fs == nil {
+ lfsdir, _ := c.Git.Get("lfs.storage")
+ c.fs = fs.New(
+ c.Os,
+ c.LocalGitDir(),
+ c.LocalWorkingDir(),
+ lfsdir,
+ )
}
- return *s
+
+ return c.fs
}
-func (c *Configuration) SkipDownloadErrors() bool {
- return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false)
+func (c *Configuration) Cleanup() error {
+ c.loading.Lock()
+ defer c.loading.Unlock()
+ return c.fs.Cleanup()
}
-func (c *Configuration) SetLockableFilesReadOnly() bool {
- return c.Os.Bool("GIT_LFS_SET_LOCKABLE_READONLY", true) && c.Git.Bool("lfs.setlockablereadonly", true)
+func (c *Configuration) OSEnv() Environment {
+ return c.Os
+}
+
+func (c *Configuration) GitEnv() Environment {
+ return c.Git
+}
+
+func (c *Configuration) GitConfig() *git.Configuration {
+ return c.gitConfig
+}
+
+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(key, val string) (string, error) {
+ return c.gitConfig.SetLocal(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(key string) (string, error) {
+ return c.gitConfig.UnsetLocalKey(key)
}
// loadGitConfig is a temporary measure to support legacy behavior dependent on
//
// 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