Skip to content

Commit 4b7989b

Browse files
committed
Merge branch 'nd/conditional-config-include'
The configuration file learned a new "includeIf.<condition>.path" that includes the contents of the given path only when the condition holds. This allows you to say "include this work-related bit only in the repositories under my ~/work/ directory". * nd/conditional-config-include: config: add conditional include config.txt: reflow the second include.path paragraph config.txt: clarify multiple key values in include.path
2 parents 45cbc37 + 3efd0be commit 4b7989b

3 files changed

Lines changed: 218 additions & 8 deletions

File tree

Documentation/config.txt

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,69 @@ escape sequences) are invalid.
7979
Includes
8080
~~~~~~~~
8181

82-
You can include one config file from another by setting the special
82+
You can include a config file from another by setting the special
8383
`include.path` variable to the name of the file to be included. The
8484
variable takes a pathname as its value, and is subject to tilde
85-
expansion.
85+
expansion. `include.path` can be given multiple times.
8686

87-
The
88-
included file is expanded immediately, as if its contents had been
87+
The included file is expanded immediately, as if its contents had been
8988
found at the location of the include directive. If the value of the
90-
`include.path` variable is a relative path, the path is considered to be
91-
relative to the configuration file in which the include directive was
92-
found. See below for examples.
89+
`include.path` variable is a relative path, the path is considered to
90+
be relative to the configuration file in which the include directive
91+
was found. See below for examples.
9392

93+
Conditional includes
94+
~~~~~~~~~~~~~~~~~~~~
95+
96+
You can include a config file from another conditionally by setting a
97+
`includeIf.<condition>.path` variable to the name of the file to be
98+
included. The variable's value is treated the same way as
99+
`include.path`. `includeIf.<condition>.path` can be given multiple times.
100+
101+
The condition starts with a keyword followed by a colon and some data
102+
whose format and meaning depends on the keyword. Supported keywords
103+
are:
104+
105+
`gitdir`::
106+
107+
The data that follows the keyword `gitdir:` is used as a glob
108+
pattern. If the location of the .git directory matches the
109+
pattern, the include condition is met.
110+
+
111+
The .git location may be auto-discovered, or come from `$GIT_DIR`
112+
environment variable. If the repository is auto discovered via a .git
113+
file (e.g. from submodules, or a linked worktree), the .git location
114+
would be the final location where the .git directory is, not where the
115+
.git file is.
116+
+
117+
The pattern can contain standard globbing wildcards and two additional
118+
ones, `**/` and `/**`, that can match multiple path components. Please
119+
refer to linkgit:gitignore[5] for details. For convenience:
120+
121+
* If the pattern starts with `~/`, `~` will be substituted with the
122+
content of the environment variable `HOME`.
123+
124+
* If the pattern starts with `./`, it is replaced with the directory
125+
containing the current config file.
126+
127+
* If the pattern does not start with either `~/`, `./` or `/`, `**/`
128+
will be automatically prepended. For example, the pattern `foo/bar`
129+
becomes `**/foo/bar` and would match `/any/path/to/foo/bar`.
130+
131+
* If the pattern ends with `/`, `**` will be automatically added. For
132+
example, the pattern `foo/` becomes `foo/**`. In other words, it
133+
matches "foo" and everything inside, recursively.
134+
135+
`gitdir/i`::
136+
This is the same as `gitdir` except that matching is done
137+
case-insensitively (e.g. on case-insensitive file sytems)
138+
139+
A few more notes on matching via `gitdir` and `gitdir/i`:
140+
141+
* Symlinks in `$GIT_DIR` are not resolved before matching.
142+
143+
* Note that "../" is not special and will match literally, which is
144+
unlikely what you want.
94145

95146
Example
96147
~~~~~~~
@@ -119,6 +170,17 @@ Example
119170
path = foo ; expand "foo" relative to the current file
120171
path = ~/foo ; expand "foo" in your `$HOME` directory
121172

173+
; include if $GIT_DIR is /path/to/foo/.git
174+
[includeIf "gitdir:/path/to/foo/.git"]
175+
path = /path/to/foo.inc
176+
177+
; include for all repositories inside /path/to/group
178+
[includeIf "gitdir:/path/to/group/"]
179+
path = /path/to/foo.inc
180+
181+
; include for all repositories inside $HOME/to/group
182+
[includeIf "gitdir:~/to/group/"]
183+
path = /path/to/foo.inc
122184

123185
Values
124186
~~~~~~

config.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "hashmap.h"
1414
#include "string-list.h"
1515
#include "utf8.h"
16+
#include "dir.h"
1617

1718
struct config_source {
1819
struct config_source *prev;
@@ -170,9 +171,94 @@ static int handle_path_include(const char *path, struct config_include_data *inc
170171
return ret;
171172
}
172173

174+
static int prepare_include_condition_pattern(struct strbuf *pat)
175+
{
176+
struct strbuf path = STRBUF_INIT;
177+
char *expanded;
178+
int prefix = 0;
179+
180+
expanded = expand_user_path(pat->buf);
181+
if (expanded) {
182+
strbuf_reset(pat);
183+
strbuf_addstr(pat, expanded);
184+
free(expanded);
185+
}
186+
187+
if (pat->buf[0] == '.' && is_dir_sep(pat->buf[1])) {
188+
const char *slash;
189+
190+
if (!cf || !cf->path)
191+
return error(_("relative config include "
192+
"conditionals must come from files"));
193+
194+
strbuf_add_absolute_path(&path, cf->path);
195+
slash = find_last_dir_sep(path.buf);
196+
if (!slash)
197+
die("BUG: how is this possible?");
198+
strbuf_splice(pat, 0, 1, path.buf, slash - path.buf);
199+
prefix = slash - path.buf + 1 /* slash */;
200+
} else if (!is_absolute_path(pat->buf))
201+
strbuf_insert(pat, 0, "**/", 3);
202+
203+
if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
204+
strbuf_addstr(pat, "**");
205+
206+
strbuf_release(&path);
207+
return prefix;
208+
}
209+
210+
static int include_by_gitdir(const char *cond, size_t cond_len, int icase)
211+
{
212+
struct strbuf text = STRBUF_INIT;
213+
struct strbuf pattern = STRBUF_INIT;
214+
int ret = 0, prefix;
215+
216+
strbuf_add_absolute_path(&text, get_git_dir());
217+
strbuf_add(&pattern, cond, cond_len);
218+
prefix = prepare_include_condition_pattern(&pattern);
219+
220+
if (prefix < 0)
221+
goto done;
222+
223+
if (prefix > 0) {
224+
/*
225+
* perform literal matching on the prefix part so that
226+
* any wildcard character in it can't create side effects.
227+
*/
228+
if (text.len < prefix)
229+
goto done;
230+
if (!icase && strncmp(pattern.buf, text.buf, prefix))
231+
goto done;
232+
if (icase && strncasecmp(pattern.buf, text.buf, prefix))
233+
goto done;
234+
}
235+
236+
ret = !wildmatch(pattern.buf + prefix, text.buf + prefix,
237+
icase ? WM_CASEFOLD : 0, NULL);
238+
239+
done:
240+
strbuf_release(&pattern);
241+
strbuf_release(&text);
242+
return ret;
243+
}
244+
245+
static int include_condition_is_true(const char *cond, size_t cond_len)
246+
{
247+
248+
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
249+
return include_by_gitdir(cond, cond_len, 0);
250+
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
251+
return include_by_gitdir(cond, cond_len, 1);
252+
253+
/* unknown conditionals are always false */
254+
return 0;
255+
}
256+
173257
int git_config_include(const char *var, const char *value, void *data)
174258
{
175259
struct config_include_data *inc = data;
260+
const char *cond, *key;
261+
int cond_len;
176262
int ret;
177263

178264
/*
@@ -185,6 +271,12 @@ int git_config_include(const char *var, const char *value, void *data)
185271

186272
if (!strcmp(var, "include.path"))
187273
ret = handle_path_include(value, inc);
274+
275+
if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
276+
(cond && include_condition_is_true(cond, cond_len)) &&
277+
!strcmp(key, "path"))
278+
ret = handle_path_include(value, inc);
279+
188280
return ret;
189281
}
190282

t/t1305-config-include.sh

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ test_expect_success 'config modification does not affect includes' '
102102

103103
test_expect_success 'missing include files are ignored' '
104104
cat >.gitconfig <<-\EOF &&
105-
[include]path = foo
105+
[include]path = non-existent
106106
[test]value = yes
107107
EOF
108108
echo yes >expect &&
@@ -152,6 +152,62 @@ test_expect_success 'relative includes from stdin line fail' '
152152
test_must_fail git config --file - test.one
153153
'
154154

155+
test_expect_success 'conditional include, both unanchored' '
156+
git init foo &&
157+
(
158+
cd foo &&
159+
echo "[includeIf \"gitdir:foo/\"]path=bar" >>.git/config &&
160+
echo "[test]one=1" >.git/bar &&
161+
echo 1 >expect &&
162+
git config test.one >actual &&
163+
test_cmp expect actual
164+
)
165+
'
166+
167+
test_expect_success 'conditional include, $HOME expansion' '
168+
(
169+
cd foo &&
170+
echo "[includeIf \"gitdir:~/foo/\"]path=bar2" >>.git/config &&
171+
echo "[test]two=2" >.git/bar2 &&
172+
echo 2 >expect &&
173+
git config test.two >actual &&
174+
test_cmp expect actual
175+
)
176+
'
177+
178+
test_expect_success 'conditional include, full pattern' '
179+
(
180+
cd foo &&
181+
echo "[includeIf \"gitdir:**/foo/**\"]path=bar3" >>.git/config &&
182+
echo "[test]three=3" >.git/bar3 &&
183+
echo 3 >expect &&
184+
git config test.three >actual &&
185+
test_cmp expect actual
186+
)
187+
'
188+
189+
test_expect_success 'conditional include, relative path' '
190+
echo "[includeIf \"gitdir:./foo/.git\"]path=bar4" >>.gitconfig &&
191+
echo "[test]four=4" >bar4 &&
192+
(
193+
cd foo &&
194+
echo 4 >expect &&
195+
git config test.four >actual &&
196+
test_cmp expect actual
197+
)
198+
'
199+
200+
test_expect_success 'conditional include, both unanchored, icase' '
201+
(
202+
cd foo &&
203+
echo "[includeIf \"gitdir/i:FOO/\"]path=bar5" >>.git/config &&
204+
echo "[test]five=5" >.git/bar5 &&
205+
echo 5 >expect &&
206+
git config test.five >actual &&
207+
test_cmp expect actual
208+
)
209+
'
210+
155211
test_expect_success 'include cycles are detected' '
156212
cat >.gitconfig <<-\EOF &&
157213
[test]value = gitconfig

0 commit comments

Comments
 (0)