From 0a34c6abe43baceeff0f7bb68223b83dfe4d3c04 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 08:43:13 -0500 Subject: [PATCH] fix: preserve staged untracked files in all-file workspaces Co-Authored-By: Paperclip --- .../src/sandbox-managed-runtime.ts | 16 ++++++++++++++- .../src/sandbox-managed-seed.test.ts | 20 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.ts b/packages/adapter-utils/src/sandbox-managed-runtime.ts index 4f45ab675b..8f39597496 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.ts @@ -1232,7 +1232,8 @@ export async function prepareSandboxManagedRuntime(input: { } ignoredPaths = managedIgnored.paths("ignored"); } - const deletedPaths = gitSnapshot?.deletedPaths ?? managedIgnored?.paths("deleted") ?? []; + let deletedPaths = gitSnapshot?.deletedPaths ?? managedIgnored?.paths("deleted") ?? []; + let filesystemDeleted: Awaited> | null = null; managedIgnored?.close(); let baselineSnapshot: DirectorySnapshot | null = null; const cleanupWorkspaceSnapshot = async () => { @@ -1241,6 +1242,7 @@ export async function prepareSandboxManagedRuntime(input: { await disposeGitWorkspaceSnapshot(gitSnapshot); if (!gitSnapshot) for (const repo of repositories) await disposeGitWorkspaceSnapshot(repo.snapshot); if (managedIgnored) await fs.rm(path.dirname(managedIgnored.filePath), { recursive: true, force: true }); + if (filesystemDeleted) await fs.rm(path.dirname(filesystemDeleted.filePath), { recursive: true, force: true }); }; try { // A selected subfolder has no cloneable Git snapshot, but its parent @@ -1291,6 +1293,18 @@ export async function prepareSandboxManagedRuntime(input: { ignoredPaths: input.workspaceBaseline.ignoredPaths, }); } + if (input.workspaceFileMode === "all" && baselineSnapshot + && (isPathManifest(deletedPaths) ? deletedPaths.count : deletedPaths.length) > 0) { + // The filesystem is authoritative in all-files mode. Git can report a + // staged untracking as deleted while its file remains in the admitted tree. + filesystemDeleted = await createWorkspaceManifest(); + try { + for (const relative of workspacePaths(deletedPaths)) { + if (!baselineSnapshot.entries.has(relative)) filesystemDeleted.add("deleted", relative); + } + deletedPaths = filesystemDeleted.paths("deleted"); + } finally { filesystemDeleted.close(); } + } const seedGeneration = input.workspaceSeedCacheDirectory && baselineSnapshot && workspaceInboundMode !== "durable_seed" ? workspaceSeedGeneration(baselineSnapshot, gitSnapshot, repositories) : null; diff --git a/packages/adapter-utils/src/sandbox-managed-seed.test.ts b/packages/adapter-utils/src/sandbox-managed-seed.test.ts index 468418066c..a3781d73c4 100644 --- a/packages/adapter-utils/src/sandbox-managed-seed.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-seed.test.ts @@ -24,6 +24,10 @@ it.each([undefined, "all"] as const)("keeps nested Git metadata sanitized in col await git(repository, "init", "-b", "main"); await fs.writeFile(path.join(repository, ".gitignore"), "ignored.txt\n"); await fs.writeFile(path.join(repository, "work.txt"), "first\n"); + if (workspaceFileMode === "all") { + await fs.writeFile(path.join(repository, "notes.txt"), "keep after untracking\n"); + await fs.writeFile(path.join(repository, "absent.txt"), "truly deleted\n"); + } await git(repository, "add", "."); await git(repository, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "first"); await git(repository, "branch", "private-fixture"); @@ -34,6 +38,10 @@ it.each([undefined, "all"] as const)("keeps nested Git metadata sanitized in col await fs.writeFile(path.join(repository, ".git/hooks/post-commit"), "private-hook-fixture"); await fs.writeFile(path.join(repository, "work.txt"), "dirty\n"); await fs.writeFile(path.join(repository, "ignored.txt"), "ordinary ignored bytes"); + if (workspaceFileMode === "all") { + await git(repository, "rm", "--cached", "notes.txt"); + await fs.unlink(path.join(repository, "absent.txt")); + } const run = async (command: string) => { await execute("sh", ["-c", command]); }; const sync = async (operations: SandboxSyncOperation[]) => { for (const operation of operations) { @@ -68,7 +76,11 @@ it.each([undefined, "all"] as const)("keeps nested Git metadata sanitized in col expect(await git(checkout, "rev-parse", "HEAD")).toBe(head); expect(await git(checkout, "show", "HEAD:work.txt")).toBe("committed"); expect(await fs.readFile(path.join(checkout, "work.txt"), "utf8")).toBe("dirty\n"); - if (workspaceFileMode === "all") expect(await fs.readFile(path.join(checkout, "ignored.txt"), "utf8")).toBe("ordinary ignored bytes"); + if (workspaceFileMode === "all") { + expect(await fs.readFile(path.join(checkout, "ignored.txt"), "utf8")).toBe("ordinary ignored bytes"); + expect(await fs.readFile(path.join(checkout, "notes.txt"), "utf8")).toBe("keep after untracking\n"); + await expect(fs.stat(path.join(checkout, "absent.txt"))).rejects.toMatchObject({ code: "ENOENT" }); + } }; const cold = await prepare(); await assertSanitized(); expect([...cold.workspaceSyncSnapshot!.baseline.entries].some(([key]) => key.startsWith(`${relative}/.git/`))).toBe(false); @@ -94,6 +106,10 @@ it.each([undefined, "all"] as const)("keeps nested Git metadata sanitized in col expect(await fs.readFile(path.join(repository, "work.txt"), "utf8")).toBe("restored dirty bytes\n"); expect(await fs.readFile(path.join(repository, ".git/config"), "utf8")).toContain("fixture-token"); expect(await fs.readFile(path.join(repository, ".git/hooks/post-commit"), "utf8")).toBe("changed private hook"); - if (workspaceFileMode === "all") expect(await fs.readFile(path.join(repository, "ignored.txt"), "utf8")).toBe("updated ordinary ignored bytes"); + if (workspaceFileMode === "all") { + expect(await fs.readFile(path.join(repository, "ignored.txt"), "utf8")).toBe("updated ordinary ignored bytes"); + expect(await fs.readFile(path.join(repository, "notes.txt"), "utf8")).toBe("keep after untracking\n"); + await expect(fs.stat(path.join(repository, "absent.txt"))).rejects.toMatchObject({ code: "ENOENT" }); + } await recovered.cleanupWorkspaceSnapshot(); await cached.cleanupWorkspaceSnapshot(); }, 30000);