Main description is already in config.txt. Here is a dev-only note about Windows support. While prepare_include_condition_pattern() is Windows-friendly (because it does not hard code '/'). The reality could be uglier because internally get_git_dir() may return a path with '/' only or worse, a mix of '/' and '\\'. At some point, we need to teach wildmatch() that '/' and '\' should be treated the same way (via a flag) as well. Then we could care less about '/' vs '\\'. But a Windows dev probably has to do it. Helped-by: Jeff King <peff@xxxxxxxx> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@xxxxxxxxx> --- Documentation/config.txt | 40 +++++++++++++++++++++ config.c | 89 +++++++++++++++++++++++++++++++++++++++++++++-- t/t1305-config-include.sh | 45 ++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) diff --git a/Documentation/config.txt b/Documentation/config.txt index 58673cf..c8ad0bf 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -91,6 +91,46 @@ found at the location of the include directive. If the value of the relative to the configuration file in which the include directive was found. See below for examples. +Included files can be grouped into subsections where subsectino name +is the condition when the files are included. The condition starts +with an condition type string, followed by a colon and a pattern. + +Only "gitdir" type is supported, where files are included if +`$GIT_DIR` matches the specified pattern. For example, + + + [include "gitdir:/path/to/foo.git"] + path = /path/to/foo.inc + +would only include "/path/to/foo.inc" if `$GIT_DIR` is +/path/to/foo.git. + +The following pattern is a wildcard pattern with two additional +wildcards `**/` and `/**`. See linkgit:gitignore[5] for more +information. For convenience: + + * 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. + + * If the pattern starts with `~/`, `~` will be substitued with the + environment variable `HOME`. + + * If the pattern starts with `./`, it is replaced with the directory + where the current config file is. For example if the config file + that contains the "include" subsection is `$HOME/.gitconfig` then + the pattern `./foo` would match the path `$HOME/foo` + +A few more notes: + + * Symlinks in `$GIT_DIR` are not resolved before matching. + + * Note that "../" is not special and will match literally, which is + unlikely what you want. + + * On case-insensitive file systems, you may need to specify + core.ignoreCase before the `include` subsections in order to match + case-insensitively if core.ignoreCase is declared in the same file. Example ~~~~~~~ diff --git a/config.c b/config.c index f51c56b..97c450e 100644 --- a/config.c +++ b/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; @@ -140,9 +141,89 @@ 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; + int prefix = 0; + + /* TODO: maybe support ~user/ too */ + if (pat->buf[0] == '~' && is_dir_sep(pat->buf[1])) { + const char *home = getenv("HOME"); + + if (!home) + return error(_("$HOME is not defined")); + + strbuf_add_absolute_path(&path, home); + strbuf_splice(pat, 0, 1, path.buf, path.len); + prefix = path.len + 1 /*slash*/; + } else 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")); + + /* TODO: escape wildcards */ + 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_condition_is_true(const char *cond, int cond_len) +{ + const char *value; + size_t value_len; + + /* no condition (i.e., "include.path") is always true */ + if (!cond) + return 1; + + if (skip_prefix_mem(cond, cond_len, "gitdir:", &value, &value_len)) { + struct strbuf text = STRBUF_INIT; + struct strbuf pattern = STRBUF_INIT; + int ret, prefix; + + strbuf_add_absolute_path(&text, get_git_dir()); + strbuf_add(&pattern, value, value_len); + prefix = prepare_include_condition_pattern(&pattern); + + if (prefix < 0) + return 0; + + if (prefix > 0 && + (text.len < prefix || + fspathncmp(pattern.buf, text.buf, prefix))) + return 0; + + ret = !wildmatch(pattern.buf + prefix, text.buf + prefix, + ignore_case ? WM_CASEFOLD : 0, + NULL); + strbuf_release(&pattern); + strbuf_release(&text); + return ret; + } + + error(_("unrecognized include condition: %.*s"), cond_len, cond); + /* 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; /* @@ -153,8 +234,12 @@ int git_config_include(const char *var, const char *value, void *data) if (ret < 0) return ret; - if (!strcmp(var, "include.path")) - ret = handle_path_include(value, inc); + if (!parse_config_key(var, "include", &cond, &cond_len, &key) && + include_condition_is_true(cond, cond_len)) { + if (!strcmp(key, "path")) + ret = handle_path_include(value, inc); + /* else we do not know about this type of include; ignore */ + } return ret; } diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh index 9ba2ba1..30351f2 100755 --- a/t/t1305-config-include.sh +++ b/t/t1305-config-include.sh @@ -152,6 +152,51 @@ 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 "[include \"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 "[include \"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 "[include \"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 "[include \"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 'include cycles are detected' ' cat >.gitconfig <<-\EOF && [test]value = gitconfig -- 2.8.2.531.gd073806 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html