authorgravatar for 76637177+SunkenInTime@users.noreply.github.comDara Adedeji <76637177+SunkenInTime@users.noreply.github.com> 2026-10-07 13:21:38-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 10:21:38-07:00
logd9068d38099fe5cb4f3fed4cb5455fee878db491
treed92ed4cab4953c86689c95913d5ffeb241fb0f76
parentdb46c42744874ca123bc170a90c1faa312f20119
signature Signed by PGP key B5690EEEBB952194

fix(server): worktree cleanup no longer deletes files hidden by showUntrackedFiles=no (#15834)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

2 files changed, 33 insertions(+), 4 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+30-3
...@@ -1351,7 +1351,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {...@@ -1351,7 +1351,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
1351 _tag: "GitCommandError",1351 _tag: "GitCommandError",
1352 operation: "GitVcsDriver.removeWorktree",1352 operation: "GitVcsDriver.removeWorktree",
1353 command: "git",1353 command: "git",
1354 argumentCount: 3,1354 argumentCount: 5,
1355 cwd,1355 cwd,
1356 });1356 });
1357 assert.notProperty(error, "cause");1357 assert.notProperty(error, "cause");
...@@ -3217,6 +3217,34 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {...@@ -3217,6 +3217,34 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
3217 }),3217 }),
3218 );3218 );
32193219
3220 it.effect("keeps a worktree whose untracked files status is configured to hide", () =>
3221 Effect.gen(function* () {
3222 const cwd = yield* makeTmpDir();
3223 const { initialBranch } = yield* initRepoWithCommit(cwd);
3224 const pathService = yield* Path.Path;
3225 const worktreePath = pathService.join(yield* makeTmpDir("git-worktrees-"), "hidden");
3226 const driver = yield* GitVcsDriver.GitVcsDriver;
3227 yield* driver.createWorktree({
3228 cwd,
3229 path: worktreePath,
3230 refName: initialBranch,
3231 newRefName: "feature/hidden",
3232 });
3233 yield* git(cwd, ["config", "status.showUntrackedFiles", "no"]);
3234 yield* writeTextFile(worktreePath, "notes.txt", "draft\n");
3235 assert.equal(yield* git(worktreePath, ["status", "--porcelain"]), "");
3236
3237 const result = yield* Effect.result(driver.removeWorktree({ cwd, path: worktreePath }));
3238
3239 assert.isTrue(Result.isFailure(result));
3240 const fileSystem = yield* FileSystem.FileSystem;
3241 assert.equal(
3242 yield* fileSystem.readFileString(pathService.join(worktreePath, "notes.txt")),
3243 "draft\n",
3244 );
3245 }),
3246 );
3247
3220 it.effect("allows worktree removal to run longer than the default command timeout", () =>3248 it.effect("allows worktree removal to run longer than the default command timeout", () =>
3221 Effect.gen(function* () {3249 Effect.gen(function* () {
3222 const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;3250 const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
...@@ -3225,8 +3253,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {...@@ -3225,8 +3253,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
3225 Effect.gen(function* () {3253 Effect.gen(function* () {
3226 if (3254 if (
3227 ChildProcess.isStandardCommand(command) &&3255 ChildProcess.isStandardCommand(command) &&
3228 command.args[0] === "worktree" &&3256 command.args.join(" ").includes("worktree remove")
3229 command.args[1] === "remove"
3230 ) {3257 ) {
3231 yield* Deferred.succeed(removalStarted, undefined);3258 yield* Deferred.succeed(removalStarted, undefined);
3232 yield* Effect.sleep("31 seconds");3259 yield* Effect.sleep("31 seconds");
apps/server/src/vcs/GitVcsDriverCore.ts+3-1
...@@ -3743,7 +3743,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -3743,7 +3743,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
3743 const removeWorktree: GitVcsDriver.GitVcsDriver["Service"]["removeWorktree"] = Effect.fn(3743 const removeWorktree: GitVcsDriver.GitVcsDriver["Service"]["removeWorktree"] = Effect.fn(
3744 "removeWorktree",3744 "removeWorktree",
3745 )(function* (input) {3745 )(function* (input) {
3746 const args = ["worktree", "remove"];3746 // Git refuses to remove a worktree with untracked files unless forced, but
3747 // its check honors `status.showUntrackedFiles=no` and would delete them.
3748 const args = ["-c", "status.showUntrackedFiles=normal", "worktree", "remove"];
3747 if (input.force) {3749 if (input.force) {
3748 args.push("--force");3750 args.push("--force");
3749 }3751 }