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

[PATCH v2 2/2] config: add conditional include

From
Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Date
Jun 28, 2016, 17:26 UTC
Message-ID
<20160628172641.26381-3-pclouds@gmail.com>
In-Reply-To
<20160628172641.26381-1-pclouds@gmail.com>

Main description is already in config.txt. Here is a dev-only note about Windows support.

While prepare_include_condition_pattern() is Windows-friendly (because it does not hard code '/'). The reality could be uglier because internally get_git_dir() may return a path with '/' only or worse, a mix of '/' and '\\'.

At some point, we need to teach wildmatch() that '/' and '\' should be treated the same way (via a flag) as well. Then we could care less about '/' vs '\\'. But a Windows dev probably has to do it.

Helped-by: Jeff King <peff@peff.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/config.txt  | 40 +++++++++++++++++++++
 config.c                  | 89 +++++++++++++++++++++++++++++++++++++++++++++--
 t/t1305-config-include.sh | 45 ++++++++++++++++++++++++
 3 files changed, 172 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 58673cf..c8ad0bf 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -91,6 +91,46 @@ found at the location of the include directive. If the value of the
 relative to the configuration file in which the include directive was
 found.  See below for examples.
 
+Included files can be grouped into subsections where subsectino name
+is the condition when the files are included. The condition starts
+with an condition type string, followed by a colon and a pattern.
+
+Only "gitdir" type is supported, where files are included if
+`$GIT_DIR` matches the specified pattern. For example,
+
+
+	[include "gitdir:/path/to/foo.git"]
+		path = /path/to/foo.inc
+
+would only include "/path/to/foo.inc" if `$GIT_DIR` is
+/path/to/foo.git.
+
+The following pattern is a wildcard pattern with two additional
+wildcards `**/` and `/**`. See linkgit:gitignore[5] for more
+information. For convenience:
+
+ * If the pattern ends with '/', '**' will be automatically added. For
+   example, the pattern 'foo/' becomes 'foo/**'. In other words, it
+   matches "foo" and everything inside, recursively.
+
+ * If the pattern starts with `~/`, `~` will be substitued with the
+   environment variable `HOME`.
+
+ * If the pattern starts with `./`, it is replaced with the directory
+   where the current config file is. For example if the config file
+   that contains the "include" subsection is `$HOME/.gitconfig` then
+   the pattern `./foo` would match the path `$HOME/foo`
+
+A few more notes:
+
+ * Symlinks in `$GIT_DIR` are not resolved before matching.
+
+ * Note that "../" is not special and will match literally, which is
+   unlikely what you want.
+
+ * On case-insensitive file systems, you may need to specify
+   core.ignoreCase before the `include` subsections in order to match
+   case-insensitively if core.ignoreCase is declared in the same file.
 
 Example
 ~~~~~~~
diff --git a/config.c b/config.c
index f51c56b..97c450e 100644
--- a/config.c
+++ b/config.c
@@ -13,6 +13,7 @@
 #include "hashmap.h"
 #include "string-list.h"
 #include "utf8.h"
+#include "dir.h"
 
 struct config_source {
 	struct config_source *prev;
@@ -140,9 +141,89 @@ static int handle_path_include(const char *path, struct config_include_data *inc
 	return ret;
 }
 
+static int prepare_include_condition_pattern(struct strbuf *pat)
+{
+	struct strbuf path = STRBUF_INIT;
+	int prefix = 0;
+
+	/* TODO: maybe support ~user/ too */
+	if (pat->buf[0] == '~' && is_dir_sep(pat->buf[1])) {
+		const char *home = getenv("HOME");
+
+		if (!home)
+			return error(_("$HOME is not defined"));
+
+		strbuf_add_absolute_path(&path, home);
+		strbuf_splice(pat, 0, 1, path.buf, path.len);
+		prefix = path.len + 1 /*slash*/;
+	} else if (pat->buf[0] == '.' && is_dir_sep(pat->buf[1])) {
+		const char *slash;
+
+		if (!cf || !cf->path)
+			return error(_("relative config include "
+				       "conditionals must come from files"));
+
+		/* TODO: escape wildcards */
+		strbuf_add_absolute_path(&path, cf->path);
+		slash = find_last_dir_sep(path.buf);
+		if (!slash)
+			die("BUG: how is this possible?");
+		strbuf_splice(pat, 0, 1, path.buf, slash - path.buf);
+		prefix = slash - path.buf + 1 /* slash */;
+	} else if (!is_absolute_path(pat->buf))
+		strbuf_insert(pat, 0, "**/", 3);
+
+	if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
+		strbuf_addstr(pat, "**");
+
+	strbuf_release(&path);
+	return prefix;
+}
+
+static int include_condition_is_true(const char *cond, int cond_len)
+{
+	const char *value;
+	size_t value_len;
+
+	/* no condition (i.e., "include.path") is always true */
+	if (!cond)
+		return 1;
+
+	if (skip_prefix_mem(cond, cond_len, "gitdir:", &value, &value_len)) {
+		struct strbuf text = STRBUF_INIT;
+		struct strbuf pattern = STRBUF_INIT;
+		int ret, prefix;
+
+		strbuf_add_absolute_path(&text, get_git_dir());
+		strbuf_add(&pattern, value, value_len);
+		prefix = prepare_include_condition_pattern(&pattern);
+
+		if (prefix < 0)
+			return 0;
+
+		if (prefix > 0 &&
+		    (text.len < prefix ||
+		     fspathncmp(pattern.buf, text.buf, prefix)))
+			return 0;
+
+		ret = !wildmatch(pattern.buf + prefix, text.buf + prefix,
+				 ignore_case ? WM_CASEFOLD : 0,
+				 NULL);
+		strbuf_release(&pattern);
+		strbuf_release(&text);
+		return ret;
+	}
+
+	error(_("unrecognized include condition: %.*s"), cond_len, cond);
+	/* unknown conditionals are always false */
+	return 0;
+}
+
 int git_config_include(const char *var, const char *value, void *data)
 {
 	struct config_include_data *inc = data;
+	const char *cond, *key;
+	int cond_len;
 	int ret;
 
 	/*
@@ -153,8 +234,12 @@ int git_config_include(const char *var, const char *value, void *data)
 	if (ret < 0)
 		return ret;
 
-	if (!strcmp(var, "include.path"))
-		ret = handle_path_include(value, inc);
+	if (!parse_config_key(var, "include", &cond, &cond_len, &key) &&
+	    include_condition_is_true(cond, cond_len)) {
+		if (!strcmp(key, "path"))
+			ret = handle_path_include(value, inc);
+		/* else we do not know about this type of include; ignore */
+	}
 	return ret;
 }
 
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
index 9ba2ba1..30351f2 100755
--- a/t/t1305-config-include.sh
+++ b/t/t1305-config-include.sh
@@ -152,6 +152,51 @@ test_expect_success 'relative includes from stdin line fail' '
 	test_must_fail git config --file - test.one
 '
 
+test_expect_success 'conditional include, both unanchored' '
+	git init foo &&
+	(
+		cd foo &&
+		echo "[include \"gitdir:foo/\"]path=bar" >>.git/config &&
+		echo "[test]one=1" >.git/bar &&
+		echo 1 >expect &&
+		git config test.one >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'conditional include, $HOME expansion' '
+	(
+		cd foo &&
+		echo "[include \"gitdir:~/foo/\"]path=bar2" >>.git/config &&
+		echo "[test]two=2" >.git/bar2 &&
+		echo 2 >expect &&
+		git config test.two >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'conditional include, full pattern' '
+	(
+		cd foo &&
+		echo "[include \"gitdir:**/foo/**\"]path=bar3" >>.git/config &&
+		echo "[test]three=3" >.git/bar3 &&
+		echo 3 >expect &&
+		git config test.three >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'conditional include, relative path' '
+	echo "[include \"gitdir:./foo/.git\"]path=bar4" >>.gitconfig &&
+	echo "[test]four=4" >bar4 &&
+	(
+		cd foo &&
+		echo 4 >expect &&
+		git config test.four >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success 'include cycles are detected' '
 	cat >.gitconfig <<-\EOF &&
 	[test]value = gitconfig
-- 
2.8.2.531.gd073806
Previous: Nguyễn Thái Ngọc DuyNext: Jeff King
Message 8 of 46 in “config: add conditional include”
  1. config: add conditional includeNguyễn Thái Ngọc Duy, Jun 26, 2016
  2. Jeff KingJun 26, 2016
  3. Duy NguyenJun 27, 2016
  4. Jeff KingJun 27, 2016
  5. Duy NguyenJun 27, 2016
  6. Jeff KingJun 27, 2016
  7. 0/2 Config conditional includeNguyễn Thái Ngọc Duy, Jun 28, 2016
  8. 2/2 config: add conditional includeNguyễn Thái Ngọc Duy, Jun 28, 2016
  9. Jeff KingJun 28, 2016
  10. Duy NguyenJun 29, 2016
  11. Eric SunshineJun 28, 2016
  12. config: add conditional includeNguyễn Thái Ngọc Duy, Jul 12, 2016
  13. Matthieu MoyJul 13, 2016
  14. Jeff KingJul 13, 2016
  15. Matthieu MoyJul 13, 2016
  16. Duy NguyenJul 13, 2016
  17. config: add conditional includeNguyễn Thái Ngọc Duy, Jul 14, 2016
  18. Johannes SchindelinJul 14, 2016
  19. Duy NguyenJul 14, 2016
  20. Johannes SchindelinJul 16, 2016
  21. Duy NguyenJul 16, 2016
  22. Jeff KingJul 16, 2016
  23. Johannes SchindelinJul 16, 2016
  24. Jeff KingJul 16, 2016
  25. Johannes SchindelinJul 17, 2016
  26. Jeff KingJul 20, 2016
  27. Junio C HamanoJul 20, 2016
  28. Jakub NarębskiJul 20, 2016
  29. Duy NguyenAug 13, 2016
  30. Jeff KingAug 19, 2016
  31. Jakub NarębskiAug 20, 2016
  32. Duy NguyenAug 22, 2016
  33. Matthieu MoyAug 22, 2016
  34. Duy NguyenAug 22, 2016
  35. Matthieu MoyAug 22, 2016
  36. Duy NguyenAug 22, 2016
  37. Johannes SchindelinAug 23, 2016
  38. Duy NguyenAug 24, 2016
  39. Jakub NarębskiAug 24, 2016
  40. Jeff KingAug 24, 2016
  41. 1/2 add skip_prefix_mem helperNguyễn Thái Ngọc Duy, Jun 28, 2016
  42. Jeff KingJun 28, 2016
  43. Matthieu MoyJun 28, 2016
  44. Jeff KingJun 28, 2016
  45. Duy NguyenJun 29, 2016
  46. Junio C HamanoJun 28, 2016

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.