Skip to content

[POC] fix(desktop): auto-repair moved tracked worktrees#2260

Closed
Kitenite wants to merge 20 commits into
superset-sh:mainfrom
Kitenite:Kitenite/rust-shield
Closed

[POC] fix(desktop): auto-repair moved tracked worktrees#2260
Kitenite wants to merge 20 commits into
superset-sh:mainfrom
Kitenite:Kitenite/rust-shield

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Mar 8, 2026

Summary

  • auto-repair tracked worktrees after nearby manual renames by locating the moved worktree and running git worktree repair
  • centralize tracked worktree path resolution so terminal, reopen, delete, and git status share the same repair flow
  • fall back to an explicit repair instruction only when the moved worktree cannot be located automatically

Testing

  • bun test apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
  • bun x biome check apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts apps/desktop/src/lib/trpc/routers/terminal/terminal.ts apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts

Summary by cubic

Auto-repair moved tracked worktrees so terminal, git status, queries, and delete keep working after directory renames or unnesting. Sidebar queries now use a lightweight display-only repair state to avoid UI lag; full repair runs only on actions like open/attach.

  • Bug Fixes

    • Delete: treat moved/missing worktrees as deletable; warn when repair is required, skip teardown checks, fall back to the stored path, and clear deletingAt on errors.
    • Git status: resolve path (with repair) before ahead/behind and PR checks; if still missing, fail with NOT_FOUND instead of showing 0/0; derive worktree name from the resolved path.
    • Terminal: create/attach and getWorkspaceCwd use the repair resolver; on attach, invalidate workspaces.get, workspaces.getAllGrouped, and terminal.getWorkspaceCwd.
    • External worktrees: open/import reuses tracked worktrees by branch and path; when importing by path, use Git’s current branch; filter out duplicates from repaired tracked paths/branches.
    • Main-repo guard: reject candidates that match the main repo path using realpathSync to canonicalize symlinks.
  • Refactors

    • Centralized repair in repair-worktree-path and repairWorktreeRegistration; added external-worktrees helpers (resolveExternalWorktreeOpenTarget, listImportableExternalWorktrees) and adopted them across create/import, status, query, and terminal flows.
    • Queries and create/import/delete resolve and return repaired worktreePath, existsOnDisk, and repair metadata (state/message/command). The sidebar now renders this display-only state without triggering repair, improving responsiveness.

Written for commit 386cebb. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Automatic detection and repair for moved Git worktrees; new repair command/message for affected worktrees.
    • Improved handling of external/importable worktrees for smoother importing/opening.
  • Bug Fixes

    • Safer workspace/worktree path resolution across create/open/delete/status flows with on-disk checks and fallback warnings.
    • Delete flows now surface warnings when repair/fallback is required.
    • Terminal create/attach now refreshes workspace and terminal state after actions.
  • Tests

    • Extensive tests covering path repair, external-worktree import, and delete fallback scenarios.

Kitenite added 9 commits March 4, 2026 17:33
When a worktree directory is moved or unnested, the stored path in the
local database becomes stale, causing terminal connections to fail with
"Workspace path does not exist" errors. This adds auto-detection of the
new path by querying `git worktree list` and matching by branch name,
then updating the database so the terminal (and git status) can reconnect.
`git worktree list` includes the main worktree. If the stale worktree's
branch happens to be checked out in the main repo, the repair would
incorrectly rebind the worktree row to mainRepoPath. Guard against this
by comparing the candidate path against the project's main repo path.
… tests

- Replace resolve() with realpathSync() to canonicalize symlinks
  (e.g. /var → /private/var on macOS) when comparing candidate path
  against main repo path.
- Add 6 regression tests covering: valid path no-op, successful repair
  after git worktree move, main repo rejection, missing project/worktree,
  and branch not found by git.
Use git rev-parse instead of hardcoding "main" so the test exercises
the guard on environments where git init defaults to "master".
…hGitStatus

After attempting path repair, check existsSync again before calling
getAheadBehindCount. Without this, a stale path silently produces
ahead=0/behind=0 which misleads the UI into showing the workspace
as up-to-date.
# Conflicts:
#	apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a repair-and-resolution subsystem for tracked Git worktree paths, replaces direct DB worktree.path usage across TRPC procedures with resolution/repair helpers, converts several handlers to async, introduces git repair utilities, and adds comprehensive tests for repair, external-worktree, and delete flows.

Changes

Cohort / File(s) Summary
Worktree Resolution Core & Tests
apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
New module implementing tracked worktree path resolution, detection of moved/missing worktrees, auto-repair commands, listing/finding helpers, test-only dependency wiring, and an extensive real-repo test suite.
External Worktrees Utilities & Tests
apps/desktop/src/lib/trpc/routers/workspaces/utils/external-worktrees.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/external-worktrees.test.ts
New helpers to discover/import external worktrees, resolve open targets (tracked vs external), list importable external worktrees, and unit tests with test-only dependency injection.
Workspace Procedures (create/git-status/query/delete)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts, .../git-status.ts, .../query.ts, .../delete.ts
Replaced direct worktree.path usage with resolveWorktreePath* helpers; added existsOnDisk flags, warnings/fallbacks for repair-required states; converted several procedures to async; introduced DeleteProcedureDeps and path-aware delete/teardown flows.
Delete Procedure Tests & DI Wiring
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.test.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Added tests covering fallback deletion and repair-required scenarios; exposed __testOnlyDeleteProcedureDeps for injecting mocked dependencies used during delete flows.
Terminal Router & Hook Adjustments
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts, apps/desktop/src/renderer/.../Terminal/hooks/useTerminalConnection.ts
Removed direct DB access in terminal flows; use resolveWorktreePathOrThrow / resolveWorktreePathWithRepair; introduced runCreateOrAttach wrapper to invalidate caches and wired ref to it.
Git Utility & DB Import Fixes
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
Added exported repairWorktreeRegistration to run git worktree repair; adjusted local-db schema import path to @superset/local-db/schema.

Sequence Diagram

sequenceDiagram
    participant Client as Terminal Client
    participant Hook as useTerminalConnection
    participant Router as TRPC Router
    participant Resolver as RepairResolver
    participant LocalDB as Local Database
    participant GitRepo as Git Repository

    Client->>Hook: createOrAttach(workspace)
    Hook->>Router: createOrAttachMutation.mutate(...)
    Router->>Resolver: resolveWorktreePathWithRepair(worktreeId)
    Resolver->>LocalDB: read worktree record
    LocalDB-->>Resolver: worktree metadata
    Resolver->>GitRepo: inspect worktree registration / path
    alt Path valid
        GitRepo-->>Resolver: path confirmed
        Resolver-->>Router: return resolved path
    else Path moved / repairable
        Resolver->>GitRepo: run git worktree repair
        GitRepo-->>Resolver: repair succeeded
        Resolver->>LocalDB: update stored path
        Resolver-->>Router: return resolved path
    else Missing / not found
        Resolver-->>Router: return missing / throw NOT_FOUND
    end
    Router-->>Hook: mutation result
    Hook->>Client: success / error callback
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐇 I hopped through gitdirs, sniffed each trail,
Found paths that wandered, nudged what went pale,
Ran tiny repairs, patched a rogue registry,
Tuned homes for worktrees, and left a happy history. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[POC] fix(desktop): auto-repair moved tracked worktrees' clearly and concisely describes the main change: adding auto-repair functionality for tracked worktrees that have been manually moved, which is the core objective of this PR.
Description check ✅ Passed The pull request description clearly outlines the changes: auto-repair of moved tracked worktrees, centralized path resolution, and fallback behavior. It includes testing instructions and references the automated summary by cubic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts:168">
P2: Do not swallow metadata read failures with an empty catch; log context so auto-repair misses are diagnosable.

(Based on your team's feedback about avoiding empty catch blocks that hide failures.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:216">
P1: Handle `getTrackedWorktreePath` failures in the delete mutation and clear deleting status before returning. Right now a `git_repair_required` throw can leave the workspace stuck in `deletingAt` state.</violation>
</file>

<file name="apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts:173">
P2: Using `split("/")` to derive the worktree name is not cross-platform and breaks on Windows paths. Use a separator-agnostic basename extraction.

(Based on your team's feedback about using cross-platform path utilities instead of split.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (1)

178-180: ⚠️ Potential issue | 🟠 Major

Resolve the worktree path before mutating delete state.

getTrackedWorktreePath() can now throw from the repair flow, but markWorkspaceAsDeleting(), updateActiveWorkspaceIfRemoved(), and killByWorkspaceId() have already been triggered by then. A repair-required workspace will therefore fail deletion after we've hidden it and started killing terminals, with no rollback here.

Also applies to: 207-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts` around
lines 178 - 180, Compute/resolve the workspace worktree path by calling
getTrackedWorktreePath(input.id) (and handle its potential throw) before making
any state changes; only after successfully obtaining the path proceed to call
markWorkspaceAsDeleting(input.id), updateActiveWorkspaceIfRemoved(input.id), and
killByWorkspaceId(input.id). Apply the same change to the other deletion branch
that calls these three mutators (the block referenced as 207-218), ensuring the
path resolution happens first and errors abort before hiding/starting kills.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`:
- Around line 340-345: Don't short-circuit and treat worktree.path as resolved
just because existsSync returned true; instead remove the immediate return for
worktree.path and run the same validation used later (check for .git/metadata
and call isMainRepoPath()) before returning status:"resolved". Apply this change
to the early branch that currently returns when existsSync(worktree.path) and to
the analogous block around the 393-400 area so both locations perform
metadata/.git validation and isMainRepoPath() guard prior to marking the path
resolved.

---

Outside diff comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts`:
- Around line 178-180: Compute/resolve the workspace worktree path by calling
getTrackedWorktreePath(input.id) (and handle its potential throw) before making
any state changes; only after successfully obtaining the path proceed to call
markWorkspaceAsDeleting(input.id), updateActiveWorkspaceIfRemoved(input.id), and
killByWorkspaceId(input.id). Apply the same change to the other deletion branch
that calls these three mutators (the block referenced as 207-218), ensuring the
path resolution happens first and errors abort before hiding/starting kills.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c526a2a5-bb99-4b38-82e5-bf9e0c22e8ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa5f92e and 4bbd965.

📒 Files selected for processing (9)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalConnection.ts

Comment on lines +340 to +345
if (existsSync(worktree.path)) {
return {
status: "resolved",
path: worktree.path,
};
}
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.

⚠️ Potential issue | 🔴 Critical

Don't treat worktree.path as resolved just because it exists.

Line 340 short-circuits on raw filesystem existence, which bypasses both the .git/metadata validation and the isMainRepoPath() guard below. If the old location gets recreated after a manual move—or an older row already contains the main repo path—we'll hand downstream delete/status/terminal flows the wrong directory.

Also applies to: 393-400

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`
around lines 340 - 345, Don't short-circuit and treat worktree.path as resolved
just because existsSync returned true; instead remove the immediate return for
worktree.path and run the same validation used later (check for .git/metadata
and call isMainRepoPath()) before returning status:"resolved". Apply this change
to the early branch that currently returns when existsSync(worktree.path) and to
the analogous block around the 393-400 area so both locations perform
metadata/.git validation and isMainRepoPath() guard prior to marking the path
resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts`:
- Around line 85-89: The helper getTrackedWorktreePath currently hides
resolution failures by returning resolveWorktreePathOrThrow(worktree.id) ??
worktree.path; change getTrackedWorktreePath to return Promise<string | null>
and return the raw result of resolveWorktreePathOrThrow (no "?? worktree.path"
fallback) so unresolved worktrees yield null; then update callers
(handleExistingWorktree, openWorktree and any branch-reopen logic) to handle a
null return from getTrackedWorktreePath/resolveWorktreePathOrThrow instead of
assuming worktree.path, and use worktree.id/worktree.path only when resolution
succeeded.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts`:
- Around line 209-217: Currently we eagerly call getWorkspacePathForQuery (which
may invoke resolveWorktreePathWithRepair) for every entry in allWorkspaces;
instead, first filter allWorkspaces by whether groupsMap has the
workspace.projectId (i.e. only keep visible projects) and then run Promise.all
on that filtered list so we don't trigger filesystem/git repairs for workspaces
that will be dropped; update the code around allWorkspaces, groupsMap, and
getWorkspacePathForQuery to perform the prefiltering before resolving worktree
paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4044ee99-d1dc-4437-aef9-7d5b70cc2dee

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbd965 and 86afa34.

📒 Files selected for processing (4)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts (1)

185-200: Consider logging or re-throwing unexpected errors in the catch block.

The empty catch {} at line 199 silently swallows all errors, including unexpected ones beyond file read failures. This could make debugging difficult if there are issues with the metadata parsing logic.

♻️ Suggested improvement
 		try {
 			const head = readFileSync(headPath, "utf8").trim();
 			const rawGitdir = readFileSync(gitdirPath, "utf8").trim();
 			const registeredGitdir = isAbsolute(rawGitdir)
 				? safeResolvePath(rawGitdir)
 				: safeResolvePath(resolve(metadataDir, rawGitdir));
 			const registeredPath = dirname(registeredGitdir);

 			if (
 				head === `ref: refs/heads/${input.context.worktree.branch}` ||
 				safeResolvePath(registeredPath) === expectedStoredPath
 			) {
 				return { metadataDir, registeredPath };
 			}
-		} catch {}
+		} catch (error) {
+			// Expected: ENOENT, EACCES for inaccessible entries
+			// Continue scanning other metadata directories
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`
around lines 185 - 200, The empty catch in the loop around reading
headPath/gitdirPath (inside repair-worktree-path logic) swallows all errors;
update the catch to surface unexpected failures by catching (err) and either
logging the error with context (include headPath, gitdirPath, metadataDir and
the caught error) using the repo's logger or re-throwing the error after
logging; ensure you still silently continue only for known benign errors (e.g.,
ENOENT) if intended, but do not leave catch {} — modify the try/catch that reads
head, rawGitdir and computes registeredGitdir/registeredPath to report the error
via the existing logging utility or rethrow it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`:
- Around line 456-469: In resolveTrackedWorktreePath, don’t short-circuit by
returning buildResolvedResult(context.worktree.path) solely because existsSync
is true; first validate the stored path using
isMainRepoPath(context.worktree.path) to reject main-repo paths and use
parseGitdirReference(context.worktree.path) (and/or verify the referenced gitdir
exists) to ensure the directory is a real Git worktree; if either check fails
return buildMissingResolutionResult() (or fall through to
resolveTrackedWorktreePathFromGitState(context)), otherwise return
buildResolvedResult(...). This preserves existing calls to
resolveTrackedWorktreePathFromGitState and uses the already-defined
parseGitdirReference and isMainRepoPath helpers.
- Around line 578-596: The comparison in findProjectWorktreeByCurrentPath can
miss matches due to differing path formats; normalize both worktreePath and
trackedWorktree.worktree.path before comparing (e.g., use
path.resolve/path.normalize and, where appropriate, fs.realpath or toLowerCase
on case-insensitive platforms) so trailing slashes, ./ segments and symlinks are
handled consistently; update the loop in findProjectWorktreeByCurrentPath to
compute normalized/real paths for worktreePath and trackedWorktree.worktree.path
(or cache normalized values from listProjectWorktreesWithCurrentPaths) and
compare those normalized strings instead of strict equality on the raw values.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`:
- Around line 185-200: The empty catch in the loop around reading
headPath/gitdirPath (inside repair-worktree-path logic) swallows all errors;
update the catch to surface unexpected failures by catching (err) and either
logging the error with context (include headPath, gitdirPath, metadataDir and
the caught error) using the repo's logger or re-throwing the error after
logging; ensure you still silently continue only for known benign errors (e.g.,
ENOENT) if intended, but do not leave catch {} — modify the try/catch that reads
head, rawGitdir and computes registeredGitdir/registeredPath to report the error
via the existing logging utility or rethrow it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75d209e9-034b-413e-87ec-2b77f48d3def

📥 Commits

Reviewing files that changed from the base of the PR and between 86afa34 and 5e8ed7b.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts

Comment on lines +456 to +469
export async function resolveTrackedWorktreePath(
worktreeId: string,
): Promise<ResolveTrackedWorktreePathResult> {
const context = getTrackedWorktreeContext(worktreeId);
if (!context) {
return buildMissingResolutionResult();
}

if (existsSync(context.worktree.path)) {
return buildResolvedResult(context.worktree.path);
}

return resolveTrackedWorktreePathFromGitState(context);
}
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.

⚠️ Potential issue | 🟠 Major

Validate worktree path before short-circuiting on existence.

Lines 464-466 return the stored path as resolved if existsSync returns true, but this bypasses:

  1. The isMainRepoPath() guard that protects against accidentally operating on the main repo
  2. Validation that the directory is actually a Git worktree (has valid .git file with gitdir reference)

If the old location gets recreated as a regular directory after a manual move, or if the stored path points to the main repo, downstream operations (terminal, delete, git-status) could operate on the wrong directory.

🔧 Proposed fix
 export async function resolveTrackedWorktreePath(
 	worktreeId: string,
 ): Promise<ResolveTrackedWorktreePathResult> {
 	const context = getTrackedWorktreeContext(worktreeId);
 	if (!context) {
 		return buildMissingResolutionResult();
 	}

-	if (existsSync(context.worktree.path)) {
-		return buildResolvedResult(context.worktree.path);
+	if (existsSync(context.worktree.path)) {
+		// Validate it's actually a worktree and not the main repo
+		const gitdirRef = parseGitdirReference(context.worktree.path);
+		if (gitdirRef && !isMainRepoPath(context, context.worktree.path)) {
+			return buildResolvedResult(context.worktree.path);
+		}
+		// Path exists but is not a valid worktree - fall through to git state resolution
 	}

 	return resolveTrackedWorktreePathFromGitState(context);
 }

Note: parseGitdirReference and isMainRepoPath are already defined in this module and can be reused here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`
around lines 456 - 469, In resolveTrackedWorktreePath, don’t short-circuit by
returning buildResolvedResult(context.worktree.path) solely because existsSync
is true; first validate the stored path using
isMainRepoPath(context.worktree.path) to reject main-repo paths and use
parseGitdirReference(context.worktree.path) (and/or verify the referenced gitdir
exists) to ensure the directory is a real Git worktree; if either check fails
return buildMissingResolutionResult() (or fall through to
resolveTrackedWorktreePathFromGitState(context)), otherwise return
buildResolvedResult(...). This preserves existing calls to
resolveTrackedWorktreePathFromGitState and uses the already-defined
parseGitdirReference and isMainRepoPath helpers.

Comment on lines +578 to +596
export async function findProjectWorktreeByCurrentPath(
projectId: string,
worktreePath: string,
): Promise<SelectWorktree | null> {
const trackedWorktrees =
await listProjectWorktreesWithCurrentPaths(projectId);

for (const trackedWorktree of trackedWorktrees) {
if (!trackedWorktree.existsOnDisk) {
continue;
}

if (trackedWorktree.worktree.path === worktreePath) {
return trackedWorktree.worktree;
}
}

return null;
}
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.

⚠️ Potential issue | 🟡 Minor

Normalize paths before comparison to avoid false negatives.

Line 590 uses strict equality for path comparison, which could fail to match equivalent paths with different formats (e.g., trailing slashes, symlink resolution, or case differences on case-insensitive filesystems).

🔧 Proposed fix
 export async function findProjectWorktreeByCurrentPath(
 	projectId: string,
 	worktreePath: string,
 ): Promise<SelectWorktree | null> {
 	const trackedWorktrees =
 		await listProjectWorktreesWithCurrentPaths(projectId);
+	const normalizedInputPath = safeRealpath(worktreePath);

 	for (const trackedWorktree of trackedWorktrees) {
 		if (!trackedWorktree.existsOnDisk) {
 			continue;
 		}

-		if (trackedWorktree.worktree.path === worktreePath) {
+		if (safeRealpath(trackedWorktree.worktree.path) === normalizedInputPath) {
 			return trackedWorktree.worktree;
 		}
 	}

 	return null;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function findProjectWorktreeByCurrentPath(
projectId: string,
worktreePath: string,
): Promise<SelectWorktree | null> {
const trackedWorktrees =
await listProjectWorktreesWithCurrentPaths(projectId);
for (const trackedWorktree of trackedWorktrees) {
if (!trackedWorktree.existsOnDisk) {
continue;
}
if (trackedWorktree.worktree.path === worktreePath) {
return trackedWorktree.worktree;
}
}
return null;
}
export async function findProjectWorktreeByCurrentPath(
projectId: string,
worktreePath: string,
): Promise<SelectWorktree | null> {
const trackedWorktrees =
await listProjectWorktreesWithCurrentPaths(projectId);
const normalizedInputPath = safeRealpath(worktreePath);
for (const trackedWorktree of trackedWorktrees) {
if (!trackedWorktree.existsOnDisk) {
continue;
}
if (safeRealpath(trackedWorktree.worktree.path) === normalizedInputPath) {
return trackedWorktree.worktree;
}
}
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.ts`
around lines 578 - 596, The comparison in findProjectWorktreeByCurrentPath can
miss matches due to differing path formats; normalize both worktreePath and
trackedWorktree.worktree.path before comparing (e.g., use
path.resolve/path.normalize and, where appropriate, fs.realpath or toLowerCase
on case-insensitive platforms) so trailing slashes, ./ segments and symlinks are
handled consistently; update the loop in findProjectWorktreeByCurrentPath to
compute normalized/real paths for worktreePath and trackedWorktree.worktree.path
(or cache normalized values from listProjectWorktreesWithCurrentPaths) and
compare those normalized strings instead of strict equality on the raw values.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (1)

910-923: ⚠️ Potential issue | 🟠 Major

Use the resolved branch for post-import side effects.

resolveExternalWorktreeOpenTarget() can correct a stale request and return a different branch than input.branch. This block already persists worktreeTarget.branch to the worktree/workspace rows, but track() and setBranchBaseConfig() still use the stale input, so base-branch config can be written for the wrong branch.

Suggested fix
 				track("workspace_created", {
 					workspace_id: workspace.id,
 					project_id: project.id,
-					branch: input.branch,
+					branch: worktreeTarget.branch,
 					base_branch: baseBranch,
 					source: "external_import",
 				});

 				await setBranchBaseConfig({
 					repoPath: project.mainRepoPath,
-					branch: input.branch,
+					branch: worktreeTarget.branch,
 					baseBranch,
 					isExplicit: false,
 				});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts` around
lines 910 - 923, The telemetry and post-import config writes are still using the
original input.branch instead of the resolved branch from
resolveExternalWorktreeOpenTarget; update the calls so they use the resolved
worktreeTarget.branch (the value persisted to the worktree/workspace) when
invoking track("workspace_created", {...}) and when calling
setBranchBaseConfig({ repoPath: project.mainRepoPath, branch: ..., baseBranch,
isExplicit: false }), ensuring you reference worktreeTarget.branch rather than
input.branch so base-branch config and analytics reflect the actual opened
branch.
♻️ Duplicate comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (1)

90-94: ⚠️ Potential issue | 🟠 Major

Stop falling back to the stale DB path here.

Line 93 hides unresolved tracked paths behind worktree.path, so reopen/open/setup flows keep operating on the stale location when resolveWorktreePathOrThrow() returns null. Let this helper return null and make callers handle the unresolved state explicitly instead of silently reviving the old path.

Suggested fix
 async function getTrackedWorktreePath(
 	worktree: typeof worktrees.$inferSelect,
-): Promise<string> {
-	return (await resolveWorktreePathOrThrow(worktree.id)) ?? worktree.path;
+): Promise<string | null> {
+	return await resolveWorktreePathOrThrow(worktree.id);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts` around
lines 90 - 94, The helper getTrackedWorktreePath should not fall back to the
stale DB path; change its implementation so it returns the resolved path or null
(i.e., return await resolveWorktreePathOrThrow(worktree.id) without defaulting
to worktree.path) and update callers of getTrackedWorktreePath to explicitly
handle the null/unresolved case instead of assuming a valid path; reference
getTrackedWorktreePath, resolveWorktreePathOrThrow, and worktree.path when
locating the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/external-worktrees.ts`:
- Around line 57-69: The branch-match fallbacks in
resolveExternalWorktreeOpenTarget are reusing tracked rows even when their
recorded paths are dead; change the fallback logic that uses
__testOnlyExternalWorktreeDeps.findWorktreeWorkspaceByBranch and
__testOnlyExternalWorktreeDeps.findOrphanedWorktreeByBranch so you only accept
their worktree if its currentPath actually exists on disk (same criterion used
by listImportableExternalWorktrees). In practice, after getting a branch match
from findWorktreeWorkspaceByBranch(...)? .worktree or
findOrphanedWorktreeByBranch(...), verify the worktree.currentPath is present
(e.g., resolve and stat/exist check) before assigning trackedWorktree; apply the
same guarded check to the other occurrence around lines 113-129.

---

Outside diff comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts`:
- Around line 910-923: The telemetry and post-import config writes are still
using the original input.branch instead of the resolved branch from
resolveExternalWorktreeOpenTarget; update the calls so they use the resolved
worktreeTarget.branch (the value persisted to the worktree/workspace) when
invoking track("workspace_created", {...}) and when calling
setBranchBaseConfig({ repoPath: project.mainRepoPath, branch: ..., baseBranch,
isExplicit: false }), ensuring you reference worktreeTarget.branch rather than
input.branch so base-branch config and analytics reflect the actual opened
branch.

---

Duplicate comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts`:
- Around line 90-94: The helper getTrackedWorktreePath should not fall back to
the stale DB path; change its implementation so it returns the resolved path or
null (i.e., return await resolveWorktreePathOrThrow(worktree.id) without
defaulting to worktree.path) and update callers of getTrackedWorktreePath to
explicitly handle the null/unresolved case instead of assuming a valid path;
reference getTrackedWorktreePath, resolveWorktreePathOrThrow, and worktree.path
when locating the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 409d3234-19b9-4207-acf3-f5531287a2d3

📥 Commits

Reviewing files that changed from the base of the PR and between 6012ecf and 01bafa8.

📒 Files selected for processing (4)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/external-worktrees.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/external-worktrees.ts

@Kitenite Kitenite changed the title fix(desktop): auto-repair moved tracked worktrees [POC] fix(desktop): auto-repair moved tracked worktrees Mar 11, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts:538">
P3: Use a cross-platform move command in this test; `mv` will fail under Windows `cmd` when `execSync` runs.

(Based on your team's feedback about gating platform-specific changes with runtime checks.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

`git -C "${mainRepo}" worktree add "${oldPath}" -b feat-backoff HEAD`,
{ stdio: "ignore" },
);
execSync(`mv "${oldPath}" "${newPath}"`, { stdio: "ignore" });
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 11, 2026

Choose a reason for hiding this comment

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

P3: Use a cross-platform move command in this test; mv will fail under Windows cmd when execSync runs.

(Based on your team's feedback about gating platform-specific changes with runtime checks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/utils/repair-worktree-path.test.ts, line 538:

<comment>Use a cross-platform move command in this test; `mv` will fail under Windows `cmd` when `execSync` runs.

(Based on your team's feedback about gating platform-specific changes with runtime checks.) </comment>

<file context>
@@ -481,5 +516,56 @@ describe("repair-worktree-path", () => {
+			`git -C "${mainRepo}" worktree add "${oldPath}" -b feat-backoff HEAD`,
+			{ stdio: "ignore" },
+		);
+		execSync(`mv "${oldPath}" "${newPath}"`, { stdio: "ignore" });
+
+		mockWorktrees.set("wt-backoff-1", {
</file context>
Suggested change
execSync(`mv "${oldPath}" "${newPath}"`, { stdio: "ignore" });
execSync(
process.platform === "win32"
? `move "${oldPath}" "${newPath}"`
: `mv "${oldPath}" "${newPath}"`,
{ stdio: "ignore" },
);
Fix with Cubic

@Kitenite Kitenite closed this by deleting the head repository Mar 28, 2026
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