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() staticmaint
						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
	
	 Junio C Hamano
						Junio C Hamano