1// Package config collects together all configuration settings 2// NOTE: Subject to change, do not rely on this package from outside git-lfs source 3package config 4 5import ( 6 "fmt" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 "unicode" 15 16 "github.com/git-lfs/git-lfs/v3/fs" 17 "github.com/git-lfs/git-lfs/v3/git" 18 "github.com/git-lfs/git-lfs/v3/tools" 19 "github.com/rubyist/tracerx" 20) 21 22var ( 23 ShowConfigWarnings = false 24 defaultRemote = "origin" 25 gitConfigWarningPrefix = "lfs." 26) 27 28type Configuration struct { 29 // Os provides a `*Environment` used to access to the system's 30 // environment through os.Getenv. It is the point of entry for all 31 // system environment configuration. 32 Os Environment 33 34 // Git provides a `*Environment` used to access to the various levels of 35 // `.gitconfig`'s. It is the point of entry for all Git environment 36 // configuration. 37 Git Environment 38 39 currentRemote *string 40 pushRemote *string 41 42 // gitConfig can fetch or modify the current Git config and track the Git 43 // version. 44 gitConfig *git.Configuration 45 46 ref *git.Ref 47 remoteRef *git.Ref 48 fs *fs.Filesystem 49 gitDir *string 50 workDir string 51 loading sync.Mutex // guards initialization of gitConfig and remotes 52 loadingGit sync.Mutex // guards initialization of local git and working dirs 53 remotes []string 54 extensions map[string]Extension 55 mask int 56 maskOnce sync.Once 57 timestamp time.Time 58} 59 60func New() *Configuration { 61 return NewIn("", "") 62} 63 64func NewIn(workdir, gitdir string) *Configuration { 65 gitConf := git.NewConfig(workdir, gitdir) 66 c := &Configuration{ 67 Os: EnvironmentOf(NewOsFetcher()), 68 gitConfig: gitConf, 69 timestamp: time.Now(), 70 } 71 72 if len(gitConf.WorkDir) > 0 { 73 c.gitDir = &gitConf.GitDir 74 c.workDir = gitConf.WorkDir 75 } 76 77 c.Git = &delayedEnvironment{ 78 callback: func() Environment { 79 sources, err := gitConf.Sources(c.LocalWorkingDir(), ".lfsconfig") 80 if err != nil { 81 fmt.Fprintf(os.Stderr, "Error reading git config: %s\n", err) 82 } 83 return c.readGitConfig(sources...) 84 }, 85 } 86 return c 87} 88 89func (c *Configuration) getMask() int { 90 // This logic is necessarily complex because Git's logic is complex. 91 c.maskOnce.Do(func() { 92 val, ok := c.Git.Get("core.sharedrepository") 93 if !ok { 94 val = "umask" 95 } else if Bool(val, false) { 96 val = "group" 97 } 98 99 switch strings.ToLower(val) { 100 case "group", "true", "1": 101 c.mask = 007 102 case "all", "world", "everybody", "2": 103 c.mask = 002 104 case "umask", "false", "0": 105 c.mask = umask() 106 default: 107 if mode, err := strconv.ParseInt(val, 8, 16); err != nil { 108 // If this doesn't look like an octal number, then it 109 // could be a falsy value, in which case we should use 110 // the umask, or it's just invalid, in which case the 111 // umask is a safe bet. 112 c.mask = umask() 113 } else { 114 c.mask = 0666 & ^int(mode) 115 } 116 } 117 }) 118 return c.mask 119} 120 121func (c *Configuration) readGitConfig(gitconfigs ...*git.ConfigurationSource) Environment { 122 gf, extensions, uniqRemotes := readGitConfig(gitconfigs...) 123 c.extensions = extensions 124 c.remotes = make([]string, 0, len(uniqRemotes)) 125 for remote := range uniqRemotes { 126 c.remotes = append(c.remotes, remote) 127 } 128 129 return EnvironmentOf(gf) 130} 131 132// Values is a convenience type used to call the NewFromValues function. It 133// specifies `Git` and `Env` maps to use as mock values, instead of calling out 134// to real `.gitconfig`s and the `os.Getenv` function. 135type Values struct { 136 // Git and Os are the stand-in maps used to provide values for their 137 // respective environments. 138 Git, Os map[string][]string 139} 140 141// NewFrom returns a new `*config.Configuration` that reads both its Git 142// and Environment-level values from the ones provided instead of the actual 143// `.gitconfig` file or `os.Getenv`, respectively. 144// 145// This method should only be used during testing. 146func NewFrom(v Values) *Configuration { 147 c := &Configuration{ 148 Os: EnvironmentOf(mapFetcher(v.Os)), 149 gitConfig: git.NewConfig("", ""), 150 timestamp: time.Now(), 151 } 152 c.Git = &delayedEnvironment{ 153 callback: func() Environment { 154 source := &git.ConfigurationSource{ 155 Lines: make([]string, 0, len(v.Git)), 156 } 157 158 for key, values := range v.Git { 159 parts := strings.Split(key, ".") 160 isCaseSensitive := len(parts) >= 3 161 hasUpper := strings.IndexFunc(key, unicode.IsUpper) > -1 162 163 // This branch should only ever trigger in 164 // tests, and only if they'd be broken. 165 if !isCaseSensitive && hasUpper { 166 panic(fmt.Sprintf("key %q has uppercase, shouldn't", key)) 167 } 168 for _, value := range values { 169 fmt.Printf("Config: %s=%s\n", key, value) 170 source.Lines = append(source.Lines, fmt.Sprintf("%s=%s", key, value)) 171 } 172 } 173 174 return c.readGitConfig(source) 175 }, 176 } 177 return c 178} 179 180// BasicTransfersOnly returns whether to only allow "basic" HTTP transfers. 181// Default is false, including if the lfs.basictransfersonly is invalid 182func (c *Configuration) BasicTransfersOnly() bool { 183 return c.Git.Bool("lfs.basictransfersonly", false) 184} 185 186// TusTransfersAllowed returns whether to only use "tus.io" HTTP transfers. 187// Default is false, including if the lfs.tustransfers is invalid 188func (c *Configuration) TusTransfersAllowed() bool { 189 return c.Git.Bool("lfs.tustransfers", false) 190} 191 192func (c *Configuration) FetchIncludePaths() []string { 193 patterns, _ := c.Git.Get("lfs.fetchinclude") 194 return tools.CleanPaths(patterns, ",") 195} 196 197func (c *Configuration) FetchExcludePaths() []string { 198 patterns, _ := c.Git.Get("lfs.fetchexclude") 199 return tools.CleanPaths(patterns, ",") 200} 201 202func (c *Configuration) CurrentRef() *git.Ref { 203 c.loading.Lock() 204 defer c.loading.Unlock() 205 if c.ref == nil { 206 r, err := git.CurrentRef() 207 if err != nil { 208 tracerx.Printf("Error loading current ref: %s", err) 209 c.ref = &git.Ref{} 210 } else { 211 c.ref = r 212 } 213 } 214 return c.ref 215} 216 217func (c *Configuration) IsDefaultRemote() bool { 218 return c.Remote() == defaultRemote 219} 220 221// Remote returns the default remote based on: 222// 1. The currently tracked remote branch, if present 223// 2. The value of remote.lfsdefault. 224// 3. Any other SINGLE remote defined in .git/config 225// 4. Use "origin" as a fallback. 226// Results are cached after the first hit. 227func (c *Configuration) Remote() string { 228 ref := c.CurrentRef() 229 230 c.loading.Lock() 231 defer c.loading.Unlock() 232 233 if c.currentRemote == nil { 234 if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.remote", ref.Name)); len(ref.Name) != 0 && ok { 235 // try tracking remote 236 c.currentRemote = &remote 237 } else if remote, ok := c.Git.Get("remote.lfsdefault"); ok { 238 // try default remote 239 c.currentRemote = &remote 240 } else if remotes := c.Remotes(); len(remotes) == 1 { 241 // use only remote if there is only 1 242 c.currentRemote = &remotes[0] 243 } else { 244 // fall back to default :( 245 c.currentRemote = &defaultRemote 246 } 247 } 248 return *c.currentRemote 249} 250 251func (c *Configuration) PushRemote() string { 252 ref := c.CurrentRef() 253 c.loading.Lock() 254 defer c.loading.Unlock() 255 256 if c.pushRemote == nil { 257 if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.pushRemote", ref.Name)); ok { 258 c.pushRemote = &remote 259 } else if remote, ok := c.Git.Get("remote.lfspushdefault"); ok { 260 c.pushRemote = &remote 261 } else if remote, ok := c.Git.Get("remote.pushDefault"); ok { 262 c.pushRemote = &remote 263 } else { 264 c.loading.Unlock() 265 remote := c.Remote() 266 c.loading.Lock() 267 268 c.pushRemote = &remote 269 } 270 } 271 272 return *c.pushRemote 273} 274 275func (c *Configuration) SetValidRemote(name string) error { 276 if err := git.ValidateRemote(name); err != nil { 277 name := git.RewriteLocalPathAsURL(name) 278 if err := git.ValidateRemote(name); err != nil { 279 return err 280 } 281 } 282 c.SetRemote(name) 283 return nil 284} 285 286func (c *Configuration) SetValidPushRemote(name string) error { 287 if err := git.ValidateRemote(name); err != nil { 288 name := git.RewriteLocalPathAsURL(name) 289 if err := git.ValidateRemote(name); err != nil { 290 return err 291 } 292 } 293 c.SetPushRemote(name) 294 return nil 295} 296 297func (c *Configuration) SetRemote(name string) { 298 c.currentRemote = &name 299} 300 301func (c *Configuration) SetPushRemote(name string) { 302 c.pushRemote = &name 303} 304 305func (c *Configuration) Remotes() []string { 306 c.loadGitConfig() 307 return c.remotes 308} 309 310func (c *Configuration) Extensions() map[string]Extension { 311 c.loadGitConfig() 312 return c.extensions 313} 314 315// SortedExtensions gets the list of extensions ordered by Priority 316func (c *Configuration) SortedExtensions() ([]Extension, error) { 317 return SortExtensions(c.Extensions()) 318} 319 320func (c *Configuration) SkipDownloadErrors() bool { 321 return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false) 322} 323 324func (c *Configuration) SetLockableFilesReadOnly() bool { 325 return c.Os.Bool("GIT_LFS_SET_LOCKABLE_READONLY", true) && c.Git.Bool("lfs.setlockablereadonly", true) 326} 327 328func (c *Configuration) ForceProgress() bool { 329 return c.Os.Bool("GIT_LFS_FORCE_PROGRESS", false) || c.Git.Bool("lfs.forceprogress", false) 330} 331 332// HookDir returns the location of the hooks owned by this repository. If the 333// core.hooksPath configuration variable is supported, we prefer that and expand 334// paths appropriately. 335func (c *Configuration) HookDir() (string, error) { 336 if git.IsGitVersionAtLeast("2.9.0") { 337 hp, ok := c.Git.Get("core.hooksPath") 338 if ok { 339 path, err := tools.ExpandPath(hp, false) 340 if err != nil { 341 return "", err 342 } 343 if filepath.IsAbs(path) { 344 return path, nil 345 } 346 return filepath.Join(c.LocalWorkingDir(), path), nil 347 } 348 } 349 return filepath.Join(c.LocalGitStorageDir(), "hooks"), nil 350} 351 352func (c *Configuration) InRepo() bool { 353 return len(c.LocalGitDir()) > 0 354} 355 356func (c *Configuration) LocalWorkingDir() string { 357 c.loadGitDirs() 358 return c.workDir 359} 360 361func (c *Configuration) LocalGitDir() string { 362 c.loadGitDirs() 363 return *c.gitDir 364} 365 366func (c *Configuration) loadGitDirs() { 367 c.loadingGit.Lock() 368 defer c.loadingGit.Unlock() 369 370 if c.gitDir != nil { 371 return 372 } 373 374 gitdir, workdir, err := git.GitAndRootDirs() 375 if err != nil { 376 errMsg := err.Error() 377 tracerx.Printf("Error running 'git rev-parse': %s", errMsg) 378 if !strings.Contains(strings.ToLower(errMsg), 379 "not a git repository") { 380 fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg) 381 } 382 c.gitDir = &gitdir 383 } 384 385 gitdir = tools.ResolveSymlinks(gitdir) 386 c.gitDir = &gitdir 387 c.workDir = tools.ResolveSymlinks(workdir) 388} 389 390func (c *Configuration) LocalGitStorageDir() string { 391 return c.Filesystem().GitStorageDir 392} 393 394func (c *Configuration) LocalReferenceDirs() []string { 395 return c.Filesystem().ReferenceDirs 396} 397 398func (c *Configuration) LFSStorageDir() string { 399 return c.Filesystem().LFSStorageDir 400} 401 402func (c *Configuration) LFSObjectDir() string { 403 return c.Filesystem().LFSObjectDir() 404} 405 406func (c *Configuration) LFSObjectExists(oid string, size int64) bool { 407 return c.Filesystem().ObjectExists(oid, size) 408} 409 410func (c *Configuration) EachLFSObject(fn func(fs.Object) error) error { 411 return c.Filesystem().EachObject(fn) 412} 413 414func (c *Configuration) LocalLogDir() string { 415 return c.Filesystem().LogDir() 416} 417 418func (c *Configuration) TempDir() string { 419 return c.Filesystem().TempDir() 420} 421 422func (c *Configuration) Filesystem() *fs.Filesystem { 423 c.loadGitDirs() 424 c.loading.Lock() 425 defer c.loading.Unlock() 426 427 if c.fs == nil { 428 lfsdir, _ := c.Git.Get("lfs.storage") 429 c.fs = fs.New( 430 c.Os, 431 c.LocalGitDir(), 432 c.LocalWorkingDir(), 433 lfsdir, 434 c.RepositoryPermissions(false), 435 ) 436 } 437 438 return c.fs 439} 440 441func (c *Configuration) Cleanup() error { 442 if c == nil { 443 return nil 444 } 445 c.loading.Lock() 446 defer c.loading.Unlock() 447 return c.fs.Cleanup() 448} 449 450func (c *Configuration) OSEnv() Environment { 451 return c.Os 452} 453 454func (c *Configuration) GitEnv() Environment { 455 return c.Git 456} 457 458func (c *Configuration) GitConfig() *git.Configuration { 459 return c.gitConfig 460} 461 462func (c *Configuration) FindGitGlobalKey(key string) string { 463 return c.gitConfig.FindGlobal(key) 464} 465 466func (c *Configuration) FindGitSystemKey(key string) string { 467 return c.gitConfig.FindSystem(key) 468} 469 470func (c *Configuration) FindGitLocalKey(key string) string { 471 return c.gitConfig.FindLocal(key) 472} 473 474func (c *Configuration) FindGitWorktreeKey(key string) string { 475 return c.gitConfig.FindWorktree(key) 476} 477 478func (c *Configuration) SetGitGlobalKey(key, val string) (string, error) { 479 return c.gitConfig.SetGlobal(key, val) 480} 481 482func (c *Configuration) SetGitSystemKey(key, val string) (string, error) { 483 return c.gitConfig.SetSystem(key, val) 484} 485 486func (c *Configuration) SetGitLocalKey(key, val string) (string, error) { 487 return c.gitConfig.SetLocal(key, val) 488} 489 490func (c *Configuration) SetGitWorktreeKey(key, val string) (string, error) { 491 return c.gitConfig.SetWorktree(key, val) 492} 493 494func (c *Configuration) UnsetGitGlobalSection(key string) (string, error) { 495 return c.gitConfig.UnsetGlobalSection(key) 496} 497 498func (c *Configuration) UnsetGitSystemSection(key string) (string, error) { 499 return c.gitConfig.UnsetSystemSection(key) 500} 501 502func (c *Configuration) UnsetGitLocalSection(key string) (string, error) { 503 return c.gitConfig.UnsetLocalSection(key) 504} 505 506func (c *Configuration) UnsetGitWorktreeSection(key string) (string, error) { 507 return c.gitConfig.UnsetWorktreeSection(key) 508} 509 510func (c *Configuration) UnsetGitLocalKey(key string) (string, error) { 511 return c.gitConfig.UnsetLocalKey(key) 512} 513 514// loadGitConfig is a temporary measure to support legacy behavior dependent on 515// accessing properties set by ReadGitConfig, namely: 516// - `c.extensions` 517// - `c.uniqRemotes` 518// - `c.gitConfig` 519// 520// Since the *gitEnvironment is responsible for setting these values on the 521// (*config.Configuration) instance, we must call that method, if it exists. 522// 523// loadGitConfig returns a bool returning whether or not `loadGitConfig` was 524// called AND the method did not return early. 525func (c *Configuration) loadGitConfig() { 526 if g, ok := c.Git.(*delayedEnvironment); ok { 527 g.Load() 528 } 529} 530 531var ( 532 // dateFormats is a list of all the date formats that Git accepts, 533 // except for the built-in one, which is handled below. 534 dateFormats = []string{ 535 "Mon, 02 Jan 2006 15:04:05 -0700", 536 "2006-01-02T15:04:05-0700", 537 "2006-01-02 15:04:05-0700", 538 "2006.01.02T15:04:05-0700", 539 "2006.01.02 15:04:05-0700", 540 "01/02/2006T15:04:05-0700", 541 "01/02/2006 15:04:05-0700", 542 "02.01.2006T15:04:05-0700", 543 "02.01.2006 15:04:05-0700", 544 "2006-01-02T15:04:05Z", 545 "2006-01-02 15:04:05Z", 546 "2006.01.02T15:04:05Z", 547 "2006.01.02 15:04:05Z", 548 "01/02/2006T15:04:05Z", 549 "01/02/2006 15:04:05Z", 550 "02.01.2006T15:04:05Z", 551 "02.01.2006 15:04:05Z", 552 } 553 554 // defaultDatePattern is the regexp for Git's native date format. 555 defaultDatePattern = regexp.MustCompile(`\A(\d+) ([+-])(\d{2})(\d{2})\z`) 556) 557 558// findUserData returns the name/email that should be used in the commit header. 559// We use the same technique as Git for finding this information, except that we 560// don't fall back to querying the system for defaults if no values are found in 561// the Git configuration or environment. 562// 563// envType should be "author" or "committer". 564func (c *Configuration) findUserData(envType string) (name, email string) { 565 var filter = func(r rune) rune { 566 switch r { 567 case '<', '>', '\n': 568 return -1 569 default: 570 return r 571 } 572 } 573 574 envType = strings.ToUpper(envType) 575 576 name, ok := c.Os.Get("GIT_" + envType + "_NAME") 577 if !ok { 578 name, _ = c.Git.Get("user.name") 579 } 580 581 email, ok = c.Os.Get("GIT_" + envType + "_EMAIL") 582 if !ok { 583 email, ok = c.Git.Get("user.email") 584 } 585 if !ok { 586 email, _ = c.Os.Get("EMAIL") 587 } 588 589 // Git filters certain characters out of the name and email fields. 590 name = strings.Map(filter, name) 591 email = strings.Map(filter, email) 592 return 593} 594 595func (c *Configuration) findUserTimestamp(envType string) time.Time { 596 date, ok := c.Os.Get(fmt.Sprintf("GIT_%s_DATE", strings.ToUpper(envType))) 597 if !ok { 598 return c.timestamp 599 } 600 601 // time.Parse doesn't parse seconds from the Epoch, like we use in the 602 // Git native format, so we have to do it ourselves. 603 strs := defaultDatePattern.FindStringSubmatch(date) 604 if strs != nil { 605 unixSecs, _ := strconv.ParseInt(strs[1], 10, 64) 606 hours, _ := strconv.Atoi(strs[3]) 607 offset, _ := strconv.Atoi(strs[4]) 608 offset = (offset + hours*60) * 60 609 if strs[2] == "-" { 610 offset = -offset 611 } 612 613 return time.Unix(unixSecs, 0).In(time.FixedZone("", offset)) 614 } 615 616 for _, format := range dateFormats { 617 if t, err := time.Parse(format, date); err == nil { 618 return t 619 } 620 } 621 622 // The user provided an invalid value, so default to the current time. 623 return c.timestamp 624} 625 626// CurrentCommitter returns the name/email that would be used to commit a change 627// with this configuration. In particular, the "user.name" and "user.email" 628// configuration values are used 629func (c *Configuration) CurrentCommitter() (name, email string) { 630 return c.findUserData("committer") 631} 632 633// CurrentCommitterTimestamp returns the timestamp that would be used to commit 634// a change with this configuration. 635func (c *Configuration) CurrentCommitterTimestamp() time.Time { 636 return c.findUserTimestamp("committer") 637} 638 639// CurrentAuthor returns the name/email that would be used to author a change 640// with this configuration. In particular, the "user.name" and "user.email" 641// configuration values are used 642func (c *Configuration) CurrentAuthor() (name, email string) { 643 return c.findUserData("author") 644} 645 646// CurrentCommitterTimestamp returns the timestamp that would be used to commit 647// a change with this configuration. 648func (c *Configuration) CurrentAuthorTimestamp() time.Time { 649 return c.findUserTimestamp("author") 650} 651 652// RepositoryPermissions returns the permissions that should be used to write 653// files in the repository. 654func (c *Configuration) RepositoryPermissions(executable bool) os.FileMode { 655 perms := os.FileMode(0666 & ^c.getMask()) 656 if executable { 657 return tools.ExecutablePermissions(perms) 658 } 659 return perms 660} 661