Merge branch 'jt/conditional-config-on-remote-url'
The conditional inclusion mechanism of configuration files using "[includeIf <condition>]" learns to base its decision on the URL of the remote repository the repository interacts with. * jt/conditional-config-on-remote-url: config: include file if remote URL matches a glob config: make git_config_include() static
This commit is contained in:
commit
13ce8f9f14
@ -159,6 +159,33 @@ all branches that begin with `foo/`. This is useful if your branches are
|
||||
organized hierarchically and you would like to apply a configuration to
|
||||
all the branches in that hierarchy.
|
||||
|
||||
`hasconfig:remote.*.url:`::
|
||||
The data that follows this keyword is taken to
|
||||
be a pattern with standard globbing wildcards and two
|
||||
additional ones, `**/` and `/**`, that can match multiple
|
||||
components. The first time this keyword is seen, the rest of
|
||||
the config files will be scanned for remote URLs (without
|
||||
applying any values). If there exists at least one remote URL
|
||||
that matches this pattern, the include condition is met.
|
||||
+
|
||||
Files included by this option (directly or indirectly) are not allowed
|
||||
to contain remote URLs.
|
||||
+
|
||||
Note that unlike other includeIf conditions, resolving this condition
|
||||
relies on information that is not yet known at the point of reading the
|
||||
condition. A typical use case is this option being present as a
|
||||
system-level or global-level config, and the remote URL being in a
|
||||
local-level config; hence the need to scan ahead when resolving this
|
||||
condition. In order to avoid the chicken-and-egg problem in which
|
||||
potentially-included files can affect whether such files are potentially
|
||||
included, Git breaks the cycle by prohibiting these files from affecting
|
||||
the resolution of these conditions (thus, prohibiting them from
|
||||
declaring remote URLs).
|
||||
+
|
||||
As for the naming of this keyword, it is for forwards compatibiliy with
|
||||
a naming scheme that supports more variable-based include conditions,
|
||||
but currently Git only supports the exact keyword described above.
|
||||
|
||||
A few more notes on matching via `gitdir` and `gitdir/i`:
|
||||
|
||||
* Symlinks in `$GIT_DIR` are not resolved before matching.
|
||||
@ -226,6 +253,14 @@ Example
|
||||
; currently checked out
|
||||
[includeIf "onbranch:foo-branch"]
|
||||
path = foo.inc
|
||||
|
||||
; include only if a remote with the given URL exists (note
|
||||
; that such a URL may be provided later in a file or in a
|
||||
; file read after this file is read, as seen in this example)
|
||||
[includeIf "hasconfig:remote.*.url:https://example.com/**"]
|
||||
path = foo.inc
|
||||
[remote "origin"]
|
||||
url = https://example.com/git
|
||||
----
|
||||
|
||||
Values
|
||||
|
132
config.c
132
config.c
@ -120,6 +120,22 @@ static long config_buf_ftell(struct config_source *conf)
|
||||
return conf->u.buf.pos;
|
||||
}
|
||||
|
||||
struct config_include_data {
|
||||
int depth;
|
||||
config_fn_t fn;
|
||||
void *data;
|
||||
const struct config_options *opts;
|
||||
struct git_config_source *config_source;
|
||||
|
||||
/*
|
||||
* All remote URLs discovered when reading all config files.
|
||||
*/
|
||||
struct string_list *remote_urls;
|
||||
};
|
||||
#define CONFIG_INCLUDE_INIT { 0 }
|
||||
|
||||
static int git_config_include(const char *var, const char *value, void *data);
|
||||
|
||||
#define MAX_INCLUDE_DEPTH 10
|
||||
static const char include_depth_advice[] = N_(
|
||||
"exceeded maximum include depth (%d) while including\n"
|
||||
@ -294,9 +310,92 @@ static int include_by_branch(const char *cond, size_t cond_len)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int include_condition_is_true(const struct config_options *opts,
|
||||
static int add_remote_url(const char *var, const char *value, void *data)
|
||||
{
|
||||
struct string_list *remote_urls = data;
|
||||
const char *remote_name;
|
||||
size_t remote_name_len;
|
||||
const char *key;
|
||||
|
||||
if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
|
||||
&key) &&
|
||||
remote_name &&
|
||||
!strcmp(key, "url"))
|
||||
string_list_append(remote_urls, value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void populate_remote_urls(struct config_include_data *inc)
|
||||
{
|
||||
struct config_options opts;
|
||||
|
||||
struct config_source *store_cf = cf;
|
||||
struct key_value_info *store_kvi = current_config_kvi;
|
||||
enum config_scope store_scope = current_parsing_scope;
|
||||
|
||||
opts = *inc->opts;
|
||||
opts.unconditional_remote_url = 1;
|
||||
|
||||
cf = NULL;
|
||||
current_config_kvi = NULL;
|
||||
current_parsing_scope = 0;
|
||||
|
||||
inc->remote_urls = xmalloc(sizeof(*inc->remote_urls));
|
||||
string_list_init_dup(inc->remote_urls);
|
||||
config_with_options(add_remote_url, inc->remote_urls, inc->config_source, &opts);
|
||||
|
||||
cf = store_cf;
|
||||
current_config_kvi = store_kvi;
|
||||
current_parsing_scope = store_scope;
|
||||
}
|
||||
|
||||
static int forbid_remote_url(const char *var, const char *value, void *data)
|
||||
{
|
||||
const char *remote_name;
|
||||
size_t remote_name_len;
|
||||
const char *key;
|
||||
|
||||
if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
|
||||
&key) &&
|
||||
remote_name &&
|
||||
!strcmp(key, "url"))
|
||||
die(_("remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int at_least_one_url_matches_glob(const char *glob, int glob_len,
|
||||
struct string_list *remote_urls)
|
||||
{
|
||||
struct strbuf pattern = STRBUF_INIT;
|
||||
struct string_list_item *url_item;
|
||||
int found = 0;
|
||||
|
||||
strbuf_add(&pattern, glob, glob_len);
|
||||
for_each_string_list_item(url_item, remote_urls) {
|
||||
if (!wildmatch(pattern.buf, url_item->string, WM_PATHNAME)) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
strbuf_release(&pattern);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int include_by_remote_url(struct config_include_data *inc,
|
||||
const char *cond, size_t cond_len)
|
||||
{
|
||||
if (inc->opts->unconditional_remote_url)
|
||||
return 1;
|
||||
if (!inc->remote_urls)
|
||||
populate_remote_urls(inc);
|
||||
return at_least_one_url_matches_glob(cond, cond_len,
|
||||
inc->remote_urls);
|
||||
}
|
||||
|
||||
static int include_condition_is_true(struct config_include_data *inc,
|
||||
const char *cond, size_t cond_len)
|
||||
{
|
||||
const struct config_options *opts = inc->opts;
|
||||
|
||||
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
|
||||
return include_by_gitdir(opts, cond, cond_len, 0);
|
||||
@ -304,12 +403,15 @@ static int include_condition_is_true(const struct config_options *opts,
|
||||
return include_by_gitdir(opts, cond, cond_len, 1);
|
||||
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
|
||||
return include_by_branch(cond, cond_len);
|
||||
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
|
||||
&cond_len))
|
||||
return include_by_remote_url(inc, cond, cond_len);
|
||||
|
||||
/* unknown conditionals are always false */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int git_config_include(const char *var, const char *value, void *data)
|
||||
static int git_config_include(const char *var, const char *value, void *data)
|
||||
{
|
||||
struct config_include_data *inc = data;
|
||||
const char *cond, *key;
|
||||
@ -328,9 +430,15 @@ int git_config_include(const char *var, const char *value, void *data)
|
||||
ret = handle_path_include(value, inc);
|
||||
|
||||
if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
|
||||
(cond && include_condition_is_true(inc->opts, cond, cond_len)) &&
|
||||
!strcmp(key, "path"))
|
||||
cond && include_condition_is_true(inc, cond, cond_len) &&
|
||||
!strcmp(key, "path")) {
|
||||
config_fn_t old_fn = inc->fn;
|
||||
|
||||
if (inc->opts->unconditional_remote_url)
|
||||
inc->fn = forbid_remote_url;
|
||||
ret = handle_path_include(value, inc);
|
||||
inc->fn = old_fn;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@ -1929,11 +2037,13 @@ int config_with_options(config_fn_t fn, void *data,
|
||||
const struct config_options *opts)
|
||||
{
|
||||
struct config_include_data inc = CONFIG_INCLUDE_INIT;
|
||||
int ret;
|
||||
|
||||
if (opts->respect_includes) {
|
||||
inc.fn = fn;
|
||||
inc.data = data;
|
||||
inc.opts = opts;
|
||||
inc.config_source = config_source;
|
||||
fn = git_config_include;
|
||||
data = &inc;
|
||||
}
|
||||
@ -1946,17 +2056,23 @@ int config_with_options(config_fn_t fn, void *data,
|
||||
* regular lookup sequence.
|
||||
*/
|
||||
if (config_source && config_source->use_stdin) {
|
||||
return git_config_from_stdin(fn, data);
|
||||
ret = git_config_from_stdin(fn, data);
|
||||
} else if (config_source && config_source->file) {
|
||||
return git_config_from_file(fn, config_source->file, data);
|
||||
ret = git_config_from_file(fn, config_source->file, data);
|
||||
} else if (config_source && config_source->blob) {
|
||||
struct repository *repo = config_source->repo ?
|
||||
config_source->repo : the_repository;
|
||||
return git_config_from_blob_ref(fn, repo, config_source->blob,
|
||||
ret = git_config_from_blob_ref(fn, repo, config_source->blob,
|
||||
data);
|
||||
} else {
|
||||
ret = do_git_config_sequence(opts, fn, data);
|
||||
}
|
||||
|
||||
return do_git_config_sequence(opts, fn, data);
|
||||
if (inc.remote_urls) {
|
||||
string_list_clear(inc.remote_urls, 0);
|
||||
FREE_AND_NULL(inc.remote_urls);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void configset_iter(struct config_set *cs, config_fn_t fn, void *data)
|
||||
|
46
config.h
46
config.h
@ -89,6 +89,15 @@ struct config_options {
|
||||
unsigned int ignore_worktree : 1;
|
||||
unsigned int ignore_cmdline : 1;
|
||||
unsigned int system_gently : 1;
|
||||
|
||||
/*
|
||||
* For internal use. Include all includeif.hasremoteurl paths without
|
||||
* checking if the repo has that remote URL, and when doing so, verify
|
||||
* that files included in this way do not configure any remote URLs
|
||||
* themselves.
|
||||
*/
|
||||
unsigned int unconditional_remote_url : 1;
|
||||
|
||||
const char *commondir;
|
||||
const char *git_dir;
|
||||
config_parser_event_fn_t event_fn;
|
||||
@ -126,6 +135,8 @@ int git_default_config(const char *, const char *, void *);
|
||||
/**
|
||||
* Read a specific file in git-config format.
|
||||
* This function takes the same callback and data parameters as `git_config`.
|
||||
*
|
||||
* Unlike git_config(), this function does not respect includes.
|
||||
*/
|
||||
int git_config_from_file(config_fn_t fn, const char *, void *);
|
||||
|
||||
@ -158,6 +169,8 @@ void read_very_early_config(config_fn_t cb, void *data);
|
||||
* will first feed the user-wide one to the callback, and then the
|
||||
* repo-specific one; by overwriting, the higher-priority repo-specific
|
||||
* value is left at the end).
|
||||
*
|
||||
* Unlike git_config_from_file(), this function respects includes.
|
||||
*/
|
||||
void git_config(config_fn_t fn, void *);
|
||||
|
||||
@ -338,39 +351,6 @@ const char *current_config_origin_type(void);
|
||||
const char *current_config_name(void);
|
||||
int current_config_line(void);
|
||||
|
||||
/**
|
||||
* Include Directives
|
||||
* ------------------
|
||||
*
|
||||
* By default, the config parser does not respect include directives.
|
||||
* However, a caller can use the special `git_config_include` wrapper
|
||||
* callback to support them. To do so, you simply wrap your "real" callback
|
||||
* function and data pointer in a `struct config_include_data`, and pass
|
||||
* the wrapper to the regular config-reading functions. For example:
|
||||
*
|
||||
* -------------------------------------------
|
||||
* int read_file_with_include(const char *file, config_fn_t fn, void *data)
|
||||
* {
|
||||
* struct config_include_data inc = CONFIG_INCLUDE_INIT;
|
||||
* inc.fn = fn;
|
||||
* inc.data = data;
|
||||
* return git_config_from_file(git_config_include, file, &inc);
|
||||
* }
|
||||
* -------------------------------------------
|
||||
*
|
||||
* `git_config` respects includes automatically. The lower-level
|
||||
* `git_config_from_file` does not.
|
||||
*
|
||||
*/
|
||||
struct config_include_data {
|
||||
int depth;
|
||||
config_fn_t fn;
|
||||
void *data;
|
||||
const struct config_options *opts;
|
||||
};
|
||||
#define CONFIG_INCLUDE_INIT { 0 }
|
||||
int git_config_include(const char *name, const char *value, void *data);
|
||||
|
||||
/*
|
||||
* Match and parse a config key of the form:
|
||||
*
|
||||
|
@ -2388,4 +2388,122 @@ test_expect_success '--get and --get-all with --fixed-value' '
|
||||
test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
|
||||
'
|
||||
|
||||
test_expect_success 'includeIf.hasconfig:remote.*.url' '
|
||||
git init hasremoteurlTest &&
|
||||
test_when_finished "rm -rf hasremoteurlTest" &&
|
||||
|
||||
cat >include-this <<-\EOF &&
|
||||
[user]
|
||||
this = this-is-included
|
||||
EOF
|
||||
cat >dont-include-that <<-\EOF &&
|
||||
[user]
|
||||
that = that-is-not-included
|
||||
EOF
|
||||
cat >>hasremoteurlTest/.git/config <<-EOF &&
|
||||
[includeIf "hasconfig:remote.*.url:foourl"]
|
||||
path = "$(pwd)/include-this"
|
||||
[includeIf "hasconfig:remote.*.url:barurl"]
|
||||
path = "$(pwd)/dont-include-that"
|
||||
[remote "foo"]
|
||||
url = foourl
|
||||
EOF
|
||||
|
||||
echo this-is-included >expect-this &&
|
||||
git -C hasremoteurlTest config --get user.this >actual-this &&
|
||||
test_cmp expect-this actual-this &&
|
||||
|
||||
test_must_fail git -C hasremoteurlTest config --get user.that
|
||||
'
|
||||
|
||||
test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins' '
|
||||
git init hasremoteurlTest &&
|
||||
test_when_finished "rm -rf hasremoteurlTest" &&
|
||||
|
||||
cat >include-two-three <<-\EOF &&
|
||||
[user]
|
||||
two = included-config
|
||||
three = included-config
|
||||
EOF
|
||||
cat >>hasremoteurlTest/.git/config <<-EOF &&
|
||||
[remote "foo"]
|
||||
url = foourl
|
||||
[user]
|
||||
one = main-config
|
||||
two = main-config
|
||||
[includeIf "hasconfig:remote.*.url:foourl"]
|
||||
path = "$(pwd)/include-two-three"
|
||||
[user]
|
||||
three = main-config
|
||||
EOF
|
||||
|
||||
echo main-config >expect-main-config &&
|
||||
echo included-config >expect-included-config &&
|
||||
|
||||
git -C hasremoteurlTest config --get user.one >actual &&
|
||||
test_cmp expect-main-config actual &&
|
||||
|
||||
git -C hasremoteurlTest config --get user.two >actual &&
|
||||
test_cmp expect-included-config actual &&
|
||||
|
||||
git -C hasremoteurlTest config --get user.three >actual &&
|
||||
test_cmp expect-main-config actual
|
||||
'
|
||||
|
||||
test_expect_success 'includeIf.hasconfig:remote.*.url globs' '
|
||||
git init hasremoteurlTest &&
|
||||
test_when_finished "rm -rf hasremoteurlTest" &&
|
||||
|
||||
printf "[user]\ndss = yes\n" >double-star-start &&
|
||||
printf "[user]\ndse = yes\n" >double-star-end &&
|
||||
printf "[user]\ndsm = yes\n" >double-star-middle &&
|
||||
printf "[user]\nssm = yes\n" >single-star-middle &&
|
||||
printf "[user]\nno = no\n" >no &&
|
||||
|
||||
cat >>hasremoteurlTest/.git/config <<-EOF &&
|
||||
[remote "foo"]
|
||||
url = https://foo/bar/baz
|
||||
[includeIf "hasconfig:remote.*.url:**/baz"]
|
||||
path = "$(pwd)/double-star-start"
|
||||
[includeIf "hasconfig:remote.*.url:**/nomatch"]
|
||||
path = "$(pwd)/no"
|
||||
[includeIf "hasconfig:remote.*.url:https:/**"]
|
||||
path = "$(pwd)/double-star-end"
|
||||
[includeIf "hasconfig:remote.*.url:nomatch:/**"]
|
||||
path = "$(pwd)/no"
|
||||
[includeIf "hasconfig:remote.*.url:https:/**/baz"]
|
||||
path = "$(pwd)/double-star-middle"
|
||||
[includeIf "hasconfig:remote.*.url:https:/**/nomatch"]
|
||||
path = "$(pwd)/no"
|
||||
[includeIf "hasconfig:remote.*.url:https://*/bar/baz"]
|
||||
path = "$(pwd)/single-star-middle"
|
||||
[includeIf "hasconfig:remote.*.url:https://*/baz"]
|
||||
path = "$(pwd)/no"
|
||||
EOF
|
||||
|
||||
git -C hasremoteurlTest config --get user.dss &&
|
||||
git -C hasremoteurlTest config --get user.dse &&
|
||||
git -C hasremoteurlTest config --get user.dsm &&
|
||||
git -C hasremoteurlTest config --get user.ssm &&
|
||||
test_must_fail git -C hasremoteurlTest config --get user.no
|
||||
'
|
||||
|
||||
test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such included files' '
|
||||
git init hasremoteurlTest &&
|
||||
test_when_finished "rm -rf hasremoteurlTest" &&
|
||||
|
||||
cat >include-with-url <<-\EOF &&
|
||||
[remote "bar"]
|
||||
url = barurl
|
||||
EOF
|
||||
cat >>hasremoteurlTest/.git/config <<-EOF &&
|
||||
[includeIf "hasconfig:remote.*.url:foourl"]
|
||||
path = "$(pwd)/include-with-url"
|
||||
EOF
|
||||
|
||||
# test with any Git command
|
||||
test_must_fail git -C hasremoteurlTest status 2>err &&
|
||||
grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
|
||||
'
|
||||
|
||||
test_done
|
||||
|
Loading…
Reference in New Issue
Block a user