Skip to content

Commit fa4e467

Browse files
fix(server): let slow worktree checkouts finish (#23802)
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent da8cc59 commit fa4e467

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

‎apps/server/src/vcs/GitVcsDriverCore.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,54 @@ it.effect("retries a fetch that lost a ref update to another Git process", () =>
920920
}).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
921921
);
922922

923+
it.effect("lets a worktree checkout that is still working run past five minutes", () =>
924+
Effect.gen(function* () {
925+
const checkoutStarted = yield* Deferred.make<void>();
926+
const checkoutFinished = yield* Deferred.make<void>();
927+
const spawner = ChildProcessSpawner.make((command) =>
928+
Effect.gen(function* () {
929+
if (!ChildProcess.isStandardCommand(command))
930+
return yield* Effect.die("expected Git command");
931+
if (!command.args.includes("worktree")) return makeSuccessfulHandle("");
932+
yield* Deferred.succeed(checkoutStarted, undefined);
933+
return ChildProcessSpawner.makeHandle({
934+
...makeSuccessfulHandle(""),
935+
exitCode: Deferred.await(checkoutFinished).pipe(
936+
Effect.as(ChildProcessSpawner.ExitCode(0)),
937+
),
938+
stderr: Stream.encodeText(
939+
Stream.concat(
940+
Stream.make("Updating files: 40% (49052/122629)\n"),
941+
Stream.fromEffect(
942+
Deferred.await(checkoutFinished).pipe(
943+
Effect.as("Updating files: 100% (122629/122629), done.\n"),
944+
),
945+
),
946+
),
947+
),
948+
});
949+
}),
950+
);
951+
const driver = yield* makeGitVcsDriverCore().pipe(
952+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
953+
);
954+
const path = yield* Path.Path;
955+
const cwd = yield* makeTmpDir();
956+
const worktreePath = path.join(cwd, "worktree");
957+
958+
const creation = yield* driver
959+
.createWorktree({ cwd, refName: "main", newRefName: "t3/slow-checkout", path: worktreePath })
960+
.pipe(Effect.forkChild);
961+
yield* Deferred.await(checkoutStarted);
962+
yield* TestClock.adjust("6 minutes");
963+
yield* Deferred.succeed(checkoutFinished, undefined);
964+
965+
const result = yield* Fiber.join(creation);
966+
assert.equal(result.worktree.path, worktreePath);
967+
assert.equal(result.worktree.refName, "t3/slow-checkout");
968+
}).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
969+
);
970+
923971
describe("rejected git refs", () => {
924972
const remoteUrl = "https://user:[email protected]/org/secret-repo.git";
925973
const failedPush = `error: failed to push some refs to '${remoteUrl}'`;

‎apps/server/src/vcs/GitVcsDriverCore.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ const branchBaseConfigKey = (branch: string) => `branch.${branch}.t3-merge-base`
6262
*/
6363
const legacyBranchBaseConfigKey = (branch: string) => `branch.${branch}.gh-merge-base`;
6464
const gitProcesses = Semaphore.makeUnsafe(8);
65-
const WORKTREE_ADD_TIMEOUT_MS = 300_000;
6665
const WORKTREE_REMOVE_TIMEOUT_MS = Duration.toMillis(Duration.minutes(5));
6766
const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
6867
const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]";
@@ -3877,7 +3876,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
38773876
["-c", `checkout.workers=${checkoutWorkers}`, ...args],
38783877
{
38793878
fallbackErrorDetail: "git worktree add failed",
3880-
timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
3879+
// Large checkouts and post-checkout hooks can legitimately run for many
3880+
// minutes; a fixed deadline kills them and leaves an orphaned worktree.
3881+
timeoutMs: null,
38813882
...(onCheckoutProgress
38823883
? {
38833884
// Git only prints checkout progress when stderr is a tty or the

0 commit comments

Comments
 (0)