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