[PATCH v3 0/2] config: read both home and xdg files for --global
- From
- Delilah Ashley Wu <delilahwu@linux.microsoft.com>
- Date
- Oct 9, 2026, 08:51 UTC
- Message-ID
- <20261009-fix-config-list-global-home-and-xdg-v3-0-f936b9c8a0cc@microsoft.com>
- In-Reply-To
- <pull.1938.git.1760058849.gitgitgadget@gmail.com>
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 = trueand `$XDG_CONFIG_HOME/git/config` contains:
[xdg]
config = trueThen, listing with `--global` shows only the home config:
$ git config list --global --show-scope --show-origin
global file:/Users/delilah/.gitconfig home.config=trueand getting the XDG configuration entry with `--global` will fail:
$ git config get --global xdg.config; echo $?
1Git 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=trueThe 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:
Show 9 quoted lines
> 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_sequencebuiltin/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 <lists@jade.fyi>
Reported-by: Nils Fahldieck <nils@fahldieck.de>
@@ 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