Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/GIT-BEHAVIOUR.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ the same results.
| `gitdir:$S/work/` | yes (trailing `/` means `/**`) |
| `gitdir:$S/work` | no |
| `gitdir:$S/work/**` | yes |
| `gitdir:$S/***`, `gitdir:$S/***/.git`, `gitdir:$S/work/****/proj/` | yes: a segment of three or more stars is `**`, matching any number of directories, zero included; mixed with other characters (`$S/***proj/`, `$S/w***/.git`) the stars are a single `*` *(2.47.3, 2.56.0)* |
| `gitdir:$S/work/proj/.git` | yes |
| `gitdir:work/` | yes (relative pattern gets an implicit `**/` prefix) |
| `gitdir/i:<upper-cased path>/WORK/` | yes |
Expand Down
7 changes: 5 additions & 2 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ type Env struct {
// 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.
// 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,
Expand Down Expand Up @@ -379,7 +380,9 @@ func (e Env) writeTarget(s Scope) (string, Scope, error) {
return "", 0, ErrNoScope
}
for _, v := range slices.Backward(e.GlobalConfig) {
if _, err := os.Lstat(v); err == nil {
// 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
}
}
Expand Down
32 changes: 32 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,12 @@ func TestEnvIncludeIf(t *testing.T) {
{"gitdir:" + s + "/work", false},
{"gitdir:" + s + "/work/**", true},
{"gitdir:" + s + "/w*/", true},
{"gitdir:" + s + "/***", true},
{"gitdir:" + s + "/***/.git", true},
{"gitdir:" + s + "/work/****/proj/", true},
{"gitdir:" + s + "/***/***/***/.git", true},
{"gitdir:" + s + "/***proj/", false},
{"gitdir:" + s + "/w***/.git", false},
{"gitdir:" + s + "/work/proj/.git", true},
{"gitdir:" + s + "/work/proj/.git/", false},
{"gitdir:" + s + "/work/proj", false},
Expand Down Expand Up @@ -1111,6 +1117,32 @@ func TestEnvWritePath(t *testing.T) {
}
}

func TestEnvWritePathDanglingLink(t *testing.T) {
// A ~/.gitconfig that links to nothing does not exist: with an XDG
// file, git writes there; without, through the link (git 2.56).
dir := tempDir(t)
xdg := filepath.Join(dir, "xdg")
home := filepath.Join(dir, "home")
symlink(t, filepath.Join(dir, "dotfiles", "gitconfig"), home)
env := gitconfig.Env{GlobalConfig: []string{xdg, home}}

got, err := env.WritePath(gitconfig.ScopeGlobal)
require.NoError(t, err)
assert.Equal(t, home, got)

writeFile(t, xdg, "")
got, err = env.WritePath(gitconfig.ScopeGlobal)
require.NoError(t, err)
assert.Equal(t, xdg, got)

require.NoError(t, env.UpdateScope(gitconfig.ScopeGlobal, func(f *gitconfig.File) error {
return f.Set("a.k", "v")
}))
_, err = os.Stat(filepath.Join(dir, "dotfiles"))
assert.ErrorIs(t, err, fs.ErrNotExist)
assert.Equal(t, "[a]\n\tk = v\n", readFile(t, xdg))
}

// unsetenv unsets an environment variable for the duration of the test.
func unsetenv(t *testing.T, key string) {
t.Setenv(key, "")
Expand Down
27 changes: 21 additions & 6 deletions glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "strings"
// The patterns of includeIf "gitdir:" and "onbranch:" are matched as paths:
// '*' and '?' do not match '/', "**/" at the start of a segment matches any
// number of directories, "**" ending the pattern after a '/' (or alone)
// matches anything, "[...]" is a class of characters ("[!...]" or "[^...]"
// matches anything, a segment of more stars ("***") is "**", "[...]" is a class of characters ("[!...]" or "[^...]"
// negated, "a-z" a range, "[:digit:]" a named class) and '\' quotes the
// next character. A pattern with an unterminated class, or an unknown class
// name, matches nothing, as in git.
Expand Down Expand Up @@ -42,18 +42,20 @@ func compileGlob(p string) ([]globToken, bool) {
for i := 0; i < len(p); {
c := p[i]
switch {
case c == '*' && segStart && i+1 < len(p) && p[i+1] == '*' && (i+2 == len(p) || p[i+2] == '/'):
if i+2 == len(p) {
case c == '*' && segStart && starSegment(p[i:]) > 1:
// As in git, a segment of three or more stars is "**" too.
i += starSegment(p[i:])
if i == len(p) {
toks = append(toks, globToken{kind: globAll})
i += 2
continue
}
if len(toks) == 0 || toks[len(toks)-1].kind != globDirs {
// "**/**/" is "**/".
toks = append(toks, globToken{kind: globDirs})
}
i += 3
continue // still at the start of a segment
// Past the '/': still at the start of a segment.
i++
continue
case c == '*':
for i < len(p) && p[i] == '*' {
i++
Expand Down Expand Up @@ -85,6 +87,19 @@ func compileGlob(p string) ([]globToken, bool) {
return toks, true
}

// starSegment returns the number of stars p starts with, if they make up a
// whole segment (the end of p or a '/' follows), 0 otherwise.
func starSegment(p string) int {
n := 0
for n < len(p) && p[n] == '*' {
n++
}
if n < len(p) && p[n] != '/' {
return 0
}
return n
}

// compileClass compiles the class starting at p[start], and returns the
// index past its end, or false if it is not terminated or names an unknown
// class. lastColon is the index of the last ":]" in p.
Expand Down
7 changes: 7 additions & 0 deletions interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,13 @@ var gitScenarios = []gitScenario{
"gitdir:" + s + "/work",
"gitdir:" + s + "/work/**",
"gitdir:" + s + "/w*/",
"gitdir:" + s + "/***",
"gitdir:" + s + "/***/.git",
"gitdir:" + s + "/work/****/proj/",
"gitdir:" + s + "/***/***/***/.git",
"gitdir:" + s + "/***proj/",
"gitdir:" + s + "/w***/.git",
"onbranch:***",
"gitdir:" + s + "/work/proj/.git",
"gitdir:" + s + "/work/proj/.git/",
"gitdir:work/",
Expand Down
Loading