From 4c0da9e5ad6b356f492eefbd48d29bdbda00a279 Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Sat, 13 Jun 2026 13:13:12 +1000 Subject: [PATCH 1/3] path: use forward slashes in XDG config on Windows Git prefers forward slashes as directory separators across all platforms. On Windows, the backslash is the native directory separator, but all Windows versions supported by Git also accept the forward slash in all but rare circumstances. Our tests expect forward slashes. Git generates relative paths with forward slashes. Forward slashes are more convenient to use in shell scripts. For these reasons, we enforced forward slashes in `interpolate_path()` in 5ca6b7bb47b (config --show-origin: report paths with forward slashes, 2016-03-23). However, other code paths may generate paths containing backslashes. For example, `config --show-origin` prints the XDG config path with mixed slashes on Windows: $ git config --list --show-origin file:C:/Program Files/Git/etc/gitconfig system.foo=bar file:"C:\\Users\\delilah/.config/git/config" xdg.foo=bar file:C:/Users/delilah/.gitconfig home.foo=bar file:.git/config local.foo=bar Let's enforce forward slashes in all code paths that directly or indirectly call `xdg_config_home_for()` by modifying it to use `convert_slashes()` on Windows. Convert slashes only in the required path segments. Calling `xdg_config_home_for(subdir, filename)` will interpolate one of two templates: either `$XDG_CONFIG_HOME//` or `$HOME/.config//`. In all call paths, the `subdir` and `filename` arguments are hardcoded to values without backslashes. This leaves `$XDG_CONFIG_HOME` and `$HOME` as the only segments that could contain a backslash, so we can restrict the `convert_slashes()` operation to those two path segments. Lastly, it's safe to perform this slash conversion because callers of `xdg_config_home_for()` handle mixed slashes correctly. If we ensure all slashes are forward slashes, it's reasonable to assume that the callers would still be able to handle it. todo(delilahwu): remove trace2 tracing. --- path.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/path.c b/path.c index c3a709a9284b7f..0107abdac3b309 100644 --- a/path.c +++ b/path.c @@ -1544,17 +1544,25 @@ int looks_like_command_line_option(const char *str) char *xdg_config_home_for(const char *subdir, const char *filename) { - const char *home, *config_home; + char *home, *config_home; assert(subdir); assert(filename); config_home = getenv("XDG_CONFIG_HOME"); - if (config_home && *config_home) + if (config_home && *config_home) { +#ifdef GIT_WINDOWS_NATIVE + convert_slashes(config_home); +#endif return mkpathdup("%s/%s/%s", config_home, subdir, filename); + } home = getenv("HOME"); - if (home) + if (home) { +#ifdef GIT_WINDOWS_NATIVE + convert_slashes(home); +#endif return mkpathdup("%s/.config/%s/%s", home, subdir, filename); + } return NULL; } From 4127dd73b5a86bd3b857248a3df72b3c1bf58b6f Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Tue, 23 Dec 2025 18:06:51 +1100 Subject: [PATCH 2/3] config: add flag to bail when no config files exist Add an argument for `do_git_config_sequence()` to return nonzero when all files in the sequence are nonexistent or cannot be read for some other reason. When bailing, the exit code is not determined by sum of the return codes of the underlying operations. Instead, the exit code is modified via a single decrement. If this is undesirable, we can change it to sum the return codes of the underlying operations instead. The next patch changes how `git config list --global` reads the global configuration. It uses this new flag to ensure the command continues to bail (as expected) when both global config files are nonexistent. Signed-off-by: Delilah Ashley Wu --- config.c | 66 +++++++++++++++++++++++++++++++++-------------- t/t1300-config.sh | 12 +++++++++ 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/config.c b/config.c index 1bdd702e7a3969..eb052601a5f07a 100644 --- a/config.c +++ b/config.c @@ -1544,11 +1544,24 @@ int git_config_system(void) return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0); } +static void attempt_git_config_from_file_with_options(config_fn_t fn, const char *filename, + void *data, enum config_scope scope, + const struct config_options *opts, + int *successful_config_count, int *cumulative_ret) +{ + int ret = git_config_from_file_with_options(fn, filename, data, scope, opts); + if (!ret) { + (*successful_config_count)++; + } + *cumulative_ret += ret; +} + static int do_git_config_sequence(const struct config_options *opts, - const struct repository *repo, - config_fn_t fn, void *data) + const struct repository *repo, config_fn_t fn, + void *data, int require_successful_config) { int ret = 0; + int successful_config_count = 0; char *system_config = git_system_config(); char *xdg_config = NULL; char *user_config = NULL; @@ -1573,32 +1586,42 @@ static int do_git_config_sequence(const struct config_options *opts, if (git_config_system() && system_config && !access_or_die(system_config, R_OK, - opts->system_gently ? ACCESS_EACCES_OK : 0)) - ret += git_config_from_file_with_options(fn, system_config, - data, CONFIG_SCOPE_SYSTEM, - NULL); + opts->system_gently ? ACCESS_EACCES_OK : 0)) { + attempt_git_config_from_file_with_options(fn, system_config, data, + CONFIG_SCOPE_SYSTEM, NULL, + &successful_config_count, &ret); + } git_global_config_paths(&user_config, &xdg_config); - if (xdg_config && !access_or_die(xdg_config, R_OK, ACCESS_EACCES_OK)) - ret += git_config_from_file_with_options(fn, xdg_config, data, - CONFIG_SCOPE_GLOBAL, NULL); + if (xdg_config && + !access_or_die(xdg_config, R_OK, ACCESS_EACCES_OK)) { + attempt_git_config_from_file_with_options(fn, xdg_config, + data, + CONFIG_SCOPE_GLOBAL, + NULL, &successful_config_count, &ret); + } - if (user_config && !access_or_die(user_config, R_OK, ACCESS_EACCES_OK)) - ret += git_config_from_file_with_options(fn, user_config, data, - CONFIG_SCOPE_GLOBAL, NULL); + if (user_config && + !access_or_die(user_config, R_OK, ACCESS_EACCES_OK)) { + attempt_git_config_from_file_with_options(fn, user_config, + data, + CONFIG_SCOPE_GLOBAL, + NULL, &successful_config_count, &ret); + } if (!opts->ignore_repo && repo_config && - !access_or_die(repo_config, R_OK, 0)) - ret += git_config_from_file_with_options(fn, repo_config, data, - CONFIG_SCOPE_LOCAL, NULL); + !access_or_die(repo_config, R_OK, 0)) { + attempt_git_config_from_file_with_options(fn, repo_config, data, + CONFIG_SCOPE_LOCAL, NULL, &successful_config_count, &ret); + } if (!opts->ignore_worktree && worktree_config && repo && repo->repository_format_worktree_config && !access_or_die(worktree_config, R_OK, 0)) { - ret += git_config_from_file_with_options(fn, worktree_config, data, - CONFIG_SCOPE_WORKTREE, - NULL); + attempt_git_config_from_file_with_options(fn, worktree_config, data, + CONFIG_SCOPE_WORKTREE, + NULL, &successful_config_count, &ret); } if (!opts->ignore_cmdline && git_config_from_parameters(fn, data) < 0) @@ -1609,6 +1632,10 @@ static int do_git_config_sequence(const struct config_options *opts, free(user_config); free(repo_config); free(worktree_config); + + if (require_successful_config && !successful_config_count) + ret--; + return ret; } @@ -1644,7 +1671,8 @@ int config_with_options(config_fn_t fn, void *data, ret = git_config_from_blob_ref(fn, repo, config_source->blob, data, config_source->scope); } else { - ret = do_git_config_sequence(opts, repo, fn, data); + ret = do_git_config_sequence(opts, repo, fn, data, + config_source && config_source->scope == CONFIG_SCOPE_GLOBAL); } if (inc.remote_urls) { diff --git a/t/t1300-config.sh b/t/t1300-config.sh index e3f8064889210a..321453bd7cba2b 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2425,6 +2425,18 @@ test_expect_success '--show-scope with --default' ' test_cmp expect actual ' +test_expect_success 'list with nonexistent global config' ' + rm -f "$HOME"/.gitconfig "$HOME"/.config/git/config && + git config ${mode_prefix}list && + git config ${mode_prefix}list --show-scope +' + +test_expect_success 'list --global with nonexistent global config' ' + rm -f "$HOME"/.gitconfig "$HOME"/.config/git/config && + test_must_fail git config ${mode_prefix}list --global && + test_must_fail git config ${mode_prefix}list --global --show-scope +' + test_expect_success 'override global and system config' ' test_when_finished rm -f \"\$HOME\"/.gitconfig && cat >"$HOME"/.gitconfig <<-EOF && From 54d8dc3368cfb3d8fa533d90a19b4ac3c3051451 Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Thu, 22 May 2025 16:32:15 +1000 Subject: [PATCH 3/3] config: read global scope via config_sequence The output of `git config list --global` should include both the home (`$HOME/.gitconfig`) and XDG (`$XDG_CONFIG_HOME/git/config`) configs, but it only reads from the former. It should include both files, to be consistent with `git config list` (not limited to `--global`), which includes entries from both files (in addition to system-wide and repository-specific entries, of course). Running `git config list --global` would only read from the home config because we assume each scope corresponds to a single config file. Under this assumption, `git config list --global` reads the global config by calling `git_config_from_file_with_options(...,"~/.gitconfig", ...)`. This function usage restricts us to a single config file. Since the global scope includes two files, we should read the configs using another method. Running `git config list --show-scope --show-origin` (without `--global`) correctly reads both the home and XDG config files. So there's existing code that respects both locations, namely the `do_git_config_sequence()` function which reads from all scopes. Introduce flags to make it possible to ignore all but the global scope (i.e. ignore system, local, worktree, and cmdline). Then, reuse the function to read only the global scope when `--global` is specified. This was the suggested solution in the bug report: https://lore.kernel.org/git/kl6ly1oze7wb.fsf@chooglen-macbookpro.roam.corp.google.com. Modify the tests to check that `git config list --global` includes both home and XDG configs. Also, add tests to ensure we do not introduce regressions to `git config list`. Specifically, check that: - The home config should take precedence over the XDG config. - Without `--global`, it should not bail on unreadable/non-existent global config files. - With `--global`, it should bail when both `$HOME/.gitconfig` and `$XDG_CONFIG_HOME/git/config` are unreadable. It should not bail if at least one of them is readable. Implementation notes: 1. The `ignore_global` flag is not set anywhere, so the `if (!opts->ignore_global)` condition is always met. We can remove this flag if desired. 2. I've assumed that `config_source->scope == CONFIG_SCOPE_GLOBAL` iff `--global` is specified. This comparison determines whether to call `do_git_config_sequence()` for the global scope, or to keep calling `git_config_from_file_with_options()` for other scopes. 3. Keep populating `opts->source.file` in `builtin/config.c` because it is used as the destination config file for write operations. The proposed changes could convolute the code because there is no single source of truth for the config file locations in the global scope. Add a comment to help clarify this. Please let me know if it's unclear. Reported-by: Jade Lovelace Suggested-by: Glen Choo Helped-by: Derrick Stolee Helped-by: Johannes Schindelin Signed-off-by: Delilah Ashley Wu --- builtin/config.c | 13 ++++++++++- config.c | 40 +++++++++++++++++--------------- config.h | 2 ++ t/t1300-config.sh | 55 ++++++++++++++++++++++++++++++++++++++++++++ t/t1306-xdg-files.sh | 3 ++- 5 files changed, 93 insertions(+), 20 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 0882899c3fbd2a..53595b11901016 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -957,6 +957,18 @@ static void location_options_init(struct config_location_options *opts, } if (opts->use_global_config) { + /* + * Since global config is sourced from more than one location, + * use `config.c#do_git_config_sequence()` with `opts->options` + * to read it. However, writing global config should point to a + * single destination, set in `opts->source.file`. + */ + opts->options.ignore_repo = 1; + opts->options.ignore_cmdline= 1; + opts->options.ignore_worktree = 1; + opts->options.ignore_system = 1; + opts->source.scope = CONFIG_SCOPE_GLOBAL; + opts->source.file = opts->file_to_free = git_global_config(); if (!opts->source.file) /* @@ -966,7 +978,6 @@ static void location_options_init(struct config_location_options *opts, * is set and points at a sane location. */ die(_("$HOME not set")); - opts->source.scope = CONFIG_SCOPE_GLOBAL; } else if (opts->use_system_config) { opts->source.file = opts->file_to_free = git_system_config(); opts->source.scope = CONFIG_SCOPE_SYSTEM; diff --git a/config.c b/config.c index eb052601a5f07a..8308ed7427f9d6 100644 --- a/config.c +++ b/config.c @@ -1584,7 +1584,7 @@ static int do_git_config_sequence(const struct config_options *opts, worktree_config = NULL; } - if (git_config_system() && system_config && + if (!opts->ignore_system && git_config_system() && system_config && !access_or_die(system_config, R_OK, opts->system_gently ? ACCESS_EACCES_OK : 0)) { attempt_git_config_from_file_with_options(fn, system_config, data, @@ -1592,22 +1592,27 @@ static int do_git_config_sequence(const struct config_options *opts, &successful_config_count, &ret); } - git_global_config_paths(&user_config, &xdg_config); + if (!opts->ignore_global) { + git_global_config_paths(&user_config, &xdg_config); - if (xdg_config && - !access_or_die(xdg_config, R_OK, ACCESS_EACCES_OK)) { - attempt_git_config_from_file_with_options(fn, xdg_config, - data, - CONFIG_SCOPE_GLOBAL, - NULL, &successful_config_count, &ret); - } + if (xdg_config && + !access_or_die(xdg_config, R_OK, ACCESS_EACCES_OK)) { + attempt_git_config_from_file_with_options(fn, xdg_config, + data, + CONFIG_SCOPE_GLOBAL, + NULL, &successful_config_count, &ret); + } - if (user_config && - !access_or_die(user_config, R_OK, ACCESS_EACCES_OK)) { - attempt_git_config_from_file_with_options(fn, user_config, - data, - CONFIG_SCOPE_GLOBAL, - NULL, &successful_config_count, &ret); + if (user_config && + !access_or_die(user_config, R_OK, ACCESS_EACCES_OK)) { + attempt_git_config_from_file_with_options(fn, user_config, + data, + CONFIG_SCOPE_GLOBAL, + NULL, &successful_config_count, &ret); + } + + free(xdg_config); + free(user_config); } if (!opts->ignore_repo && repo_config && @@ -1628,8 +1633,6 @@ static int do_git_config_sequence(const struct config_options *opts, die(_("unable to parse command-line config")); free(system_config); - free(xdg_config); - free(user_config); free(repo_config); free(worktree_config); @@ -1663,7 +1666,8 @@ int config_with_options(config_fn_t fn, void *data, */ if (config_source && config_source->use_stdin) { ret = git_config_from_stdin(fn, data, config_source->scope); - } else if (config_source && config_source->file) { + } else if (config_source && config_source->file && + config_source->scope != CONFIG_SCOPE_GLOBAL) { ret = git_config_from_file_with_options(fn, config_source->file, data, config_source->scope, NULL); diff --git a/config.h b/config.h index 31fe3e29611e11..eb2d7a2843c499 100644 --- a/config.h +++ b/config.h @@ -87,6 +87,8 @@ typedef int (*config_parser_event_fn_t)(enum config_event_t type, struct config_options { unsigned int respect_includes : 1; + unsigned int ignore_system : 1; + unsigned int ignore_global : 1; unsigned int ignore_repo : 1; unsigned int ignore_worktree : 1; unsigned int ignore_cmdline : 1; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 321453bd7cba2b..fabfea01ab4106 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2437,6 +2437,61 @@ test_expect_success 'list --global with nonexistent global config' ' test_must_fail git config ${mode_prefix}list --global --show-scope ' +test_expect_success 'list --global with only home' ' + rm -f "$HOME"/.config/git/config && + + test_when_finished rm -f \"\$HOME\"/.gitconfig && + cat >"$HOME"/.gitconfig <<-EOF && + [home] + config = true + EOF + + cat >expect <<-EOF && + global home.config=true + EOF + git config ${mode_prefix}list --global --show-scope >output && + test_cmp expect output +' + +test_expect_success 'list --global with only xdg' ' + rm -f "$HOME"/.gitconfig && + + test_when_finished rm -rf \"\$HOME\"/.config/git && + mkdir -p "$HOME"/.config/git && + cat >"$HOME"/.config/git/config <<-EOF && + [xdg] + config = true + EOF + + cat >expect <<-EOF && + global xdg.config=true + EOF + git config ${mode_prefix}list --global --show-scope >output && + test_cmp expect output +' + +test_expect_success 'list --global with both home and xdg' ' + test_when_finished rm -f \"\$HOME\"/.gitconfig && + cat >"$HOME"/.gitconfig <<-EOF && + [home] + config = true + EOF + + test_when_finished rm -rf \"\$HOME\"/.config/git && + mkdir -p "$HOME"/.config/git && + cat >"$HOME"/.config/git/config <<-EOF && + [xdg] + config = true + EOF + + cat >expect <<-EOF && + global file:$HOME/.config/git/config xdg.config=true + global file:$HOME/.gitconfig home.config=true + EOF + git config ${mode_prefix}list --global --show-scope --show-origin >output && + test_cmp expect output +' + test_expect_success 'override global and system config' ' test_when_finished rm -f \"\$HOME\"/.gitconfig && cat >"$HOME"/.gitconfig <<-EOF && diff --git a/t/t1306-xdg-files.sh b/t/t1306-xdg-files.sh index 40d3c42618c04f..475bd26abaaa81 100755 --- a/t/t1306-xdg-files.sh +++ b/t/t1306-xdg-files.sh @@ -68,7 +68,8 @@ test_expect_success 'read with --list: xdg file exists and ~/.gitconfig exists' >.gitconfig && echo "[user]" >.gitconfig && echo " name = read_gitconfig" >>.gitconfig && - echo user.name=read_gitconfig >expected && + echo user.name=read_config >expected && + echo user.name=read_gitconfig >>expected && git config --global --list >actual && test_cmp expected actual '