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

* Re: [BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist
  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
  0 siblings, 1 reply; 3+ messages in thread
From: Junio C Hamano @ 2026-07-30 20:24 UTC (permalink / raw)
  To: Nils Fahldieck; +Cc: git

Nils Fahldieck <nils@fahldieck.de> writes:

> 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".

I do not understand this part.  If you have a file that is not even
readable by you, it is not very useful and no better than the case
the file did not exist.  Also, if the file exists and readable,
between a 0-byte and one liner ~/.gitconfig there shouldn't be any
difference in behaviour, no?

So, "when the file does not exist or even if the file exists is not
readble, then it is not used and the other file is used instead"
would probably be technically more correct, but I am not sure if
such a change has much value (unless you are trying to be very
pedantic).

> 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.

The documentation needs to be corrected, I think.

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

* Re: [BUG] git config --global: doc and behaviour disagree when ~/.gitconfig and XDG config file coexist
  2026-07-30 20:24 ` Junio C Hamano
@ 2026-07-31  0:14   ` Ben Knoble
  0 siblings, 0 replies; 3+ messages in thread
From: Ben Knoble @ 2026-07-31  0:14 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Nils Fahldieck, git


> Le 31 juil. 2026 à 05:35, Junio C Hamano <gitster@pobox.com> a écrit :
> 
> Nils Fahldieck <nils@fahldieck.de> writes:
> 
>> 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".
> 
> I do not understand this part.  If you have a file that is not even
> readable by you, it is not very useful and no better than the case
> the file did not exist.  Also, if the file exists and readable,
> between a 0-byte and one liner ~/.gitconfig there shouldn't be any
> difference in behaviour, no?
> 
> So, "when the file does not exist or even if the file exists is not
> readble, then it is not used and the other file is used instead"
> would probably be technically more correct, but I am not sure if
> such a change has much value (unless you are trying to be very
> pedantic).

My thoughts as well. A readable 0-byte file is not a counterexample to the docs; if such a file is readable then it exists, and is used as documented, no?

>> 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.
> 
> The documentation needs to be corrected, I think.

Agreed based on recent thread <20260720113402.0dc16abe@frustcomp.hnjs.home.arpa> (subject « git config: unintuitive behavior with - -global and - -no-includes » in case I have mis-transcribed the message ID, a necessity to maintain plain-text mail from my mobile phone, ahem).

Best,
Ben


^ 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.