fix(legacy): detect Git worktree as its own project root#111
Open
pjcdawkins wants to merge 4 commits into
Open
fix(legacy): detect Git worktree as its own project root#111pjcdawkins wants to merge 4 commits into
pjcdawkins wants to merge 4 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,
.gitis a file (a gitlink), not a directory. Root detection gated onis_dir('.git'), which is false for worktrees, so the worktree was skipped and detection walked up to the parent.Changes
LocalProject::getProjectConfig()andGit::getRoot()usefile_existsinstead ofis_dir.file_existsis true for both a.gitdirectory (normal repo) and a.gitgitlink 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 fileenvironment_select_test.go) runsenv:infoacross 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 nororigin. The worktree case exercisesLocalProject::getProjectRoot()andwriteGitExclude().TestProjectSetRemotegains a worktree subtest coveringGit::getRoot(): runningset-remotein 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 nullabletempDirto a non-null parameter) is fixed with a type assertion, which lets four per-class baseline suppressions be removed.