git-lfs/config/config.go
Chris Darroch 802e24aeb6 update inline comments to Go v1.19 doc format
Go version 1.19 adds more support for documentation comments and
now also reformats such comments automatically, so we update a
few of them to meet the new standards.  See also:

  https://tip.golang.org/doc/go1.19#go-doc
  https://tip.golang.org/doc/comment
2022-09-25 18:15:31 -07:00

670 lines
18 KiB
Go

// Package config collects together all configuration settings
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
package config
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/fs"
"github.com/git-lfs/git-lfs/v3/git"
"github.com/git-lfs/git-lfs/v3/tools"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/rubyist/tracerx"
)
var (
ShowConfigWarnings = false
defaultRemote = "origin"
gitConfigWarningPrefix = "lfs."
)
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
// system environment configuration.
Os Environment
// Git provides a `*Environment` used to access to the various levels of
// `.gitconfig`'s. It is the point of entry for all Git environment
// configuration.
Git Environment
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
mask int
maskOnce sync.Once
timestamp time.Time
}
func New() *Configuration {
return NewIn("", "")
}
func NewIn(workdir, gitdir string) *Configuration {
gitConf := git.NewConfig(workdir, gitdir)
c := &Configuration{
Os: EnvironmentOf(NewOsFetcher()),
gitConfig: gitConf,
timestamp: time.Now(),
}
if len(gitConf.WorkDir) > 0 {
c.gitDir = &gitConf.GitDir
c.workDir = gitConf.WorkDir
}
c.Git = &delayedEnvironment{
callback: func() Environment {
sources, err := gitConf.Sources(c.LocalWorkingDir(), ".lfsconfig")
if err != nil {
fmt.Fprintln(os.Stderr, tr.Tr.Get("Error reading `git config`: %s", err))
}
return c.readGitConfig(sources...)
},
}
return c
}
func (c *Configuration) getMask() int {
// This logic is necessarily complex because Git's logic is complex.
c.maskOnce.Do(func() {
val, ok := c.Git.Get("core.sharedrepository")
if !ok {
val = "umask"
} else if Bool(val, false) {
val = "group"
}
switch strings.ToLower(val) {
case "group", "true", "1":
c.mask = 007
case "all", "world", "everybody", "2":
c.mask = 002
case "umask", "false", "0":
c.mask = umask()
default:
if mode, err := strconv.ParseInt(val, 8, 16); err != nil {
// If this doesn't look like an octal number, then it
// could be a falsy value, in which case we should use
// the umask, or it's just invalid, in which case the
// umask is a safe bet.
c.mask = umask()
} else {
c.mask = 0666 & ^int(mode)
}
}
})
return c.mask
}
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 := range uniqRemotes {
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.
type Values struct {
// Git and Os are the stand-in maps used to provide values for their
// respective environments.
Git, Os map[string][]string
}
// NewFrom returns a new `*config.Configuration` that reads both its Git
// and Environment-level values from the ones provided instead of the actual
// `.gitconfig` file or `os.Getenv`, respectively.
//
// This method should only be used during testing.
func NewFrom(v Values) *Configuration {
c := &Configuration{
Os: EnvironmentOf(mapFetcher(v.Os)),
gitConfig: git.NewConfig("", ""),
timestamp: time.Now(),
}
c.Git = &delayedEnvironment{
callback: func() Environment {
source := &git.ConfigurationSource{
Lines: make([]string, 0, len(v.Git)),
}
for key, values := range v.Git {
parts := strings.Split(key, ".")
isCaseSensitive := len(parts) >= 3
hasUpper := strings.IndexFunc(key, unicode.IsUpper) > -1
// This branch should only ever trigger in
// tests, and only if they'd be broken.
if !isCaseSensitive && hasUpper {
panic(tr.Tr.Get("key %q has uppercase, shouldn't", key))
}
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)
},
}
return c
}
// 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)
}
// 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) FetchIncludePaths() []string {
patterns, _ := c.Git.Get("lfs.fetchinclude")
return tools.CleanPaths(patterns, ",")
}
func (c *Configuration) FetchExcludePaths() []string {
patterns, _ := c.Git.Get("lfs.fetchexclude")
return tools.CleanPaths(patterns, ",")
}
func (c *Configuration) CurrentRef() *git.Ref {
c.loading.Lock()
defer c.loading.Unlock()
if c.ref == nil {
r, err := git.CurrentRef()
if err != nil {
tracerx.Printf("Error loading current ref: %s", err)
c.ref = &git.Ref{}
} else {
c.ref = r
}
}
return c.ref
}
func (c *Configuration) IsDefaultRemote() bool {
return c.Remote() == defaultRemote
}
func (c *Configuration) AutoDetectRemoteEnabled() bool {
return c.Git.Bool("lfs.remote.autodetect", false)
}
func (c *Configuration) SearchAllRemotesEnabled() bool {
return c.Git.Bool("lfs.remote.searchall", false)
}
// Remote returns the default remote based on:
// 1. The currently tracked remote branch, if present
// 2. The value of remote.lfsdefault.
// 3. Any other SINGLE remote defined in .git/config
// 4. 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 remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.remote", ref.Name)); len(ref.Name) != 0 && ok {
// try tracking remote
c.currentRemote = &remote
} else if remote, ok := c.Git.Get("remote.lfsdefault"); ok {
// try default remote
c.currentRemote = &remote
} else if remotes := c.Remotes(); len(remotes) == 1 {
// use only remote if there is only 1
c.currentRemote = &remotes[0]
} else {
// fall back to default :(
c.currentRemote = &defaultRemote
}
}
return *c.currentRemote
}
func (c *Configuration) PushRemote() string {
ref := c.CurrentRef()
c.loading.Lock()
defer c.loading.Unlock()
if c.pushRemote == nil {
if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.pushRemote", ref.Name)); ok {
c.pushRemote = &remote
} else if remote, ok := c.Git.Get("remote.lfspushdefault"); ok {
c.pushRemote = &remote
} else if remote, ok := c.Git.Get("remote.pushDefault"); ok {
c.pushRemote = &remote
} else {
c.loading.Unlock()
remote := c.Remote()
c.loading.Lock()
c.pushRemote = &remote
}
}
return *c.pushRemote
}
func (c *Configuration) SetValidRemote(name string) error {
if err := git.ValidateRemote(name); err != nil {
name := git.RewriteLocalPathAsURL(name)
if err := git.ValidateRemote(name); err != nil {
return err
}
}
c.SetRemote(name)
return nil
}
func (c *Configuration) SetValidPushRemote(name string) error {
if err := git.ValidateRemote(name); err != nil {
name := git.RewriteLocalPathAsURL(name)
if err := git.ValidateRemote(name); err != nil {
return err
}
}
c.SetPushRemote(name)
return nil
}
func (c *Configuration) SetRemote(name string) {
c.currentRemote = &name
}
func (c *Configuration) SetPushRemote(name string) {
c.pushRemote = &name
}
func (c *Configuration) Remotes() []string {
c.loadGitConfig()
return c.remotes
}
func (c *Configuration) Extensions() map[string]Extension {
c.loadGitConfig()
return c.extensions
}
// SortedExtensions gets the list of extensions ordered by Priority
func (c *Configuration) SortedExtensions() ([]Extension, error) {
return SortExtensions(c.Extensions())
}
func (c *Configuration) SkipDownloadErrors() bool {
return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false)
}
func (c *Configuration) SetLockableFilesReadOnly() bool {
return c.Os.Bool("GIT_LFS_SET_LOCKABLE_READONLY", true) && c.Git.Bool("lfs.setlockablereadonly", true)
}
func (c *Configuration) ForceProgress() bool {
return c.Os.Bool("GIT_LFS_FORCE_PROGRESS", false) || c.Git.Bool("lfs.forceprogress", false)
}
// HookDir returns the location of the hooks owned by this repository. If the
// core.hooksPath configuration variable is supported, we prefer that and expand
// paths appropriately.
func (c *Configuration) HookDir() (string, error) {
if git.IsGitVersionAtLeast("2.9.0") {
hp, ok := c.Git.Get("core.hooksPath")
if ok {
path, err := tools.ExpandPath(hp, false)
if err != nil {
return "", err
}
if filepath.IsAbs(path) {
return path, nil
}
return filepath.Join(c.LocalWorkingDir(), path), nil
}
}
return filepath.Join(c.LocalGitStorageDir(), "hooks"), nil
}
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
}
gitdir, workdir, err := git.GitAndRootDirs()
if err != nil {
errMsg := err.Error()
tracerx.Printf("Error running 'git rev-parse': %s", errMsg)
if errors.ExitStatus(err) != 128 {
fmt.Fprintln(os.Stderr, tr.Tr.Get("Error: %s", errMsg))
}
c.gitDir = &gitdir
}
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,
c.RepositoryPermissions(false),
)
}
return c.fs
}
func (c *Configuration) Cleanup() error {
if c == nil {
return nil
}
c.loading.Lock()
defer c.loading.Unlock()
return c.fs.Cleanup()
}
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) FindGitWorktreeKey(key string) string {
return c.gitConfig.FindWorktree(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) SetGitWorktreeKey(key, val string) (string, error) {
return c.gitConfig.SetWorktree(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) UnsetGitWorktreeSection(key string) (string, error) {
return c.gitConfig.UnsetWorktreeSection(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
// accessing properties set by ReadGitConfig, namely:
// - `c.extensions`
// - `c.uniqRemotes`
// - `c.gitConfig`
//
// Since the *gitEnvironment is responsible for setting these values on the
// (*config.Configuration) instance, we must call that method, if it exists.
//
// loadGitConfig returns a bool returning whether or not `loadGitConfig` was
// called AND the method did not return early.
func (c *Configuration) loadGitConfig() {
if g, ok := c.Git.(*delayedEnvironment); ok {
g.Load()
}
}
var (
// dateFormats is a list of all the date formats that Git accepts,
// except for the built-in one, which is handled below.
dateFormats = []string{
"Mon, 02 Jan 2006 15:04:05 -0700",
"2006-01-02T15:04:05-0700",
"2006-01-02 15:04:05-0700",
"2006.01.02T15:04:05-0700",
"2006.01.02 15:04:05-0700",
"01/02/2006T15:04:05-0700",
"01/02/2006 15:04:05-0700",
"02.01.2006T15:04:05-0700",
"02.01.2006 15:04:05-0700",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04:05Z",
"2006.01.02T15:04:05Z",
"2006.01.02 15:04:05Z",
"01/02/2006T15:04:05Z",
"01/02/2006 15:04:05Z",
"02.01.2006T15:04:05Z",
"02.01.2006 15:04:05Z",
}
// defaultDatePattern is the regexp for Git's native date format.
defaultDatePattern = regexp.MustCompile(`\A(\d+) ([+-])(\d{2})(\d{2})\z`)
)
// findUserData returns the name/email that should be used in the commit header.
// We use the same technique as Git for finding this information, except that we
// don't fall back to querying the system for defaults if no values are found in
// the Git configuration or environment.
//
// envType should be "author" or "committer".
func (c *Configuration) findUserData(envType string) (name, email string) {
var filter = func(r rune) rune {
switch r {
case '<', '>', '\n':
return -1
default:
return r
}
}
envType = strings.ToUpper(envType)
name, ok := c.Os.Get("GIT_" + envType + "_NAME")
if !ok {
name, _ = c.Git.Get("user.name")
}
email, ok = c.Os.Get("GIT_" + envType + "_EMAIL")
if !ok {
email, ok = c.Git.Get("user.email")
}
if !ok {
email, _ = c.Os.Get("EMAIL")
}
// Git filters certain characters out of the name and email fields.
name = strings.Map(filter, name)
email = strings.Map(filter, email)
return
}
func (c *Configuration) findUserTimestamp(envType string) time.Time {
date, ok := c.Os.Get(fmt.Sprintf("GIT_%s_DATE", strings.ToUpper(envType)))
if !ok {
return c.timestamp
}
// time.Parse doesn't parse seconds from the Epoch, like we use in the
// Git native format, so we have to do it ourselves.
strs := defaultDatePattern.FindStringSubmatch(date)
if strs != nil {
unixSecs, _ := strconv.ParseInt(strs[1], 10, 64)
hours, _ := strconv.Atoi(strs[3])
offset, _ := strconv.Atoi(strs[4])
offset = (offset + hours*60) * 60
if strs[2] == "-" {
offset = -offset
}
return time.Unix(unixSecs, 0).In(time.FixedZone("", offset))
}
for _, format := range dateFormats {
if t, err := time.Parse(format, date); err == nil {
return t
}
}
// The user provided an invalid value, so default to the current time.
return c.timestamp
}
// CurrentCommitter returns the name/email that would be used to commit a change
// with this configuration. In particular, the "user.name" and "user.email"
// configuration values are used
func (c *Configuration) CurrentCommitter() (name, email string) {
return c.findUserData("committer")
}
// CurrentCommitterTimestamp returns the timestamp that would be used to commit
// a change with this configuration.
func (c *Configuration) CurrentCommitterTimestamp() time.Time {
return c.findUserTimestamp("committer")
}
// CurrentAuthor returns the name/email that would be used to author a change
// with this configuration. In particular, the "user.name" and "user.email"
// configuration values are used
func (c *Configuration) CurrentAuthor() (name, email string) {
return c.findUserData("author")
}
// CurrentCommitterTimestamp returns the timestamp that would be used to commit
// a change with this configuration.
func (c *Configuration) CurrentAuthorTimestamp() time.Time {
return c.findUserTimestamp("author")
}
// RepositoryPermissions returns the permissions that should be used to write
// files in the repository.
func (c *Configuration) RepositoryPermissions(executable bool) os.FileMode {
perms := os.FileMode(0666 & ^c.getMask())
if executable {
return tools.ExecutablePermissions(perms)
}
return perms
}