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:
Junio C Hamano 2022-02-09 14:20:58 -08:00
commit 13ce8f9f14
4 changed files with 290 additions and 41 deletions

View File

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

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

View File

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

View File

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