diff --git a/include/git2/config.h b/include/git2/config.h index 5a2f956fd..724788ae0 100644 --- a/include/git2/config.h +++ b/include/git2/config.h @@ -194,6 +194,21 @@ GIT_EXTERN(int) git_config_open_level( const git_config *parent, unsigned int level); +/** + * Open the global/XDG configuration file according to git's rules + * + * Git allows you to store your global configuration at + * `$HOME/.config` or `$XDG_CONFIG_HOME/git/config`. For backwards + * compatability, the XDG file shouldn't be used unless the use has + * created it explicitly. With this function you'll open the correct + * one to write to. + * + * @param out pointer in which to store the config object + * @param config the config object in which to look + */ +GIT_EXTERN(int) git_config_open_global(git_config **out, git_config *config); + + /** * Reload changed config files * diff --git a/src/config.c b/src/config.c index 3f475ea63..bd629f7c4 100644 --- a/src/config.c +++ b/src/config.c @@ -91,13 +91,15 @@ int git_config_add_file_ondisk( int force) { git_config_backend *file = NULL; + struct stat st; int res; assert(cfg && path); - if (!git_path_isfile(path)) { - giterr_set(GITERR_CONFIG, "Cannot find config file '%s'", path); - return GIT_ENOTFOUND; + res = p_stat(path, &st); + if (res < 0 && errno != ENOENT) { + giterr_set(GITERR_CONFIG, "Error stat'ing config file '%s'", path); + return -1; } if (git_config_file__ondisk(&file, path) < 0) @@ -225,6 +227,14 @@ static int git_config__add_internal( return 0; } +int git_config_open_global(git_config **cfg_out, git_config *cfg) +{ + if (!git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_XDG)) + return 0; + + return git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_GLOBAL); +} + int git_config_open_level( git_config **cfg_out, const git_config *cfg_parent, @@ -381,7 +391,6 @@ int git_config_set_string(git_config *cfg, const char *name, const char *value) internal = git_vector_get(&cfg->files, 0); if (!internal) - /* Should we auto-vivify .git/config? Tricky from this location */ return config_error_nofiles(name); file = internal->file; @@ -598,6 +607,33 @@ int git_config_find_system(char *system_config_path, size_t length) system_config_path, length, git_config_find_system_r); } +int git_config__global_location(git_buf *buf) +{ + const git_buf *paths; + const char *sep, *start; + size_t len; + + if (git_futils_dirs_get(&paths, GIT_FUTILS_DIR_GLOBAL) < 0) + return -1; + + /* no paths, so give up */ + if (git_buf_len(paths) == 0) + return -1; + + start = git_buf_cstr(paths); + sep = strchr(start, GIT_PATH_LIST_SEPARATOR); + + if (sep) + len = sep - start; + else + len = paths->size; + + if (git_buf_set(buf, start, len) < 0) + return -1; + + return git_buf_joinpath(buf, buf->ptr, GIT_CONFIG_FILENAME_GLOBAL); +} + int git_config_open_default(git_config **out) { int error; @@ -606,9 +642,12 @@ int git_config_open_default(git_config **out) error = git_config_new(&cfg); - if (!error && !git_config_find_global_r(&buf)) + if (!error && (!git_config_find_global_r(&buf) || + !git_config__global_location(&buf))) { error = git_config_add_file_ondisk(cfg, buf.ptr, GIT_CONFIG_LEVEL_GLOBAL, 0); + } else { + } if (!error && !git_config_find_xdg_r(&buf)) error = git_config_add_file_ondisk(cfg, buf.ptr, diff --git a/src/config.h b/src/config.h index c43e47e82..c5c11ae14 100644 --- a/src/config.h +++ b/src/config.h @@ -28,6 +28,9 @@ extern int git_config_find_global_r(git_buf *global_config_path); extern int git_config_find_xdg_r(git_buf *system_config_path); extern int git_config_find_system_r(git_buf *system_config_path); + +extern int git_config__global_location(git_buf *buf); + extern int git_config_rename_section( git_repository *repo, const char *old_section_name, /* eg "branch.dummy" */ diff --git a/src/repository.c b/src/repository.c index 44e7ca3c4..e2cedc0f7 100644 --- a/src/repository.c +++ b/src/repository.c @@ -594,6 +594,10 @@ int git_repository_config__weakptr(git_config **out, git_repository *repo) git_config_find_xdg_r(&xdg_buf); git_config_find_system_r(&system_buf); + /* If there is no global file, open a backend for it anyway */ + if (git_buf_len(&global_buf) == 0) + git_config__global_location(&global_buf); + error = load_config( &config, repo, path_unless_empty(&global_buf), diff --git a/tests-clar/config/global.c b/tests-clar/config/global.c new file mode 100644 index 000000000..2ecdf97d8 --- /dev/null +++ b/tests-clar/config/global.c @@ -0,0 +1,67 @@ +#include "clar_libgit2.h" +#include "buffer.h" +#include "fileops.h" + +void test_config_global__initialize(void) +{ + git_buf path = GIT_BUF_INIT; + + cl_must_pass(p_mkdir("home", 0777)); + cl_git_pass(git_path_prettify(&path, "home", NULL)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, path.ptr)); + + cl_must_pass(p_mkdir("xdg", 0777)); + cl_git_pass(git_path_prettify(&path, "xdg", NULL)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_SYSTEM, path.ptr)); + + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_XDG, NULL)); + + git_buf_free(&path); +} + +void test_config_global__cleanup(void) +{ + cl_git_pass(git_futils_rmdir_r("home", NULL, GIT_RMDIR_REMOVE_FILES)); + cl_git_pass(git_futils_rmdir_r("xdg", NULL, GIT_RMDIR_REMOVE_FILES)); +} + +void test_config_global__open_global(void) +{ + git_config *cfg, *global, *selected, *dummy; + + cl_git_pass(git_config_open_default(&cfg)); + cl_git_pass(git_config_open_level(&global, cfg, GIT_CONFIG_LEVEL_GLOBAL)); + cl_git_fail(git_config_open_level(&dummy, cfg, GIT_CONFIG_LEVEL_XDG)); + cl_git_pass(git_config_open_global(&selected, cfg)); + + git_config_free(selected); + git_config_free(global); + git_config_free(cfg); +} + +void test_config_global__open_xdg(void) +{ + git_config *cfg, *xdg, *selected; + const char *val, *str = "teststring"; + const char *key = "this.variable"; + + p_setenv("XDG_CONFIG_HOME", "xdg", 1); + + cl_must_pass(p_mkdir("xdg/git/", 0777)); + cl_git_mkfile("xdg/git/config", ""); + + cl_git_pass(git_config_open_default(&cfg)); + cl_git_pass(git_config_open_level(&xdg, cfg, GIT_CONFIG_LEVEL_XDG)); + cl_git_pass(git_config_open_global(&selected, cfg)); + + cl_git_pass(git_config_set_string(xdg, key, str)); + cl_git_pass(git_config_get_string(&val, selected, key)); + cl_assert_equal_s(str, val); + + git_config_free(selected); + git_config_free(xdg); + git_config_free(cfg); +} diff --git a/tests-clar/config/read.c b/tests-clar/config/read.c index c85826886..9f943d0f6 100644 --- a/tests-clar/config/read.c +++ b/tests-clar/config/read.c @@ -449,10 +449,3 @@ void test_config_read__can_load_and_parse_an_empty_config_file(void) git_config_free(cfg); } - -void test_config_read__cannot_load_a_non_existing_config_file(void) -{ - git_config *cfg; - - cl_assert_equal_i(GIT_ENOTFOUND, git_config_open_ondisk(&cfg, "./no.config")); -} diff --git a/tests-clar/repo/config.c b/tests-clar/repo/config.c new file mode 100644 index 000000000..086fb5e4f --- /dev/null +++ b/tests-clar/repo/config.c @@ -0,0 +1,75 @@ +#include "clar_libgit2.h" +#include "fileops.h" +#include + +git_buf path = GIT_BUF_INIT; + +void test_repo_config__initialize(void) +{ + cl_fixture_sandbox("empty_standard_repo"); + cl_git_pass(cl_rename("empty_standard_repo/.gitted", "empty_standard_repo/.git")); + + git_buf_clear(&path); + + cl_must_pass(p_mkdir("alternate", 0777)); + cl_git_pass(git_path_prettify(&path, "alternate", NULL)); + +} + +void test_repo_config__cleanup(void) +{ + cl_git_pass(git_futils_rmdir_r(path.ptr, NULL, GIT_RMDIR_REMOVE_FILES)); + + git_buf_free(&path); + cl_fixture_cleanup("empty_standard_repo"); +} + +void test_repo_config__open_missing_global(void) +{ + git_repository *repo; + git_config *config, *global; + + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, path.ptr)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_SYSTEM, path.ptr)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_XDG, path.ptr)); + + cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); + cl_git_pass(git_repository_config(&config, repo)); + cl_git_pass(git_config_open_level(&global, config, GIT_CONFIG_LEVEL_GLOBAL)); + + cl_git_pass(git_config_set_string(global, "test.set", "42")); + + git_config_free(global); + git_config_free(config); + git_repository_free(repo); +} + +void test_repo_config__open_missing_global_with_separators(void) +{ + git_repository *repo; + git_config *config, *global; + + cl_git_pass(git_buf_printf(&path, "%c%s", GIT_PATH_LIST_SEPARATOR, "dummy")); + + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, path.ptr)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_SYSTEM, path.ptr)); + cl_git_pass(git_libgit2_opts( + GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_XDG, path.ptr)); + + git_buf_free(&path); + + cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); + cl_git_pass(git_repository_config(&config, repo)); + cl_git_pass(git_config_open_level(&global, config, GIT_CONFIG_LEVEL_GLOBAL)); + + cl_git_pass(git_config_set_string(global, "test.set", "42")); + + git_config_free(global); + git_config_free(config); + git_repository_free(repo); +} diff --git a/tests-clar/repo/open.c b/tests-clar/repo/open.c index 6b5253797..840858586 100644 --- a/tests-clar/repo/open.c +++ b/tests-clar/repo/open.c @@ -309,7 +309,7 @@ void test_repo_open__no_config(void) cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); cl_git_pass(git_repository_config(&config, repo)); - cl_git_fail(git_config_set_string(config, "test.set", "42")); + cl_git_pass(git_config_set_string(config, "test.set", "42")); git_config_free(config); git_repository_free(repo);