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
This commit is contained in:
commit
4b7989b103
@ -79,18 +79,69 @@ escape sequences) are invalid.
|
||||
Includes
|
||||
~~~~~~~~
|
||||
|
||||
You can include one config file from another by setting the special
|
||||
You can include a config file from another by setting the special
|
||||
`include.path` variable to the name of the file to be included. The
|
||||
variable takes a pathname as its value, and is subject to tilde
|
||||
expansion.
|
||||
expansion. `include.path` can be given multiple times.
|
||||
|
||||
The
|
||||
included file is expanded immediately, as if its contents had been
|
||||
The included file is expanded immediately, as if its contents had been
|
||||
found at the location of the include directive. If the value of the
|
||||
`include.path` variable is a relative path, the path is considered to be
|
||||
relative to the configuration file in which the include directive was
|
||||
found. See below for examples.
|
||||
`include.path` variable is a relative path, the path is considered to
|
||||
be relative to the configuration file in which the include directive
|
||||
was found. See below for examples.
|
||||
|
||||
Conditional includes
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can include a config file from another conditionally by setting a
|
||||
`includeIf.<condition>.path` variable to the name of the file to be
|
||||
included. The variable's value is treated the same way as
|
||||
`include.path`. `includeIf.<condition>.path` can be given multiple times.
|
||||
|
||||
The condition starts with a keyword followed by a colon and some data
|
||||
whose format and meaning depends on the keyword. Supported keywords
|
||||
are:
|
||||
|
||||
`gitdir`::
|
||||
|
||||
The data that follows the keyword `gitdir:` is used as a glob
|
||||
pattern. If the location of the .git directory matches the
|
||||
pattern, the include condition is met.
|
||||
+
|
||||
The .git location may be auto-discovered, or come from `$GIT_DIR`
|
||||
environment variable. If the repository is auto discovered via a .git
|
||||
file (e.g. from submodules, or a linked worktree), the .git location
|
||||
would be the final location where the .git directory is, not where the
|
||||
.git file is.
|
||||
+
|
||||
The pattern can contain standard globbing wildcards and two additional
|
||||
ones, `**/` and `/**`, that can match multiple path components. Please
|
||||
refer to linkgit:gitignore[5] for details. For convenience:
|
||||
|
||||
* If the pattern starts with `~/`, `~` will be substituted with the
|
||||
content of the environment variable `HOME`.
|
||||
|
||||
* If the pattern starts with `./`, it is replaced with the directory
|
||||
containing the current config file.
|
||||
|
||||
* If the pattern does not start with either `~/`, `./` or `/`, `**/`
|
||||
will be automatically prepended. For example, the pattern `foo/bar`
|
||||
becomes `**/foo/bar` and would match `/any/path/to/foo/bar`.
|
||||
|
||||
* If the pattern ends with `/`, `**` will be automatically added. For
|
||||
example, the pattern `foo/` becomes `foo/**`. In other words, it
|
||||
matches "foo" and everything inside, recursively.
|
||||
|
||||
`gitdir/i`::
|
||||
This is the same as `gitdir` except that matching is done
|
||||
case-insensitively (e.g. on case-insensitive file sytems)
|
||||
|
||||
A few more notes on matching via `gitdir` and `gitdir/i`:
|
||||
|
||||
* Symlinks in `$GIT_DIR` are not resolved before matching.
|
||||
|
||||
* Note that "../" is not special and will match literally, which is
|
||||
unlikely what you want.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
@ -119,6 +170,17 @@ Example
|
||||
path = foo ; expand "foo" relative to the current file
|
||||
path = ~/foo ; expand "foo" in your `$HOME` directory
|
||||
|
||||
; include if $GIT_DIR is /path/to/foo/.git
|
||||
[includeIf "gitdir:/path/to/foo/.git"]
|
||||
path = /path/to/foo.inc
|
||||
|
||||
; include for all repositories inside /path/to/group
|
||||
[includeIf "gitdir:/path/to/group/"]
|
||||
path = /path/to/foo.inc
|
||||
|
||||
; include for all repositories inside $HOME/to/group
|
||||
[includeIf "gitdir:~/to/group/"]
|
||||
path = /path/to/foo.inc
|
||||
|
||||
Values
|
||||
~~~~~~
|
||||
|
92
config.c
92
config.c
@ -13,6 +13,7 @@
|
||||
#include "hashmap.h"
|
||||
#include "string-list.h"
|
||||
#include "utf8.h"
|
||||
#include "dir.h"
|
||||
|
||||
struct config_source {
|
||||
struct config_source *prev;
|
||||
@ -170,9 +171,94 @@ static int handle_path_include(const char *path, struct config_include_data *inc
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int prepare_include_condition_pattern(struct strbuf *pat)
|
||||
{
|
||||
struct strbuf path = STRBUF_INIT;
|
||||
char *expanded;
|
||||
int prefix = 0;
|
||||
|
||||
expanded = expand_user_path(pat->buf);
|
||||
if (expanded) {
|
||||
strbuf_reset(pat);
|
||||
strbuf_addstr(pat, expanded);
|
||||
free(expanded);
|
||||
}
|
||||
|
||||
if (pat->buf[0] == '.' && is_dir_sep(pat->buf[1])) {
|
||||
const char *slash;
|
||||
|
||||
if (!cf || !cf->path)
|
||||
return error(_("relative config include "
|
||||
"conditionals must come from files"));
|
||||
|
||||
strbuf_add_absolute_path(&path, cf->path);
|
||||
slash = find_last_dir_sep(path.buf);
|
||||
if (!slash)
|
||||
die("BUG: how is this possible?");
|
||||
strbuf_splice(pat, 0, 1, path.buf, slash - path.buf);
|
||||
prefix = slash - path.buf + 1 /* slash */;
|
||||
} else if (!is_absolute_path(pat->buf))
|
||||
strbuf_insert(pat, 0, "**/", 3);
|
||||
|
||||
if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
|
||||
strbuf_addstr(pat, "**");
|
||||
|
||||
strbuf_release(&path);
|
||||
return prefix;
|
||||
}
|
||||
|
||||
static int include_by_gitdir(const char *cond, size_t cond_len, int icase)
|
||||
{
|
||||
struct strbuf text = STRBUF_INIT;
|
||||
struct strbuf pattern = STRBUF_INIT;
|
||||
int ret = 0, prefix;
|
||||
|
||||
strbuf_add_absolute_path(&text, get_git_dir());
|
||||
strbuf_add(&pattern, cond, cond_len);
|
||||
prefix = prepare_include_condition_pattern(&pattern);
|
||||
|
||||
if (prefix < 0)
|
||||
goto done;
|
||||
|
||||
if (prefix > 0) {
|
||||
/*
|
||||
* perform literal matching on the prefix part so that
|
||||
* any wildcard character in it can't create side effects.
|
||||
*/
|
||||
if (text.len < prefix)
|
||||
goto done;
|
||||
if (!icase && strncmp(pattern.buf, text.buf, prefix))
|
||||
goto done;
|
||||
if (icase && strncasecmp(pattern.buf, text.buf, prefix))
|
||||
goto done;
|
||||
}
|
||||
|
||||
ret = !wildmatch(pattern.buf + prefix, text.buf + prefix,
|
||||
icase ? WM_CASEFOLD : 0, NULL);
|
||||
|
||||
done:
|
||||
strbuf_release(&pattern);
|
||||
strbuf_release(&text);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int include_condition_is_true(const char *cond, size_t cond_len)
|
||||
{
|
||||
|
||||
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
|
||||
return include_by_gitdir(cond, cond_len, 0);
|
||||
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
|
||||
return include_by_gitdir(cond, cond_len, 1);
|
||||
|
||||
/* unknown conditionals are always false */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int git_config_include(const char *var, const char *value, void *data)
|
||||
{
|
||||
struct config_include_data *inc = data;
|
||||
const char *cond, *key;
|
||||
int cond_len;
|
||||
int ret;
|
||||
|
||||
/*
|
||||
@ -185,6 +271,12 @@ int git_config_include(const char *var, const char *value, void *data)
|
||||
|
||||
if (!strcmp(var, "include.path"))
|
||||
ret = handle_path_include(value, inc);
|
||||
|
||||
if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
|
||||
(cond && include_condition_is_true(cond, cond_len)) &&
|
||||
!strcmp(key, "path"))
|
||||
ret = handle_path_include(value, inc);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ test_expect_success 'config modification does not affect includes' '
|
||||
|
||||
test_expect_success 'missing include files are ignored' '
|
||||
cat >.gitconfig <<-\EOF &&
|
||||
[include]path = foo
|
||||
[include]path = non-existent
|
||||
[test]value = yes
|
||||
EOF
|
||||
echo yes >expect &&
|
||||
@ -152,6 +152,62 @@ test_expect_success 'relative includes from stdin line fail' '
|
||||
test_must_fail git config --file - test.one
|
||||
'
|
||||
|
||||
test_expect_success 'conditional include, both unanchored' '
|
||||
git init foo &&
|
||||
(
|
||||
cd foo &&
|
||||
echo "[includeIf \"gitdir:foo/\"]path=bar" >>.git/config &&
|
||||
echo "[test]one=1" >.git/bar &&
|
||||
echo 1 >expect &&
|
||||
git config test.one >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'conditional include, $HOME expansion' '
|
||||
(
|
||||
cd foo &&
|
||||
echo "[includeIf \"gitdir:~/foo/\"]path=bar2" >>.git/config &&
|
||||
echo "[test]two=2" >.git/bar2 &&
|
||||
echo 2 >expect &&
|
||||
git config test.two >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'conditional include, full pattern' '
|
||||
(
|
||||
cd foo &&
|
||||
echo "[includeIf \"gitdir:**/foo/**\"]path=bar3" >>.git/config &&
|
||||
echo "[test]three=3" >.git/bar3 &&
|
||||
echo 3 >expect &&
|
||||
git config test.three >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'conditional include, relative path' '
|
||||
echo "[includeIf \"gitdir:./foo/.git\"]path=bar4" >>.gitconfig &&
|
||||
echo "[test]four=4" >bar4 &&
|
||||
(
|
||||
cd foo &&
|
||||
echo 4 >expect &&
|
||||
git config test.four >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'conditional include, both unanchored, icase' '
|
||||
(
|
||||
cd foo &&
|
||||
echo "[includeIf \"gitdir/i:FOO/\"]path=bar5" >>.git/config &&
|
||||
echo "[test]five=5" >.git/bar5 &&
|
||||
echo 5 >expect &&
|
||||
git config test.five >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'include cycles are detected' '
|
||||
cat >.gitconfig <<-\EOF &&
|
||||
[test]value = gitconfig
|
||||
|
Loading…
Reference in New Issue
Block a user