Imported Upstream version 2.5.1
[scm/test.git] / config / config.go
1 // Package config collects together all configuration settings
2 // NOTE: Subject to change, do not rely on this package from outside git-lfs source
3 package config
4
5 import (
6         "fmt"
7         "os"
8         "path/filepath"
9         "strings"
10         "sync"
11
12         "github.com/git-lfs/git-lfs/fs"
13         "github.com/git-lfs/git-lfs/git"
14         "github.com/git-lfs/git-lfs/tools"
15         "github.com/rubyist/tracerx"
16 )
17
18 var (
19         ShowConfigWarnings     = false
20         defaultRemote          = "origin"
21         gitConfigWarningPrefix = "lfs."
22 )
23
24 type Configuration struct {
25         // Os provides a `*Environment` used to access to the system's
26         // environment through os.Getenv. It is the point of entry for all
27         // system environment configuration.
28         Os Environment
29
30         // Git provides a `*Environment` used to access to the various levels of
31         // `.gitconfig`'s. It is the point of entry for all Git environment
32         // configuration.
33         Git Environment
34
35         currentRemote *string
36         pushRemote    *string
37
38         // gitConfig can fetch or modify the current Git config and track the Git
39         // version.
40         gitConfig *git.Configuration
41
42         ref        *git.Ref
43         remoteRef  *git.Ref
44         fs         *fs.Filesystem
45         gitDir     *string
46         workDir    string
47         loading    sync.Mutex // guards initialization of gitConfig and remotes
48         loadingGit sync.Mutex // guards initialization of local git and working dirs
49         remotes    []string
50         extensions map[string]Extension
51 }
52
53 func New() *Configuration {
54         return NewIn("", "")
55 }
56
57 func NewIn(workdir, gitdir string) *Configuration {
58         gitConf := git.NewConfig(workdir, gitdir)
59         c := &Configuration{
60                 Os:        EnvironmentOf(NewOsFetcher()),
61                 gitConfig: gitConf,
62         }
63
64         if len(gitConf.WorkDir) > 0 {
65                 c.gitDir = &gitConf.GitDir
66                 c.workDir = gitConf.WorkDir
67         }
68
69         c.Git = &delayedEnvironment{
70                 callback: func() Environment {
71                         sources, err := gitConf.Sources(filepath.Join(c.LocalWorkingDir(), ".lfsconfig"))
72                         if err != nil {
73                                 fmt.Fprintf(os.Stderr, "Error reading git config: %s\n", err)
74                         }
75                         return c.readGitConfig(sources...)
76                 },
77         }
78         return c
79 }
80
81 func (c *Configuration) readGitConfig(gitconfigs ...*git.ConfigurationSource) Environment {
82         gf, extensions, uniqRemotes := readGitConfig(gitconfigs...)
83         c.extensions = extensions
84         c.remotes = make([]string, 0, len(uniqRemotes))
85         for remote := range uniqRemotes {
86                 c.remotes = append(c.remotes, remote)
87         }
88
89         return EnvironmentOf(gf)
90 }
91
92 // Values is a convenience type used to call the NewFromValues function. It
93 // specifies `Git` and `Env` maps to use as mock values, instead of calling out
94 // to real `.gitconfig`s and the `os.Getenv` function.
95 type Values struct {
96         // Git and Os are the stand-in maps used to provide values for their
97         // respective environments.
98         Git, Os map[string][]string
99 }
100
101 // NewFrom returns a new `*config.Configuration` that reads both its Git
102 // and Enviornment-level values from the ones provided instead of the actual
103 // `.gitconfig` file or `os.Getenv`, respectively.
104 //
105 // This method should only be used during testing.
106 func NewFrom(v Values) *Configuration {
107         c := &Configuration{
108                 Os:        EnvironmentOf(mapFetcher(v.Os)),
109                 gitConfig: git.NewConfig("", ""),
110         }
111         c.Git = &delayedEnvironment{
112                 callback: func() Environment {
113                         source := &git.ConfigurationSource{
114                                 Lines: make([]string, 0, len(v.Git)),
115                         }
116
117                         for key, values := range v.Git {
118                                 for _, value := range values {
119                                         fmt.Printf("Config: %s=%s\n", key, value)
120                                         source.Lines = append(source.Lines, fmt.Sprintf("%s=%s", key, value))
121                                 }
122                         }
123
124                         return c.readGitConfig(source)
125                 },
126         }
127         return c
128 }
129
130 // BasicTransfersOnly returns whether to only allow "basic" HTTP transfers.
131 // Default is false, including if the lfs.basictransfersonly is invalid
132 func (c *Configuration) BasicTransfersOnly() bool {
133         return c.Git.Bool("lfs.basictransfersonly", false)
134 }
135
136 // TusTransfersAllowed returns whether to only use "tus.io" HTTP transfers.
137 // Default is false, including if the lfs.tustransfers is invalid
138 func (c *Configuration) TusTransfersAllowed() bool {
139         return c.Git.Bool("lfs.tustransfers", false)
140 }
141
142 func (c *Configuration) FetchIncludePaths() []string {
143         patterns, _ := c.Git.Get("lfs.fetchinclude")
144         return tools.CleanPaths(patterns, ",")
145 }
146
147 func (c *Configuration) FetchExcludePaths() []string {
148         patterns, _ := c.Git.Get("lfs.fetchexclude")
149         return tools.CleanPaths(patterns, ",")
150 }
151
152 func (c *Configuration) CurrentRef() *git.Ref {
153         c.loading.Lock()
154         defer c.loading.Unlock()
155         if c.ref == nil {
156                 r, err := git.CurrentRef()
157                 if err != nil {
158                         tracerx.Printf("Error loading current ref: %s", err)
159                         c.ref = &git.Ref{}
160                 } else {
161                         c.ref = r
162                 }
163         }
164         return c.ref
165 }
166
167 func (c *Configuration) IsDefaultRemote() bool {
168         return c.Remote() == defaultRemote
169 }
170
171 // Remote returns the default remote based on:
172 // 1. The currently tracked remote branch, if present
173 // 2. Any other SINGLE remote defined in .git/config
174 // 3. Use "origin" as a fallback.
175 // Results are cached after the first hit.
176 func (c *Configuration) Remote() string {
177         ref := c.CurrentRef()
178
179         c.loading.Lock()
180         defer c.loading.Unlock()
181
182         if c.currentRemote == nil {
183                 if len(ref.Name) == 0 {
184                         c.currentRemote = &defaultRemote
185                         return defaultRemote
186                 }
187
188                 if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.remote", ref.Name)); ok {
189                         // try tracking remote
190                         c.currentRemote = &remote
191                 } else if remotes := c.Remotes(); len(remotes) == 1 {
192                         // use only remote if there is only 1
193                         c.currentRemote = &remotes[0]
194                 } else {
195                         // fall back to default :(
196                         c.currentRemote = &defaultRemote
197                 }
198         }
199         return *c.currentRemote
200 }
201
202 func (c *Configuration) PushRemote() string {
203         ref := c.CurrentRef()
204         c.loading.Lock()
205         defer c.loading.Unlock()
206
207         if c.pushRemote == nil {
208                 if remote, ok := c.Git.Get(fmt.Sprintf("branch.%s.pushRemote", ref.Name)); ok {
209                         c.pushRemote = &remote
210                 } else if remote, ok := c.Git.Get("remote.pushDefault"); ok {
211                         c.pushRemote = &remote
212                 } else {
213                         c.loading.Unlock()
214                         remote := c.Remote()
215                         c.loading.Lock()
216
217                         c.pushRemote = &remote
218                 }
219         }
220
221         return *c.pushRemote
222 }
223
224 func (c *Configuration) SetValidRemote(name string) error {
225         if err := git.ValidateRemote(name); err != nil {
226                 return err
227         }
228         c.SetRemote(name)
229         return nil
230 }
231
232 func (c *Configuration) SetValidPushRemote(name string) error {
233         if err := git.ValidateRemote(name); err != nil {
234                 return err
235         }
236         c.SetPushRemote(name)
237         return nil
238 }
239
240 func (c *Configuration) SetRemote(name string) {
241         c.currentRemote = &name
242 }
243
244 func (c *Configuration) SetPushRemote(name string) {
245         c.pushRemote = &name
246 }
247
248 func (c *Configuration) Remotes() []string {
249         c.loadGitConfig()
250         return c.remotes
251 }
252
253 func (c *Configuration) Extensions() map[string]Extension {
254         c.loadGitConfig()
255         return c.extensions
256 }
257
258 // SortedExtensions gets the list of extensions ordered by Priority
259 func (c *Configuration) SortedExtensions() ([]Extension, error) {
260         return SortExtensions(c.Extensions())
261 }
262
263 func (c *Configuration) SkipDownloadErrors() bool {
264         return c.Os.Bool("GIT_LFS_SKIP_DOWNLOAD_ERRORS", false) || c.Git.Bool("lfs.skipdownloaderrors", false)
265 }
266
267 func (c *Configuration) SetLockableFilesReadOnly() bool {
268         return c.Os.Bool("GIT_LFS_SET_LOCKABLE_READONLY", true) && c.Git.Bool("lfs.setlockablereadonly", true)
269 }
270
271 func (c *Configuration) HookDir() string {
272         if git.IsGitVersionAtLeast("2.9.0") {
273                 hp, ok := c.Git.Get("core.hooksPath")
274                 if ok {
275                         return hp
276                 }
277         }
278         return filepath.Join(c.LocalGitDir(), "hooks")
279 }
280
281 func (c *Configuration) InRepo() bool {
282         return len(c.LocalGitDir()) > 0
283 }
284
285 func (c *Configuration) LocalWorkingDir() string {
286         c.loadGitDirs()
287         return c.workDir
288 }
289
290 func (c *Configuration) LocalGitDir() string {
291         c.loadGitDirs()
292         return *c.gitDir
293 }
294
295 func (c *Configuration) loadGitDirs() {
296         c.loadingGit.Lock()
297         defer c.loadingGit.Unlock()
298
299         if c.gitDir != nil {
300                 return
301         }
302
303         gitdir, workdir, err := git.GitAndRootDirs()
304         if err != nil {
305                 errMsg := err.Error()
306                 tracerx.Printf("Error running 'git rev-parse': %s", errMsg)
307                 if !strings.Contains(strings.ToLower(errMsg),
308                         "not a git repository") {
309                         fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
310                 }
311                 c.gitDir = &gitdir
312         }
313
314         gitdir = tools.ResolveSymlinks(gitdir)
315         c.gitDir = &gitdir
316         c.workDir = tools.ResolveSymlinks(workdir)
317 }
318
319 func (c *Configuration) LocalGitStorageDir() string {
320         return c.Filesystem().GitStorageDir
321 }
322
323 func (c *Configuration) LocalReferenceDirs() []string {
324         return c.Filesystem().ReferenceDirs
325 }
326
327 func (c *Configuration) LFSStorageDir() string {
328         return c.Filesystem().LFSStorageDir
329 }
330
331 func (c *Configuration) LFSObjectDir() string {
332         return c.Filesystem().LFSObjectDir()
333 }
334
335 func (c *Configuration) LFSObjectExists(oid string, size int64) bool {
336         return c.Filesystem().ObjectExists(oid, size)
337 }
338
339 func (c *Configuration) EachLFSObject(fn func(fs.Object) error) error {
340         return c.Filesystem().EachObject(fn)
341 }
342
343 func (c *Configuration) LocalLogDir() string {
344         return c.Filesystem().LogDir()
345 }
346
347 func (c *Configuration) TempDir() string {
348         return c.Filesystem().TempDir()
349 }
350
351 func (c *Configuration) Filesystem() *fs.Filesystem {
352         c.loadGitDirs()
353         c.loading.Lock()
354         defer c.loading.Unlock()
355
356         if c.fs == nil {
357                 lfsdir, _ := c.Git.Get("lfs.storage")
358                 c.fs = fs.New(
359                         c.Os,
360                         c.LocalGitDir(),
361                         c.LocalWorkingDir(),
362                         lfsdir,
363                 )
364         }
365
366         return c.fs
367 }
368
369 func (c *Configuration) Cleanup() error {
370         c.loading.Lock()
371         defer c.loading.Unlock()
372         return c.fs.Cleanup()
373 }
374
375 func (c *Configuration) OSEnv() Environment {
376         return c.Os
377 }
378
379 func (c *Configuration) GitEnv() Environment {
380         return c.Git
381 }
382
383 func (c *Configuration) GitConfig() *git.Configuration {
384         return c.gitConfig
385 }
386
387 func (c *Configuration) FindGitGlobalKey(key string) string {
388         return c.gitConfig.FindGlobal(key)
389 }
390
391 func (c *Configuration) FindGitSystemKey(key string) string {
392         return c.gitConfig.FindSystem(key)
393 }
394
395 func (c *Configuration) FindGitLocalKey(key string) string {
396         return c.gitConfig.FindLocal(key)
397 }
398
399 func (c *Configuration) SetGitGlobalKey(key, val string) (string, error) {
400         return c.gitConfig.SetGlobal(key, val)
401 }
402
403 func (c *Configuration) SetGitSystemKey(key, val string) (string, error) {
404         return c.gitConfig.SetSystem(key, val)
405 }
406
407 func (c *Configuration) SetGitLocalKey(key, val string) (string, error) {
408         return c.gitConfig.SetLocal(key, val)
409 }
410
411 func (c *Configuration) UnsetGitGlobalSection(key string) (string, error) {
412         return c.gitConfig.UnsetGlobalSection(key)
413 }
414
415 func (c *Configuration) UnsetGitSystemSection(key string) (string, error) {
416         return c.gitConfig.UnsetSystemSection(key)
417 }
418
419 func (c *Configuration) UnsetGitLocalSection(key string) (string, error) {
420         return c.gitConfig.UnsetLocalSection(key)
421 }
422
423 func (c *Configuration) UnsetGitLocalKey(key string) (string, error) {
424         return c.gitConfig.UnsetLocalKey(key)
425 }
426
427 // loadGitConfig is a temporary measure to support legacy behavior dependent on
428 // accessing properties set by ReadGitConfig, namely:
429 //  - `c.extensions`
430 //  - `c.uniqRemotes`
431 //  - `c.gitConfig`
432 //
433 // Since the *gitEnvironment is responsible for setting these values on the
434 // (*config.Configuration) instance, we must call that method, if it exists.
435 //
436 // loadGitConfig returns a bool returning whether or not `loadGitConfig` was
437 // called AND the method did not return early.
438 func (c *Configuration) loadGitConfig() {
439         if g, ok := c.Git.(*delayedEnvironment); ok {
440                 g.Load()
441         }
442 }
443
444 // CurrentCommitter returns the name/email that would be used to author a commit
445 // with this configuration. In particular, the "user.name" and "user.email"
446 // configuration values are used
447 func (c *Configuration) CurrentCommitter() (name, email string) {
448         name, _ = c.Git.Get("user.name")
449         email, _ = c.Git.Get("user.email")
450         return
451 }