Commit 071b6c0652ff0a0d6ce8c8c35d538d6edd67a52e

Patrick Steinhardt 2017-05-24T11:13:36

config_file: implement conditional "gitdir" includes Upstream git.git has implemented the ability to include other configuration files based on conditions. Right now, this only includes the ability to include a file based on the gitdir-location of the repository the currently parsed configuration file belongs to. This commit implements handling these conditional includes for the case-sensitive "gitdir" condition.

diff --git a/src/config_file.c b/src/config_file.c
index 800e584..ba520fc 100644
--- a/src/config_file.c
+++ b/src/config_file.c
@@ -1612,6 +1612,86 @@ static int parse_include(struct reader *reader,
 	return result;
 }
 
+static int conditional_match_gitdir(
+	int *matches,
+	const git_repository *repo,
+	const char *cfg_file,
+	const char *value)
+{
+	git_buf path = GIT_BUF_INIT;
+	int error, fnmatch_flags;
+
+	if (value[0] == '.' && git_path_is_dirsep(value[1])) {
+		git_path_dirname_r(&path, cfg_file);
+		git_buf_joinpath(&path, path.ptr, value + 2);
+	} else if (value[0] == '~' && git_path_is_dirsep(value[1]))
+		git_sysdir_expand_global_file(&path, value + 1);
+	else if (!git_path_is_absolute(value))
+		git_buf_joinpath(&path, "**", value);
+	else
+		git_buf_sets(&path, value);
+
+	if (git_buf_oom(&path)) {
+		error = -1;
+		goto out;
+	}
+
+	if (git_path_is_dirsep(value[strlen(value) - 1]))
+		git_buf_puts(&path, "**");
+
+	fnmatch_flags = FNM_PATHNAME|FNM_LEADING_DIR;
+
+	if ((error = p_fnmatch(path.ptr, git_repository_path(repo), fnmatch_flags)) < 0)
+
+		goto out;
+
+	*matches = (error == 0);
+
+out:
+	git_buf_free(&path);
+	return error;
+}
+
+static const struct {
+	const char *prefix;
+	int (*matches)(int *matches, const git_repository *repo, const char *cfg, const char *value);
+} conditions[] = {
+	{ "gitdir:", conditional_match_gitdir }
+};
+
+static int parse_conditional_include(struct reader *reader,
+	struct parse_data *parse_data, const char *section, const char *file)
+{
+	char *condition;
+	size_t i;
+	int error = 0, matches;
+
+	if (!parse_data->repo)
+		return 0;
+
+	condition = git__substrdup(section + strlen("includeIf."),
+				   strlen(section) - strlen("includeIf.") - strlen(".path"));
+
+	for (i = 0; i < ARRAY_SIZE(conditions); i++) {
+		if (git__prefixcmp(condition, conditions[i].prefix))
+			continue;
+
+		if ((error = conditions[i].matches(&matches,
+						   parse_data->repo,
+						   parse_data->file_path,
+						   condition + strlen(conditions[i].prefix))) < 0)
+			break;
+
+		if (matches)
+			error = parse_include(reader, parse_data, file);
+
+		break;
+	}
+
+	git__free(condition);
+	return error;
+}
+
 static int read_on_variable(
 	struct reader *reader,
 	const char *current_section,
@@ -1656,6 +1736,11 @@ static int read_on_variable(
 	/* Add or append the new config option */
 	if (!git__strcmp(var->entry->name, "include.path"))
 		result = parse_include(reader, parse_data, var->entry->value);
+	else if (!git__prefixcmp(var->entry->name, "includeif.") &&
+	         !git__suffixcmp(var->entry->name, ".path"))
+		result = parse_conditional_include(reader, parse_data,
+						   var->entry->name, var->entry->value);
+
 
 	return result;
 }
diff --git a/tests/config/conditionals.c b/tests/config/conditionals.c
new file mode 100644
index 0000000..323bbae
--- /dev/null
+++ b/tests/config/conditionals.c
@@ -0,0 +1,82 @@
+#include "clar_libgit2.h"
+#include "buffer.h"
+#include "fileops.h"
+
+#ifdef GIT_WIN32
+# define ROOT_PREFIX "C:"
+#else
+# define ROOT_PREFIX
+#endif
+
+static git_repository *_repo;
+
+void test_config_conditionals__initialize(void)
+{
+	_repo = cl_git_sandbox_init("empty_standard_repo");
+}
+
+void test_config_conditionals__cleanup(void)
+{
+	cl_git_sandbox_cleanup();
+}
+
+static void assert_condition_includes(const char *keyword, const char *path, bool expected)
+{
+	git_config *cfg;
+	git_buf buf = GIT_BUF_INIT;
+
+	git_buf_printf(&buf, "[includeIf \"%s:%s\"]\n", keyword, path);
+	git_buf_puts(&buf, "path = other\n");
+
+	cl_git_mkfile("empty_standard_repo/.git/config", buf.ptr);
+	cl_git_mkfile("empty_standard_repo/.git/other", "[foo]\nbar=baz\n");
+	_repo = cl_git_sandbox_reopen();
+
+	cl_git_pass(git_repository_config(&cfg, _repo));
+
+	if (expected) {
+		git_buf_clear(&buf);
+		cl_git_pass(git_config_get_string_buf(&buf, cfg, "foo.bar"));
+		cl_assert_equal_s("baz", git_buf_cstr(&buf));
+	} else {
+		cl_git_fail_with(GIT_ENOTFOUND,
+				 git_config_get_string_buf(&buf, cfg, "foo.bar"));
+	}
+
+	git_buf_free(&buf);
+	git_config_free(cfg);
+}
+
+void test_config_conditionals__gitdir(void)
+{
+	git_buf path = GIT_BUF_INIT;
+
+	assert_condition_includes("gitdir", ROOT_PREFIX "/", true);
+	assert_condition_includes("gitdir", "empty_standard_repo", true);
+	assert_condition_includes("gitdir", "empty_standard_repo/", true);
+	assert_condition_includes("gitdir", "./", true);
+
+	assert_condition_includes("gitdir", ROOT_PREFIX "/nonexistent", false);
+	assert_condition_includes("gitdir", ROOT_PREFIX "/empty_standard_repo", false);
+	assert_condition_includes("gitdir", "empty_stand", false);
+	assert_condition_includes("gitdir", "~/empty_standard_repo", false);
+
+	git_buf_joinpath(&path, clar_sandbox_path(), "/");
+	assert_condition_includes("gitdir", path.ptr, true);
+
+	git_buf_joinpath(&path, clar_sandbox_path(), "/*");
+	assert_condition_includes("gitdir", path.ptr, true);
+
+	git_buf_joinpath(&path, clar_sandbox_path(), "empty_standard_repo");
+	assert_condition_includes("gitdir", path.ptr, true);
+
+	git_buf_joinpath(&path, clar_sandbox_path(), "Empty_Standard_Repo");
+	assert_condition_includes("gitdir", path.ptr, false);
+
+	git_buf_free(&path);
+}
+
+void test_config_conditionals__invalid_conditional_fails(void)
+{
+	assert_condition_includes("foobar", ".git", false);
+}