From 40986b2331e3f3adbe27d943862cd2c1e9d9e4bb Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 02:19:12 +0000 Subject: [PATCH 1/5] fix(server): keep worktrees containing nested repositories Co-authored-by: Yukun Shan <92423096+nateEc@users.noreply.github.com> --- apps/server/src/storageCleanup.test.ts | 82 +++++++++++++++++++++++++- apps/server/src/storageCleanup.ts | 44 +++++++++++--- 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index c05d4aed4cfb..f27c9f7a097b 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -482,6 +482,35 @@ const runCleanupTest = ( >, ) => effect.pipe(Effect.provide(cleanupTestLayer), Effect.scoped); +it.live.each([ + { policy: "tracked-changes", kind: "worktree" }, + { policy: "tracked-changes", kind: "repository" }, + { policy: "uncommitted-changes", kind: "worktree" }, + { policy: "uncommitted-changes", kind: "repository" }, +] as const)("keeps an ignored nested $policy / $kind", ({ policy, kind }) => + runCleanupTest( + Effect.gen(function* () { + const { service, fs, repo, worktree, command, setPolicy } = yield* cleanupFixture(); + setPolicy(policy); + const nested = `${worktree}/dist/nested`; + if (kind === "worktree") { + yield* command(repo, ["worktree", "add", "-b", "nested", nested]); + } else { + yield* command(repo, ["clone", "--no-hardlinks", repo, nested]); + } + yield* fs.writeFileString(`${nested}/uncommitted.txt`, "irreplaceable work\n"); + expect((yield* command(worktree, ["status", "--porcelain"])).stdout).toBe(""); + const report = yield* service.runNow; + expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ + outcome: "kept", + reason: "Contains a nested Git repository", + }); + expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("irreplaceable work\n"); + expect((yield* command(nested, ["rev-parse", "--show-toplevel"])).stdout.trim()).toBe(nested); + }), + ), +); + const submoduleCleanupFixture = () => Effect.gen(function* () { const fixture = yield* cleanupFixture(); @@ -506,6 +535,48 @@ const submoduleCleanupFixture = () => return { ...fixture, submodule, child, recordSubmodules }; }); +it.live("inspects repositories even in a pnpm store excluded from reclaimed bytes", () => + runCleanupTest( + Effect.gen(function* () { + const { service, fs, repo, worktree, command, setPolicy } = yield* cleanupFixture(); + setPolicy("tracked-changes"); + const storeDir = yield* fs.makeTempDirectoryScoped({ prefix: "cleanup-store-" }); + const nested = `${worktree}/node_modules/.pnpm/nested`; + yield* command(repo, ["clone", "--no-hardlinks", repo, nested]); + yield* fs.writeFileString(`${nested}/uncommitted.txt`, "keep me"); + yield* fs.writeFileString( + `${worktree}/node_modules/.modules.yaml`, + JSON.stringify({ storeDir, virtualStoreDir: ".pnpm" }), + ); + const report = yield* service.runNow; + expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ + outcome: "kept", + reason: "Contains a nested Git repository", + }); + expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("keep me"); + }), + ), +); + +it.live("keeps an ignored repository inside a registered submodule", () => + runCleanupTest( + Effect.gen(function* () { + const { service, fs, repo, worktree, submodule, command, setPolicy } = + yield* submoduleCleanupFixture(); + setPolicy("tracked-changes"); + const nested = `${submodule}/dist/nested`; + yield* command(repo, ["clone", "--no-hardlinks", repo, nested]); + yield* fs.writeFileString(`${nested}/uncommitted.txt`, "keep me"); + const report = yield* service.runNow; + expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ + outcome: "kept", + reason: "Contains a nested Git repository", + }); + expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("keep me"); + }), + ), +); + it.live("removes clean submodules recursively, including deinitialized repositories", () => runCleanupTest( Effect.gen(function* () { @@ -1428,7 +1499,7 @@ describe("storage cleanup reports and local file policies", () => { }), ), ); - it.live("still removes a worktree when measuring its size fails", () => + it.live("keeps a worktree when the inspection cannot read its directories", () => runCleanupTest( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -1439,9 +1510,14 @@ describe("storage cleanup reports and local file policies", () => { }), ); const report = yield* service.runNow; - expect(report.entries[0]).toMatchObject({ outcome: "removed", bytes: null, files: null }); + expect(report.entries[0]).toMatchObject({ + outcome: "kept", + reason: "Worktree could not be fully inspected", + bytes: null, + files: null, + }); expect(report.bytesFreed).toBe(0); - expect(yield* fs.exists(worktree)).toBe(false); + expect(yield* fs.exists(worktree)).toBe(true); }), ), ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 15d214719d59..6d5dd4e473fc 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -279,15 +279,34 @@ const make = Effect.gen(function* () { ); const measureWorktree = Effect.fn("storageCleanup.measureWorktree")( - function* (worktreePath: string) { + function* (worktreePath: string, inspectRepositories = false) { const root = yield* fs.realPath(worktreePath); const skipped = yield* sharedPnpmVirtualStore(root); + const incomplete = { bytes: null, reason: "worktree could not be fully inspected" }; + const repositories = new Set([pathKey(root)]); + if (inspectRepositories && (yield* fs.exists(path.join(root, ".gitmodules")))) { + const submodules = yield* git.execute({ + operation: "StorageCleanup.nestedRepositories", + cwd: root, + args: [ + "submodule", + "foreach", + "--recursive", + "--quiet", + 'printf "%s\\0" "$toplevel/$sm_path"', + ], + maxOutputBytes: 64 * 1024, + }); + if (submodules.stdoutTruncated || submodules.stderrTruncated) return incomplete; + for (const submodule of submodules.stdout.split("\0").filter(Boolean)) + repositories.add(pathKey(path.resolve(submodule))); + } const pending = [root]; let entries = 1; let bytes = 0; while (pending.length > 0) { const target = pending.pop()!; - if (target === skipped) continue; + if (target === skipped && !inspectRepositories) continue; // Effect's stat follows links. Probe with readLink first to get lstat // semantics, including skipping dangling links and directory links. const isLink = yield* fs.readLink(target).pipe( @@ -301,9 +320,10 @@ const make = Effect.gen(function* () { ), ); if (isLink) continue; - if ((yield* fs.realPath(target)) !== target) return null; + if ((yield* fs.realPath(target)) !== target) return incomplete; const stat = yield* fs.stat(target); if (stat.type === "File") { + if (skipped !== null && inside(skipped, target)) continue; // Removing a shared hardlink leaves its blocks allocated. if (Option.getOrElse(stat.nlink, () => 1n) !== 1n) continue; bytes += Option.match(stat.blocks, { @@ -312,19 +332,23 @@ const make = Effect.gen(function* () { }); } else if (stat.type === "Directory") { const names = yield* fs.readDirectory(target); + // Git status omits ignored repositories, and worktree remove deletes them. + // Registered submodules have their own recursive safety checks below. + if (inspectRepositories && names.includes(".git") && !repositories.has(pathKey(target))) + return { bytes: null, reason: "contains a nested Git repository" }; entries += names.length; - if (entries > 2_000_000) return null; + if (entries > 2_000_000) return incomplete; for (const name of names) { const child = path.join(target, name); - if (!inside(root, child)) return null; + if (!inside(root, child)) return incomplete; pending.push(child); } } } - return bytes; + return { bytes, reason: null }; }, Effect.timeout("30 seconds"), - Effect.orElseSucceed(() => null), + Effect.orElseSucceed(() => ({ bytes: null, reason: "worktree could not be fully inspected" })), ); // Large snapshots take seconds; use the reader worker instead of blocking the event loop. @@ -433,7 +457,7 @@ const make = Effect.gen(function* () { firstMeasurement ? Effect.map(Option.some) : Effect.timeoutOption(remaining), ); firstMeasurement = false; - if (Option.isSome(measured)) orphanSizes.set(key, measured.value); + if (Option.isSome(measured)) orphanSizes.set(key, measured.value.bytes); } const bytes = orphanSizes.get(key) ?? null; entries.push({ @@ -982,7 +1006,9 @@ const make = Effect.gen(function* () { }); const blocked = yield* blockedSinceCheck(); if (blocked !== null) return keep(blocked); - const bytes = yield* measureWorktree(worktreePath); + const inspection = yield* measureWorktree(worktreePath, true); + if (inspection.reason !== null) return keep(inspection.reason); + const bytes = inspection.bytes; const blockedWhileMeasuring = yield* blockedSinceCheck(); if (blockedWhileMeasuring !== null) return keep(blockedWhileMeasuring); if (yield* branchChanged(worktreePath, thread.branch)) From 3c3546fecf6c6de8be80e344e1406d74c59d777a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 02:29:46 +0000 Subject: [PATCH 2/5] fix(server): preserve nested bare repositories during cleanup Co-authored-by: Yukun Shan <92423096+nateEc@users.noreply.github.com> --- apps/server/src/storageCleanup.test.ts | 19 +++++++++++++++++-- apps/server/src/storageCleanup.ts | 7 ++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index f27c9f7a097b..8e03d6ee8ec7 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -485,8 +485,10 @@ const runCleanupTest = ( it.live.each([ { policy: "tracked-changes", kind: "worktree" }, { policy: "tracked-changes", kind: "repository" }, + { policy: "tracked-changes", kind: "bare" }, { policy: "uncommitted-changes", kind: "worktree" }, { policy: "uncommitted-changes", kind: "repository" }, + { policy: "uncommitted-changes", kind: "bare" }, ] as const)("keeps an ignored nested $policy / $kind", ({ policy, kind }) => runCleanupTest( Effect.gen(function* () { @@ -496,7 +498,13 @@ it.live.each([ if (kind === "worktree") { yield* command(repo, ["worktree", "add", "-b", "nested", nested]); } else { - yield* command(repo, ["clone", "--no-hardlinks", repo, nested]); + yield* command(repo, [ + "clone", + "--no-hardlinks", + ...(kind === "bare" ? ["--bare"] : []), + repo, + nested, + ]); } yield* fs.writeFileString(`${nested}/uncommitted.txt`, "irreplaceable work\n"); expect((yield* command(worktree, ["status", "--porcelain"])).stdout).toBe(""); @@ -506,7 +514,14 @@ it.live.each([ reason: "Contains a nested Git repository", }); expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("irreplaceable work\n"); - expect((yield* command(nested, ["rev-parse", "--show-toplevel"])).stdout.trim()).toBe(nested); + expect( + (yield* command(nested, [ + "-c", + "safe.bareRepository=all", + "rev-parse", + "--is-bare-repository", + ])).stdout.trim(), + ).toBe(String(kind === "bare")); }), ), ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 6d5dd4e473fc..66aef145ba0f 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -334,7 +334,12 @@ const make = Effect.gen(function* () { const names = yield* fs.readDirectory(target); // Git status omits ignored repositories, and worktree remove deletes them. // Registered submodules have their own recursive safety checks below. - if (inspectRepositories && names.includes(".git") && !repositories.has(pathKey(target))) + if ( + inspectRepositories && + !repositories.has(pathKey(target)) && + (names.includes(".git") || + (names.includes("HEAD") && names.includes("objects") && names.includes("refs"))) + ) return { bytes: null, reason: "contains a nested Git repository" }; entries += names.length; if (entries > 2_000_000) return incomplete; From 45a34e45ab538829e129c35669734daccb2fbb4e Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 02:36:45 +0000 Subject: [PATCH 3/5] fix(server): allow cleanup of old-form registered submodules Co-authored-by: Yukun Shan <92423096+nateEc@users.noreply.github.com> --- apps/server/src/storageCleanup.test.ts | 4 ++++ apps/server/src/storageCleanup.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index 8e03d6ee8ec7..625278345517 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -605,6 +605,10 @@ it.live("removes clean submodules recursively, including deinitialized repositor repo, "stored", ]); + const legacy = `${worktree}/legacy`; + yield* command(repo, ["clone", "--no-hardlinks", repo, legacy]); + yield* command(worktree, ["submodule", "add", repo, "legacy"]); + expect((yield* fs.stat(`${legacy}/.git`)).type).toBe("Directory"); yield* recordSubmodules(); yield* command(submodule, ["submodule", "deinit", "stored"]); const admin = (yield* command(worktree, ["rev-parse", "--absolute-git-dir"])).stdout.trim(); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 66aef145ba0f..f2f274f77cce 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -298,8 +298,10 @@ const make = Effect.gen(function* () { maxOutputBytes: 64 * 1024, }); if (submodules.stdoutTruncated || submodules.stderrTruncated) return incomplete; - for (const submodule of submodules.stdout.split("\0").filter(Boolean)) + for (const submodule of submodules.stdout.split("\0").filter(Boolean)) { repositories.add(pathKey(path.resolve(submodule))); + repositories.add(pathKey(path.resolve(submodule, ".git"))); + } } const pending = [root]; let entries = 1; From 74a933cd0f2bcff4c2853d1cd6ae5a1d184bdf0e Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 27 Oct 2026 01:39:49 +0000 Subject: [PATCH 4/5] fix(server): inspect repository markers and legacy submodule stores --- apps/server/src/storageCleanup.test.ts | 38 +++++++++++++++++++++++++- apps/server/src/storageCleanup.ts | 8 ++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index 625278345517..72a700854091 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -485,9 +485,11 @@ const runCleanupTest = ( it.live.each([ { policy: "tracked-changes", kind: "worktree" }, { policy: "tracked-changes", kind: "repository" }, + { policy: "tracked-changes", kind: "uppercase" }, { policy: "tracked-changes", kind: "bare" }, { policy: "uncommitted-changes", kind: "worktree" }, { policy: "uncommitted-changes", kind: "repository" }, + { policy: "uncommitted-changes", kind: "uppercase" }, { policy: "uncommitted-changes", kind: "bare" }, ] as const)("keeps an ignored nested $policy / $kind", ({ policy, kind }) => runCleanupTest( @@ -495,7 +497,7 @@ it.live.each([ const { service, fs, repo, worktree, command, setPolicy } = yield* cleanupFixture(); setPolicy(policy); const nested = `${worktree}/dist/nested`; - if (kind === "worktree") { + if (kind === "worktree" || kind === "uppercase") { yield* command(repo, ["worktree", "add", "-b", "nested", nested]); } else { yield* command(repo, [ @@ -506,6 +508,7 @@ it.live.each([ nested, ]); } + if (kind === "uppercase") yield* fs.rename(`${nested}/.git`, `${nested}/.GIT`); yield* fs.writeFileString(`${nested}/uncommitted.txt`, "irreplaceable work\n"); expect((yield* command(worktree, ["status", "--porcelain"])).stdout).toBe(""); const report = yield* service.runNow; @@ -516,6 +519,12 @@ it.live.each([ expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("irreplaceable work\n"); expect( (yield* command(nested, [ + ...(kind === "uppercase" + ? [ + `--git-dir=${(yield* fs.readFileString(`${nested}/.GIT`)).trim().slice(8)}`, + "--work-tree=.", + ] + : []), "-c", "safe.bareRepository=all", "rev-parse", @@ -609,6 +618,33 @@ it.live("removes clean submodules recursively, including deinitialized repositor yield* command(repo, ["clone", "--no-hardlinks", repo, legacy]); yield* command(worktree, ["submodule", "add", repo, "legacy"]); expect((yield* fs.stat(`${legacy}/.git`)).type).toBe("Directory"); + yield* command(legacy, [ + "-c", + "protocol.file.allow=always", + "submodule", + "add", + repo, + "child", + ]); + yield* command(legacy, [ + "-c", + "protocol.file.allow=always", + "submodule", + "add", + repo, + "stored", + ]); + yield* command(legacy, [ + "-c", + "user.name=Cleanup test", + "-c", + "user.email=cleanup@example.test", + "commit", + "-am", + "record legacy children", + ]); + yield* command(legacy, ["push", "origin", "HEAD:legacy"]); + yield* command(legacy, ["submodule", "deinit", "stored"]); yield* recordSubmodules(); yield* command(submodule, ["submodule", "deinit", "stored"]); const admin = (yield* command(worktree, ["rev-parse", "--absolute-git-dir"])).stdout.trim(); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index ec95bdcd200a..54036074501d 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -284,6 +284,7 @@ const make = Effect.gen(function* () { const skipped = yield* sharedPnpmVirtualStore(root); const incomplete = { bytes: null, reason: "worktree could not be fully inspected" }; const repositories = new Set([pathKey(root)]); + const moduleStores: string[] = []; if (inspectRepositories && (yield* fs.exists(path.join(root, ".gitmodules")))) { const submodules = yield* git.execute({ operation: "StorageCleanup.nestedRepositories", @@ -301,6 +302,7 @@ const make = Effect.gen(function* () { for (const submodule of submodules.stdout.split("\0").filter(Boolean)) { repositories.add(pathKey(path.resolve(submodule))); repositories.add(pathKey(path.resolve(submodule, ".git"))); + moduleStores.push(pathKey(path.resolve(submodule, ".git", "modules"))); } } const pending = [root]; @@ -334,13 +336,15 @@ const make = Effect.gen(function* () { }); } else if (stat.type === "Directory") { const names = yield* fs.readDirectory(target); + const markers = inspectRepositories ? names.map((name) => name.toLowerCase()) : []; // Git status omits ignored repositories, and worktree remove deletes them. // Registered submodules have their own recursive safety checks below. if ( inspectRepositories && !repositories.has(pathKey(target)) && - (names.includes(".git") || - (names.includes("HEAD") && names.includes("objects") && names.includes("refs"))) + !moduleStores.some((store) => pathKey(target) === store || inside(store, target)) && + (markers.includes(".git") || + (markers.includes("head") && markers.includes("objects") && markers.includes("refs"))) ) return { bytes: null, reason: "contains a nested Git repository" }; entries += names.length; From bf4a1a6bd9a0c54ec6db498cf31ef4b3c4e5d4eb Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 27 Oct 2026 01:55:40 +0000 Subject: [PATCH 5/5] fix(server): skip excluded pnpm file metadata during cleanup --- apps/server/src/storageCleanup.test.ts | 61 ++++++++++++++++---------- apps/server/src/storageCleanup.ts | 13 +++++- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index 72a700854091..79d79b81d51e 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -559,27 +559,34 @@ const submoduleCleanupFixture = () => return { ...fixture, submodule, child, recordSubmodules }; }); -it.live("inspects repositories even in a pnpm store excluded from reclaimed bytes", () => - runCleanupTest( - Effect.gen(function* () { - const { service, fs, repo, worktree, command, setPolicy } = yield* cleanupFixture(); - setPolicy("tracked-changes"); - const storeDir = yield* fs.makeTempDirectoryScoped({ prefix: "cleanup-store-" }); - const nested = `${worktree}/node_modules/.pnpm/nested`; - yield* command(repo, ["clone", "--no-hardlinks", repo, nested]); - yield* fs.writeFileString(`${nested}/uncommitted.txt`, "keep me"); - yield* fs.writeFileString( - `${worktree}/node_modules/.modules.yaml`, - JSON.stringify({ storeDir, virtualStoreDir: ".pnpm" }), - ); - const report = yield* service.runNow; - expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ - outcome: "kept", - reason: "Contains a nested Git repository", - }); - expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("keep me"); - }), - ), +it.live.each(["repository", "worktree"] as const)( + "inspects a %s in a pnpm store excluded from reclaimed bytes", + (kind) => + runCleanupTest( + Effect.gen(function* () { + const { service, fs, repo, worktree, command, setPolicy } = yield* cleanupFixture(); + setPolicy("tracked-changes"); + const storeDir = yield* fs.makeTempDirectoryScoped({ prefix: "cleanup-store-" }); + const nested = `${worktree}/node_modules/.pnpm/nested`; + yield* command( + repo, + kind === "worktree" + ? ["worktree", "add", "-b", "nested", nested] + : ["clone", "--no-hardlinks", repo, nested], + ); + yield* fs.writeFileString(`${nested}/uncommitted.txt`, "keep me"); + yield* fs.writeFileString( + `${worktree}/node_modules/.modules.yaml`, + JSON.stringify({ storeDir, virtualStoreDir: ".pnpm" }), + ); + const report = yield* service.runNow; + expect(report.entries.find((entry) => entry.path === worktree)).toMatchObject({ + outcome: "kept", + reason: "Contains a nested Git repository", + }); + expect(yield* fs.readFileString(`${nested}/uncommitted.txt`)).toBe("keep me"); + }), + ), ); it.live("keeps an ignored repository inside a registered submodule", () => @@ -1478,11 +1485,13 @@ describe("storage cleanup reports and local file policies", () => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const storeDir = yield* fs.makeTempDirectoryScoped({ prefix: "pnpm-store-" }); + const statted: string[] = []; const { service, worktree, setPolicy } = yield* cleanupFixture().pipe( Effect.provideService(FileSystem.FileSystem, { ...fs, - stat: (target) => - fs + stat: (target) => { + statted.push(target); + return fs .stat(target) .pipe( Effect.map((info) => @@ -1490,7 +1499,8 @@ describe("storage cleanup reports and local file policies", () => { ? { ...info, dev: info.dev + 1n } : info, ), - ), + ); + }, }), ); setPolicy("tracked-changes"); @@ -1521,6 +1531,9 @@ describe("storage cleanup reports and local file policies", () => { ); const report = yield* service.runNow; expect(report.entries[0]).toMatchObject({ outcome: "removed", bytes }); + expect(statted.includes(`${nodeModules}/.pnpm/pkg@1.0.0/index.js`)).toBe( + store !== "same-device", + ); }), ), ); diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 54036074501d..cd98f30d27f2 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -1,3 +1,5 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeFSP from "node:fs/promises"; import { GitCommandError, OrchestrationV2AppThreadJson, @@ -335,7 +337,11 @@ const make = Effect.gen(function* () { onSome: (blocks) => Number(blocks * 512n), }); } else if (stat.type === "Directory") { - const names = yield* fs.readDirectory(target); + const dirents = + skipped !== null && (target === skipped || inside(skipped, target)) + ? yield* Effect.tryPromise(() => NodeFSP.readdir(target, { withFileTypes: true })) + : null; + const names = dirents?.map((entry) => entry.name) ?? (yield* fs.readDirectory(target)); const markers = inspectRepositories ? names.map((name) => name.toLowerCase()) : []; // Git status omits ignored repositories, and worktree remove deletes them. // Registered submodules have their own recursive safety checks below. @@ -349,7 +355,10 @@ const make = Effect.gen(function* () { return { bytes: null, reason: "contains a nested Git repository" }; entries += names.length; if (entries > 2_000_000) return incomplete; - for (const name of names) { + const directories = dirents + ?.filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); + for (const name of directories ?? names) { const child = path.join(target, name); if (!inside(root, child)) return incomplete; pending.push(child);