Skip to content

Read global git config behind symlinked dirs#1278

Merged
Soph merged 3 commits into
mainfrom
soph/config-bug
May 27, 2026
Merged

Read global git config behind symlinked dirs#1278
Soph merged 3 commits into
mainfrom
soph/config-bug

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented May 27, 2026

https://entire.io/gh/entireio/cli/trails/434

Problem

A customer reported a flood of warnings during checkpoint push:

[entire] Syncing entire/checkpoints/v1 with remote......
warning: failed to load global git config: path escapes from parent: "Users/jag/.config/git/config"
warning: failed to load global git config: path escapes from parent: "Users/jag/.config/git/config"
... (repeated per cherry-picked commit)

Root cause

The customer has ~/.config (or another intermediate path component) as an absolute symlink — standard behavior for dotfile managers (chezmoi, GNU Stow, yadm, …). With no ~/.gitconfig, go-git falls back to the XDG path ~/.config/git/config.

go-git's default config loader (xconfig.NewAuto on osfs.Default) reads config files through Go's os.Root, which rejects an absolute symlink in any path component — even one that resolves back inside the root — returning path escapes from parent. Plain os.Open/os.ReadFile follow the symlink fine; only os.Root chokes. (Reproduced verbatim, missing leading slash and all.)

This wasn't just noise. The global config was silently dropped, so:

  1. Author identity fell back to Unknown/unknown@local for checkpoint commits (GetGitAuthorFromReporepo.ConfigScoped).
  2. Commit signing was skipped (commit.gpgsign never read).
  3. The warning printed once per commit during the push/rebase loop — the flood.

Fix

cmd/entire/cli/checkpoint/configloader.go registers a ConfigLoader plugin backed by osSymlinkFS, a minimal billy.Basic using plain os calls so config reads follow symlinks the way git itself does. plugin.Register replaces go-git's default factory during package init (before any plugin.Get).

The adapter returns raw os errors unwrapped because go-git's auto loader uses os.IsNotExist(err) (the legacy non-unwrapping check) to fall through absent config files — wrapping would break that. Hence the per-file wrapcheck exclusion, with the reason documented in .golangci.yaml.

Tests

  • TestOSSymlinkFS_ReadsGlobalConfigBehindSymlink — reproduces the exact path escapes from parent failure with the default loader and confirms osSymlinkFS reads through the symlink.
  • TestGetGitAuthorFromRepo_GlobalConfigBehindSymlink — end-to-end: author identity now resolves from a global config behind a symlinked ~/.config instead of falling back to Unknown. (Verified it fails with name = "Unknown" when reverted to the default loader.)

Full suite green (5210 tests), lint clean.

Upstream

This is fundamentally a go-git/go-billy limitation (the auto loader reading trusted host config through os.Root, diverging from git's symlink-following behavior). A separate upstream conversation/PR will pursue the durable fix; this workaround is forward-compatible and can be dropped once upstream lands.

Draft notes / open questions

  • Left the deprecated config.LoadConfig(GlobalScope) fallback at committed.go:1913 (still uses osfs.Default) untouched, since it's only reached when ConfigScoped returns empty — which the fix now prevents. Could harden it too.
  • The init()-based registry override is slightly implicit; open to moving it to an explicit call in root setup if preferred.

🤖 Generated with Claude Code


Note

Low Risk
Localized checkpoint package init and filesystem adapter for config reads; behavior change only affects environments where global config was previously unreadable.

Overview
Fixes global git config not loading when ~/.config (or similar) is an absolute symlink—common with dotfile managers. go-git’s default loader used os.Root and failed with path escapes from parent, so repo.ConfigScoped dropped global settings: checkpoint commits used Unknown author, GPG signing was skipped, and push/sync logged the same warning once per cherry-picked commit.

Adds configloader.go: at package init, registers a go-git ConfigLoader backed by osSymlinkFS (billy.Basic over plain os.Open/os.Stat) so config paths follow symlinks like git. GetGitAuthorFromRepo comments point at this loader. Lint: allow billy.File in ireturn, wrapcheck off for configloader.go (raw os errors required for os.IsNotExist fall-through). Tests reproduce the default-loader failure and assert author resolution end-to-end (non-Windows).

Reviewed by Cursor Bugbot for commit 4b31870. Configure here.

go-git's default config loader (xconfig.NewAuto on osfs.Default) reads
config files through Go's os.Root, which rejects an absolute symlink in
any path component — even one that resolves back inside the root — with
"path escapes from parent". Users whose global config lives behind a
symlinked directory (e.g. ~/.config managed by a dotfile tool such as
chezmoi, GNU Stow, or yadm) therefore had their global config silently
dropped: checkpoint-commit author identity fell back to "Unknown", commit
signing was skipped, and the push hook printed a "failed to load global
git config: path escapes from parent" warning for every cherry-picked
commit during a sync/rebase.

Register a ConfigLoader plugin backed by osSymlinkFS, a minimal
billy.Basic using plain os calls so config reads follow symlinks the way
git itself does. The adapter returns raw os errors unwrapped so go-git's
os.IsNotExist() fall-through for absent config files keeps working
(hence the per-file wrapcheck exclusion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 61fed68aab4b
Copilot AI review requested due to automatic review settings May 27, 2026 15:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes go-git failing to read global git config when $HOME/.config (or another path component) is an absolute symlink, which previously caused global config to be dropped (e.g., author identity/signing) and produced repeated warnings during checkpoint operations.

Changes:

  • Registers a custom go-git ConfigLoader (via a symlink-following osSymlinkFS) to load global/system config using plain os calls.
  • Adds tests that reproduce the os.Root “path escapes from parent” failure and validate the fix end-to-end via GetGitAuthorFromRepo.
  • Updates lint config/docs to support the new adapter and document the behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/entire/cli/checkpoint/configloader.go Adds symlink-following ConfigLoader override via osSymlinkFS.
cmd/entire/cli/checkpoint/configloader_test.go Repro + end-to-end tests for symlinked global config behavior.
cmd/entire/cli/checkpoint/committed.go Updates comments to reference the new config loader behavior.
CLAUDE.md Documents the new configloader.go component and rationale.
.golangci.yaml Adjusts lint allowances/exclusions to accommodate the new adapter.

Comment thread cmd/entire/cli/checkpoint/configloader_test.go
Comment thread cmd/entire/cli/checkpoint/configloader.go Outdated
Comment thread .golangci.yaml
- Assert the symlink-rejection with errors.Is(err, osfs.ErrPathEscapesParent)
  instead of substring matching; go-billy wraps the sentinel with %w.
- Extract registerConfigLoaderForTest so useAutoConfigLoader and
  useSymlinkConfigLoader share the reset/register/restore dance; the symlink
  helper now reuses the production registerSymlinkConfigLoader so test and
  prod can't drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1dfe9e3ab575
@Soph Soph marked this pull request as ready for review May 27, 2026 15:47
@Soph Soph requested a review from a team as a code owner May 27, 2026 15:47
- Rename the Create receiver so it no longer shadows the io/fs package.
- Handle os.Unsetenv's error in the test env helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4025a2a93d51
@Soph Soph merged commit 9d3c8a2 into main May 27, 2026
9 checks passed
@Soph Soph deleted the soph/config-bug branch May 27, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants