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) => {
13511351 _tag: "GitCommandError",
13521352 operation: "GitVcsDriver.removeWorktree",
13531353 command: "git",
1354 argumentCount: 3,
1354 argumentCount: 5,
13551355 cwd,
13561356 });
13571357 assert.notProperty(error, "cause");
......@@ -3217,6 +3217,34 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
32173217 }),
32183218 );
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
32203248 it.effect("allows worktree removal to run longer than the default command timeout", () =>
32213249 Effect.gen(function* () {
32223250 const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
......@@ -3225,8 +3253,7 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
32253253 Effect.gen(function* () {
32263254 if (
32273255 ChildProcess.isStandardCommand(command) &&
3228 command.args[0] === "worktree" &&
3229 command.args[1] === "remove"
3256 command.args.join(" ").includes("worktree remove")
32303257 ) {
32313258 yield* Deferred.succeed(removalStarted, undefined);
32323259 yield* Effect.sleep("31 seconds");
apps/server/src/vcs/GitVcsDriverCore.ts+3-1
......@@ -3743,7 +3743,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
37433743 const removeWorktree: GitVcsDriver.GitVcsDriver["Service"]["removeWorktree"] = Effect.fn(
37443744 "removeWorktree",
37453745 )(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"];
37473749 if (input.force) {
37483750 args.push("--force");
37493751 }