Commit a24e656a4e6278157d2aec885e0d300f47f74938

Edward Thomson 2021-09-04T10:16:41

common: support custom repository extensions Allow users to specify additional repository extensions that they want to support. For example, callers can specify that they support `preciousObjects` and then may open repositories that support `extensions.preciousObjects`. Similarly, callers may opt out of supporting extensions that the library itself supports.

diff --git a/include/git2/common.h b/include/git2/common.h
index d278c01..2ee8290 100644
--- a/include/git2/common.h
+++ b/include/git2/common.h
@@ -209,7 +209,9 @@ typedef enum {
 	GIT_OPT_GET_MWINDOW_FILE_LIMIT,
 	GIT_OPT_SET_MWINDOW_FILE_LIMIT,
 	GIT_OPT_SET_ODB_PACKED_PRIORITY,
-	GIT_OPT_SET_ODB_LOOSE_PRIORITY
+	GIT_OPT_SET_ODB_LOOSE_PRIORITY,
+	GIT_OPT_GET_EXTENSIONS,
+	GIT_OPT_SET_EXTENSIONS
 } git_libgit2_opt_t;
 
 /**
@@ -431,6 +433,22 @@ typedef enum {
  *      > Override the default priority of the loose ODB backend which
  *      > is added when default backends are assigned to a repository
  *
+ *   opts(GIT_OPT_GET_EXTENSIONS, git_strarray *out)
+ *      > Returns the list of git extensions that are supported.  This
+ *      > is the list of built-in extensions supported by libgit2 and
+ *      > custom extensions that have been added with
+ *      > `GIT_OPT_SET_EXTENSIONS`.  Extensions that have been negated
+ *      > will not be returned.  The returned list should be released
+ *      > with `git_strarray_dispose`.
+ *
+ *   opts(GIT_OPT_SET_EXTENSIONS, const char **extensions, size_t len)
+ *      > Set that the given git extensions are supported by the caller.
+ *      > Extensions supported by libgit2 may be negated by prefixing
+ *      > them with a `!`.  For example: setting extensions to
+ *      > { "!noop", "newext" } indicates that the caller does not want
+ *      > to support repositories with the `noop` extension but does want
+ *      > to support repositories with the `newext` extension.
+ *
  * @param option Option key
  * @param ... value to set the option
  * @return 0 on success, <0 on failure
diff --git a/src/libgit2.c b/src/libgit2.c
index 09f7ab5..cc793b4 100644
--- a/src/libgit2.c
+++ b/src/libgit2.c
@@ -52,6 +52,7 @@ static void libgit2_settings_global_shutdown(void)
 {
 	git__free(git__user_agent);
 	git__free(git__ssl_ciphers);
+	git_repository__free_extensions();
 }
 
 static int git_libgit2_settings_global_init(void)
@@ -367,6 +368,28 @@ int git_libgit2_opts(int key, ...)
 		git_odb__loose_priority = va_arg(ap, int);
 		break;
 
+	case GIT_OPT_SET_EXTENSIONS:
+		{
+			const char **extensions = va_arg(ap, const char **);
+			size_t len = va_arg(ap, size_t);
+			error = git_repository__set_extensions(extensions, len);
+		}
+		break;
+
+	case GIT_OPT_GET_EXTENSIONS:
+		{
+			git_strarray *out = va_arg(ap, git_strarray *);
+			char **extensions;
+			size_t len;
+
+			if ((error = git_repository__extensions(&extensions, &len)) < 0)
+				break;
+
+			out->strings = extensions;
+			out->count = len;
+		}
+		break;
+
 	default:
 		git_error_set(GIT_ERROR_INVALID, "invalid option key");
 		error = -1;
diff --git a/src/repository.c b/src/repository.c
index aae0c91..d321113 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -1427,15 +1427,60 @@ static int check_repositoryformatversion(int *version, git_config *config)
 	return 0;
 }
 
+static const char *builtin_extensions[] = {
+	"noop"
+};
+
+static git_vector user_extensions = GIT_VECTOR_INIT;
+
 static int check_valid_extension(const git_config_entry *entry, void *payload)
 {
+	git_buf cfg = GIT_BUF_INIT;
+	bool reject;
+	const char *extension;
+	size_t i;
+	int error = 0;
+
 	GIT_UNUSED(payload);
 
-	if (!strcmp(entry->name, "extensions.noop"))
-		return 0;
+	git_vector_foreach (&user_extensions, i, extension) {
+		git_buf_clear(&cfg);
+
+		/*
+		 * Users can specify that they don't want to support an
+		 * extension with a '!' prefix.
+		 */
+		if ((reject = (extension[0] == '!')) == true)
+			extension = &extension[1];
+
+		if ((error = git_buf_printf(&cfg, "extensions.%s", extension)) < 0)
+			goto done;
 
+		if (strcmp(entry->name, cfg.ptr) == 0) {
+			if (reject)
+				goto fail;
+
+			goto done;
+		}
+	}
+
+	for (i = 0; i < ARRAY_SIZE(builtin_extensions); i++) {
+		extension = builtin_extensions[i];
+
+		if ((error = git_buf_printf(&cfg, "extensions.%s", extension)) < 0)
+			goto done;
+
+		if (strcmp(entry->name, cfg.ptr) == 0)
+			goto done;
+	}
+
+fail:
 	git_error_set(GIT_ERROR_REPOSITORY, "unsupported extension name %s", entry->name);
-	return -1;
+	error = -1;
+
+done:
+	git_buf_dispose(&cfg);
+	return error;
 }
 
 static int check_extensions(git_config *config, int version)
@@ -1446,6 +1491,70 @@ static int check_extensions(git_config *config, int version)
 	return git_config_foreach_match(config, "^extensions\\.", check_valid_extension, NULL);
 }
 
+int git_repository__extensions(char ***out, size_t *out_len)
+{
+	git_vector extensions;
+	const char *builtin, *user;
+	char *extension;
+	size_t i, j;
+
+	if (git_vector_init(&extensions, 8, NULL) < 0)
+		return -1;
+
+	for (i = 0; i < ARRAY_SIZE(builtin_extensions); i++) {
+		bool match = false;
+
+		builtin = builtin_extensions[i];
+
+		git_vector_foreach (&user_extensions, j, user) {
+			if (user[0] == '!' && strcmp(builtin, &user[1]) == 0) {
+				match = true;
+				break;
+			}
+		}
+
+		if (match)
+			continue;
+
+		if ((extension = git__strdup(builtin)) == NULL ||
+		    git_vector_insert(&extensions, extension) < 0)
+			return -1;
+	}
+
+	git_vector_foreach (&user_extensions, i, user) {
+		if (user[0] == '!')
+			continue;
+
+		if ((extension = git__strdup(user)) == NULL ||
+		    git_vector_insert(&extensions, extension) < 0)
+			return -1;
+	}
+
+	*out = (char **)git_vector_detach(out_len, NULL, &extensions);
+	return 0;
+}
+
+int git_repository__set_extensions(const char **extensions, size_t len)
+{
+	char *extension;
+	size_t i;
+
+	git_repository__free_extensions();
+
+	for (i = 0; i < len; i++) {
+		if ((extension = git__strdup(extensions[i])) == NULL ||
+		    git_vector_insert(&user_extensions, extension) < 0)
+			return -1;
+	}
+
+	return 0;
+}
+
+void git_repository__free_extensions(void)
+{
+	git_vector_free_deep(&user_extensions);
+}
+
 int git_repository_create_head(const char *git_dir, const char *ref_name)
 {
 	git_buf ref_path = GIT_BUF_INIT;
diff --git a/src/repository.h b/src/repository.h
index f48dd9e..cbc1601 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -249,4 +249,8 @@ int git_repository_initialbranch(git_buf *out, git_repository *repo);
  */
 int git_repository_workdir_path(git_buf *out, git_repository *repo, const char *path);
 
+int git_repository__extensions(char ***out, size_t *out_len);
+int git_repository__set_extensions(const char **extensions, size_t len);
+void git_repository__free_extensions(void);
+
 #endif
diff --git a/tests/core/opts.c b/tests/core/opts.c
index 72408cb..e8f65d5 100644
--- a/tests/core/opts.c
+++ b/tests/core/opts.c
@@ -1,6 +1,11 @@
 #include "clar_libgit2.h"
 #include "cache.h"
 
+void test_core_opts__cleanup(void)
+{
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, NULL, 0));
+}
+
 void test_core_opts__readwrite(void)
 {
 	size_t old_val = 0;
@@ -23,3 +28,44 @@ void test_core_opts__invalid_option(void)
 	cl_git_fail(git_libgit2_opts(-1, "foobar"));
 }
 
+void test_core_opts__extensions_query(void)
+{
+	git_strarray out = { 0 };
+
+	cl_git_pass(git_libgit2_opts(GIT_OPT_GET_EXTENSIONS, &out));
+
+	cl_assert_equal_sz(out.count, 1);
+	cl_assert_equal_s("noop", out.strings[0]);
+
+	git_strarray_dispose(&out);
+}
+
+void test_core_opts__extensions_add(void)
+{
+	const char *in[] = { "foo" };
+	git_strarray out = { 0 };
+
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, in, ARRAY_SIZE(in)));
+	cl_git_pass(git_libgit2_opts(GIT_OPT_GET_EXTENSIONS, &out));
+
+	cl_assert_equal_sz(out.count, 2);
+	cl_assert_equal_s("noop", out.strings[0]);
+	cl_assert_equal_s("foo", out.strings[1]);
+
+	git_strarray_dispose(&out);
+}
+
+void test_core_opts__extensions_remove(void)
+{
+	const char *in[] = { "bar", "!negate", "!noop", "baz" };
+	git_strarray out = { 0 };
+
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, in, ARRAY_SIZE(in)));
+	cl_git_pass(git_libgit2_opts(GIT_OPT_GET_EXTENSIONS, &out));
+
+	cl_assert_equal_sz(out.count, 2);
+	cl_assert_equal_s("bar", out.strings[0]);
+	cl_assert_equal_s("baz", out.strings[1]);
+
+	git_strarray_dispose(&out);
+}
diff --git a/tests/repo/extensions.c b/tests/repo/extensions.c
index 8ba89f1..e7772ac 100644
--- a/tests/repo/extensions.c
+++ b/tests/repo/extensions.c
@@ -19,6 +19,7 @@ void test_repo_extensions__initialize(void)
 void test_repo_extensions__cleanup(void)
 {
 	cl_git_sandbox_cleanup();
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, NULL, 0));
 }
 
 void test_repo_extensions__builtin(void)
@@ -33,6 +34,19 @@ void test_repo_extensions__builtin(void)
 	git_repository_free(extended);
 }
 
+void test_repo_extensions__negate_builtin(void)
+{
+	const char *in[] = { "foo", "!noop", "baz" };
+	git_repository *extended;
+
+	cl_repo_set_string(repo, "extensions.noop", "foobar");
+
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, in, ARRAY_SIZE(in)));
+
+	cl_git_fail(git_repository_open(&extended, "empty_bare.git"));
+	git_repository_free(extended);
+}
+
 void test_repo_extensions__unsupported(void)
 {
 	git_repository *extended = NULL;
@@ -42,3 +56,17 @@ void test_repo_extensions__unsupported(void)
 	cl_git_fail(git_repository_open(&extended, "empty_bare.git"));
 	git_repository_free(extended);
 }
+
+void test_repo_extensions__adds_extension(void)
+{
+	const char *in[] = { "foo", "!noop", "newextension", "baz" };
+	git_repository *extended;
+
+	cl_repo_set_string(repo, "extensions.newextension", "foobar");
+	cl_git_pass(git_libgit2_opts(GIT_OPT_SET_EXTENSIONS, in, ARRAY_SIZE(in)));
+
+	cl_git_pass(git_repository_open(&extended, "empty_bare.git"));
+	cl_assert(git_repository_path(extended) != NULL);
+	cl_assert(git__suffixcmp(git_repository_path(extended), "/") == 0);
+	git_repository_free(extended);
+}