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:
Junio C Hamano 2017-03-21 15:07:18 -07:00
commit 4b7989b103
3 changed files with 218 additions and 8 deletions

View file

@ -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
~~~~~~

View file

@ -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;
}

View file

@ -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