Skip to content

Commit 13ce8f9

Browse files
committed
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
2 parents 87bfbd5 + 399b198 commit 13ce8f9

File tree

4 files changed

+290
-41
lines changed

4 files changed

+290
-41
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: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,22 @@ static long config_buf_ftell(struct config_source *conf)
120120
return conf->u.buf.pos;
121121
}
122122

123+
struct config_include_data {
124+
int depth;
125+
config_fn_t fn;
126+
void *data;
127+
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;
134+
};
135+
#define CONFIG_INCLUDE_INIT { 0 }
136+
137+
static int git_config_include(const char *var, const char *value, void *data);
138+
123139
#define MAX_INCLUDE_DEPTH 10
124140
static const char include_depth_advice[] = N_(
125141
"exceeded maximum include depth (%d) while including\n"
@@ -294,22 +310,108 @@ static int include_by_branch(const char *cond, size_t cond_len)
294310
return ret;
295311
}
296312

297-
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,
298396
const char *cond, size_t cond_len)
299397
{
398+
const struct config_options *opts = inc->opts;
300399

301400
if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
302401
return include_by_gitdir(opts, cond, cond_len, 0);
303402
else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
304403
return include_by_gitdir(opts, cond, cond_len, 1);
305404
else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
306405
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);
307409

308410
/* unknown conditionals are always false */
309411
return 0;
310412
}
311413

312-
int git_config_include(const char *var, const char *value, void *data)
414+
static int git_config_include(const char *var, const char *value, void *data)
313415
{
314416
struct config_include_data *inc = data;
315417
const char *cond, *key;
@@ -328,9 +430,15 @@ int git_config_include(const char *var, const char *value, void *data)
328430
ret = handle_path_include(value, inc);
329431

330432
if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
331-
(cond && include_condition_is_true(inc->opts, cond, cond_len)) &&
332-
!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;
333439
ret = handle_path_include(value, inc);
440+
inc->fn = old_fn;
441+
}
334442

335443
return ret;
336444
}
@@ -1929,11 +2037,13 @@ int config_with_options(config_fn_t fn, void *data,
19292037
const struct config_options *opts)
19302038
{
19312039
struct config_include_data inc = CONFIG_INCLUDE_INIT;
2040+
int ret;
19322041

19332042
if (opts->respect_includes) {
19342043
inc.fn = fn;
19352044
inc.data = data;
19362045
inc.opts = opts;
2046+
inc.config_source = config_source;
19372047
fn = git_config_include;
19382048
data = &inc;
19392049
}
@@ -1946,17 +2056,23 @@ int config_with_options(config_fn_t fn, void *data,
19462056
* regular lookup sequence.
19472057
*/
19482058
if (config_source && config_source->use_stdin) {
1949-
return git_config_from_stdin(fn, data);
2059+
ret = git_config_from_stdin(fn, data);
19502060
} else if (config_source && config_source->file) {
1951-
return git_config_from_file(fn, config_source->file, data);
2061+
ret = git_config_from_file(fn, config_source->file, data);
19522062
} else if (config_source && config_source->blob) {
19532063
struct repository *repo = config_source->repo ?
19542064
config_source->repo : the_repository;
1955-
return git_config_from_blob_ref(fn, repo, config_source->blob,
2065+
ret = git_config_from_blob_ref(fn, repo, config_source->blob,
19562066
data);
2067+
} else {
2068+
ret = do_git_config_sequence(opts, fn, data);
19572069
}
19582070

1959-
return do_git_config_sequence(opts, fn, data);
2071+
if (inc.remote_urls) {
2072+
string_list_clear(inc.remote_urls, 0);
2073+
FREE_AND_NULL(inc.remote_urls);
2074+
}
2075+
return ret;
19602076
}
19612077

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

config.h

Lines changed: 13 additions & 33 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;
@@ -126,6 +135,8 @@ int git_default_config(const char *, const char *, void *);
126135
/**
127136
* Read a specific file in git-config format.
128137
* This function takes the same callback and data parameters as `git_config`.
138+
*
139+
* Unlike git_config(), this function does not respect includes.
129140
*/
130141
int git_config_from_file(config_fn_t fn, const char *, void *);
131142

@@ -158,6 +169,8 @@ void read_very_early_config(config_fn_t cb, void *data);
158169
* will first feed the user-wide one to the callback, and then the
159170
* repo-specific one; by overwriting, the higher-priority repo-specific
160171
* value is left at the end).
172+
*
173+
* Unlike git_config_from_file(), this function respects includes.
161174
*/
162175
void git_config(config_fn_t fn, void *);
163176

@@ -338,39 +351,6 @@ const char *current_config_origin_type(void);
338351
const char *current_config_name(void);
339352
int current_config_line(void);
340353

341-
/**
342-
* Include Directives
343-
* ------------------
344-
*
345-
* By default, the config parser does not respect include directives.
346-
* However, a caller can use the special `git_config_include` wrapper
347-
* callback to support them. To do so, you simply wrap your "real" callback
348-
* function and data pointer in a `struct config_include_data`, and pass
349-
* the wrapper to the regular config-reading functions. For example:
350-
*
351-
* -------------------------------------------
352-
* int read_file_with_include(const char *file, config_fn_t fn, void *data)
353-
* {
354-
* struct config_include_data inc = CONFIG_INCLUDE_INIT;
355-
* inc.fn = fn;
356-
* inc.data = data;
357-
* return git_config_from_file(git_config_include, file, &inc);
358-
* }
359-
* -------------------------------------------
360-
*
361-
* `git_config` respects includes automatically. The lower-level
362-
* `git_config_from_file` does not.
363-
*
364-
*/
365-
struct config_include_data {
366-
int depth;
367-
config_fn_t fn;
368-
void *data;
369-
const struct config_options *opts;
370-
};
371-
#define CONFIG_INCLUDE_INIT { 0 }
372-
int git_config_include(const char *name, const char *value, void *data);
373-
374354
/*
375355
* Match and parse a config key of the form:
376356
*

0 commit comments

Comments
 (0)