From: Delilah Ashley Wu Date: Fri, 09 Oct 2026 08:51:44 GMT Subject: [PATCH v3 0/2] config: read both home and xdg files for --global Message-ID: <20261009-fix-config-list-global-home-and-xdg-v3-0-f936b9c8a0cc@microsoft.com> In-Reply-To: Hi all, thanks again for your patience. I think I've addressed all the feedback from Junio's review. As reported in [1], `$HOME/.gitconfig` and `$XDG_CONFIG_HOME/git/config` are both valid global configuration locations. However, when both files exist, `git config list --global` only reads from the former location whereas `git config list` (without `--global`) reads from both. The same issue was reported for `git config get` in [2]. This inconsistency has no good justification and contradicts the documented behaviour. Suppose that `$HOME/.gitconfig` contains: [home] config = true and `$XDG_CONFIG_HOME/git/config` contains: [xdg] config = true Then, listing with `--global` shows only the home config: $ git config list --global --show-scope --show-origin global file:/Users/delilah/.gitconfig home.config=true and getting the XDG configuration entry with `--global` will fail: $ git config get --global xdg.config; echo $? 1 Git still reads the XDG config as part of its effective configuration, as shown by listing the configuration without `--global`: $ git config list --show-scope --show-origin global file:/Users/delilah/.config/git/config xdg.config=true global file:/Users/delilah/.gitconfig home.config=true The documentation, quoted in [1] and [2], states that `--global` should read from both files, so its output should be the same as above. Here's the relevant excerpt: > OPTIONS > --global:: > For writing options: write to global `~/.gitconfig` file > rather than the repository `.git/config`, write to > `$XDG_CONFIG_HOME/git/config` file if this file exists and the > `~/.gitconfig` file doesn't. > > For reading options: read only from global `~/.gitconfig` and from > `$XDG_CONFIG_HOME/git/config` rather than from all available files. To be consistent with the documentation and the behaviour without `--global`, we should read both configuration files when `--global` is passed. Writing or editing the global configuration remains unchanged and retains the current destinations as discussed in [3]. [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/xmqqo6esti9o.fsf@gitster.g/ Thanks for your time! Delilah --- Changes in v3: - Drop patch 1 ("path: use forward slashes in XDG config on Windows"), then set `XDG_CONFIG_HOME` during a `--show-origin` test so the path assertions still pass on Windows. - Drop implementation changes from v2 patch 2 ("config: let sequence require a successful file"), leaving its tests as v3 patch 1 ("t1300: test list with missing global config"). This removes `attempt_git_config_from_file_with_options()` and `success_count` in favour of a new `global_had_success` flag in v3 patch 2. - Teach `git_config_system()` to respect the new `ignore_system` flag. - Refactor `config_with_options()` changes for shorter lines. - Remove the unused `ignore_global` flag. - Test that other scopes are ignored when `--global` is specified. - Link to v2: https://patch.msgid.link/20260823-fix-config-list-global-home-and-xdg-v2-0-b29cc63f017b@microsoft.com/ Changes in v2: - Squash test-only patches into their corresponding implementation patches. - Reorder patches to prevent a regression from being introduced and then fixed in a later patch. - Narrow the scope of slash conversion to `xdg_config_home_for()` and avoid modifying `cleanup_path()`, which could've broken callers that do not expect normalised slashes. - Clarify that some tests only check the return code of a `git config` command; we do not care about the output. - Link to v1: https://patch.msgid.link/pull.1938.git.1760058849.gitgitgadget@gmail.com/ --- Delilah Ashley Wu (2): t1300: test list with missing global config config: read global scope via config_sequence builtin/config.c | 11 +++++ builtin/var.c | 4 +- config.c | 50 +++++++++++++++------- config.h | 3 +- t/t1300-config.sh | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ t/t1306-xdg-files.sh | 5 ++- 6 files changed, 172 insertions(+), 17 deletions(-) Range-diff versus v2: 1: 5a2a98c38b < -: ---------- path: use forward slashes in XDG config on Windows 2: 75e1deab27 < -: ---------- config: let sequence require a successful file -: ---------- > 1: 283725f0c4 t1300: test list with missing global config 3: 4a6f67fdf4 ! 2: 482d05e24d config: read global scope via config_sequence @@ Commit message 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]. + an `ignore_system` flag and modify `git_config_system()` to respect it. + This makes `config_options` the primary way to disable parts of the + configuration sequence and allows callers of `do_git_config_sequence()` + to ignore all but the global scope (i.e. ignore system, local, worktree, + and cmdline). 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. + The previous patch, "t1300: test list with missing global config", + recorded existing behaviour: when no global configuration file exists, + `git config list` should succeed whereas `git config list --global` + should fail. Since the configuration sequence gracefully ignores missing + global configuration files, track whether we successfully read at least + one of them and adjust the return code accordingly. - - 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. + Keep populating `opts->source.file` in `builtin/config.c` as the + destination for write operations. Read operations use the configuration + sequence instead, which can be confusing. Add a comment to explain this + distinction. - 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. + Lastly, modify tests to check that both home and XDG configuration files + are respected during `--global` read operations. [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 + [3] https://lore.kernel.org/git/kl6ly1oze7wb.fsf@chooglen-macbookpro.roam.corp.google.com/ Reported-by: Jade Lovelace Reported-by: Nils Fahldieck @@ builtin/config.c: static void location_options_init(struct config_location_optio if (!opts->source.file) /* + ## builtin/var.c ## +@@ builtin/var.c: 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; + ## config.c ## +@@ config.c: 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 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_global_scope_success) + { ++ int tmp_ret; + int ret = 0; ++ int global_had_success = 0; + char *system_config = git_system_config(); + char *xdg_config = NULL; + char *user_config = NULL; @@ config.c: 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 && ++ if (git_config_system(opts) && 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, - CONFIG_SCOPE_SYSTEM, NULL, - &success_count, &ret); + ret += git_config_from_file_with_options(fn, system_config, +@@ config.c: static int do_git_config_sequence(const struct config_options *opts, -- git_global_config_paths(&user_config, &xdg_config); -+ if (!opts->ignore_global) { -+ git_global_config_paths(&user_config, &xdg_config); + 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, &success_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, &success_count, &ret); +- 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)) { ++ tmp_ret = git_config_from_file_with_options(fn, xdg_config, ++ data, CONFIG_SCOPE_GLOBAL, ++ NULL); ++ ret += tmp_ret; ++ if (!tmp_ret) ++ global_had_success = 1; ++ } - 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, &success_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, &success_count, &ret); -+ -+ free(xdg_config); -+ free(user_config); +- 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)) { ++ tmp_ret = git_config_from_file_with_options(fn, user_config, ++ data, CONFIG_SCOPE_GLOBAL, ++ NULL); ++ ret += tmp_ret; ++ if (!tmp_ret) ++ global_had_success = 1; + } if (!opts->ignore_repo && repo_config && !access_or_die(repo_config, R_OK, 0)) @@ config.c: 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(user_config); free(repo_config); free(worktree_config); ++ ++ if (require_global_scope_success && !global_had_success && !ret) ++ ret = -1; ++ + return ret; + } @@ config.c: int config_with_options(config_fn_t fn, void *data, + } + + /* +- * If we have a specific filename, use it. Otherwise, follow the +- * regular lookup sequence. ++ * Use the specified file when provided, except for the global scope, ++ * which can come from more than one file. With no explicit source, ++ * follow the regular lookup sequence. */ 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) { ++ config_source->scope == CONFIG_SCOPE_GLOBAL) { ++ ret = do_git_config_sequence(opts, repo, fn, data, 1); + } else if (config_source && config_source->file) { ret = git_config_from_file_with_options(fn, config_source->file, data, config_source->scope, - NULL); @@ config.c: 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, 0); -+ ret = do_git_config_sequence(opts, repo, fn, data, -+ config_source && config_source->scope == CONFIG_SCOPE_GLOBAL); +- ret = do_git_config_sequence(opts, repo, fn, data); ++ ret = do_git_config_sequence(opts, repo, fn, data, 0); } if (inc.remote_urls) { @@ config.h: 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; +@@ config.h: 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()) ## t/t1300-config.sh ## @@ t/t1300-config.sh: test_expect_success 'list --global with nonexistent global config fails' ' @@ t/t1300-config.sh: test_expect_success 'list --global with nonexistent global co + 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 && ++ # Git uses HOME and XDG_CONFIG_HOME to determine the location of the ++ # XDG config file. On Windows, the default values lead to mixed forward ++ # and backward slashes in the output --show-origin path for XDG config. ++ # Explicitly set XDG_CONFIG_HOME with only forward slashes, allowing us ++ # to use the same expected path across all platforms. ++ XDG_CONFIG_HOME="$HOME/.config" \ ++ git config ${mode_prefix}list --global \ ++ --show-scope --show-origin >actual && + test_cmp expect actual && + + echo xdg >expect && @@ t/t1300-config.sh: test_expect_success 'list --global with nonexistent global co + git config ${mode_get} --global home.config >actual && + test_cmp expect actual +' ++ ++test_expect_success 'list --global ignores other scopes' ' ++ test_when_finished rm -f system_config && ++ cat >system_config <<-EOF && ++ [system] ++ config = true ++ EOF ++ ++ test_config core.repositoryformatversion 1 && ++ test_config extensions.worktreeConfig true && ++ test_config --worktree worktree.config true && ++ ++ test_when_finished rm -f \"\$HOME\"/.gitconfig && ++ cat >"$HOME"/.gitconfig <<-EOF && ++ [home] ++ config = home ++ EOF ++ ++ test_config repo.config true && ++ ++ echo "global home.config=home" >expect && ++ GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=system_config \ ++ git -c cmdline.config=true config ${mode_prefix}list \ ++ --global --show-scope >actual && ++ test_cmp expect actual ++' + test_expect_success 'override global and system config' ' test_when_finished rm -f \"\$HOME\"/.gitconfig && --- base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd change-id: 20260808-fix-config-list-global-home-and-xdg-9bcaac093a1b