git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v4 2/2] config: include file if remote URL matches a glob

From
JTJonathan Tan <jonathantanmy@google.com>
Date
Nov 29, 2021, 20:23 UTC
Message-ID
<3b3af0da983c65ac8282b14141495f6859cd575a.1638217387.git.jonathantanmy@google.com>
In-Reply-To
<cover.1638217387.git.jonathantanmy@google.com>

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/cover.1623881977.git.jonathantanmy@google.com/
Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
---
 Documentation/config.txt |  12 ++++
 config.c                 | 121 ++++++++++++++++++++++++++++++++++++---
 config.h                 |   7 +++
 t/t1300-config.sh        | 118 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 250 insertions(+), 8 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0c0e6b859f..bfc9e22d78 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -159,6 +159,18 @@ 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.
 
+`hasremoteurl`::
+	The data that follows the keyword `hasremoteurl:` 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.
+
 A few more notes on matching via `gitdir` and `gitdir/i`:
 
  * Symlinks in `$GIT_DIR` are not resolved before matching.
diff --git a/config.c b/config.c
index 94ad5ce913..4ffc1e87e9 100644
--- a/config.c
+++ b/config.c
@@ -125,6 +125,12 @@ struct config_include_data {
 	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 }
 
@@ -316,12 +322,83 @@ static int include_condition_is_true(const struct config_options *opts,
 	return 0;
 }
 
+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.hasremoteurl"));
+	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 git_config_include(const char *var, const char *value, void *data)
 {
 	struct config_include_data *inc = data;
 	const char *cond, *key;
 	size_t cond_len;
-	int ret;
+	int ret = 0;
 
 	/*
 	 * Pass along all values, including "include" directives; this makes it
@@ -335,9 +412,29 @@ static 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"))
-		ret = handle_path_include(value, inc);
+	    cond && !strcmp(key, "path")) {
+		const char *url;
+		size_t url_len;
+
+		if (skip_prefix_mem(cond, cond_len, "hasremoteurl:", &url,
+				    &url_len)) {
+			if (inc->opts->unconditional_remote_url) {
+				config_fn_t old_fn = inc->fn;
+
+				inc->fn = forbid_remote_url;
+				ret = handle_path_include(value, inc);
+				inc->fn = old_fn;
+			} else {
+				if (!inc->remote_urls)
+					populate_remote_urls(inc);
+				if (at_least_one_url_matches_glob(
+						url, url_len, inc->remote_urls))
+					ret = handle_path_include(value, inc);
+			}
+		} else if (include_condition_is_true(inc->opts, cond, cond_len)) {
+			ret = handle_path_include(value, inc);
+		}
+	}
 
 	return ret;
 }
@@ -1933,11 +2030,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;
 	}
@@ -1950,17 +2049,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)
diff --git a/config.h b/config.h
index 48a5e472ca..c24458b10a 100644
--- a/config.h
+++ b/config.h
@@ -89,6 +89,13 @@ 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.
+	 */
+	unsigned int unconditional_remote_url : 1;
+
 	const char *commondir;
 	const char *git_dir;
 	config_parser_event_fn_t event_fn;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9ff46f3b04..9bd299d3f8 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2387,4 +2387,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.hasremoteurl' '
+	git init hasremoteurlTest &&
+	test_when_finished "rm -rf hasremoteurlTest" &&
+
+	cat >"$(pwd)"/include-this <<-\EOF &&
+	[user]
+		this = this-is-included
+	EOF
+	cat >"$(pwd)"/dont-include-that <<-\EOF &&
+	[user]
+		that = that-is-not-included
+	EOF
+	cat >>hasremoteurlTest/.git/config <<-EOF &&
+	[includeIf "hasremoteurl:foo"]
+		path = "$(pwd)/include-this"
+	[includeIf "hasremoteurl:bar"]
+		path = "$(pwd)/dont-include-that"
+	[remote "foo"]
+		url = foo
+	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.hasremoteurl respects last-config-wins' '
+	git init hasremoteurlTest &&
+	test_when_finished "rm -rf hasremoteurlTest" &&
+
+	cat >"$(pwd)"/include-two-three <<-\EOF &&
+	[user]
+		two = included-config
+		three = included-config
+	EOF
+	cat >>hasremoteurlTest/.git/config <<-EOF &&
+	[remote "foo"]
+		url = foo
+	[user]
+		one = main-config
+		two = main-config
+	[includeIf "hasremoteurl:foo"]
+		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.hasremoteurl globs' '
+	git init hasremoteurlTest &&
+	test_when_finished "rm -rf hasremoteurlTest" &&
+
+	printf "[user]\ndss = yes\n" >"$(pwd)/double-star-start" &&
+	printf "[user]\ndse = yes\n" >"$(pwd)/double-star-end" &&
+	printf "[user]\ndsm = yes\n" >"$(pwd)/double-star-middle" &&
+	printf "[user]\nssm = yes\n" >"$(pwd)/single-star-middle" &&
+	printf "[user]\nno = no\n" >"$(pwd)/no" &&
+
+	cat >>hasremoteurlTest/.git/config <<-EOF &&
+	[remote "foo"]
+		url = https://foo/bar/baz
+	[includeIf "hasremoteurl:**/baz"]
+		path = "$(pwd)/double-star-start"
+	[includeIf "hasremoteurl:**/nomatch"]
+		path = "$(pwd)/no"
+	[includeIf "hasremoteurl:https:/**"]
+		path = "$(pwd)/double-star-end"
+	[includeIf "hasremoteurl:nomatch:/**"]
+		path = "$(pwd)/no"
+	[includeIf "hasremoteurl:https:/**/baz"]
+		path = "$(pwd)/double-star-middle"
+	[includeIf "hasremoteurl:https:/**/nomatch"]
+		path = "$(pwd)/no"
+	[includeIf "hasremoteurl:https://*/bar/baz"]
+		path = "$(pwd)/single-star-middle"
+	[includeIf "hasremoteurl: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.hasremoteurl forbids remote url in such included files' '
+	git init hasremoteurlTest &&
+	test_when_finished "rm -rf hasremoteurlTest" &&
+
+	cat >"$(pwd)"/include-with-url <<-\EOF &&
+	[remote "bar"]
+		url = bar
+	EOF
+	cat >>hasremoteurlTest/.git/config <<-EOF &&
+	[includeIf "hasremoteurl:foo"]
+		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.hasremoteurl" err
+'
+
 test_done
-- 
2.34.0.rc2.393.gf8c9666880-goog
Previous: Junio C HamanoNext: Junio C Hamano
Message 49 of 87 in “Conditional config includes based on remote URL”
  1. 0/2 Conditional config includes based on remote URLJonathan Tan, Oct 12, 2021
  2. 1/2 config: make git_config_include() staticJonathan Tan, Oct 12, 2021
  3. Jeff KingOct 12, 2021
  4. Junio C HamanoOct 12, 2021
  5. Ævar Arnfjörð BjarmasonOct 13, 2021
  6. Junio C HamanoOct 13, 2021
  7. Jonathan TanOct 13, 2021
  8. 2/2 config: include file if remote URL matches a globJonathan Tan, Oct 12, 2021
  9. Jeff KingOct 12, 2021
  10. Jonathan TanOct 13, 2021
  11. Jeff KingOct 27, 2021
  12. Jonathan TanOct 27, 2021
  13. Junio C HamanoOct 12, 2021
  14. Jonathan TanOct 13, 2021
  15. brian m. carlsonOct 13, 2021
  16. Jonathan TanOct 13, 2021
  17. Jonathan TanOct 18, 2021
  18. Emily ShafferOct 22, 2021
  19. Jeff KingOct 27, 2021
  20. Jonathan TanOct 27, 2021
  21. Jeff KingOct 27, 2021
  22. Ævar Arnfjörð BjarmasonOct 25, 2021
  23. Jonathan TanOct 25, 2021
  24. Ævar Arnfjörð BjarmasonOct 26, 2021
  25. 0/2 Conditional config includes based on remote URLJonathan Tan, Oct 29, 2021
  26. 1/2 config: make git_config_include() staticJonathan Tan, Oct 29, 2021
  27. Emily ShafferNov 5, 2021
  28. 2/2 config: include file if remote URL matches a globJonathan Tan, Oct 29, 2021
  29. Emily ShafferNov 5, 2021
  30. Ævar Arnfjörð BjarmasonNov 6, 2021
  31. Jonathan TanNov 9, 2021
  32. Jonathan TanNov 9, 2021
  33. 0/2 Conditional config includes based on remote URLJonathan Tan, Nov 16, 2021
  34. 1/2 config: make git_config_include() staticJonathan Tan, Nov 16, 2021
  35. 2/2 config: include file if remote URL matches a globJonathan Tan, Nov 16, 2021
  36. Glen ChooNov 22, 2021
  37. Jonathan TanNov 29, 2021
  38. Junio C HamanoNov 23, 2021
  39. Jonathan TanNov 29, 2021
  40. Junio C HamanoDec 1, 2021
  41. Jonathan TanDec 2, 2021
  42. Ævar Arnfjörð BjarmasonNov 23, 2021
  43. Jonathan TanNov 29, 2021
  44. Ævar Arnfjörð BjarmasonNov 29, 2021
  45. 0/2 Conditional config includes based on remote URLJonathan Tan, Nov 29, 2021
  46. 1/2 config: make git_config_include() staticJonathan Tan, Nov 29, 2021
  47. Ævar Arnfjörð BjarmasonNov 29, 2021
  48. Junio C HamanoNov 30, 2021
  49. 2/2 config: include file if remote URL matches a globJonathan Tan, Nov 29, 2021
  50. Junio C HamanoDec 2, 2021
  51. Jonathan TanDec 2, 2021
  52. 0/2 Conditional config includes based on remote URLJonathan Tan, Dec 2, 2021
  53. 1/2 config: make git_config_include() staticJonathan Tan, Dec 2, 2021
  54. 2/2 config: include file if remote URL matches a globJonathan Tan, Dec 2, 2021
  55. Glen ChooDec 6, 2021
  56. Jonathan TanDec 7, 2021
  57. Ævar Arnfjörð BjarmasonDec 6, 2021
  58. Jonathan TanDec 7, 2021
  59. Ævar Arnfjörð BjarmasonDec 7, 2021
  60. Jonathan TanDec 7, 2021
  61. 0/2 Conditional config includes based on remote URLJonathan Tan, Dec 7, 2021
  62. 1/2 config: make git_config_include() staticJonathan Tan, Dec 7, 2021
  63. 2/2 config: include file if remote URL matches a globJonathan Tan, Dec 7, 2021
  64. Glen ChooDec 8, 2021
  65. Jonathan TanDec 9, 2021
  66. Glen ChooDec 8, 2021
  67. Jonathan TanDec 9, 2021
  68. Glen ChooDec 9, 2021
  69. Jonathan TanDec 13, 2021
  70. Junio C HamanoDec 10, 2021
  71. Jonathan TanDec 13, 2021
  72. 0/2 Conditional config includes based on remote URLJonathan Tan, Dec 14, 2021
  73. 1/2 config: make git_config_include() staticJonathan Tan, Dec 14, 2021
  74. 2/2 config: include file if remote URL matches a globJonathan Tan, Dec 14, 2021
  75. Glen ChooDec 16, 2021
  76. Elijah NewrenDec 28, 2021
  77. Jonathan TanJan 10, 2022
  78. Glen ChooDec 16, 2021
  79. Elijah NewrenDec 28, 2021
  80. Glen ChooDec 28, 2021
  81. Jonathan TanJan 10, 2022
  82. Elijah NewrenJan 10, 2022
  83. Scalar vs JGit, was Re: [PATCH v7 0/2] Conditional config includes based on remote URLJohannes Schindelin, Jan 25, 2022
  84. 0/2 Conditional config includes based on remote URLJonathan Tan, Jan 18, 2022
  85. 1/2 config: make git_config_include() staticJonathan Tan, Jan 18, 2022
  86. 2/2 config: include file if remote URL matches a globJonathan Tan, Jan 18, 2022
  87. Elijah NewrenJan 18, 2022

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.