Skip to content

refactor(worktree): resolve worktree path from stored value, not derived from name#2709

Open
haacked wants to merge 3 commits into
mainfrom
haacked/refactor-worktree-paths
Open

refactor(worktree): resolve worktree path from stored value, not derived from name#2709
haacked wants to merge 3 commits into
mainfrom
haacked/refactor-worktree-paths

Conversation

@haacked

@haacked haacked commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Problem

Note

This is pretty much a pure refactoring to make room for some future Worktree improvements.

WorkspaceService ignored the worktrees.path column 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 path column 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 TaskAssociation worktree variant carries the authoritative path (read straight from the row that findTaskAssociation/getAllTaskAssociations already load) alongside the now-cosmetic name. All 9 read, delete, and occupancy call sites use assoc.path instead of re-deriving: the branch-rename watchers (handleFocusBranchRenamed, handleGitStateChanged), deleteWorkspace, verifyWorkspaceExists, getWorkspace, getWorkspaceInfo, getAllWorkspaces, and getWorktreeTasks. The private deriveWorktreePath helper is removed from this service.

cleanupRepoWorktreeFolder is 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 uses path.relative, not startsWith, which would false-positive /base-evil against /base.

This is a behavioral no-op for app-created worktrees. I checked the local dev and prod SQLite worktrees tables and every stored path already equals <base>/<name>/<repo>, which is exactly what deriveWorktreePath returned, so no data migration is needed.

The legacy name-recovery hack the original scope referenced (findExistingWorktreeForBranch and the finalSegment === repoName ? parent : finalSegment logic) no longer exists in the tree, already removed in a prior change, so that item produced no diff. The shared worktree-path/worktree-path.ts module stays because archive, shell, and suspension still use it. The "import existing worktree" UI and the adopted deletion flag remain out of scope for a future PR.

How did you test this?

pnpm --filter @posthog/workspace-server test workspace.test passes 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 whose path is not under the base path: its projection resolves to the stored path, occupancy via getWorktreeTasks matches 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 typecheck passes and Biome lint/format is clean on the changed files.

The repo's repositories.test.ts DB round-trip suite fails locally with a pre-existing better-sqlite3 native ABI mismatch (node 20 versus the expected node 22), unrelated to this change and in files I did not touch.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

…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.
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 1791182.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix 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

Comment thread packages/workspace-server/src/services/workspace/workspace.test.ts Outdated
@haacked haacked marked this pull request as draft June 16, 2026 20:13
haacked added 2 commits June 16, 2026 13:19
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.
@haacked haacked marked this pull request as ready for review June 16, 2026 22:56
@haacked haacked requested a review from a team June 16, 2026 22:56
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "Add test for worktree verification with ..." | Re-trigger Greptile

@haacked haacked added the Stamphog This will request an autostamp by stamphog on small changes label Jun 17, 2026
@haacked haacked requested review from charlesvien and Copilot June 17, 2026 15:58

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

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 authoritative path from the worktree row and updates all relevant call sites to use it.
  • Removes derived-path logic from WorkspaceService and 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.

Comment on lines +89 to +93
/** 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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants