authorgravatar for jakeleventhal@me.comJake Leventhal <jakeleventhal@me.com> 2026-09-03 21:17:36-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 18:17:36-07:00
logf54ab901fa77f76eeb1e1cdfa103b9ed6f5ee4ca
treebdd680399ad1705007fac2a1dcd5618ae1fbf719
parent6319a9714881a1d25549f797c468fabebae92813
signature Signed by PGP key B5690EEEBB952194

Fix worktree removal timing out on large install trees (#3902)


2 files changed, 53 insertions(+), 1 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+45
...@@ -1517,6 +1517,51 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -1517,6 +1517,51 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
1517 }),1517 }),
1518 );1518 );
15191519
1520 it.effect("allows worktree removal to run longer than the default command timeout", () =>
1521 Effect.gen(function* () {
1522 const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
1523 const removalStarted = yield* Deferred.make<void>();
1524 const delayedRemovalSpawner = ChildProcessSpawner.make((command) =>
1525 Effect.gen(function* () {
1526 if (
1527 ChildProcess.isStandardCommand(command) &&
1528 command.args[0] === "worktree" &&
1529 command.args[1] === "remove"
1530 ) {
1531 yield* Deferred.succeed(removalStarted, undefined);
1532 yield* Effect.sleep("31 seconds");
1533 }
1534 return yield* delegate.spawn(command);
1535 }),
1536 );
1537 const driver = yield* makeGitVcsDriverCore().pipe(
1538 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, delayedRemovalSpawner),
1539 Effect.provide(ServerConfigLayer),
1540 );
1541 const cwd = yield* makeTmpDir();
1542 const { initialBranch } = yield* initRepoWithCommit(cwd);
1543 const fileSystem = yield* FileSystem.FileSystem;
1544 const pathService = yield* Path.Path;
1545 const worktreePath = pathService.join(yield* makeTmpDir("git-worktrees-"), "slow-removal");
1546
1547 yield* driver.createWorktree({
1548 cwd,
1549 path: worktreePath,
1550 refName: initialBranch,
1551 newRefName: "feature/slow-removal",
1552 });
1553
1554 const removal = yield* driver
1555 .removeWorktree({ cwd, path: worktreePath, force: true })
1556 .pipe(Effect.forkChild({ startImmediately: true }));
1557 yield* Deferred.await(removalStarted);
1558 yield* TestClock.adjust("31 seconds");
1559 yield* Fiber.join(removal);
1560
1561 assert.equal(yield* fileSystem.exists(worktreePath), false);
1562 }),
1563 );
1564
1520 it.effect("removes the same worktree path twice without failing", () =>1565 it.effect("removes the same worktree path twice without failing", () =>
1521 Effect.gen(function* () {1566 Effect.gen(function* () {
1522 const cwd = yield* makeTmpDir();1567 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+8-1
...@@ -43,6 +43,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;...@@ -43,6 +43,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;
43// take well beyond the default 30s (e.g. a 375k-file repo takes ~40s on an idle43// take well beyond the default 30s (e.g. a 375k-file repo takes ~40s on an idle
44// machine). Give it generous headroom while still bounding a genuinely hung git.44// machine). Give it generous headroom while still bounding a genuinely hung git.
45const WORKTREE_ADD_TIMEOUT_MS = 300_000;45const WORKTREE_ADD_TIMEOUT_MS = 300_000;
46const WORKTREE_REMOVE_TIMEOUT_MS = Duration.toMillis(Duration.minutes(5));
46const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;47const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
47const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]";48const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]";
48const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49_000;49const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49_000;
...@@ -3085,7 +3086,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -3085,7 +3086,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
3085 "GitVcsDriver.removeWorktree",3086 "GitVcsDriver.removeWorktree",
3086 input.cwd,3087 input.cwd,
3087 args,3088 args,
3088 { timeoutMs: 15_000, allowNonZeroExit: true },3089 {
3090 // Removing dependency-heavy worktrees is filesystem-bound and can take
3091 // minutes, especially on Windows. Keep it bounded without interrupting
3092 // git midway through cleanup.
3093 timeoutMs: WORKTREE_REMOVE_TIMEOUT_MS,
3094 allowNonZeroExit: true,
3095 },
3089 );3096 );
3090 if (result.exitCode === 0) {3097 if (result.exitCode === 0) {
3091 return;3098 return;