Repository navigation
Expand file tree
/
Copy pathenv.go
More file actions
466 lines (442 loc) · 16.8 KB
/
Copy pathenv.go
File metadata and controls
466 lines (442 loc) · 16.8 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
package gitconfig
import (
"os"
"path/filepath"
"slices"
"strconv"
"strings"
)
// Scope is one layer of git's configuration. Values from later scopes take
// precedence over earlier ones.
//
// ScopeFile, the zero value, is not a layer: it marks entries of a file read
// on its own (Parse, Env.LoadFile, Env.UpdateFile), which may come from
// anyone. git reports such entries as "command"; this package keeps them
// apart so that they are never mistaken for the protected command scope.
type Scope int
const (
ScopeFile Scope = iota // a file read on its own, as with git config -f
ScopeSystem // /etc/gitconfig or GIT_CONFIG_SYSTEM
ScopeGlobal // ~/.gitconfig, the XDG file, or GIT_CONFIG_GLOBAL
ScopeLocal // the repository's config
ScopeWorktree // config.worktree, if extensions.worktreeConfig is enabled
ScopeCommand // git -c, GIT_CONFIG_COUNT/KEY/VALUE; read-only
)
// String returns the scope as git names it ("system", "global", ...), and
// "file" for ScopeFile.
func (s Scope) String() string {
switch s {
case ScopeFile:
return "file"
case ScopeSystem:
return "system"
case ScopeGlobal:
return "global"
case ScopeLocal:
return "local"
case ScopeWorktree:
return "worktree"
case ScopeCommand:
return "command"
}
return "Scope(" + strconv.Itoa(int(s)) + ")"
}
// Env says where git's configuration comes from: the system file, the global
// files, the repository and the command line. Discover fills it as git
// does; tests and embedders can fill it by hand. Every field is used as given,
// and a zero source field means "none": an Env reads nothing it was not given.
// An Env is a value, but copies share the GlobalConfig and Command slices:
// replace them rather than modify them in place.
//
// Configuration can come from someone else: a repository on a shared disk,
// an unpacked archive, a file reached through include.path. Loading such a
// file is safe (bounded, see Load), but its values are not to be trusted.
// Discover refuses a repository git would refuse (ErrUntrusted); an Env
// filled by hand is not checked, as git does not check a repository named
// by GIT_DIR.
type Env struct {
// Home is the user's home directory. Load uses it to expand "~/" in
// include paths; pass it to Config.Path and Entry.Path for values.
// "" makes such paths an error.
Home string
// GitDir is the repository's git directory, "" outside a repository.
// It must be absolute: includeIf "gitdir:" matches absolute paths, and
// a relative GitDir makes Load fail with ErrNotAbsolute rather than
// silently not match. For a linked worktree, this is the worktree's
// own git directory (.git/worktrees/<name>): the shared config is found
// through its commondir file, and config.worktree is read from it.
// includeIf "onbranch:" reads <GitDir>/HEAD when such a condition is
// met.
GitDir string
// SystemConfig is the system configuration file, "" for none.
SystemConfig string
// GlobalConfig lists the global configuration files, read in order.
// Writes go to the last one that exists, or to the last one if none
// exists: for git's [XDG file, ~/.gitconfig] this picks ~/.gitconfig
// unless only the XDG file exists. A symbolic link exists if its target
// does.
GlobalConfig []string
// Command holds the values given on the command line (git -c,
// GIT_CONFIG_COUNT, GIT_CONFIG_PARAMETERS), in order.
Command []CommandEntry
}
// CommandEntry is one value given on the command line. Key is validated by
// Load: a key git rejects makes Load fail with ErrInvalidKey, as git does.
// An include.path given this way is followed; a relative one is
// ErrNotAbsolute, as git refuses relative includes that do not come from a
// file.
type CommandEntry struct {
Key string
Value string
Implicit bool // "git -c key" without "=": true as a boolean
}
// Discover finds where git's configuration comes from, the way git does,
// using the running process: environment variables (GIT_CONFIG_GLOBAL,
// XDG_CONFIG_HOME, …), the user's home directory, the working directory and
// the platform's system configuration file.
//
// dir is where the repository is found, as git finds it when started there
// (git -C dir): a working tree (a directory holding .git), its .git, or a
// git directory, such as a bare repository; it is made absolute. A .git
// file holding "gitdir: <path>", as in a linked worktree or a submodule, is
// followed. Finding the repository is left to the caller: Discover does not
// search parent directories and does not read GIT_DIR. Pass "" when not in
// a repository: only the system, global and command-line scopes exist then.
// A dir that is not a repository fails with ErrNotRepository, in a
// *FileError naming the git directory expected: its .git when it exists (or
// the directory a gitdir file names), dir itself otherwise. As for git, a
// git directory must hold a HEAD file, and its common directory objects and
// refs directories; any other would have its config read and written as
// the local scope. A caller that wants the repository only when there is
// one can fall back to Discover("") on ErrNotRepository.
//
// The repository must be trusted, as git requires: owned by the current
// user (on Unix, also the SUDO_UID user for root; on Windows, the
// Administrators group for an administrator), or listed in safe.directory.
// Otherwise Discover fails with ErrUntrusted, in a *FileError naming the
// path to list. As in git, that path is the working tree when dir is one,
// dir itself otherwise: pass the working tree when there is one.
//
// Without a home directory, Home is "" and the global files are those git
// reads then: the XDG file if XDG_CONFIG_HOME is set, no ~/.gitconfig.
//
// On Windows, SystemConfig is "" unless GIT_CONFIG_SYSTEM is set: the
// system file lives in git's installation directory, which Discover does
// not look for. Set the field to read it.
//
// It fails if a gitdir file is malformed (a *FileError), or if
// GIT_CONFIG_NOSYSTEM, GIT_CONFIG_COUNT or GIT_CONFIG_PARAMETERS is
// malformed (an *EnvError), as git does.
//
// It is the only function of this package that reads the process
// environment.
func Discover(dir string) (Env, error) {
return discover(dir, osEnviron{})
}
// Load returns the effective configuration: system, global, local, worktree
// and command-line scopes, in that order of precedence, with includes
// followed. This is what git itself uses. Each entry's Origin.Scope tells
// which scope it comes from. Missing files are skipped.
//
// Includes are followed as git does (include.path and includeIf with the
// gitdir:, gitdir/i: and onbranch: conditions; hasconfig: is never
// true), up to 10 levels deep (ErrIncludeDepth). Two safety bounds git lacks
// are added: an include must be a regular file (ErrNotRegular, so a FIFO or a
// device cannot hang or exhaust the process), and one load reads at most
// 1000 files, 4 MiB and 2¹⁶ entries and distinct section headers in total
// from files (ErrLoadLimit, so that a file including itself many times cannot
// multiply the work exponentially, and a file of tiny entries cannot exhaust
// memory).
// Real configurations never come near these bounds.
//
// As in git, includes are followed as a file is read: an error in an
// included file is reported before a syntax error further down the file
// that includes it.
//
// A global file that cannot be read for lack of permission is skipped, as
// git does; LoadProtected skips such a system file too.
func (e Env) Load() (*Config, error) {
l, err := e.newLoader()
if err != nil {
return nil, err
}
if err := l.system(false); err != nil {
return nil, err
}
if err := l.global(); err != nil {
return nil, err
}
if l.repo != nil {
start := len(l.entries)
if err := l.loadFile(l.repo.localConfig(), ScopeLocal, false); err != nil {
return nil, err
}
ok, err := worktreeConfigIn(l.entries[start:], l.repo.localConfig())
if err != nil {
return nil, err
}
if ok {
if err := l.loadFile(l.repo.worktreeConfig(), ScopeWorktree, false); err != nil {
return nil, err
}
}
}
if err := l.command(); err != nil {
return nil, err
}
return l.config(), nil
}
// system loads the system scope, if any. If skipUnreadable, a file that
// cannot be read for lack of permission is skipped.
func (l *loader) system(skipUnreadable bool) error {
if l.env.SystemConfig == "" {
return nil
}
return l.loadFile(l.env.SystemConfig, ScopeSystem, skipUnreadable)
}
// global loads the global scope, skipping the files that cannot be read for
// lack of permission, as git does.
func (l *loader) global() error {
for _, path := range l.env.GlobalConfig {
if err := l.loadFile(path, ScopeGlobal, true); err != nil {
return err
}
}
return nil
}
// LoadProtected returns the configuration of the protected scopes only, with
// includes followed: system, global and command line, in that order. This is
// what git uses for keys a repository must not be able to set, such as
// safe.directory or safe.bareRepository. The repository's configuration is
// not read.
func (e Env) LoadProtected() (*Config, error) {
l, err := e.newLoader()
if err != nil {
return nil, err
}
if err := l.system(true); err != nil {
return nil, err
}
if err := l.global(); err != nil {
return nil, err
}
if err := l.command(); err != nil {
return nil, err
}
return l.config(), nil
}
// safeDirectory reports whether dir is allowed by the safe.directory values
// of c, with git's matching rules ("*", a trailing "/*", an empty value
// resetting the list). dir may be relative to the working directory;
// symbolic links are resolved on both sides.
func safeDirectory(c *Config, dir, home string) bool {
dir = realPath(dir)
if !filepath.IsAbs(dir) {
// The entries that count are absolute. Joining the real working
// directory drops a leading ".." correctly: it has no links.
if wd, err := os.Getwd(); err == nil {
dir = filepath.Join(realPath(wd), dir)
}
}
safe := false
for _, entry := range c.GetAll("safe.directory") {
switch v := entry.Value; {
case v == "":
safe = false
case v == "*":
safe = true
case strings.HasSuffix(v, "/*"):
prefix, err := expandPath(strings.TrimSuffix(v, "*"), home)
if err != nil || !filepath.IsAbs(prefix) {
// As for exact entries, relative ones are skipped.
continue
}
if p, err := filepath.EvalSymlinks(prefix); err == nil {
if strings.HasPrefix(dir, strings.TrimSuffix(p, string(filepath.Separator))+string(filepath.Separator)) {
safe = true
}
}
default:
p, err := expandPath(v, home)
if err != nil || !filepath.IsAbs(p) {
// "." and unexpandable values are skipped.
continue
}
if p, err := filepath.EvalSymlinks(p); err == nil && p == dir {
safe = true
}
}
}
return safe
}
// realPath returns path with symbolic links resolved, or cleaned if it
// cannot be resolved.
func realPath(path string) string {
if p, err := filepath.EvalSymlinks(path); err == nil {
return p
}
return filepath.Clean(path)
}
// LoadScope returns the configuration of exactly one scope, with includes
// followed, like `git config --includes --<scope>` (git itself follows
// includes there only with --includes). Lower scopes are not included.
//
// ScopeGlobal is the whole global layer, every file of GlobalConfig, as in
// Load. `git config --global` differs: it reads only the file it writes to
// (WritePath), so keys of the XDG file are not listed while ~/.gitconfig
// exists.
//
// A scope with no file (SystemConfig "", a global or worktree file that
// does not exist) is empty, as for `git config --get`. ScopeWorktree reads
// the file WritePath gives: without extensions.worktreeConfig, that is the
// repository's config, and its entries have ScopeLocal, as git reports.
// It fails with ErrNoScope for ScopeFile, and for ScopeLocal and
// ScopeWorktree where WritePath does.
func (e Env) LoadScope(s Scope) (*Config, error) {
l, err := e.newLoader()
if err != nil {
return nil, err
}
switch s {
case ScopeSystem:
err = l.system(false)
case ScopeGlobal:
err = l.global()
case ScopeLocal, ScopeWorktree:
var path string
var scope Scope
path, scope, err = scopeFile(l.repo, s)
if err == nil {
err = l.loadFile(path, scope, false)
}
case ScopeCommand:
err = l.command()
default:
return nil, ErrNoScope
}
if err != nil {
return nil, err
}
return l.config(), nil
}
// LoadFile returns the configuration of the file at path, with includes
// followed, like `git config --includes -f <path>` (git itself follows
// includes there only with --includes). Its entries have ScopeFile. A
// missing file is empty.
func (e Env) LoadFile(path string) (*Config, error) {
l, err := e.newLoader()
if err != nil {
return nil, err
}
if err := l.loadFile(path, ScopeFile, false); err != nil {
return nil, err
}
return l.config(), nil
}
// WritePath returns the file that `git config --<scope>` writes to. For
// ScopeWorktree, that is config.worktree if extensions.worktreeConfig is
// set, otherwise the repository's config as for ScopeLocal, provided the
// repository has a single worktree. It fails with ErrNoScope if the scope
// cannot be written in this Env: ScopeFile and ScopeCommand always,
// ScopeSystem with no SystemConfig, ScopeGlobal with no GlobalConfig,
// ScopeLocal and ScopeWorktree outside a repository, ScopeWorktree in a
// repository with several worktrees and no extensions.worktreeConfig.
func (e Env) WritePath(s Scope) (string, error) {
path, _, err := e.writeTarget(s)
return path, err
}
// writeTarget returns the file WritePath returns, and the scope its entries
// have.
func (e Env) writeTarget(s Scope) (string, Scope, error) {
switch s {
case ScopeSystem:
if e.SystemConfig == "" {
return "", 0, ErrNoScope
}
return e.SystemConfig, s, nil
case ScopeGlobal:
if len(e.GlobalConfig) == 0 {
return "", 0, ErrNoScope
}
for _, v := range slices.Backward(e.GlobalConfig) {
// A link to nothing does not exist, as for git: the write
// goes to the other file, not to the link's target.
if _, err := os.Stat(v); err == nil {
return v, s, nil
}
}
return e.GlobalConfig[len(e.GlobalConfig)-1], s, nil
case ScopeLocal, ScopeWorktree:
r, err := e.repo()
if err != nil {
return "", 0, err
}
return scopeFile(r, s)
}
return "", 0, ErrNoScope
}
// scopeFile returns the file of the local or worktree scope of r, nil
// outside a repository, and the scope its entries have: without
// extensions.worktreeConfig, the worktree scope is the local config, if the
// repository has a single worktree.
func scopeFile(r *repo, s Scope) (string, Scope, error) {
if r == nil {
return "", 0, ErrNoScope
}
if s == ScopeLocal {
return r.localConfig(), ScopeLocal, nil
}
ok, err := r.worktreeConfigEnabled()
switch {
case err != nil:
return "", 0, err
case ok:
return r.worktreeConfig(), ScopeWorktree, nil
case r.severalWorktrees():
return "", 0, ErrNoScope
}
return r.localConfig(), ScopeLocal, nil
}
// UpdateScope edits the file of scope s, which WritePath gives, as
// UpdateFile does; it fails with ErrNoScope where WritePath does. The
// File's entries have the scope LoadScope gives them. Writes never go into
// an included file.
func (e Env) UpdateScope(s Scope, fn func(*File) error) error {
path, scope, err := e.writeTarget(s)
if err != nil {
return err
}
return updateFile(path, scope, fn)
}
// UpdateFile edits the file at path with git's lock protocol, like
// `git config -f <path>`:
//
// 1. if path is a symbolic link, follow it (up to 5 levels, relative
// targets resolved against the link's directory) and edit the target:
// the link itself is kept;
// 2. create <target>.lock exclusively, never through a symbolic link, or
// fail with ErrLocked immediately, as git does;
// 3. read and parse the target under the lock (a missing file is empty),
// into a File;
// 4. call fn, exactly once;
// 5. if the content changed, write it to the lock file, give it the
// target's permission bits exactly (chmod, so the umask does not apply;
// a new file gets 0666 minus the umask), and rename it over the target;
// otherwise leave the target alone.
//
// On Windows, opening or replacing the target is retried for up to a second
// while another process holds it, as a concurrent writer or a reader does
// for a moment.
//
// If fn returns an error or panics, the lock is released and nothing is
// written; the panic then continues. Readers see either the old or the new
// file, never a partial one. After fn returns, edits of the File fail with
// ErrClosed.
//
// Following the symbolic link in step 1 is what git does, and what dotfile
// managers rely on. It also means that whoever controls the directory of
// path controls which file is written: do not edit the configuration of a
// repository that Discover has not accepted.
func (e Env) UpdateFile(path string, fn func(*File) error) error {
return updateFile(path, ScopeFile, fn)
}