-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfigs.go
More file actions
571 lines (503 loc) · 15.1 KB
/
configs.go
File metadata and controls
571 lines (503 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
package gitconfig
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/gopasspw/gopass/pkg/appdir"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/set"
)
// Configs represents all git configuration files for a repository.
//
// Configs manages multiple Config objects from different scopes with a unified
// interface. It handles loading and merging configurations from multiple sourc with priority.
//
// Scope Priority (highest to lowest):
// 1. Environment variables (GIT_CONFIG_*)
// 2. Worktree-specific config (.git/config.worktree)
// 3. Local/repository config (.git/config)
// 4. Global/user config (~/.gitconfig)
// 5. System config (/etc/gitconfig)
// 6. Preset/built-in defaults
//
// Fields:
// - Preset: Built-in default configuration (optional)
// - system, global, local, worktree, env: Config objects for each scope
// - workdir: Working directory (used to locate local and worktree configs)
// - Name: Configuration set name (e.g., "git" or "gopass")
// - SystemConfig, GlobalConfig, LocalConfig, WorktreeConfig: File paths
// - EnvPrefix: Prefix for environment variables (e.g., "GIT_CONFIG")
// - NoWrites: If true, prevents all writes to disk
//
// Usage:
//
// cfg := New()
// cfg.LoadAll(".") // Load from current directory
// value := cfg.Get("core.editor") // Reads from all scopes
// cfg.SetLocal("core.pager", "less") // Write to local only
type Configs struct {
Preset *Config
system *Config
global *Config
local *Config
worktree *Config
env *Config
workdir string
Name string
SystemConfig string
GlobalConfig string
LocalConfig string
WorktreeConfig string
EnvPrefix string
NoWrites bool
}
// New creates a new Configs instance with default configuration.
//
// The returned instance is not yet loaded. Call LoadAll() to load configurations.
//
// Default settings:
// - Name: "git"
// - SystemConfig: "/etc/gitconfig" (Unix) or auto-detected (Windows)
// - GlobalConfig: "~/.gitconfig"
// - LocalConfig: "config" (relative to workdir)
// - WorktreeConfig: "config.worktree" (relative to workdir)
// - EnvPrefix: "GIT_CONFIG"
// - NoWrites: false (allows persisting changes)
//
// These settings can be customized before calling LoadAll():
//
// cfg := New()
// cfg.SystemConfig = "/etc/myapp/config"
// cfg.EnvPrefix = "MYAPP_CONFIG"
// cfg.LoadAll(".")
func New() *Configs {
return &Configs{
system: &Config{
readonly: true,
},
global: &Config{
path: globalConfigFile(name),
},
local: &Config{},
worktree: &Config{},
env: &Config{
noWrites: true,
},
Name: name,
SystemConfig: systemConfig,
GlobalConfig: globalConfig,
LocalConfig: localConfig,
WorktreeConfig: worktreeConfig,
EnvPrefix: envPrefix,
}
}
// Reload reloads all configuration files from disk.
//
// This is useful when configuration files have been modified externally.
// Uses the same workdir that was provided to the last LoadAll call.
func (cs *Configs) Reload() {
cs.LoadAll(cs.workdir)
}
// String implements fmt.Stringer for debugging.
func (cs *Configs) String() string {
return fmt.Sprintf("GitConfigs{Name: %s - Workdir: %s - Env: %s - System: %s - Global: %s - Local: %s - Worktree: %s}", cs.Name, cs.workdir, cs.EnvPrefix, cs.SystemConfig, cs.GlobalConfig, cs.LocalConfig, cs.WorktreeConfig)
}
// LoadAll loads all known configuration files from their configured locations.
//
// Behavior:
// - Loads configs from all scopes (system, global, local, worktree, env)
// - Missing or invalid files are silently ignored
// - Never returns an error (always returns &cs for chaining)
// - workdir is optional; if empty, local and worktree configs are not loaded
// - Processes include and includeIf directives
// - Merges all configs with proper scope priority
//
// Parameters:
// - workdir: Working directory (usually repo root) to locate local/worktree configs
//
// Example:
//
// cfg := New()
// cfg.LoadAll("/path/to/repo")
// // Now ready to use Get, Set, etc.
func (cs *Configs) LoadAll(workdir string) *Configs {
cs.workdir = workdir
debug.Log("Loading gitconfigs for %s", cs.Name)
// load the system config, if any
if os.Getenv(cs.EnvPrefix+"_NOSYSTEM") == "" {
c, err := LoadConfig(cs.SystemConfig)
if err != nil {
debug.V(1).Log("[%s] failed to load system config: %s", cs.Name, err)
} else {
debug.V(1).Log("[%s] loaded system config from %s", cs.Name, cs.SystemConfig)
cs.system = c
// the system config should generally not be written from gopass.
// in almost any scenario gopass shouldn't have write access
// and even if it does we shouldn't accidentially change it.
// It's for operators and package mainatiners.
cs.system.readonly = true
}
}
// load the "global" (per user) config, if any
cs.loadGlobalConfigs()
cs.global.noWrites = cs.NoWrites
// load the local config, if any
if workdir != "" {
localConfigPath := filepath.Join(workdir, cs.LocalConfig)
c, err := LoadConfig(localConfigPath)
if err != nil {
debug.V(1).Log("[%s] failed to load local config from %s: %s", cs.Name, localConfigPath, err)
// set the path just in case we want to modify / write to it later
cs.local.path = localConfigPath
} else {
debug.V(1).Log("[%s] loaded local config from %s", cs.Name, localConfigPath)
cs.local = c
}
}
cs.local.noWrites = cs.NoWrites
// load the worktree config, if any
if workdir != "" {
worktreeConfigPath := filepath.Join(workdir, cs.WorktreeConfig)
c, err := LoadConfig(worktreeConfigPath)
if err != nil {
debug.V(3).Log("[%s] failed to load worktree config from %s: %s", cs.Name, worktreeConfigPath, err)
// set the path just in case we want to modify / write to it later
cs.worktree.path = worktreeConfigPath
} else {
debug.V(1).Log("[%s] loaded worktree config from %s", cs.Name, worktreeConfigPath)
cs.worktree = c
}
}
cs.worktree.noWrites = cs.NoWrites
// load any env vars
cs.env = LoadConfigFromEnv(cs.EnvPrefix)
return cs
}
// globalConfigFile returns the path to the global (per-user) config file using XDG base directory spec.
//
// The defaultlocation is $XDG_CONFIG_HOME/<name>/config (typically ~/.config/git/config for Git).
// This follows the XDG Base Directory specification for user-specific configuration files.
func globalConfigFile(name string) string {
// $XDG_CONFIG_HOME/git/config
return filepath.Join(appdir.New(name).UserConfig(), "config")
}
// loadGlobalConfigs will try to load the per-user (Git calls them "global") configs.
// Since we might need to try different locations but only want to use the first one
// it's easier to handle this in its own method.
func (cs *Configs) loadGlobalConfigs() string {
locs := []string{
globalConfigFile(cs.Name),
}
if cs.GlobalConfig != "" {
// ~/.gitconfig
locs = append(locs, filepath.Join(appdir.UserHome(), cs.GlobalConfig))
}
// if we already have a global config we can just reload it instead of trying all locations
if !cs.global.IsEmpty() {
if p := cs.global.path; p != "" {
debug.V(1).Log("[%s] reloading existing global config from %s", cs.Name, p)
cfg, err := LoadConfig(p)
if err != nil {
debug.V(1).Log("[%s] failed to reload global config from %s", cs.Name, p)
} else {
cs.global = cfg
return p
}
}
}
debug.V(1).Log("[%s] trying to find global configs in %v", cs.Name, locs)
for _, p := range locs {
// GlobalConfig might be set to an empty string to disable it
// and instead of the XDG_CONFIG_HOME path only.
if p == "" {
continue
}
cfg, err := LoadConfig(p)
if err != nil {
debug.V(1).Log("[%s] failed to load global config from %s: %s", cs.Name, p, err)
continue
}
debug.V(1).Log("[%s] loaded global config from %s", cs.Name, p)
cs.global = cfg
return p
}
debug.V(1).Log("[%s] no global config found", cs.Name)
// set the path to the default one in case we want to write to it (create it) later
cs.global = &Config{
path: globalConfigFile(cs.Name),
}
return ""
}
// HasGlobalConfig indicates if a per-user config can be found.
//
// Returns true if a global config file exists at one of the configured locations.
func (cs *Configs) HasGlobalConfig() bool {
return cs.loadGlobalConfigs() != ""
}
// Get returns the value for the given key from the first scope that contains it.
//
// Lookup Order (by scope priority):
// 1. Environment variables (GIT_CONFIG_*)
// 2. Worktree config (.git/config.worktree)
// 3. Local config (.git/config)
// 4. Global config (~/.gitconfig)
// 5. System config (/etc/gitconfig)
// 6. Preset/defaults
//
// The search stops at the first scope that has the key. Earlier scopes override later ones.
//
// Returns the value as a string. For keys with multiple values, returns the first one.
// Returns empty string if key not found in any scope.
//
// Example:
//
// editor := cfg.Get("core.editor")
// if editor != "" {
// fmt.Printf("Using editor: %s\n", editor)
// }
func (cs *Configs) Get(key string) string {
for _, cfg := range []*Config{
cs.env,
cs.worktree,
cs.local,
cs.global,
cs.system,
cs.Preset,
} {
if cfg == nil || cfg.vars == nil {
continue
}
if v, found := cfg.Get(key); found {
return v
}
}
debug.V(3).Log("[%s] no value for %s found", cs.Name, key)
return ""
}
// GetAll returns all values for the given key from the first scope that contains it.
//
// Like Get but returns all values for keys that can have multiple entries.
// See Get documentation for scope priority.
//
// Returns nil if key not found in any scope.
func (cs *Configs) GetAll(key string) []string {
for _, cfg := range []*Config{
cs.env,
cs.worktree,
cs.local,
cs.global,
cs.system,
cs.Preset,
} {
if cfg == nil || cfg.vars == nil {
continue
}
if vs, found := cfg.GetAll(key); found {
return vs
}
}
debug.V(3).Log("[%s] no value for %s found", cs.Name, key)
return nil
}
// GetFrom returns the value for the given key from the given scope. Valid scopes are:
// env, worktree, local, global, system and preset.
func (cs *Configs) GetFrom(key string, scope string) (string, bool) {
switch strings.ToLower(scope) {
case "env":
return cs.env.Get(key)
case "worktree":
return cs.worktree.Get(key)
case "local":
return cs.local.Get(key)
case "global":
return cs.global.Get(key)
case "system":
return cs.system.Get(key)
case "preset":
return cs.Preset.Get(key)
default:
debug.V(3).Log("[%s] unknown config scope %s for key %s", cs.Name, scope, key)
return "", false
}
}
// GetGlobal specifically asks the per-user (global) config for a key.
//
// This bypasses the scope priority and only reads from the global config.
// Useful when you specifically want settings from ~/.gitconfig.
//
// Returns empty string if the key is not found in the global config.
//
// Example:
//
// name, _ := cfg.GetGlobal("user.name")
func (cs *Configs) GetGlobal(key string) string {
if cs.global == nil {
return ""
}
if v, found := cs.global.Get(key); found {
return v
}
debug.V(3).Log("[%s] no value for %s found", cs.Name, key)
return ""
}
// GetLocal specifically asks the per-directory (local) config for a key.
//
// This bypasses the scope priority and only reads from the local config (.git/config).
// Useful when you specifically want settings from the repository's config.
//
// Returns empty string if the key is not found in the local config.
//
// Example:
//
// url, _ := cfg.GetLocal("remote.origin.url")
func (cs *Configs) GetLocal(key string) string {
if cs.local == nil {
return ""
}
if v, found := cs.local.Get(key); found {
return v
}
debug.V(3).Log("[%s] no value for %s found", cs.Name, key)
return ""
}
// IsSet returns true if this key is set in any of our configs.
func (cs *Configs) IsSet(key string) bool {
for _, cfg := range []*Config{
cs.env,
cs.worktree,
cs.local,
cs.global,
cs.system,
cs.Preset,
} {
if cfg != nil && cfg.IsSet(key) {
return true
}
}
return false
}
// SetLocal sets (or adds) a key only in the per-directory (local) config.
func (cs *Configs) SetLocal(key, value string) error {
if cs.workdir == "" {
return ErrWorkdirNotSet
}
if cs.local == nil {
cs.local = &Config{
path: filepath.Join(cs.workdir, cs.LocalConfig),
}
}
if cs.local.path == "" {
cs.local.path = filepath.Join(cs.workdir, cs.LocalConfig)
}
return cs.local.Set(key, value)
}
// SetGlobal sets (or adds) a key only in the per-user (global) config.
func (cs *Configs) SetGlobal(key, value string) error {
if cs.global == nil {
cs.global = &Config{
path: globalConfigFile(cs.Name),
}
}
return cs.global.Set(key, value)
}
// SetEnv sets (or adds) a key in the per-process (env) config. Useful
// for persisting flags during the invocation.
func (cs *Configs) SetEnv(key, value string) error {
if cs.env == nil {
cs.env = &Config{
noWrites: true,
}
}
return cs.env.Set(key, value)
}
// UnsetLocal deletes a key from the local config.
func (cs *Configs) UnsetLocal(key string) error {
if cs.local == nil {
return nil
}
return cs.local.Unset(key)
}
// UnsetGlobal deletes a key from the global config.
func (cs *Configs) UnsetGlobal(key string) error {
if cs.global == nil {
return nil
}
return cs.global.Unset(key)
}
// Keys returns a list of all keys from all available scopes. Every key has section and possibly
// a subsection. They are separated by dots. The subsection itself may contain dots. The final
// key name and the section MUST NOT contain dots.
//
// Examples
// - remote.gist.gopass.pw.path -> section: remote, subsection: gist.gopass.pw, key: path
// - core.timeout -> section: core, key: timeout
func (cs *Configs) Keys() []string {
keys := make([]string, 0, 128)
for _, cfg := range []*Config{
cs.Preset,
cs.system,
cs.global,
cs.local,
cs.worktree,
cs.env,
} {
if cfg == nil {
continue
}
for k := range cfg.vars {
keys = append(keys, k)
}
}
return set.Sorted(keys)
}
// List returns all keys matching the given prefix. The prefix can be empty,
// then this is identical to Keys().
func (cs *Configs) List(prefix string) []string {
return set.SortedFiltered(cs.Keys(), func(k string) bool {
return strings.HasPrefix(k, prefix)
})
}
// ListSections returns a sorted list of all sections.
func (cs *Configs) ListSections() []string {
return set.Sorted(set.Apply(cs.Keys(), func(k string) string {
section, _, _ := splitKey(k)
return section
}))
}
// ListSubsections returns a sorted list of all subsections
// in the given section.
func (cs *Configs) ListSubsections(wantSection string) []string {
// apply extracts the subsection and matches it to the empty string
// if it doesn't belong to the section we're looking for. Then the
// filter func filters out any empty string.
return set.SortedFiltered(set.Apply(cs.Keys(), func(k string) string {
section, subsection, _ := splitKey(k)
if section != wantSection {
return ""
}
return subsection
}), func(s string) bool {
return s != ""
})
}
// KVList returns a list of all keys and values matching the given prefix.
func (cs *Configs) KVList(prefix, sep string) []string {
if sep == "" {
sep = "="
}
keys := cs.List(prefix)
kv := make([]string, 0, len(keys))
for _, k := range keys {
vs := cs.GetAll(k)
for _, v := range vs {
if v == "" {
continue
}
kv = append(kv, fmt.Sprintf("%s%s%s", k, sep, v))
}
}
sort.Strings(kv)
return kv
}