Skip to content

Commit 3efd0be

Browse files
pcloudsgitster
authored andcommitted
config: add conditional include
Sometimes a set of repositories want to share configuration settings among themselves that are distinct from other such sets of repositories. A user may work on two projects, each of which have multiple repositories, and use one user.email for one project while using another for the other. Setting $GIT_DIR/.config works, but if the penalty of forgetting to update $GIT_DIR/.config is high (especially when you end up cloning often), it may not be the best way to go. Having the settings in ~/.gitconfig, which would work for just one set of repositories, would not well in such a situation. Having separate ${HOME}s may add more problems than it solves. Extend the include.path mechanism that lets a config file include another config file, so that the inclusion can be done only when some conditions hold. Then ~/.gitconfig can say "include config-project-A only when working on project-A" for each project A the user works on. In this patch, the only supported grouping is based on $GIT_DIR (in absolute path), so you would need to group repositories by directory, or something like that to take advantage of it. We already have include.path for unconditional includes. This patch goes with includeIf.<condition>.path to make it clearer that a condition is required. The new config has the same backward compatibility approach as include.path: older git versions that don't understand includeIf will simply ignore them. Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 1050e98 commit 3efd0be

File tree

3 files changed

+212
-1
lines changed

3 files changed

+212
-1
lines changed

Documentation/config.txt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,58 @@ found at the location of the include directive. If the value of the
9090
be relative to the configuration file in which the include directive
9191
was found. See below for examples.
9292

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.
93145

94146
Example
95147
~~~~~~~
@@ -118,6 +170,17 @@ Example
118170
path = foo ; expand "foo" relative to the current file
119171
path = ~/foo ; expand "foo" in your `$HOME` directory
120172

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
121184

122185
Values
123186
~~~~~~

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)