All of lore.kernel.org
 help / color / mirror / Atom feed
* [BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist
@ 2026-07-30 18:18 Nils Fahldieck
  2026-07-30 20:24 ` Junio C Hamano
  0 siblings, 1 reply; 3+ messages in thread
From: Nils Fahldieck @ 2026-07-30 18:18 UTC (permalink / raw)
  To: git

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

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-07-31  0:14 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30 18:18 [BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist Nils Fahldieck
2026-07-30 20:24 ` Junio C Hamano
2026-07-31  0:14   ` Ben Knoble

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.