Skip to content

Support linked worktrees and submodules, where .git is a file - #146

Merged
EsmeYi merged 2 commits into
mainfrom
fix/git-dir-is-a-file
Oct 9, 2026
Merged

EsmeYi merged 2 commits into
mainfrom
fix/git-dir-is-a-file

Conversation

@EsmeYi

@EsmeYi EsmeYi commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

In a linked worktree or a submodule, .git is a file (gitdir: ...), not a directory, so every <repo>/.git/tracybot/... path breaks. Found while reviewing #145; this is already broken on main, not a regression from that PR.

Reproduced on real repos (plain repo vs linked worktree vs submodule), running the real init.py and then a real git commit through the installed hooks:

init.py before init.py after
plain repo ok ok
linked worktree NotADirectoryError ok, commit creates a snapshot
submodule NotADirectoryError: no config, no hooks, no snapshots at all ok, commit creates a snapshot

Same assumption elsewhere: the three agent plugins couldn't find the config (so nothing was recorded), and writing the Research Mode consent file (repoConsent.ts) threw ENOTDIR, e.g. clicking "I agree to share my data" in a worktree.

Fix

Resolve the real location instead of assuming .git/. Everything Tracybot keeps under the git dir goes in the common git dir. That is where the hooks are installed and where pre-commit.py already reads the config from (the parent of the hooks dir), so init and consent are per repository, not per worktree.

  • init.py, post-fetch.py: git rev-parse --git-common-dir
  • opencode / claude-code / codex plugins and repoConsent.ts: new resolveGitDir() that follows the .git file and the worktree's commondir pointer. No git process needed. vscode-extension/src/gitDir.ts is the reusable one.

Related: #145

tracyInitCheck.ts (the extension's check for "is this repo initialized?") now goes through resolveGitDir too, otherwise init would succeed in a worktree while the extension kept thinking it was uninitialized and re-ran it on every activation. #145 rewrites that code and moves it into a new initState.ts, so whichever of the two merges second needs a small conflict resolution, and initState.ts must use resolveGitDir (from src/gitDir.ts) as well. Use the common dir, not git rev-parse --git-path: that gives a per-worktree path that the hooks don't read.

post-fetch.py isn't a hook name git actually runs, so that change is for consistency only.

Testing

  • vscode-extension unit tests: 66/66 (new gitDir.test.ts with a real linked worktree and submodule, plus a consent round-trip from a worktree)
  • claude-code-plugin and codex-plugin bun test: 12/12 each (new tests for resolveTracyPath in a worktree and a submodule)
  • Confirmed the new worktree/submodule tests fail without the fix (consent test in the extension; 2 tests in each plugin)
  • All four packages type-check, plugins build
  • End-to-end init.py + real hooked commit in a plain repo, linked worktree and submodule, before and after (table above)
  • The opencode plugin has no test harness; it uses the same resolver code and type-checks/builds, but I did not exercise it against a live OpenCode session

🤖 Generated with Claude Code

EsmeYi and others added 2 commits October 9, 2026 16:07
In a linked worktree or a submodule `.git` is a file ("gitdir: ..."), not a
directory, so every `<repo>/.git/tracybot/...` path failed: init.py crashed
with NotADirectoryError (a submodule got no config, no hooks and no
snapshots), the agent plugins couldn't find the config, and writing the
Research Mode consent file threw ENOTDIR.

Resolve the real location instead. Everything Tracybot keeps under the git
dir lives in the *common* git dir, the same place the hooks are installed
and where pre-commit.py already reads the config from (the parent of the
hooks dir), so init and consent are per repository, not per worktree.

- init.py, post-fetch.py: ask git via `rev-parse --git-common-dir`
- opencode/claude-code/codex plugins and the extension's repoConsent.ts:
  new resolveGitDir() that follows the `.git` file and the worktree's
  `commondir` pointer (no git process needed)
- tests use real linked worktrees and submodules

tracyInitCheck.ts is left alone on purpose: #145 rewrites that code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
init.py now writes the config to the common git dir, but tracyInitCheck
still looked for <repo>/.git/tracybot/config. In a linked worktree that path
can't exist, so initialization succeeded yet the extension kept treating the
worktree as uninitialized and re-ran init (and re-showed the "repository
initialized" message) on every activation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@EsmeYi
EsmeYi merged commit 0bd65da into main Oct 9, 2026
4 checks passed
@EsmeYi
EsmeYi deleted the fix/git-dir-is-a-file branch October 9, 2026 14:54
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.

1 participant