Merge pull request #6756 from libgit2/ethomson/worktree-config

Separate config reader and writer backend priorities (for worktree configs)
This commit is contained in:
Edward Thomson
2024-03-16 16:36:22 +00:00
committed by GitHub
16 changed files with 565 additions and 292 deletions

View File

@@ -22,8 +22,19 @@ GIT_BEGIN_DECL
/**
* Priority level of a config file.
*
* These priority levels correspond to the natural escalation logic
* (from higher to lower) when searching for config entries in git.git.
* (from higher to lower) when reading or searching for config entries
* in git.git. Meaning that for the same key, the configuration in
* the local configuration is preferred over the configuration in
* the system configuration file.
*
* Callers can add their own custom configuration, beginning at the
* `GIT_CONFIG_LEVEL_APP` level.
*
* Writes, by default, occur in the highest priority level backend
* that is writable. This ordering can be overridden with
* `git_config_set_writeorder`.
*
* git_config_open_default() and git_repository_config() honor those
* priority levels as well.
@@ -48,9 +59,13 @@ typedef enum {
*/
GIT_CONFIG_LEVEL_LOCAL = 5,
/** Worktree specific configuration file; $GIT_DIR/config.worktree
*/
GIT_CONFIG_LEVEL_WORKTREE = 6,
/** Application specific configuration file; freely defined by applications
*/
GIT_CONFIG_LEVEL_APP = 6,
GIT_CONFIG_LEVEL_APP = 7,
/** Represents the highest level available config file (i.e. the most
* specific config file available that actually is loaded)
@@ -296,6 +311,11 @@ GIT_EXTERN(int) git_config_open_level(
*/
GIT_EXTERN(int) git_config_open_global(git_config **out, git_config *config);
GIT_EXTERN(int) git_config_set_writeorder(
git_config *cfg,
git_config_level_t *levels,
size_t len);
/**
* Create a snapshot of the configuration
*

View File

@@ -19,20 +19,20 @@ GIT_BEGIN_DECL
/** Generic return codes */
typedef enum {
GIT_OK = 0, /**< No error */
GIT_OK = 0, /**< No error */
GIT_ERROR = -1, /**< Generic error */
GIT_ENOTFOUND = -3, /**< Requested object could not be found */
GIT_EEXISTS = -4, /**< Object exists preventing operation */
GIT_EAMBIGUOUS = -5, /**< More than one object matches */
GIT_EBUFS = -6, /**< Output buffer too short to hold data */
GIT_ERROR = -1, /**< Generic error */
GIT_ENOTFOUND = -3, /**< Requested object could not be found */
GIT_EEXISTS = -4, /**< Object exists preventing operation */
GIT_EAMBIGUOUS = -5, /**< More than one object matches */
GIT_EBUFS = -6, /**< Output buffer too short to hold data */
/**
* GIT_EUSER is a special error that is never generated by libgit2
* code. You can return it from a callback (e.g to stop an iteration)
* to know that it was generated by the callback and not by libgit2.
*/
GIT_EUSER = -7,
GIT_EUSER = -7,
GIT_EBAREREPO = -8, /**< Operation not allowed on bare repository */
GIT_EUNBORNBRANCH = -9, /**< HEAD refers to branch with no commits */
@@ -61,7 +61,8 @@ typedef enum {
GIT_EOWNER = -36, /**< The object is not owned by the current user */
GIT_TIMEOUT = -37, /**< The operation timed out */
GIT_EUNCHANGED = -38, /**< There were no changes */
GIT_ENOTSUPPORTED = -39 /**< An option is not supported */
GIT_ENOTSUPPORTED = -39, /**< An option is not supported */
GIT_EREADONLY = -40 /**< The subject is read-only */
} git_error_code;
/**

View File

@@ -499,6 +499,7 @@ typedef enum {
GIT_REPOSITORY_ITEM_PACKED_REFS,
GIT_REPOSITORY_ITEM_REMOTES,
GIT_REPOSITORY_ITEM_CONFIG,
GIT_REPOSITORY_ITEM_WORKTREE_CONFIG,
GIT_REPOSITORY_ITEM_INFO,
GIT_REPOSITORY_ITEM_HOOKS,
GIT_REPOSITORY_ITEM_LOGS,

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,8 @@
struct git_config {
git_refcount rc;
git_vector backends;
git_vector readers;
git_vector writers;
};
extern int git_config__global_location(git_str *buf);
@@ -94,17 +95,21 @@ int git_config_lookup_map_enum(git_configmap_t *type_out,
size_t map_n, int enum_val);
/**
* Unlock the backend with the highest priority
* Unlock the given backend that was previously locked.
*
* Unlocking will allow other writers to update the configuration
* file. Optionally, any changes performed since the lock will be
* applied to the configuration.
*
* @param cfg the configuration
* @param config the config instance
* @param data the config data passed to git_transaction_new
* @param commit boolean which indicates whether to commit any changes
* done since locking
* @return 0 or an error code
*/
GIT_EXTERN(int) git_config_unlock(git_config *cfg, int commit);
GIT_EXTERN(int) git_config_unlock(
git_config *config,
void *data,
int commit);
#endif

View File

@@ -58,6 +58,7 @@ static const struct {
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "packed-refs", false },
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "remotes", true },
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "config", false },
{ GIT_REPOSITORY_ITEM_GITDIR, GIT_REPOSITORY_ITEM_GITDIR, "config.worktree", false },
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "info", true },
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "hooks", true },
{ GIT_REPOSITORY_ITEM_COMMONDIR, GIT_REPOSITORY_ITEM_GITDIR, "logs", true },
@@ -1273,6 +1274,24 @@ int git_repository_discover(
return error;
}
static int has_config_worktree(bool *out, git_config *cfg)
{
int worktreeconfig = 0, error;
*out = false;
error = git_config_get_bool(&worktreeconfig, cfg, "extensions.worktreeconfig");
if (error == 0)
*out = worktreeconfig;
else if (error == GIT_ENOTFOUND)
*out = false;
else
return error;
return 0;
}
static int load_config(
git_config **out,
git_repository *repo,
@@ -1281,9 +1300,11 @@ static int load_config(
const char *system_config_path,
const char *programdata_path)
{
int error;
git_str config_path = GIT_STR_INIT;
git_config *cfg = NULL;
git_config_level_t write_order;
bool has_worktree;
int error;
GIT_ASSERT_ARG(out);
@@ -1297,6 +1318,14 @@ static int load_config(
if (error && error != GIT_ENOTFOUND)
goto on_error;
if ((error = has_config_worktree(&has_worktree, cfg)) == 0 &&
has_worktree &&
(error = git_repository__item_path(&config_path, repo, GIT_REPOSITORY_ITEM_WORKTREE_CONFIG)) == 0)
error = git_config_add_file_ondisk(cfg, config_path.ptr, GIT_CONFIG_LEVEL_WORKTREE, repo, 0);
if (error && error != GIT_ENOTFOUND)
goto on_error;
git_str_dispose(&config_path);
}
@@ -1326,6 +1355,11 @@ static int load_config(
git_error_clear(); /* clear any lingering ENOTFOUND errors */
write_order = GIT_CONFIG_LEVEL_LOCAL;
if ((error = git_config_set_writeorder(cfg, &write_order, 1)) < 0)
goto on_error;
*out = cfg;
return 0;
@@ -1845,7 +1879,8 @@ static int check_repositoryformatversion(int *version, git_config *config)
static const char *builtin_extensions[] = {
"noop",
"objectformat"
"objectformat",
"worktreeconfig",
};
static git_vector user_extensions = { 0, git__strcmp_cb };

View File

@@ -49,12 +49,16 @@ struct git_transaction {
git_repository *repo;
git_refdb *db;
git_config *cfg;
void *cfg_data;
git_strmap *locks;
git_pool pool;
};
int git_transaction_config_new(git_transaction **out, git_config *cfg)
int git_transaction_config_new(
git_transaction **out,
git_config *cfg,
void *data)
{
git_transaction *tx;
@@ -66,6 +70,8 @@ int git_transaction_config_new(git_transaction **out, git_config *cfg)
tx->type = TRANSACTION_CONFIG;
tx->cfg = cfg;
tx->cfg_data = data;
*out = tx;
return 0;
}
@@ -333,8 +339,9 @@ int git_transaction_commit(git_transaction *tx)
GIT_ASSERT_ARG(tx);
if (tx->type == TRANSACTION_CONFIG) {
error = git_config_unlock(tx->cfg, true);
error = git_config_unlock(tx->cfg, tx->cfg_data, true);
tx->cfg = NULL;
tx->cfg_data = NULL;
return error;
}
@@ -369,10 +376,8 @@ void git_transaction_free(git_transaction *tx)
return;
if (tx->type == TRANSACTION_CONFIG) {
if (tx->cfg) {
git_config_unlock(tx->cfg, false);
git_config_free(tx->cfg);
}
if (tx->cfg)
git_config_unlock(tx->cfg, tx->cfg_data, false);
git__free(tx);
return;

View File

@@ -9,6 +9,9 @@
#include "common.h"
int git_transaction_config_new(git_transaction **out, git_config *cfg);
int git_transaction_config_new(
git_transaction **out,
git_config *cfg,
void *data);
#endif

View File

@@ -71,3 +71,21 @@ void test_config_configlevel__fetching_a_level_from_an_empty_compound_config_ret
git_config_free(cfg);
}
void test_config_configlevel__can_override_local_with_worktree(void)
{
git_config *cfg;
git_buf buf = GIT_BUF_INIT;
cl_git_pass(git_config_new(&cfg));
cl_git_pass(git_config_add_file_ondisk(cfg, cl_fixture("config/config19"),
GIT_CONFIG_LEVEL_WORKTREE, NULL, 1));
cl_git_pass(git_config_add_file_ondisk(cfg, cl_fixture("config/config18"),
GIT_CONFIG_LEVEL_LOCAL, NULL, 1));
cl_git_pass(git_config_get_string_buf(&buf, cfg, "core.stringglobal"));
cl_assert_equal_s("don't find me!", buf.ptr);
git_buf_dispose(&buf);
git_config_free(cfg);
}

View File

@@ -24,7 +24,7 @@ void test_config_readonly__writing_to_readonly_fails(void)
backend->readonly = 1;
cl_git_pass(git_config_add_backend(cfg, backend, GIT_CONFIG_LEVEL_GLOBAL, NULL, 0));
cl_git_fail_with(GIT_ENOTFOUND, git_config_set_string(cfg, "foo.bar", "baz"));
cl_git_fail_with(GIT_EREADONLY, git_config_set_string(cfg, "foo.bar", "baz"));
cl_assert(!git_fs_path_exists("global"));
}

View File

@@ -696,6 +696,36 @@ void test_config_write__locking(void)
git_config_free(cfg);
}
void test_config_write__abort_lock(void)
{
git_config *cfg;
git_config_entry *entry;
git_transaction *tx;
const char *filename = "locked-file";
/* Open the config and lock it */
cl_git_mkfile(filename, "[section]\n\tname = value\n");
cl_git_pass(git_config_open_ondisk(&cfg, filename));
cl_git_pass(git_config_get_entry(&entry, cfg, "section.name"));
cl_assert_equal_s("value", entry->value);
git_config_entry_free(entry);
cl_git_pass(git_config_lock(&tx, cfg));
/* Change entries in the locked backend */
cl_git_pass(git_config_set_string(cfg, "section.name", "other value"));
cl_git_pass(git_config_set_string(cfg, "section2.name3", "more value"));
git_transaction_free(tx);
/* Now that we've unlocked it, we should see no changes */
cl_git_pass(git_config_get_entry(&entry, cfg, "section.name"));
cl_assert_equal_s("value", entry->value);
git_config_entry_free(entry);
cl_git_fail_with(GIT_ENOTFOUND, git_config_get_entry(&entry, cfg, "section2.name3"));
git_config_free(cfg);
}
void test_config_write__repeated(void)
{
const char *filename = "config-repeated";

View File

@@ -34,9 +34,10 @@ void test_core_opts__extensions_query(void)
cl_git_pass(git_libgit2_opts(GIT_OPT_GET_EXTENSIONS, &out));
cl_assert_equal_sz(out.count, 2);
cl_assert_equal_sz(out.count, 3);
cl_assert_equal_s("noop", out.strings[0]);
cl_assert_equal_s("objectformat", out.strings[1]);
cl_assert_equal_s("worktreeconfig", out.strings[2]);
git_strarray_dispose(&out);
}
@@ -49,10 +50,11 @@ void test_core_opts__extensions_add(void)
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, 3);
cl_assert_equal_sz(out.count, 4);
cl_assert_equal_s("foo", out.strings[0]);
cl_assert_equal_s("noop", out.strings[1]);
cl_assert_equal_s("objectformat", out.strings[2]);
cl_assert_equal_s("worktreeconfig", out.strings[3]);
git_strarray_dispose(&out);
}
@@ -65,10 +67,11 @@ void test_core_opts__extensions_remove(void)
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, 3);
cl_assert_equal_sz(out.count, 4);
cl_assert_equal_s("bar", out.strings[0]);
cl_assert_equal_s("baz", out.strings[1]);
cl_assert_equal_s("objectformat", out.strings[2]);
cl_assert_equal_s("worktreeconfig", out.strings[3]);
git_strarray_dispose(&out);
}
@@ -81,11 +84,12 @@ void test_core_opts__extensions_uniq(void)
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, 4);
cl_assert_equal_sz(out.count, 5);
cl_assert_equal_s("bar", out.strings[0]);
cl_assert_equal_s("foo", out.strings[1]);
cl_assert_equal_s("noop", out.strings[2]);
cl_assert_equal_s("objectformat", out.strings[3]);
cl_assert_equal_s("worktreeconfig", out.strings[4]);
git_strarray_dispose(&out);
}

View File

@@ -6,15 +6,19 @@
static worktree_fixture fixture =
WORKTREE_FIXTURE_INIT(COMMON_REPO, WORKTREE_REPO);
static worktree_fixture submodule =
WORKTREE_FIXTURE_INIT("submodules", "submodules-worktree-parent");
void test_worktree_config__initialize(void)
{
setup_fixture_worktree(&fixture);
setup_fixture_worktree(&submodule);
}
void test_worktree_config__cleanup(void)
{
cleanup_fixture_worktree(&fixture);
cleanup_fixture_worktree(&submodule);
}
void test_worktree_config__open(void)
@@ -27,7 +31,7 @@ void test_worktree_config__open(void)
git_config_free(cfg);
}
void test_worktree_config__set(void)
void test_worktree_config__set_level_local(void)
{
git_config *cfg;
int32_t val;
@@ -45,3 +49,78 @@ void test_worktree_config__set(void)
cl_assert_equal_i(val, 5);
git_config_free(cfg);
}
void test_worktree_config__requires_extension(void)
{
git_config *cfg;
git_config *wtcfg;
int extension = 0;
/*
* the "submodules" repo does not have extensions.worktreeconfig
* set, the worktree configuration should not be available.
*/
cl_git_pass(git_repository_config(&cfg, submodule.repo));
cl_git_fail_with(GIT_ENOTFOUND, git_config_get_bool(&extension, cfg, "extensions.worktreeconfig"));
cl_assert_equal_i(0, extension);
cl_git_fail_with(GIT_ENOTFOUND, git_config_open_level(&wtcfg, cfg, GIT_CONFIG_LEVEL_WORKTREE));
git_config_free(cfg);
/* the "testrepo" repo does have the configuration set. */
cl_git_pass(git_repository_config(&cfg, fixture.repo));
cl_git_pass(git_config_get_bool(&extension, cfg, "extensions.worktreeconfig"));
cl_assert_equal_i(1, extension);
cl_git_pass(git_config_open_level(&wtcfg, cfg, GIT_CONFIG_LEVEL_WORKTREE));
git_config_free(wtcfg);
git_config_free(cfg);
}
void test_worktree_config__exists(void)
{
git_config *cfg, *wtcfg, *snap;
const char *str;
cl_git_pass(git_repository_config(&cfg, fixture.repo));
cl_git_pass(git_repository_config(&wtcfg, fixture.worktree));
cl_git_pass(git_config_snapshot(&snap, cfg));
cl_git_pass(git_config_get_string(&str, snap, "worktreetest.config"));
cl_assert_equal_s("mainrepo", str);
git_config_free(snap);
cl_git_pass(git_config_snapshot(&snap, wtcfg));
cl_git_pass(git_config_get_string(&str, snap, "worktreetest.config"));
cl_assert_equal_s("worktreerepo", str);
git_config_free(snap);
git_config_free(cfg);
git_config_free(wtcfg);
}
void test_worktree_config__set_level_worktree(void)
{
git_config *cfg;
git_config *wtcfg;
int32_t val;
cl_git_pass(git_repository_config(&cfg, fixture.repo));
cl_git_pass(git_config_open_level(&wtcfg, cfg, GIT_CONFIG_LEVEL_WORKTREE));
cl_git_pass(git_config_set_int32(wtcfg, "worktree.specific", 42));
cl_git_pass(git_config_get_int32(&val, cfg, "worktree.specific"));
cl_assert_equal_i(val, 42);
/* reopen to verify config has been set */
git_config_free(cfg);
cl_git_pass(git_repository_config(&cfg, fixture.repo));
cl_git_pass(git_config_get_int32(&val, cfg, "worktree.specific"));
cl_assert_equal_i(val, 42);
cl_git_fail_with(GIT_ENOTFOUND, git_config_delete_entry(cfg, "worktree.specific"));
cl_git_pass(git_config_delete_entry(wtcfg, "worktree.specific"));
cl_git_fail_with(GIT_ENOTFOUND, git_config_get_int32(&val, cfg, "worktree.specific"));
git_config_free(cfg);
git_config_free(wtcfg);
}

View File

@@ -3,6 +3,8 @@
filemode = true
bare = false
logallrefupdates = true
[extensions]
worktreeconfig = true
[remote "test"]
url = git://github.com/libgit2/libgit2
fetch = +refs/heads/*:refs/remotes/test/*

View File

@@ -0,0 +1,2 @@
[worktreetest]
config = mainrepo

View File

@@ -0,0 +1,2 @@
[worktreetest]
config = worktreerepo