Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 111 additions & 90 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { existsSync, realpathSync } from "node:fs";
import { resolve } from "node:path";
import type { SelectWorktree } from "@superset/local-db";
import { worktrees } from "@superset/local-db";
import { eq } from "drizzle-orm";
import { track } from "main/lib/analytics";
import { localDb } from "main/lib/local-db";
import { workspaceInitManager } from "main/lib/workspace-init-manager";
import { getWorkspaceRuntimeRegistry } from "main/lib/workspace-runtime";
import { z } from "zod";
Expand Down Expand Up @@ -267,43 +270,51 @@ export const createDeleteProcedures = () => {
await workspaceInitManager.acquireProjectLock(project.id);

try {
// Only delete from disk if this worktree was created by Superset
// External worktrees should only have their DB records removed
if (worktree.createdBySuperset) {
// Safety: Double-check it's not actually external (catches race conditions)
const externalWorktrees = await listExternalWorktrees(
project.mainRepoPath,
);
const worktreePathNorm = normalizePath(worktree.path);
const isActuallyExternal = externalWorktrees.some(
(wt) => normalizePath(wt.path) === worktreePathNorm,
);
// Safety: Check if this worktree is tracked in our database
// Prevents deletion of worktrees that were never properly imported
// Get all git worktrees
const allGitWorktrees = await listExternalWorktrees(
project.mainRepoPath,
);

if (isActuallyExternal) {
console.warn(
`[workspace/delete] Worktree at ${worktree.path} marked as created by Superset but found in external list - preserving as safety measure`,
);
track("worktree_delete_safety_trigger", {
workspace_id: input.id,
worktree_id: worktree.id,
worktree_path: worktree.path,
reason: "external_detection_mismatch",
});
} else {
// Confirmed safe to delete
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
clearWorkspaceDeletingStatus(input.id);
return removeResult;
}
}
} else {
console.log(
`[workspace/delete] Skipping disk deletion for external worktree at ${worktree.path}`,
// Get all tracked worktree paths from database for this project
const trackedWorktrees = localDb
.select({ path: worktrees.path })
.from(worktrees)
.where(eq(worktrees.projectId, project.id))
.all();
const trackedPaths = new Set(
trackedWorktrees.map((wt) => normalizePath(wt.path)),
);

// Check if this worktree exists in git but is NOT tracked in our DB
const worktreePathNorm = normalizePath(worktree.path);
const existsInGit = allGitWorktrees.some(
(wt) => normalizePath(wt.path) === worktreePathNorm,
);
const isActuallyExternal =
existsInGit && !trackedPaths.has(worktreePathNorm);
Comment on lines +295 to +296
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P0: This external-detection condition misclassifies imported external worktrees as safe-to-delete, which can delete user-managed worktree directories from disk.

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/procedures/delete.ts, line 295:

<comment>This external-detection condition misclassifies imported external worktrees as safe-to-delete, which can delete user-managed worktree directories from disk.</comment>

<file context>
@@ -267,43 +270,51 @@ export const createDeleteProcedures = () => {
+						const existsInGit = allGitWorktrees.some(
+							(wt) => normalizePath(wt.path) === worktreePathNorm,
+						);
+						const isActuallyExternal =
+							existsInGit && !trackedPaths.has(worktreePathNorm);
+
</file context>
Suggested change
const isActuallyExternal =
existsInGit && !trackedPaths.has(worktreePathNorm);
const isActuallyExternal =
!worktree.createdBySuperset ||
(existsInGit && !trackedPaths.has(worktreePathNorm));
Fix with Cubic


if (isActuallyExternal) {
console.warn(
`[workspace/delete] Worktree at ${worktree.path} exists in git but not tracked in database - preserving as safety measure`,
);
track("worktree_delete_safety_trigger", {
workspace_id: input.id,
worktree_id: worktree.id,
worktree_path: worktree.path,
reason: "untracked_worktree_detected",
});
} else {
// Safe to delete - worktree is tracked in our database
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
clearWorkspaceDeletingStatus(input.id);
return removeResult;
}
}
} finally {
workspaceInitManager.releaseProjectLock(project.id);
Expand Down Expand Up @@ -486,68 +497,78 @@ export const createDeleteProcedures = () => {
worktree.path,
);

// Only delete from disk if this worktree was created by Superset
if (worktree.createdBySuperset) {
// Safety: Double-check it's not actually external (catches race conditions)
const externalWorktrees = await listExternalWorktrees(
project.mainRepoPath,
);
const isActuallyExternal = externalWorktrees.some(
(wt) => wt.path === worktree.path,
);
// Safety: Check if this worktree is tracked in our database
// Prevents deletion of worktrees that were never properly imported
// Get all git worktrees
const allGitWorktrees = await listExternalWorktrees(
project.mainRepoPath,
);

if (isActuallyExternal) {
console.warn(
`[worktree/delete] Worktree at ${worktree.path} marked as created by Superset but found in external list - preserving as safety measure`,
);
track("worktree_delete_safety_trigger", {
worktree_id: input.worktreeId,
worktree_path: worktree.path,
reason: "external_detection_mismatch",
// Get all tracked worktree paths from database for this project
const trackedWorktrees = localDb
.select({ path: worktrees.path })
.from(worktrees)
.where(eq(worktrees.projectId, project.id))
.all();
const trackedPaths = new Set(
trackedWorktrees.map((wt) => normalizePath(wt.path)),
);

// Check if this worktree exists in git but is NOT tracked in our DB
const worktreePathNorm = normalizePath(worktree.path);
const existsInGit = allGitWorktrees.some(
(wt) => normalizePath(wt.path) === worktreePathNorm,
);
const isActuallyExternal =
existsInGit && !trackedPaths.has(worktreePathNorm);

if (isActuallyExternal) {
console.warn(
`[worktree/delete] Worktree at ${worktree.path} exists in git but not tracked in database - preserving as safety measure`,
);
track("worktree_delete_safety_trigger", {
worktree_id: input.worktreeId,
worktree_path: worktree.path,
reason: "untracked_worktree_detected",
});
} else {
// Safe to delete - worktree is tracked in our database
if (exists) {
const teardownResult = await runTeardown({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
workspaceName: worktree.branch,
projectId: project.id,
});
} else {
// Confirmed safe to delete
if (exists) {
const teardownResult = await runTeardown({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
workspaceName: worktree.branch,
projectId: project.id,
});
if (!teardownResult.success) {
if (input.force) {
console.warn(
`[worktree/delete] Teardown failed but force=true, continuing deletion:`,
teardownResult.error,
);
} else {
return {
success: false,
error: `Teardown failed: ${teardownResult.error}`,
output: teardownResult.output,
};
}
if (!teardownResult.success) {
if (input.force) {
console.warn(
`[worktree/delete] Teardown failed but force=true, continuing deletion:`,
teardownResult.error,
);
} else {
return {
success: false,
error: `Teardown failed: ${teardownResult.error}`,
output: teardownResult.output,
};
}
}
}

if (exists) {
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
return removeResult;
}
} else {
console.warn(
`Worktree ${worktree.path} not found in git, skipping removal`,
);
if (exists) {
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
return removeResult;
}
} else {
console.warn(
`Worktree ${worktree.path} not found in git, skipping removal`,
);
}
} else {
console.log(
`[worktree/delete] Skipping disk deletion for external worktree at ${worktree.path}`,
);
}
} finally {
workspaceInitManager.releaseProjectLock(project.id);
Expand Down
8 changes: 1 addition & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading