From d54dbe328bdce9bc4ebac113f7b8f0c986b60781 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:50:43 +1100 Subject: [PATCH 1/3] fix(server): reverting files removes what a new .gitignore from the turn was hiding Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriver.test.ts | 2 ++ apps/server/src/vcs/GitVcsDriver.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 9cb8807219402..39cd1a11f7a54 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -1083,6 +1083,8 @@ it.effect("restores empty checkpoints without changing paths outside the workspa yield* fileSystem.writeFileString(path.join(cwd, "ignored.txt"), "keep\n"); yield* fileSystem.makeDirectory(path.join(cwd, "untracked")); yield* fileSystem.writeFileString(path.join(cwd, "untracked", "file.txt"), "remove\n"); + yield* fileSystem.writeFileString(path.join(cwd, "untracked", ".gitignore"), "hidden.txt\n"); + yield* fileSystem.writeFileString(path.join(cwd, "untracked", "hidden.txt"), "remove\n"); assert.isTrue( yield* driver.checkpoints.restoreCheckpoint({ cwd, checkpointRef, fallbackToHead: false }), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index d46c195155f3c..c3ed31141a865 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -1296,12 +1296,18 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( ), ); yield* recreateWorkspace; - const cleaned = yield* execute({ + const clean = execute({ operation, cwd: input.cwd, args: [...longPaths, "clean", "-fd", "--", "."], allowNonZeroExit: true, }); + let cleaned = yield* clean; + // Removing a new .gitignore exposes the files it hid; clean again until nothing is left. + for (let pass = 1; pass < 4 && cleaned.exitCode === 0 && cleaned.stdout.length > 0; pass++) { + yield* recreateWorkspace; + cleaned = yield* clean; + } if (cleaned.exitCode !== 0) { // Git can remove every child, then fail trying to remove './' itself. const emptiedWorkspace = From 6bd5263aa644ffd9c00f6691f40f9c6a1b4ad644 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:24:52 +1100 Subject: [PATCH 2/3] fix(server): checkpoint restore reports files left after the clean pass limit Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriver.test.ts | 12 ++++++++++++ apps/server/src/vcs/GitVcsDriver.ts | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 39cd1a11f7a54..6beb7ff9eca27 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -1102,6 +1102,18 @@ it.effect("restores empty checkpoints without changing paths outside the workspa }); assert.strictEqual(staged.stdout.trim(), "outside.txt"); } + let deepest = cwd; + for (let depth = 0; depth < 4; depth++) { + yield* fileSystem.writeFileString(path.join(deepest, ".gitignore"), "d/.gitignore\n"); + deepest = path.join(deepest, "d"); + yield* fileSystem.makeDirectory(deepest); + } + yield* fileSystem.writeFileString(path.join(deepest, ".gitignore"), "hidden.txt\n"); + yield* fileSystem.writeFileString(path.join(deepest, "hidden.txt"), "remove\n"); + const incomplete = yield* driver.checkpoints + .restoreCheckpoint({ cwd, checkpointRef, fallbackToHead: false }) + .pipe(Effect.flip); + assert.strictEqual(incomplete._tag, "VcsProcessExitError"); } }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index c3ed31141a865..e93d80548ee13 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -1308,6 +1308,22 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( yield* recreateWorkspace; cleaned = yield* clean; } + if (cleaned.exitCode === 0 && cleaned.stdout.length > 0) { + yield* recreateWorkspace; + const remaining = yield* execute({ + operation, + cwd: input.cwd, + args: [...longPaths, "clean", "-dn", "--", "."], + }); + if (remaining.stdout.length > 0) + return yield* new VcsProcessExitError({ + operation, + command: "git clean", + cwd: input.cwd, + exitCode: 0, + detail: "Could not clean the checkpoint workspace: untracked files remain.", + }); + } if (cleaned.exitCode !== 0) { // Git can remove every child, then fail trying to remove './' itself. const emptiedWorkspace = From e7ac91124693daeb4e2f26fd9785ed7c554c3b21 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 04:17:14 +0000 Subject: [PATCH 3/3] fix(server): preserve nested repositories during checkpoint cleanup --- apps/server/src/vcs/GitVcsDriver.test.ts | 26 +++++++++++++++ apps/server/src/vcs/GitVcsDriver.ts | 41 +++++++++++++++--------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.test.ts b/apps/server/src/vcs/GitVcsDriver.test.ts index 6beb7ff9eca27..a33eeb9fc931a 100644 --- a/apps/server/src/vcs/GitVcsDriver.test.ts +++ b/apps/server/src/vcs/GitVcsDriver.test.ts @@ -193,6 +193,7 @@ it.effect("checkpoint capture skips untracked nested repositories without a comm `160000 commit ${nestedHead}\tcommitted\n`, ); assert.deepEqual(yield* fileSystem.readFile(path.join(cwd, ".git", "index")), originalIndex); + assert.isTrue(yield* driver.checkpoints.restoreCheckpoint({ cwd, checkpointRef })); assert.strictEqual( yield* fileSystem.readFileString(path.join(cwd, nested, "private.txt")), "nested\n", @@ -1102,6 +1103,9 @@ it.effect("restores empty checkpoints without changing paths outside the workspa }); assert.strictEqual(staged.stdout.trim(), "outside.txt"); } + yield* fileSystem.writeFileString(path.join(cwd, "checkpoint.txt"), "checkpoint\n"); + yield* driver.checkpoints.captureCheckpoint({ cwd, checkpointRef }); + yield* fileSystem.writeFileString(path.join(cwd, "checkpoint.txt"), "later\n"); let deepest = cwd; for (let depth = 0; depth < 4; depth++) { yield* fileSystem.writeFileString(path.join(deepest, ".gitignore"), "d/.gitignore\n"); @@ -1114,6 +1118,28 @@ it.effect("restores empty checkpoints without changing paths outside the workspa .restoreCheckpoint({ cwd, checkpointRef, fallbackToHead: false }) .pipe(Effect.flip); assert.strictEqual(incomplete._tag, "VcsProcessExitError"); + assert.strictEqual( + yield* fileSystem.readFileString(path.join(cwd, "checkpoint.txt")), + "checkpoint\n", + ); + assert.strictEqual( + (yield* driver.execute({ + operation: "test", + cwd, + args: ["diff", "--cached", "--name-only", "--", "."], + })).stdout, + "", + ); + if (nested) { + assert.strictEqual( + (yield* driver.execute({ + operation: "test", + cwd: root, + args: ["diff", "--cached", "--name-only"], + })).stdout.trim(), + "outside.txt", + ); + } } }).pipe(Effect.scoped, Effect.provide(layerGitContract)), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index e93d80548ee13..bfc4e364d4bf5 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -1295,27 +1295,45 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( }), ), ); + const resetIndex = Effect.gen(function* () { + // On Windows clean can remove './' and exit 0. + yield* recreateWorkspace; + if (yield* hasHeadCommit(input.cwd)) { + yield* execute({ + operation, + cwd: input.cwd, + args: [...longPaths, "reset", "--quiet", "--", "."], + }); + } + }); yield* recreateWorkspace; const clean = execute({ operation, cwd: input.cwd, args: [...longPaths, "clean", "-fd", "--", "."], + env: { LC_ALL: "C" }, allowNonZeroExit: true, }); let cleaned = yield* clean; // Removing a new .gitignore exposes the files it hid; clean again until nothing is left. - for (let pass = 1; pass < 4 && cleaned.exitCode === 0 && cleaned.stdout.length > 0; pass++) { + for ( + let pass = 1; + pass < 4 && cleaned.exitCode === 0 && /^Removing /m.test(cleaned.stdout); + pass++ + ) { yield* recreateWorkspace; cleaned = yield* clean; } - if (cleaned.exitCode === 0 && cleaned.stdout.length > 0) { + if (cleaned.exitCode === 0 && /^Removing /m.test(cleaned.stdout)) { yield* recreateWorkspace; const remaining = yield* execute({ operation, cwd: input.cwd, args: [...longPaths, "clean", "-dn", "--", "."], + env: { LC_ALL: "C" }, }); - if (remaining.stdout.length > 0) + if (/^Would remove /m.test(remaining.stdout)) { + yield* resetIndex; return yield* new VcsProcessExitError({ operation, command: "git clean", @@ -1323,6 +1341,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( exitCode: 0, detail: "Could not clean the checkpoint workspace: untracked files remain.", }); + } } if (cleaned.exitCode !== 0) { // Git can remove every child, then fail trying to remove './' itself. @@ -1333,7 +1352,8 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( Effect.map((entries) => entries.length === 0), Effect.orElseSucceed(() => false), )); - if (!emptiedWorkspace) + if (!emptiedWorkspace) { + yield* resetIndex; return yield* new VcsProcessExitError({ operation, command: "git clean", @@ -1341,18 +1361,9 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( exitCode: cleaned.exitCode, detail: cleaned.stderr.trim() || "Could not clean the checkpoint workspace.", }); + } } - // On Windows the same clean can remove './' and exit 0. - yield* recreateWorkspace; - - const headExists = yield* hasHeadCommit(input.cwd); - if (headExists) { - yield* execute({ - operation, - cwd: input.cwd, - args: [...longPaths, "reset", "--quiet", "--", "."], - }); - } + yield* resetIndex; return true; }),