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
|
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
|
`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
|
variable takes a pathname as its value, and is subject to tilde
|
||||||
expansion.
|
expansion. `include.path` can be given multiple times.
|
||||||
|
|
||||||
The
|
The included file is expanded immediately, as if its contents had been
|
||||||
included file is expanded immediately, as if its contents had been
|
|
||||||
found at the location of the include directive. If the value of the
|
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
|
`include.path` variable is a relative path, the path is considered to
|
||||||
relative to the configuration file in which the include directive was
|
be relative to the configuration file in which the include directive
|
||||||
found. See below for examples.
|
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
|
Example
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -119,6 +170,17 @@ Example
|
|||||||
path = foo ; expand "foo" relative to the current file
|
path = foo ; expand "foo" relative to the current file
|
||||||
path = ~/foo ; expand "foo" in your `$HOME` directory
|
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
|
Values
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
92
config.c
92
config.c
@ -13,6 +13,7 @@
|
|||||||
#include "hashmap.h"
|
#include "hashmap.h"
|
||||||
#include "string-list.h"
|
#include "string-list.h"
|
||||||
#include "utf8.h"
|
#include "utf8.h"
|
||||||
|
#include "dir.h"
|
||||||
|
|
||||||
struct config_source {
|
struct config_source {
|
||||||
struct config_source *prev;
|
struct config_source *prev;
|
||||||
@ -170,9 +171,94 @@ static int handle_path_include(const char *path, struct config_include_data *inc
|
|||||||
return ret;
|
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)
|
int git_config_include(const char *var, const char *value, void *data)
|
||||||
{
|
{
|
||||||
struct config_include_data *inc = data;
|
struct config_include_data *inc = data;
|
||||||
|
const char *cond, *key;
|
||||||
|
int cond_len;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -185,6 +271,12 @@ int git_config_include(const char *var, const char *value, void *data)
|
|||||||
|
|
||||||
if (!strcmp(var, "include.path"))
|
if (!strcmp(var, "include.path"))
|
||||||
ret = handle_path_include(value, inc);
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ test_expect_success 'config modification does not affect includes' '
|
|||||||
|
|
||||||
test_expect_success 'missing include files are ignored' '
|
test_expect_success 'missing include files are ignored' '
|
||||||
cat >.gitconfig <<-\EOF &&
|
cat >.gitconfig <<-\EOF &&
|
||||||
[include]path = foo
|
[include]path = non-existent
|
||||||
[test]value = yes
|
[test]value = yes
|
||||||
EOF
|
EOF
|
||||||
echo yes >expect &&
|
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_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' '
|
test_expect_success 'include cycles are detected' '
|
||||||
cat >.gitconfig <<-\EOF &&
|
cat >.gitconfig <<-\EOF &&
|
||||||
[test]value = gitconfig
|
[test]value = gitconfig
|
||||||
|
Loading…
Reference in New Issue
Block a user