refactor(worktree): resolve worktree path from stored value, not derived from name#2709
refactor(worktree): resolve worktree path from stored value, not derived from name#2709haacked wants to merge 3 commits into
Conversation
…ved from name Make the persisted `worktrees.path` column the source of truth for a task's worktree path. The `TaskAssociation` worktree variant now carries the stored `path` alongside the cosmetic `name`, and every read/delete/occupancy path in WorkspaceService uses it instead of recomputing `<base>/<name>/<repo>` via the private `deriveWorktreePath` helper (now removed here). Guard `cleanupRepoWorktreeFolder` so it only reclaims the managed `<base>/<repo>` parent folder when the deleted worktree actually lived under the managed base path. Externally located worktrees never created it, so their parent dirs are left untouched. Behavioral no-op for app-created worktrees: their stored `path` already equals the previously derived path, so no data migration is needed. The legacy-layout name-recovery hack the original scope referenced (findExistingWorktreeForBranch / finalSegment) no longer exists in the tree, so that item produced no diff.
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
packages/workspace-server/src/services/workspace/workspace.test.ts:370-416
**Delete guard tests could be parameterised**
Tests 3 and 4 exercise the same `deleteWorkspace` method with the only meaningful difference being whether the stored worktree path lives under the managed base. Per the team's preference for parameterised tests, these two cases are a natural `it.each` table: each row supplies a `worktreePath` factory (external temp dir vs `path.join(base, "some-name", "myrepo")`) and the expected post-delete existence of `managedParent`. The current form repeats the identical boilerplate — `mkTemp("wt-base-")`, `mkdirSync(managedParent)`, `seedWorktreeTask`, `deleteWorkspace`, `expect(existsSync(managedParent))` — across two independent `it` blocks.
Reviews (1): Last reviewed commit: "refactor(worktree): resolve worktree pat..." | Re-trigger Greptile |
Fold the now-redundant folderPath guard in getWorktreeInfo's worktree branch into the branch condition, matching the sibling local branch. Parameterise the two delete-guard tests into a single it.each table since they share setup and differ only by whether the stored path is under the base path.
Adds test case verifying that verifyWorkspaceExists correctly uses the stored external path rather than deriving it from the worktree name. Also improves comments in cleanupRepoWorktreeFolder for clarity.
|
Reviews (2): Last reviewed commit: "Add test for worktree verification with ..." | Re-trigger Greptile |
There was a problem hiding this comment.
Clean refactor that reads worktree paths from the stored DB row instead of re-deriving them, fixing correctness for external worktree locations. The isPathUnder helper is sound, the null-safety concern is covered by TypeScript flow narrowing at the call site, and the PR includes thorough test coverage for all changed behaviors including the it.each pattern the bot comment suggested.
There was a problem hiding this comment.
Pull request overview
Refactors WorkspaceService worktree path resolution so the persisted worktrees.path column becomes the source of truth, enabling support for externally located worktrees (a prerequisite for future “import existing worktree” functionality).
Changes:
- Extends the internal
TaskAssociation(worktree mode) to carry the authoritativepathfrom the worktree row and updates all relevant call sites to use it. - Removes derived-path logic from
WorkspaceServiceand guards managed-folder cleanup so it only applies to worktrees actually under the configured base path. - Adds unit tests covering externally located worktrees (projection, occupancy matching, existence verification, and delete/cleanup behavior).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/workspace-server/src/services/workspace/workspace.ts | Switches worktree path usage to the stored DB value; adds a base-path guard for managed-folder cleanup. |
| packages/workspace-server/src/services/workspace/workspace.test.ts | Adds coverage for external worktree paths and for cleanup behavior discrimination (external vs managed). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** True when `child` resolves to a path strictly inside `parent`. */ | ||
| function isPathUnder(child: string, parent: string): boolean { | ||
| const rel = path.relative(parent, child); | ||
| return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); | ||
| } |
Problem
Note
This is pretty much a pure refactoring to make room for some future Worktree improvements.
WorkspaceServiceignored theworktrees.pathcolumn and recomputed each task's worktree path everywhere from the worktree name, assuming it lives at<base>/<name>/<repo>. That means a worktree located outside the app's managed base path can't round-trip through the name-to-path formula, so it can't be attached to a task. This is a standalone prerequisite that unblocks a later feature: importing worktrees created by other tools.Changes
The persisted
pathcolumn is now the source of truth. A task resolves its worktree path by reading the worktree row, never by deriving it from name plus base path.The
TaskAssociationworktree variant carries the authoritativepath(read straight from the row thatfindTaskAssociation/getAllTaskAssociationsalready load) alongside the now-cosmeticname. All 9 read, delete, and occupancy call sites useassoc.pathinstead of re-deriving: the branch-rename watchers (handleFocusBranchRenamed,handleGitStateChanged),deleteWorkspace,verifyWorkspaceExists,getWorkspace,getWorkspaceInfo,getAllWorkspaces, andgetWorktreeTasks. The privatederiveWorktreePathhelper is removed from this service.cleanupRepoWorktreeFolderis guarded so it only reclaims the managed<base>/<repo>parent folder when the deleted worktree actually lived under the managed base path. Externally located worktrees never created it, so their parent dirs stay untouched. The under-path check usespath.relative, notstartsWith, which would false-positive/base-evilagainst/base.This is a behavioral no-op for app-created worktrees. I checked the local dev and prod SQLite
worktreestables and every storedpathalready equals<base>/<name>/<repo>, which is exactly whatderiveWorktreePathreturned, so no data migration is needed.The legacy name-recovery hack the original scope referenced (
findExistingWorktreeForBranchand thefinalSegment === repoName ? parent : finalSegmentlogic) no longer exists in the tree, already removed in a prior change, so that item produced no diff. The sharedworktree-path/worktree-path.tsmodule stays becausearchive,shell, andsuspensionstill use it. The "import existing worktree" UI and theadopteddeletion flag remain out of scope for a future PR.How did you test this?
pnpm --filter @posthog/workspace-server test workspace.testpasses with 16 tests: the 12 existing ones unchanged (proving the no-op for managed worktrees) plus 4 new ones. The new coverage exercises a worktree whosepathis not under the base path: its projection resolves to the stored path, occupancy viagetWorktreeTasksmatches by stored path, and delete uses the stored path without removing any parent folder. A contrast test with a managed path under the base proves the cleanup guard discriminates rather than being a blanket no-op. I confirmed it is non-vacuous by mutation: removing the guard fails exactly the external-worktree test.pnpm --filter @posthog/workspace-server typecheckpasses and Biome lint/format is clean on the changed files.The repo's
repositories.test.tsDB round-trip suite fails locally with a pre-existingbetter-sqlite3native ABI mismatch (node 20 versus the expected node 22), unrelated to this change and in files I did not touch.Automatic notifications