Skip to content

Commit 399b198

Browse files
jonathantanmygitster
authored andcommitted
config: include file if remote URL matches a glob
This is a feature that supports config file inclusion conditional on whether the repo has a remote with a URL that matches a glob. Similar to my previous work on remote-suggested hooks [1], the main motivation is to allow remote repo administrators to provide recommended configs in a way that can be consumed more easily (e.g. through a package installable by a package manager - it could, for example, contain a file to be included conditionally and a post-install script that adds the include directive to the system-wide config file). In order to do this, Git reruns the config parsing mechanism upon noticing the first URL-conditional include in order to find all remote URLs, and these remote URLs are then used to determine if that first and all subsequent includes are executed. Remote URLs are not allowed to be configued in any URL-conditionally-included file. [1] https://lore.kernel.org/git/[email protected]/ Signed-off-by: Jonathan Tan <[email protected]> Acked-by: Elijah Newren <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent ed69e11 commit 399b198

File tree

4 files changed

+275
-7
lines changed

4 files changed

+275
-7
lines changed

Documentation/config.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,33 @@ all branches that begin with `foo/`. This is useful if your branches are
159159
organized hierarchically and you would like to apply a configuration to
160160
all the branches in that hierarchy.
161161

162+
`hasconfig:remote.*.url:`::
163+
The data that follows this keyword is taken to
164+
be a pattern with standard globbing wildcards and two
165+
additional ones, `**/` and `/**`, that can match multiple
166+
components. The first time this keyword is seen, the rest of
167+
the config files will be scanned for remote URLs (without
168+
applying any values). If there exists at least one remote URL
169+
that matches this pattern, the include condition is met.
170+
+
171+
Files included by this option (directly or indirectly) are not allowed
172+
to contain remote URLs.
173+
+
174+
Note that unlike other includeIf conditions, resolving this condition
175+
relies on information that is not yet known at the point of reading the
176+
condition. A typical use case is this option being present as a
177+
system-level or global-level config, and the remote URL being in a
178+
local-level config; hence the need to scan ahead when resolving this
179+
condition. In order to avoid the chicken-and-egg problem in which
180+
potentially-included files can affect whether such files are potentially
181+
included, Git breaks the cycle by prohibiting these files from affecting
182+
the resolution of these conditions (thus, prohibiting them from
183+
declaring remote URLs).
184+
+
185+
As for the naming of this keyword, it is for forwards compatibiliy with
186+
a naming scheme that supports more variable-based include conditions,
187+
but currently Git only supports the exact keyword described above.
188+
162189
A few more notes on matching via `gitdir` and `gitdir/i`:
163190

164191
* Symlinks in `$GIT_DIR` are not resolved before matching.
@@ -226,6 +253,14 @@ Example
226253
; currently checked out
227254
[includeIf "onbranch:foo-branch"]
228255
path = foo.inc
256+
257+
; include only if a remote with the given URL exists (note
258+
; that such a URL may be provided later in a file or in a
259+
; file read after this file is read, as seen in this example)
260+
[includeIf "hasconfig:remote.*.url:https://example.com/**"]
261+
path = foo.inc
262+
[remote "origin"]
263+
url = https://example.com/git
229264
----
230265

231266
Values

config.c

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ struct config_include_data {
125125
config_fn_t fn;
126126
void *data;
127127
const struct config_options *opts;
128+
struct git_config_source *config_source;
129+
130+
/*
131+
* All remote URLs discovered when reading all config files.
132+
*/
133+
struct string_list *remote_urls;
128134
};
129135
#define CONFIG_INCLUDE_INIT { 0 }
130136

@@ -304,16 +310,102 @@ static int include_by_branch(const char *cond, size_t cond_len)
304310
return ret;
305311
}
306312

307-
static int include_condition_is_true(const struct config_options *opts,
313+
static int add_remote_url(const char *var, const char *value, void *data)
314+
{
315+
struct string_list *remote_urls = data;
316+
const char *remote_name;
317+
size_t remote_name_len;
318+
const char *key;
319+
320+
if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
321+
&key) &&
322+
remote_name &&
323+
!strcmp(key, "url"))
324+
string_list_append(remote_urls, value);
325+
return 0;
326+
}
327+
328+
static void populate_remote_urls(struct config_include_data *inc)
329+
{
330+
struct config_options opts;
331+
332+
struct config_source *store_cf = cf;
333+
struct key_value_info *store_kvi = current_config_kvi;
334+
enum config_scope store_scope = current_parsing_scope;
335+
336+
opts = *inc->opts;
337+
opts.unconditional_remote_url = 1;
338+
339+
cf = NULL;
340+
current_config_kvi = NULL;
341+
current_parsing_scope = 0;
342+
343+
inc->remote_urls = xmalloc(sizeof(*inc->remote_urls));
344+
string_list_init_dup(inc->remote_urls);
345+
config_with_options(add_remote_url, inc->remote_urls, inc->config_source, &opts);
346+
347+
cf = store_cf;
348+
current_config_kvi = store_kvi;
349+
current_parsing_scope = store_scope;
350+
}
351+
352+
static int forbid_remote_url(const char *var, const char *value, void *data)
353+
{
354+
const char *remote_name;
355+
size_t remote_name_len;
356+
const char *key;
357+
358+
if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
359+
&key) &&
360+
remote_name &&
361+
!strcmp(key, "url"))
362+
die(_("remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"));
363+
return 0;
364+
}
365+
366+
static int at_least_one_url_matches_glob(const char *glob, int glob_len,
367+
struct string_list *remote_urls)
368+
{
369+
struct strbuf pattern = STRBUF_INIT;
370+
struct string_list_item *url_item;
371+
int found = 0;
372+
373+
strbuf_add(&pattern, glob, glob_len);
374+
for_each_string_list_item(url_item, remote_urls) {
375+
if (!wildmatch(pattern.buf, url_item->string, WM_PATHNAME)) {
376+
found = 1;
377+
break;
378+
}
379+
}
380+
strbuf_release(&pattern);
381+
return found;
382+
}
383+
384+
static int include_by_remote_url(struct config_include_data *inc,
385+
const char *cond, size_t cond_len)
386+
{
387+
if (inc->opts->unconditional_remote_url)
388+
return 1;
389+
if (!inc->remote_urls)
390+
populate_remote_urls(inc);
391+
return at_least_one_url_matches_glob(cond, cond_len,
392+
inc->remote_urls);
393+
}
394+
395+
static int include_condition_is_true(struct config_include_data *inc,
308396
const char *cond, size_t cond_len)
309397
{
398+
const struct config_options *opts = inc->opts;
310399

311400
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
312401
return include_by_gitdir(opts, cond, cond_len, 0);
313402
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
314403
return include_by_gitdir(opts, cond, cond_len, 1);
315404
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
316405
return include_by_branch(cond, cond_len);
406+
else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
407+
&cond_len))
408+
return include_by_remote_url(inc, cond, cond_len);
317409

318410
/* unknown conditionals are always false */
319411
return 0;
@@ -338,9 +430,15 @@ static int git_config_include(const char *var, const char *value, void *data)
338430
ret = handle_path_include(value, inc);
339431

340432
if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
341-
(cond && include_condition_is_true(inc->opts, cond, cond_len)) &&
342-
!strcmp(key, "path"))
433+
cond && include_condition_is_true(inc, cond, cond_len) &&
434+
!strcmp(key, "path")) {
435+
config_fn_t old_fn = inc->fn;
436+
437+
if (inc->opts->unconditional_remote_url)
438+
inc->fn = forbid_remote_url;
343439
ret = handle_path_include(value, inc);
440+
inc->fn = old_fn;
441+
}
344442

345443
return ret;
346444
}
@@ -1936,11 +2034,13 @@ int config_with_options(config_fn_t fn, void *data,
19362034
const struct config_options *opts)
19372035
{
19382036
struct config_include_data inc = CONFIG_INCLUDE_INIT;
2037+
int ret;
19392038

19402039
if (opts->respect_includes) {
19412040
inc.fn = fn;
19422041
inc.data = data;
19432042
inc.opts = opts;
2043+
inc.config_source = config_source;
19442044
fn = git_config_include;
19452045
data = &inc;
19462046
}
@@ -1953,17 +2053,23 @@ int config_with_options(config_fn_t fn, void *data,
19532053
* regular lookup sequence.
19542054
*/
19552055
if (config_source && config_source->use_stdin) {
1956-
return git_config_from_stdin(fn, data);
2056+
ret = git_config_from_stdin(fn, data);
19572057
} else if (config_source && config_source->file) {
1958-
return git_config_from_file(fn, config_source->file, data);
2058+
ret = git_config_from_file(fn, config_source->file, data);
19592059
} else if (config_source && config_source->blob) {
19602060
struct repository *repo = config_source->repo ?
19612061
config_source->repo : the_repository;
1962-
return git_config_from_blob_ref(fn, repo, config_source->blob,
2062+
ret = git_config_from_blob_ref(fn, repo, config_source->blob,
19632063
data);
2064+
} else {
2065+
ret = do_git_config_sequence(opts, fn, data);
19642066
}
19652067

1966-
return do_git_config_sequence(opts, fn, data);
2068+
if (inc.remote_urls) {
2069+
string_list_clear(inc.remote_urls, 0);
2070+
FREE_AND_NULL(inc.remote_urls);
2071+
}
2072+
return ret;
19672073
}
19682074

19692075
static void configset_iter(struct config_set *cs, config_fn_t fn, void *data)

config.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ struct config_options {
8989
unsigned int ignore_worktree : 1;
9090
unsigned int ignore_cmdline : 1;
9191
unsigned int system_gently : 1;
92+
93+
/*
94+
* For internal use. Include all includeif.hasremoteurl paths without
95+
* checking if the repo has that remote URL, and when doing so, verify
96+
* that files included in this way do not configure any remote URLs
97+
* themselves.
98+
*/
99+
unsigned int unconditional_remote_url : 1;
100+
92101
const char *commondir;
93102
const char *git_dir;
94103
config_parser_event_fn_t event_fn;

t/t1300-config.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2387,4 +2387,122 @@ test_expect_success '--get and --get-all with --fixed-value' '
23872387
test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
23882388
'
23892389

2390+
test_expect_success 'includeIf.hasconfig:remote.*.url' '
2391+
git init hasremoteurlTest &&
2392+
test_when_finished "rm -rf hasremoteurlTest" &&
2393+
2394+
cat >include-this <<-\EOF &&
2395+
[user]
2396+
this = this-is-included
2397+
EOF
2398+
cat >dont-include-that <<-\EOF &&
2399+
[user]
2400+
that = that-is-not-included
2401+
EOF
2402+
cat >>hasremoteurlTest/.git/config <<-EOF &&
2403+
[includeIf "hasconfig:remote.*.url:foourl"]
2404+
path = "$(pwd)/include-this"
2405+
[includeIf "hasconfig:remote.*.url:barurl"]
2406+
path = "$(pwd)/dont-include-that"
2407+
[remote "foo"]
2408+
url = foourl
2409+
EOF
2410+
2411+
echo this-is-included >expect-this &&
2412+
git -C hasremoteurlTest config --get user.this >actual-this &&
2413+
test_cmp expect-this actual-this &&
2414+
2415+
test_must_fail git -C hasremoteurlTest config --get user.that
2416+
'
2417+
2418+
test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins' '
2419+
git init hasremoteurlTest &&
2420+
test_when_finished "rm -rf hasremoteurlTest" &&
2421+
2422+
cat >include-two-three <<-\EOF &&
2423+
[user]
2424+
two = included-config
2425+
three = included-config
2426+
EOF
2427+
cat >>hasremoteurlTest/.git/config <<-EOF &&
2428+
[remote "foo"]
2429+
url = foourl
2430+
[user]
2431+
one = main-config
2432+
two = main-config
2433+
[includeIf "hasconfig:remote.*.url:foourl"]
2434+
path = "$(pwd)/include-two-three"
2435+
[user]
2436+
three = main-config
2437+
EOF
2438+
2439+
echo main-config >expect-main-config &&
2440+
echo included-config >expect-included-config &&
2441+
2442+
git -C hasremoteurlTest config --get user.one >actual &&
2443+
test_cmp expect-main-config actual &&
2444+
2445+
git -C hasremoteurlTest config --get user.two >actual &&
2446+
test_cmp expect-included-config actual &&
2447+
2448+
git -C hasremoteurlTest config --get user.three >actual &&
2449+
test_cmp expect-main-config actual
2450+
'
2451+
2452+
test_expect_success 'includeIf.hasconfig:remote.*.url globs' '
2453+
git init hasremoteurlTest &&
2454+
test_when_finished "rm -rf hasremoteurlTest" &&
2455+
2456+
printf "[user]\ndss = yes\n" >double-star-start &&
2457+
printf "[user]\ndse = yes\n" >double-star-end &&
2458+
printf "[user]\ndsm = yes\n" >double-star-middle &&
2459+
printf "[user]\nssm = yes\n" >single-star-middle &&
2460+
printf "[user]\nno = no\n" >no &&
2461+
2462+
cat >>hasremoteurlTest/.git/config <<-EOF &&
2463+
[remote "foo"]
2464+
url = https://foo/bar/baz
2465+
[includeIf "hasconfig:remote.*.url:**/baz"]
2466+
path = "$(pwd)/double-star-start"
2467+
[includeIf "hasconfig:remote.*.url:**/nomatch"]
2468+
path = "$(pwd)/no"
2469+
[includeIf "hasconfig:remote.*.url:https:/**"]
2470+
path = "$(pwd)/double-star-end"
2471+
[includeIf "hasconfig:remote.*.url:nomatch:/**"]
2472+
path = "$(pwd)/no"
2473+
[includeIf "hasconfig:remote.*.url:https:/**/baz"]
2474+
path = "$(pwd)/double-star-middle"
2475+
[includeIf "hasconfig:remote.*.url:https:/**/nomatch"]
2476+
path = "$(pwd)/no"
2477+
[includeIf "hasconfig:remote.*.url:https://*/bar/baz"]
2478+
path = "$(pwd)/single-star-middle"
2479+
[includeIf "hasconfig:remote.*.url:https://*/baz"]
2480+
path = "$(pwd)/no"
2481+
EOF
2482+
2483+
git -C hasremoteurlTest config --get user.dss &&
2484+
git -C hasremoteurlTest config --get user.dse &&
2485+
git -C hasremoteurlTest config --get user.dsm &&
2486+
git -C hasremoteurlTest config --get user.ssm &&
2487+
test_must_fail git -C hasremoteurlTest config --get user.no
2488+
'
2489+
2490+
test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such included files' '
2491+
git init hasremoteurlTest &&
2492+
test_when_finished "rm -rf hasremoteurlTest" &&
2493+
2494+
cat >include-with-url <<-\EOF &&
2495+
[remote "bar"]
2496+
url = barurl
2497+
EOF
2498+
cat >>hasremoteurlTest/.git/config <<-EOF &&
2499+
[includeIf "hasconfig:remote.*.url:foourl"]
2500+
path = "$(pwd)/include-with-url"
2501+
EOF
2502+
2503+
# test with any Git command
2504+
test_must_fail git -C hasremoteurlTest status 2>err &&
2505+
grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
2506+
'
2507+
23902508
test_done

0 commit comments

Comments
 (0)