[BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist
Nils Fahldieck <[email protected]> Thu, 30 Jul 2026 20:18:43 +0200
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <CAAdFe9yhBk-WecVzCTsjQ-4Z3AZAbpP+w+B076ouM3qX6d1WAg@mail.gmail.com> |
Hi,
I ran into a confusing discrepancy between what the git-config(1) man
page promises for --global and what the code actually does when both
~/.gitconfig and ~/.config/git/config exist.
BACKGROUND
----------
Git recognises two "global" config files (described in the FILES
section of git-config(1)):
$XDG_CONFIG_HOME/git/config
(falls back to ~/.config/git/config when $XDG_CONFIG_HOME is unset)
~/.gitconfig
When --global is NOT passed, git config --get reads both files via
do_git_config_sequence() in config.c. The XDG file is processed first,
~/.gitconfig second, so ~/.gitconfig takes precedence on any key that
appears in both. This is correct and consistent with the documentation.
The bug is in what happens when --global IS explicitly passed.
WHAT THE DOCS CLAIM
-------------------
From Documentation/git-config.adoc, the --global entry reads:
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.
WHAT THE CODE ACTUALLY DOES
----------------------------
Both read and write with --global go through the same function,
git_global_config() in config.c (around line 1505), which returns
exactly ONE path. That single path is then set as the only source
file in builtin/config.c (around line 960).
The selection logic in git_global_config() is:
/* access_or_warn returns non-zero on FAILURE, zero on success */
if (access_or_warn(user_config, R_OK, 0) && xdg_config &&
!access_or_warn(xdg_config, R_OK, 0)) {
return xdg_config; /* ~/.gitconfig unreadable AND XDG readable */
} else {
return user_config; /* otherwise always return ~/.gitconfig */
}
This means:
1. The writing claim is inaccurate.
The docs say XDG is used when ~/.gitconfig "doesn't exist". The
code tests READABILITY (R_OK), not existence. A zero-byte file
created by "touch ~/.gitconfig" is readable, so access_or_warn
returns 0 (success), the condition is false, and XDG is silently
ignored even though ~/.gitconfig is empty.
The condition should be described as "when ~/.gitconfig is not
readable", not "when it doesn't exist".
2. The reading claim is outright wrong.
The docs say --global reads from BOTH files. The code reads from
ONE. git_global_config() selects a winner and frees the other
path. There is no code path under --global that reads both files.
REPRODUCER
----------
# Setup: only the XDG file exists and contains user.name = "My Name"
$ ls ~/.config/git/config # exists, has user.name
$ ls ~/.gitconfig # does not exist
$ git config --get user.name
My Name
$ git config --global user.name
My Name
$ touch ~/.gitconfig # create empty but readable ~/.gitconfig
$ git config --get user.name
My Name # correct: reads both, XDG value survives
$ git config --global user.name
# BUG: empty output -- git_global_config()
# returned ~/.gitconfig (readable but empty)
# and silently discarded the XDG file
$ rm ~/.gitconfig
$ git config --global user.name
My Name # back to normal: ~/.gitconfig gone, XDG used
Tested on macOS with git version 2.55.0 built and installed via Homebrew.
THE FIX -- TWO OPTIONS
----------------------
Option A -- Fix the code to match the documented intent (preferred):
Make git config --global for reading behave like do_git_config_sequence():
read both global files when both are accessible and let later values
take precedence (i.e. ~/.gitconfig wins over XDG, same as normal reads).
This is what the documentation describes and what users expect.
Option B -- Fix the docs to match the actual code:
Document the real rule: "--global selects a single file: ~/.gitconfig
if it is readable, otherwise the XDG file if it is readable."
Option B alone closes the documentation bug but leaves the underlying
asymmetry: "git config --get" and "git config --global" silently
disagree whenever both global files coexist. That asymmetry is a
usability bug in its own right regardless of what the docs say.
I prefer Option A.
RELEVANT CODE LOCATIONS
-----------------------
config.c ~1505-1523 git_global_config() -- picks one file
config.c ~1525-1537 git_global_config_paths() -- builds both paths
config.c ~1580-1586 do_git_config_sequence() -- reads both correctly
builtin/config.c ~960 --global wires git_global_config() as sole source
path.c ~1545-1560 xdg_config_home_for() -- XDG fallback to ~/.config/
Documentation/git-config.adoc ~147-154 the inaccurate --global entry
I am interested in actually contributing a patch once we agreed on a fix. If the
behaviour is intended, though, I am interested in the reasoning. Maybe it is
also a translation issue since English is not my native language.
Kind regards
Nils