Skip to content
65 changes: 65 additions & 0 deletions apps/server/src/storageCleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,71 @@ describe("orphan worktree reports", () => {
),
);

it.live("reports linked worktrees nested deeper inside a plain folder", () =>
runCleanupTest(
Effect.gen(function* () {
const fixture = yield* cleanupFixture();
fixture.setThreads([]);
const replay = `${fixture.config.worktreesDir}/repository/replay`;
const deep = `${fixture.config.worktreesDir}/repository/deep`;
const worktrees = [`${deep}/a/b`, `${replay}/1376`];
for (const [index, worktree] of worktrees.entries()) {
yield* fixture.command(fixture.repo, [
"worktree",
"add",
"-b",
`nested-${index}`,
worktree,
]);
}
for (const folder of [...worktrees, `${deep}/a`, deep, replay]) {
yield* fixture.fs.utimes(folder, 1, 1);
}
const report = yield* fixture.service.runNow;
expect(
report.entries
.map((entry) => ({ path: entry.path, reason: entry.reason }))
.sort((left, right) => String(left.path).localeCompare(String(right.path))),
).toEqual(worktrees.map((path) => ({ path, reason: "No thread uses this worktree" })));
}),
),
);

it.live("still reports a plain folder whose nested worktree registration is stale", () =>
runCleanupTest(
Effect.gen(function* () {
const fixture = yield* cleanupFixture();
fixture.setThreads([]);
const replay = `${fixture.config.worktreesDir}/repository/replay`;
const worktree = `${replay}/1376`;
yield* fixture.command(fixture.repo, ["worktree", "add", "-b", "locked", worktree]);
yield* fixture.command(fixture.repo, ["worktree", "lock", worktree]);
yield* fixture.fs.rename(worktree, `${replay}/moved`);
yield* fixture.fs.makeDirectory(worktree);
yield* fixture.fs.utimes(replay, 1, 1);
expect((yield* fixture.service.runNow).entries.map((entry) => entry.path)).toEqual([
replay,
]);
}),
),
);

it.live("does not report nested worktrees inside a project checkout", () =>
runCleanupTest(
Effect.gen(function* () {
const fixture = yield* cleanupFixture();
fixture.setThreads([]);
const project = `${fixture.config.worktreesDir}/repository/replay/project`;
const worktree = `${project}/nested`;
yield* fixture.fs.makeDirectory(project, { recursive: true });
fixture.setProjectRoots([fixture.repo, project]);
yield* fixture.command(fixture.repo, ["worktree", "add", "-b", "nested", worktree]);
for (const folder of [worktree, project]) yield* fixture.fs.utimes(folder, 1, 1);
expect((yield* fixture.service.runNow).entries).toEqual([]);
}),
),
);

it.live("continues after an inaccessible directory's git probe", (context) =>
runCleanupTest(
Effect.gen(function* () {
Expand Down
23 changes: 22 additions & 1 deletion apps/server/src/storageCleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,26 @@ const make = Effect.gen(function* () {
},
Effect.orElseSucceed(() => undefined),
);
// Linked worktrees can sit deeper inside a plain folder, like <repository>/replay/<n>. Ask
// the projects' Git for them rather than walking folders that may hold large non-git trees.
let registered: ReadonlyArray<string> | undefined;
const nestedWorktrees = Effect.fnUntraced(function* (folder: string, managedRoot: string) {
if (yield* fs.exists(path.join(folder, ".git"))) return [];
registered ??= (yield* Effect.forEach(new Set(projectRoots), (root) =>
git.listWorktreePaths(root).pipe(Effect.orElseSucceed((): string[] => [])),
)).flat();
// Locked registrations outlive their directory, whose path may since hold something else.
return yield* Effect.filter(
registered.filter(
(worktree) => inside(folder, worktree) && !inProject(worktree, managedRoot),
),
(worktree) =>
Effect.gen(function* () {
if (pathKey(yield* fs.realPath(worktree)) !== pathKey(worktree)) return false;
return (yield* fs.stat(path.join(worktree, ".git"))).type === "File";
}).pipe(Effect.orElseSucceed(() => false)),
);
});
for (const root of roots) {
// Worktrees can be <root>/<repository>/<branch> or explicitly <root>/<name>.
for (const projectDirectory of yield* directories(root, root)) {
Expand All @@ -463,7 +483,8 @@ const make = Effect.gen(function* () {
return;
}
for (const worktreePath of yield* directories(projectDirectory, root)) {
yield* report(worktreePath);
const nested = yield* nestedWorktrees(worktreePath, root);
for (const target of nested.length > 0 ? nested : [worktreePath]) yield* report(target);
}
}).pipe(Effect.orElseSucceed(() => undefined));
}
Expand Down
Loading