From 3fa31eec8bcd3aad0e4c0b3db9c3b24692258a33 Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Sat, 13 Jun 2026 13:13:12 +1000 Subject: [PATCH 1/4] 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 displays 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 construct 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 These mixed slashes occur because the `$HOME` and `$XDG_CONFIG_HOME` environment variables usually contain backslashes on Windows, and `xdg_config_home_for()` interpolates them into templates that use hardcoded forward slashes. Since callers of `xdg_config_home_for()` handle mixed slashes correctly, it is reasonable to assume that they can handle paths with only forward slashes. Let's enforce forward slashes in `xdg_config_home_for()` by using `convert_slashes()` on Windows. Also, there are no tests for the XDG path with `--show-origin`. Add a test for slash conversion and a confidence check for the default path. Signed-off-by: Delilah Ashley Wu --- path.c | 16 ++++++++++------ t/t1300-config.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/path.c b/path.c index c3a709a9284b7f..f17595fd1bbb2b 100644 --- a/path.c +++ b/path.c @@ -1544,19 +1544,23 @@ int looks_like_command_line_option(const char *str) char *xdg_config_home_for(const char *subdir, const char *filename) { + char *ret; const char *home, *config_home; assert(subdir); assert(filename); config_home = getenv("XDG_CONFIG_HOME"); if (config_home && *config_home) - return mkpathdup("%s/%s/%s", config_home, subdir, filename); - - home = getenv("HOME"); - if (home) - return mkpathdup("%s/.config/%s/%s", home, subdir, filename); + ret = mkpathdup("%s/%s/%s", config_home, subdir, filename); + else if ((home = getenv("HOME"))) + ret = mkpathdup("%s/.config/%s/%s", home, subdir, filename); + else + return NULL; - return NULL; +#ifdef GIT_WINDOWS_NATIVE + convert_slashes(ret); +#endif + return ret; } char *xdg_config_home(const char *filename) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index e3f8064889210a..329407a73dc066 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2350,6 +2350,38 @@ test_expect_success '--show-origin with --default' ' test_cmp expect actual ' +test_expect_success 'set up xdg config --show-origin tests' ' + mkdir -p "$HOME"/.config/git && + cat >"$HOME"/.config/git/config <<-EOF + [xdg] + config = true + EOF +' + +test_expect_success MINGW '--show-origin converts backslashes in xdg path to forward slashes on Windows' ' + backslash_home="$(echo "$HOME" | tr / \\\\)" && + echo "file:$HOME/.config/git/config true" >expect && + + ( + sane_unset XDG_CONFIG_HOME && + HOME="$backslash_home" git config ${mode_get} --show-origin xdg.config >actual + ) && + test_cmp expect actual && + + XDG_CONFIG_HOME="$backslash_home\\.config" git config ${mode_get} --show-origin xdg.config >actual && + test_cmp expect actual +' + +test_expect_success '--show-origin with default xdg path' ' + echo "file:$HOME/.config/git/config true" >expect && + git config ${mode_get} --show-origin xdg.config >actual && + test_cmp expect actual +' + +test_expect_success 'clean up xdg config --show-origin tests' ' + rm -rf "$HOME"/.config/git +' + test_expect_success '--show-scope with --list' ' cat >expect <<-EOF && global user.global=true From 032481f7ce1fc1ecd2cf5536a861cab2c283896f Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Tue, 23 Dec 2025 18:06:51 +1100 Subject: [PATCH 2/4] config: let sequence require a successful file Teach `do_git_config_sequence()` to optionally report an error if no configuration files in the sequence were successfully processed. Gate this new behaviour with a flag and keep it disabled for now. Add tests to record existing behaviour and prevent regressions in the next patch, "config: read global scope via config_sequence", which adds a code path that enables the flag. When no global configuration file exists, `git config list` succeeds whereas `git config list --global` fails. Only check the exit code, as we're not interested in the exact output messages. Signed-off-by: Delilah Ashley Wu --- config.c | 48 ++++++++++++++++++++++++++++++----------------- t/t1300-config.sh | 12 ++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/config.c b/config.c index 1bdd702e7a3969..c94888c79274ab 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 int try_config(config_fn_t fn, const char *filename, + void *data, enum config_scope scope, + const struct config_options *opts, + int *success_count) +{ + int ret = git_config_from_file_with_options(fn, filename, data, + scope, opts); + if (!ret) + (*success_count)++; + return 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 success_count = 0; char *system_config = git_system_config(); char *xdg_config = NULL; char *user_config = NULL; @@ -1574,32 +1587,29 @@ 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); + ret += try_config(fn, system_config, data, CONFIG_SCOPE_SYSTEM, + NULL, &success_count); 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); + ret += try_config(fn, xdg_config, data, CONFIG_SCOPE_GLOBAL, + NULL, &success_count); 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); + ret += try_config(fn, user_config, data, CONFIG_SCOPE_GLOBAL, + NULL, &success_count); 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); + ret += try_config(fn, repo_config, data, CONFIG_SCOPE_LOCAL, + NULL, &success_count); 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); - } + !access_or_die(worktree_config, R_OK, 0)) + ret += try_config(fn, worktree_config, data, CONFIG_SCOPE_WORKTREE, + NULL, &success_count); if (!opts->ignore_cmdline && git_config_from_parameters(fn, data) < 0) die(_("unable to parse command-line config")); @@ -1609,6 +1619,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 && !success_count && !ret) + ret = -1; + return ret; } @@ -1644,7 +1658,7 @@ 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, 0); } if (inc.remote_urls) { diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 329407a73dc066..2ce85b76ff7ef2 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2457,6 +2457,18 @@ test_expect_success '--show-scope with --default' ' test_cmp expect actual ' +test_expect_success 'list with nonexistent global config gracefully exits' ' + 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 fails' ' + 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 d6bab74f7b1bd402b5a9b83ccf0ab2d9b116e200 Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Wed, 2 Sep 2026 22:06:36 +1000 Subject: [PATCH 3/4] config: optionally ignore system scope in sequence Add an `ignore_system` option so callers of `do_config_sequence()` can choose to ignore the system scope. This is required for the next patch, config: read global scope via config_sequence, which calls `do_config_sequence()` to read only the global configuration (by ignoring every scope except the global scope). As a side effect, teach `git_config_system()` to respect the new `config_options.ignore_system` flag (in addition to checking the `GIT_CONFIG_NOSYSTEM` environment variable) when determining whether to read the system configuration. The alternative is to get rid of the `git_config_system()` function, have its callers check the `ignore_system` flag themselves, and hoist the environment variable check into instantiators of `struct config_options`. This would blur the separation of caller intention and effective behaviour, making the approach undesirable. The struct `config_options` represents what the caller wants to read. Environment variables that are used to override the caller's intention are a separate concern and should continue to be handled in `do_config_sequence()`. In 4179b489 config: allow overriding of global and system configuration, we introduced the `GIT_CONFIG_GLOBAL` environment variable and stated that the preferred method to disable reading of the global configuration is to set `GIT_CONFIG_GLOBAL=/dev/null`. The reconciliation of `config_options` with `GIT_CONFIG_GLOBAL` occurs in `do_config_sequence()`, so we should continue to maintain the separation of caller intention and effective behaviour. Signed-off-by: Delilah Ashley Wu --- builtin/var.c | 3 ++- config.c | 6 +++--- config.h | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/builtin/var.c b/builtin/var.c index cc3a43cde25a6c..da990e3a6ab0a9 100644 --- a/builtin/var.c +++ b/builtin/var.c @@ -82,7 +82,8 @@ static char *git_attr_val_global(int ident_flag UNUSED) static char *git_config_val_system(int ident_flag UNUSED) { - if (git_config_system()) { + const struct config_options opts = { 0 }; + if (git_config_system(&opts)) { char *file = git_system_config(); normalize_path_copy(file, file); return file; diff --git a/config.c b/config.c index c94888c79274ab..43d4c774f3153c 100644 --- a/config.c +++ b/config.c @@ -1539,9 +1539,9 @@ void git_global_config_paths(char **user_out, char **xdg_out) *xdg_out = xdg_config; } -int git_config_system(void) +int git_config_system(const struct config_options *opts) { - return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0); + return !opts->ignore_system && !git_env_bool("GIT_CONFIG_NOSYSTEM", 0); } static int try_config(config_fn_t fn, const char *filename, @@ -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 (git_config_system(opts) && system_config && !access_or_die(system_config, R_OK, opts->system_gently ? ACCESS_EACCES_OK : 0)) ret += try_config(fn, system_config, data, CONFIG_SCOPE_SYSTEM, diff --git a/config.h b/config.h index 31fe3e29611e11..448b8b5967d907 100644 --- a/config.h +++ b/config.h @@ -87,6 +87,7 @@ 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_repo : 1; unsigned int ignore_worktree : 1; unsigned int ignore_cmdline : 1; @@ -408,7 +409,7 @@ int repo_config_rename_section(struct repository *, const char *, const char *); int repo_config_rename_section_in_file(struct repository *, const char *, const char *, const char *); int repo_config_copy_section(struct repository *, const char *, const char *); int repo_config_copy_section_in_file(struct repository *, const char *, const char *, const char *); -int git_config_system(void); +int git_config_system(const struct config_options *opts); int config_error_nonbool(const char *); #if defined(__GNUC__) #define config_error_nonbool(s) (config_error_nonbool(s), const_error()) From 6c6cc9c2cc333425b6076909040acb118eeed30e Mon Sep 17 00:00:00 2001 From: Delilah Ashley Wu Date: Thu, 22 May 2025 16:32:15 +1000 Subject: [PATCH 4/4] config: read global scope via config_sequence When both `$HOME/.gitconfig` and `$XDG_CONFIG_HOME/git/config` exist, `git config list --global` and `git config get --global` read the home configuration file but ignore the XDG file. Bug reporters expected these `--global` scoped commands to read both files [1][2], which would be consistent with the documentation and the behaviour of the unscoped variants. For example, `git config list` and `git config get` (without `--global`) read from both files (in addition to system-wide and repository-specific entries). We should address this inconsistency by respecting both files during `--global` read operations. The implementation assumes that each configuration scope corresponds to a single file. So during `--global` read operations, Git selects one file path to pass to `git_config_from_file_with_options(file)`. Because the global scope can come from more than one file, we should use another method to read the global configuration. Since `git config list --show-scope --show-origin` reads both the home and XDG files, there must be existing code that respects both locations, namely `do_git_config_sequence()` which reads from all scopes. Introduce flags 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 [3] in the original bug report [1]. Modify tests to check that both configuration files are respected during `--global` read operations. Also, add additional tests to supplement the regression tests from the previous patch, "config: let sequence require a successful file". The expected behaviour of `git config list` is: - 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 when one or more of them is readable. Implementation notes: - The `ignore_global` flag is not set anywhere, so the `if (!opts->ignore_global)` condition is always met. Include the flag for completeness, but we can remove it if desired. - 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 clarify this. [1] https://lore.kernel.org/git/CAFA9we-QLQRzJdGMMCPatmfrk1oHeiUu9msMRXXk1MLE5HRxBQ@mail.gmail.com/ [2] https://lore.kernel.org/git/CAAdFe9yhBk-WecVzCTsjQ-4Z3AZAbpP+w+B076ouM3qX6d1WAg@mail.gmail.com/ [3] https://lore.kernel.org/git/kl6ly1oze7wb.fsf@chooglen-macbookpro.roam.corp.google.com Reported-by: Jade Lovelace Reported-by: Nils Fahldieck Suggested-by: Glen Choo Helped-by: Derrick Stolee Helped-by: Johannes Schindelin Signed-off-by: Delilah Ashley Wu --- builtin/config.c | 11 +++++++ config.c | 10 +++++-- t/t1300-config.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++ t/t1306-xdg-files.sh | 5 +++- 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/builtin/config.c b/builtin/config.c index 0882899c3fbd2a..a7468e86d3e5b7 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -957,6 +957,17 @@ 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, + * read it using `do_git_config_sequence()` with other scopes + * ignored. 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.file = opts->file_to_free = git_global_config(); if (!opts->source.file) /* diff --git a/config.c b/config.c index 43d4c774f3153c..68db03628424e2 100644 --- a/config.c +++ b/config.c @@ -1651,9 +1651,13 @@ 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) { - ret = git_config_from_file_with_options(fn, config_source->file, - data, config_source->scope, - NULL); + if (config_source->scope == CONFIG_SCOPE_GLOBAL) { + ret = do_git_config_sequence(opts, repo, fn, data, 1); + } else { + ret = git_config_from_file_with_options(fn, config_source->file, + data, config_source->scope, + NULL); + } } else if (config_source && config_source->blob) { ret = git_config_from_blob_ref(fn, repo, config_source->blob, data, config_source->scope); diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 2ce85b76ff7ef2..b6fd6e24ea18ca 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2469,6 +2469,77 @@ test_expect_success 'list --global with nonexistent global config fails' ' test_must_fail git config ${mode_prefix}list --global --show-scope ' +test_expect_success 'list and get --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 >actual && + test_cmp expect actual && + + echo true >expect && + git config ${mode_get} --global home.config >actual && + test_cmp expect actual +' + +test_expect_success 'list and get --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 >actual && + test_cmp expect actual && + + echo true >expect && + git config ${mode_get} --global xdg.config >actual && + test_cmp expect actual +' + +test_expect_success 'list and get --global with both home and xdg' ' + test_when_finished rm -f \"\$HOME\"/.gitconfig && + cat >"$HOME"/.gitconfig <<-EOF && + [home] + config = home + EOF + + test_when_finished rm -rf \"\$HOME\"/.config/git && + mkdir -p "$HOME"/.config/git && + cat >"$HOME"/.config/git/config <<-EOF && + [xdg] + config = xdg + EOF + + cat >expect <<-EOF && + global file:$HOME/.config/git/config xdg.config=xdg + global file:$HOME/.gitconfig home.config=home + EOF + git config ${mode_prefix}list --global --show-scope --show-origin >actual && + test_cmp expect actual && + + echo xdg >expect && + git config ${mode_get} --global xdg.config >actual && + test_cmp expect actual && + + echo home >expect && + git config ${mode_get} --global home.config >actual && + test_cmp expect actual +' + 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..3a9a04bcc143fd 100755 --- a/t/t1306-xdg-files.sh +++ b/t/t1306-xdg-files.sh @@ -52,6 +52,8 @@ test_expect_success 'read with --get: xdg file exists and ~/.gitconfig exists' ' echo " name = read_gitconfig" >>.gitconfig && echo read_gitconfig >expected && git config --get user.name >actual && + test_cmp expected actual && + git config --global --get user.name >actual && test_cmp expected actual ' @@ -68,7 +70,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 '