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