Skip to content

fix(sync): handle detached HEAD by skipping pull and ingesting local working tree#635

Closed
tmchow wants to merge 1 commit into
garrytan:masterfrom
tmchow:fix/sync-detached-head
Closed

fix(sync): handle detached HEAD by skipping pull and ingesting local working tree#635
tmchow wants to merge 1 commit into
garrytan:masterfrom
tmchow:fix/sync-detached-head

Conversation

@tmchow
Copy link
Copy Markdown
Contributor

@tmchow tmchow commented May 5, 2026

Summary

gbrain sync on a detached-HEAD repo no longer silently no-ops. It now detects the detached state up front, skips git pull --ff-only, and falls back to ingesting the working tree (tracked diff against HEAD plus untracked files) so local-only edits get picked up.

Why this matters

Issue #574 describes the exact failure mode: in a detached-HEAD worktree, git pull --ff-only errors with "You are not currently on a branch", the existing try/catch swallows it as Warning: git pull failed:, and the rest of sync sees lastCommit === HEAD, short-circuits to "Already up to date.", and never reads the working tree. Reporter cited the gstack gstack-brain-sync federation flow where two worktrees can't share main, so the brain worktree gets stuck in detached HEAD and silently no-ops every subsequent sync.

Approach

Picked option (b) from the issue body (skip pull, ingest working tree) over (a) hard-fail and (c) skip + non-zero exit. (b) preserves the federation use case the reporter described while still surfacing what happened in the user-facing message.

Changes in src/commands/sync.ts:

  1. Added isDetachedHead(repoPath) using git symbolic-ref --quiet HEAD (exit code 1 -> detached). Wrapped in try/catch so the existing git() helper's exception path doesn't surface to the user.
  2. Detection runs unconditionally, NOT inside if (!opts.noPull). This way gbrain sync --no-pull on a detached worktree also gets the working-tree fallback, not just the default-pull path.
  3. The actual git pull is now gated on !opts.noPull && !detachedHead. Existing pull semantics for the attached-HEAD path are unchanged.
  4. Added buildDetachedWorkingTreeManifest(repoPath) that combines git diff --name-status HEAD (tracked changes) and git ls-files --others --exclude-standard (untracked files) into the same SyncManifest shape the rest of the pipeline uses. The downstream sync flow processes added/modified/deleted/renamed without caring whether the source was a commit diff or a working-tree diff.

The user-facing message is the literal string from the issue's option (b): Detached HEAD on <repoPath>; skipping git pull. Syncing from local working tree. Exit code stays 0 because we did sync, just from a different source.

If you'd rather have option (a) hard-fail or (c) explicit skip + non-zero exit, the delta is small. Happy to switch.

Testing

bun run typecheck clean. bun test test/sync.test.ts detached-HEAD tests both pass:

  • detached HEAD skips git pull and ingests local working-tree files — default-pull path. Asserts no git pull failed warning, the local-only file gets imported, and result.added === 1.
  • detached HEAD with --no-pull also ingests local working-tree files--no-pull path. Asserts the working-tree fallback fires regardless of pull mode, since detection is outside the pull gate.

Both tests use a real git worktree-style detached checkout (git checkout --detach HEAD) and write a markdown file that doesn't exist anywhere in git history, then verify the page is imported with the expected title.

Fixes #574

Compound Engineering


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

@garrytan
Copy link
Copy Markdown
Owner

Closing — your fix landed in master via the v0.30.3 fix-wave PR #776 (merged at ff53a4c9): "handle detached-HEAD repos in gbrain sync".

Thank you for the contribution — credit is preserved in the v0.30.3 CHANGELOG entry. 🙏

@garrytan garrytan closed this May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: sync silently no-ops with misleading 'Already up to date' when target path has detached HEAD

2 participants