Skip to content

Commit f5bc64b

Browse files
only21milclaude
andauthored
fix(web): deleting a thread no longer offers to remove a shared worktree (#24489)
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent 64f3042 commit f5bc64b

2 files changed

Lines changed: 35 additions & 15 deletions

File tree

‎apps/web/src/worktreeCleanup.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,29 @@ describe("getOrphanedWorktreePathForThread", () => {
5151
expect(result).toBeNull();
5252
});
5353

54+
it.each([
55+
["/tmp/repo/worktrees/feature-a", "/tmp/repo/worktrees/feature-a/"],
56+
["/tmp/repo/worktrees/feature-a", "/tmp/repo/worktrees/feature-a//"],
57+
["C:\\repo\\worktrees\\feature-a", "c:/repo/worktrees/feature-a/"],
58+
])("treats %s and %s as the same worktree", (targetPath, otherPath) => {
59+
const threads = [
60+
makeThread({ id: ThreadId.make("thread-1"), worktreePath: targetPath }),
61+
makeThread({ id: ThreadId.make("thread-2"), worktreePath: otherPath }),
62+
];
63+
expect(getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"))).toBeNull();
64+
expect(getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-2"))).toBeNull();
65+
});
66+
67+
it("keeps case-sensitive POSIX worktrees apart", () => {
68+
const threads = [
69+
makeThread({ id: ThreadId.make("thread-1"), worktreePath: "/tmp/repo/worktrees/Feature" }),
70+
makeThread({ id: ThreadId.make("thread-2"), worktreePath: "/tmp/repo/worktrees/feature" }),
71+
];
72+
expect(getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"))).toBe(
73+
"/tmp/repo/worktrees/Feature",
74+
);
75+
});
76+
5477
it("ignores threads linked to different worktrees", () => {
5578
const threads = [
5679
makeThread({

‎apps/web/src/worktreeCleanup.ts‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import type { ThreadShell } from "./types";
1+
import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
22

3-
function normalizeWorktreePath(path: string | null): string | null {
4-
const trimmed = path?.trim();
5-
if (!trimmed) {
6-
return null;
7-
}
8-
return trimmed;
9-
}
3+
import type { ThreadShell } from "./types";
104

115
export function getOrphanedWorktreePathForThread(
126
threads: ReadonlyArray<Pick<ThreadShell, "id" | "worktreePath">>,
@@ -17,17 +11,20 @@ export function getOrphanedWorktreePathForThread(
1711
return null;
1812
}
1913

20-
const targetWorktreePath = normalizeWorktreePath(targetThread.worktreePath);
14+
const targetWorktreePath = targetThread.worktreePath?.trim();
2115
if (!targetWorktreePath) {
2216
return null;
2317
}
2418

25-
const isShared = threads.some((thread) => {
26-
if (thread.id === threadId) {
27-
return false;
28-
}
29-
return normalizeWorktreePath(thread.worktreePath) === targetWorktreePath;
30-
});
19+
// Threads can store one worktree as `/repo/wt` and `/repo/wt/`, and
20+
// confirming deletion force-removes the checkout for every thread on it.
21+
const targetKey = normalizeProjectPathForComparison(targetWorktreePath);
22+
const isShared = threads.some(
23+
(thread) =>
24+
thread.id !== threadId &&
25+
thread.worktreePath !== null &&
26+
normalizeProjectPathForComparison(thread.worktreePath) === targetKey,
27+
);
3128

3229
return isShared ? null : targetWorktreePath;
3330
}

0 commit comments

Comments
 (0)