diff --git a/include/git2/config.h b/include/git2/config.h index 332e62036d0..63293dbdaec 100644 --- a/include/git2/config.h +++ b/include/git2/config.h @@ -48,9 +48,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) diff --git a/include/git2/repository.h b/include/git2/repository.h index 0ff0856510f..9ad6176f940 100644 --- a/include/git2/repository.h +++ b/include/git2/repository.h @@ -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, diff --git a/src/libgit2/config.c b/src/libgit2/config.c index 04f3ec2fee2..986d4ae7071 100644 --- a/src/libgit2/config.c +++ b/src/libgit2/config.c @@ -592,37 +592,55 @@ static const char *uses[] = { static int get_backend_for_use(git_config_backend **out, git_config *cfg, const char *name, backend_use use) { - size_t i; + size_t len; + int error = 0; backend_internal *backend; + git_config *cfg_single; *out = NULL; - if (git_vector_length(&cfg->backends) == 0) { + len = git_vector_length(&cfg->backends); + if (len <= 0) { + /* todo: tweak message? */ git_error_set(GIT_ERROR_CONFIG, "cannot %s value for '%s' when no config backends exist", uses[use], name); return GIT_ENOTFOUND; } - git_vector_foreach(&cfg->backends, i, backend) { - if (!backend->backend->readonly) { - *out = backend->backend; - return 0; + if (len == 1) { + backend = git_vector_get(&cfg->backends, 0); + + if (backend == NULL) { + /* todo: tweak message? */ + git_error_set(GIT_ERROR_CONFIG, "failed to retrieve config backend"); + return GIT_ENOTFOUND; + } + + if (backend->backend->readonly) { + /* todo: tweak message? */ + git_error_set(GIT_ERROR_CONFIG, "cannot %s value for '%s' when config backend is readonly", uses[use], name); + return GIT_ENOTFOUND; } + + *out = backend->backend; + return 0; } - git_error_set(GIT_ERROR_CONFIG, - "cannot %s value for '%s' when all config backends are readonly", - uses[use], name); - return GIT_ENOTFOUND; + if ((error = git_config_open_level(&cfg_single, cfg, GIT_CONFIG_LEVEL_LOCAL)) < 0) { + return error; + } + + return get_backend_for_use(out, cfg_single, name, use); } int git_config_delete_entry(git_config *cfg, const char *name) { git_config_backend *backend; + int error = 0; - if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) - return GIT_ENOTFOUND; + if ((error = get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE)) < 0) + return error; return backend->del(backend, name); } @@ -654,8 +672,8 @@ int git_config_set_string(git_config *cfg, const char *name, const char *value) return -1; } - if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_SET) < 0) - return GIT_ENOTFOUND; + if ((error = get_backend_for_use(&backend, cfg, name, BACKEND_USE_SET)) < 0) + return error; error = backend->set(backend, name, value); @@ -1095,9 +1113,10 @@ int git_config_multivar_iterator_new(git_config_iterator **out, const git_config int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value) { git_config_backend *backend; + int error = 0; - if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) - return GIT_ENOTFOUND; + if ((error = get_backend_for_use(&backend, cfg, name, BACKEND_USE_SET)) < 0) + return error; return backend->set_multivar(backend, name, regexp, value); } @@ -1105,9 +1124,10 @@ int git_config_set_multivar(git_config *cfg, const char *name, const char *regex int git_config_delete_multivar(git_config *cfg, const char *name, const char *regexp) { git_config_backend *backend; + int error = 0; - if (get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE) < 0) - return GIT_ENOTFOUND; + if ((error = get_backend_for_use(&backend, cfg, name, BACKEND_USE_DELETE)) < 0) + return error; return backend->del_multivar(backend, name, regexp); } diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c index 4eb3449133b..be938b5b9d0 100644 --- a/src/libgit2/repository.c +++ b/src/libgit2/repository.c @@ -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_COMMONDIR, 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 }, @@ -1291,6 +1292,12 @@ static int load_config( return error; if (repo) { + if ((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; + if ((error = git_repository__item_path(&config_path, repo, GIT_REPOSITORY_ITEM_CONFIG)) == 0) error = git_config_add_file_ondisk(cfg, config_path.ptr, GIT_CONFIG_LEVEL_LOCAL, repo, 0); @@ -1845,7 +1852,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 }; diff --git a/tests/libgit2/config/configlevel.c b/tests/libgit2/config/configlevel.c index 8422d32c944..3534fbc2c84 100644 --- a/tests/libgit2/config/configlevel.c +++ b/tests/libgit2/config/configlevel.c @@ -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); +} diff --git a/tests/libgit2/config/readonly.c b/tests/libgit2/config/readonly.c index a8901e394c0..0140bcb414d 100644 --- a/tests/libgit2/config/readonly.c +++ b/tests/libgit2/config/readonly.c @@ -49,6 +49,7 @@ void test_config_readonly__writing_to_cfg_with_rw_precedence_succeeds(void) void test_config_readonly__writing_to_cfg_with_ro_precedence_succeeds(void) { git_config_backend *backend; + git_config *cfg_global; cl_git_pass(git_config_backend_from_file(&backend, "local")); backend->readonly = 1; @@ -57,7 +58,10 @@ void test_config_readonly__writing_to_cfg_with_ro_precedence_succeeds(void) cl_git_pass(git_config_backend_from_file(&backend, "global")); cl_git_pass(git_config_add_backend(cfg, backend, GIT_CONFIG_LEVEL_GLOBAL, NULL, 0)); - cl_git_pass(git_config_set_string(cfg, "foo.bar", "baz")); + cl_git_fail_with(GIT_ENOTFOUND, git_config_set_string(cfg, "foo.bar", "baz")); + + git_config_open_global(&cfg_global, cfg); + cl_git_pass(git_config_set_string(cfg_global, "foo.bar", "baz")); cl_assert(!git_fs_path_exists("local")); cl_assert(git_fs_path_exists("global")); diff --git a/tests/libgit2/core/opts.c b/tests/libgit2/core/opts.c index 1aa095adf4c..cbef29f991d 100644 --- a/tests/libgit2/core/opts.c +++ b/tests/libgit2/core/opts.c @@ -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); } diff --git a/tests/libgit2/remote/httpproxy.c b/tests/libgit2/remote/httpproxy.c index 60fc67dec31..2ea5bf099b2 100644 --- a/tests/libgit2/remote/httpproxy.c +++ b/tests/libgit2/remote/httpproxy.c @@ -115,11 +115,14 @@ static void assert_global_config_match(const char *config, const char *expected) git_remote *remote; char *proxy; git_config* cfg; + git_config* cfg_global; if (config) { cl_git_pass(git_config_open_default(&cfg)); - git_config_set_string(cfg, config, expected); + cl_git_pass(git_config_open_global(&cfg_global, cfg)); + git_config_set_string(cfg_global, config, expected); git_config_free(cfg); + git_config_free(cfg_global); } cl_git_pass(git_remote_create_detached(&remote, "https://github.com/libgit2/libgit2")); diff --git a/tests/libgit2/worktree/config.c b/tests/libgit2/worktree/config.c index 81dcfe1fa51..c23cd044fba 100644 --- a/tests/libgit2/worktree/config.c +++ b/tests/libgit2/worktree/config.c @@ -27,7 +27,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 +45,31 @@ void test_worktree_config__set(void) cl_assert_equal_i(val, 5); git_config_free(cfg); } + +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_assert(git_config_delete_entry(cfg, "worktree.specific") == GIT_ENOTFOUND); + + cl_git_pass(git_config_delete_entry(wtcfg, "worktree.specific")); + cl_assert(git_config_get_int32(&val, cfg, "worktree.specific") == GIT_ENOTFOUND); + + git_config_free(cfg); + git_config_free(wtcfg); +}