git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist

From
Nils Fahldieck <nils@fahldieck.de>
Date
Jul 30, 2026, 18:18 UTC
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

Next: Junio C Hamano
Message 1 of 7 in “[BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist”
  1. Nils FahldieckJul 30, 2026
  2. Junio C HamanoJul 30, 2026
  3. Ben KnobleJul 31, 2026
  4. Nils FahldieckJul 31, 2026
  5. Delilah Ashley WuAug 13, 2026
  6. Nils FahldieckAug 21, 2026
  7. Delilah Ashley WuAug 22, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.