Skip to content

fix(legacy): detect Git worktree as its own project root#111

Open
pjcdawkins wants to merge 4 commits into
mainfrom
cli-154-worktree-project-root
Open

fix(legacy): detect Git worktree as its own project root#111
pjcdawkins wants to merge 4 commits into
mainfrom
cli-154-worktree-project-root

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Problem

When the CLI runs from inside a git worktree, it fails to recognize the worktree as the project root. If the worktree is nested inside another checkout of the same repository (e.g. repo/.claude/worktrees/<name>), project-root detection climbs up into the enclosing repository and reads that checkout's branch. The CLI then auto-selects an environment matching the parent's branch (e.g. main) instead of the worktree's actual branch.

Cause

In a git worktree, .git is a file (a gitlink), not a directory. Root detection gated on is_dir('.git'), which is false for worktrees, so the worktree was skipped and detection walked up to the parent.

Changes

  • LocalProject::getProjectConfig() and Git::getRoot() use file_exists instead of is_dir. file_exists is true for both a .git directory (normal repo) and a .git gitlink file (worktree/submodule).
  • LocalProject::writeGitExclude() resolved the exclude path as .git/info/exclude, which does not exist in a worktree. It now asks git for the path (rev-parse --git-path info/exclude), which resolves to the shared common directory. Without this, writing the cached project config in a worktree would fail once root detection correctly pointed at the worktree.

Git::getCurrentBranch() (git symbolic-ref --short HEAD) was already correct; the problem was only that the wrong directory was chosen as the root.

Tests

Coverage is added as Go integration tests in integration-tests/, which run the built CLI as a shell command against a mock API. These are kept rather than PHPUnit tests because they characterize the behavior end-to-end and survive the eventual move of this logic from PHP to Go.

  • TestEnvironmentSelectionByLayout (new file environment_select_test.go) runs env:info across a range of local checkout layouts and asserts which environment is auto-selected (or that none is): git vs no git, worktree vs not, project identified by the Git remote vs .platform/local/project.yaml, a branch with or without a matching environment, and a remote that is neither the configured name nor origin. The worktree case exercises LocalProject::getProjectRoot() and writeGitExclude().
  • TestProjectSetRemote gains a worktree subtest covering Git::getRoot(): running set-remote in a worktree nested inside a parent checkout must write the local config in the worktree, not the parent.

A latent PHPStan issue in HasTempDirTrait (passing a nullable tempDir to a non-null parameter) is fixed with a type assertion, which lets four per-class baseline suppressions be removed.

Project and Git root detection gated on is_dir('.git'), which is false in
a Git worktree where .git is a file (a gitlink). When a worktree was nested
inside another checkout of the same repository, detection climbed up to the
parent repo and read its branch, so the CLI auto-selected an environment
matching the parent's branch (e.g. main) instead of the worktree's branch.

- LocalProject::getProjectConfig() and Git::getRoot() now use file_exists,
  which is true for both a .git directory and a .git gitlink file.
- writeGitExclude() resolved the exclude path as .git/info/exclude, which
  does not exist in a worktree; it now asks git for the path
  (rev-parse --git-path info/exclude), which points at the shared common
  directory.

Add regression tests covering getRoot() and getProjectRoot() in a worktree
nested inside a parent repository.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 19, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

pjcdawkins and others added 3 commits June 19, 2026 14:39
LocalProjectTest uses HasTempDirTrait, and PHPStan analyses the trait's
methods in the context of each using class. createTempSubDir() passed the
nullable $tempDir to createTempDir(), which expects a non-null string.

Add an assert() after tempDirSetUp() to narrow the type, and drop the four
now-obsolete baseline entries that suppressed this error per class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the PHPUnit worktree regression tests with a table-driven Go
integration test in integration-tests/. The PHPUnit tests were in the
"slow" group, which the legacy test run excludes (phpunit --exclude-group
slow), so they did not run routinely. A Go integration test also survives
the eventual move of this logic from PHP to Go.

TestEnvironmentSelectionByLayout runs "env:info" across a range of local
checkout layouts (git/no-git, worktree or not, project identified by the
Git remote or by .platform/local/project.yaml, branch with or without a
matching environment) and asserts the selected environment, exercising the
same worktree project-root detection as the removed tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set-remote resolves the repository root via Git::getRoot(). Add a subtest
that runs it in a worktree nested inside a parent checkout and asserts the
local project config is written in the worktree, not the parent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants