authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 05:15:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 05:15:50-07:00
logdd5c7bd09e83bbeb7c3f63aa787e6ffdd3cc6221
tree3c6fe1a84fe127484e55907fc8bfce41c49c0086
parentdf8df0d53b39bfd6a5d8d69fabade19bd668e0a5
signature Signed by PGP key B5690EEEBB952194

fix(server): new worktrees no longer fail when a background fetch runs at the same time (#19411)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 79 insertions(+), 23 deletions(-)

apps/server/src/vcs/GitVcsDriverCore.test.ts+45-1
......@@ -998,11 +998,25 @@ it.effect.each([
998998 name: "reference lock",
999999 stderr: "error: cannot lock ref 'refs/remotes/origin/main': is at abc but expected def",
10001000 expected: "could not update a local reference",
1001 attempts: 2,
10011002 },
10021003 {
10031004 name: "lock file",
10041005 stderr: "fatal: Unable to create '/repo/.git/FETCH_HEAD.lock': File exists.",
10051006 expected: "could not update a local reference",
1007 attempts: 2,
1008 },
1009 {
1010 name: "reference race on Git 2.53",
1011 stderr: "error: fetching ref refs/remotes/origin/main failed: incorrect old value provided",
1012 expected: "could not update a local reference",
1013 attempts: 2,
1014 },
1015 {
1016 name: "held reference lock on Git 2.53",
1017 stderr: "error: fetching ref refs/remotes/origin/main failed: reference already exists",
1018 expected: "could not update a local reference",
1019 attempts: 2,
10061020 },
10071021 {
10081022 name: "unrelated remote chatter",
......@@ -1063,7 +1077,37 @@ it.effect.each([
10631077 assert.notInclude(yield* encodeGitCommandError(error), secret);
10641078 assert.notProperty(error, "stderr");
10651079 assert.notProperty(error, "args");
1066 assert.equal(yield* Ref.get(attempts), 1);
1080 assert.equal(yield* Ref.get(attempts), "attempts" in scenario ? scenario.attempts : 1);
1081 }).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
1082);
1083
1084it.effect("retries a fetch that lost a ref update to another Git process", () =>
1085 Effect.gen(function* () {
1086 const attempts = yield* Ref.make(0);
1087 const spawner = ChildProcessSpawner.make((command) =>
1088 Effect.gen(function* () {
1089 if (!ChildProcess.isStandardCommand(command))
1090 return yield* Effect.die("expected Git command");
1091 if (command.args[0] !== "fetch") return makeNonRepositoryHandle();
1092 const attempt = yield* Ref.updateAndGet(attempts, (count) => count + 1);
1093 if (attempt > 1) return makeSuccessfulHandle("");
1094 return ChildProcessSpawner.makeHandle({
1095 ...makeNonRepositoryHandle(),
1096 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(1)),
1097 stderr: Stream.encodeText(
1098 Stream.make(
1099 "error: fetching ref refs/remotes/origin/main failed: incorrect old value provided",
1100 ),
1101 ),
1102 });
1103 }),
1104 );
1105 const driver = yield* makeGitVcsDriverCore().pipe(
1106 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
1107 );
1108 const cwd = yield* makeTmpDir();
1109 yield* driver.fetchRemote({ cwd, remoteName: "origin", refName: "main" });
1110 assert.equal(yield* Ref.get(attempts), 2);
10671111 }).pipe(Effect.provide(layerServerConfig.pipe(Layer.provideMerge(NodeServices.layer)))),
10681112);
10691113
apps/server/src/vcs/GitVcsDriverCore.ts+34-22
......@@ -579,18 +579,24 @@ function fetchFailureDetail(stderr: string): string | undefined {
579579 ) {
580580 return "Git could not access the remote repository. Check the remote URL and repository permissions on the server.";
581581 }
582 if (
583 lines.some((line) =>
584 /^(?:(?:error|fatal): cannot lock ref\b|fatal: Unable to create ['"].+\.lock['"]:)/i.test(
585 line,
586 ),
587 )
588 ) {
582 if (isRefLockFailure(stderr)) {
589583 return "Git could not update a local reference. Another Git operation or a stale lock may be blocking the fetch; check the repository on the server, then retry.";
590584 }
591585 return undefined;
592586}
593587
588// Matches a fetch that lost a ref update to another Git process. Git 2.53+
589// reports it as "fetching ref <ref> failed: ..."; older Git says "cannot lock ref".
590function isRefLockFailure(stderr: string): boolean {
591 return stderr
592 .split(/\r?\n/)
593 .some((line) =>
594 /^(?:(?:error|fatal): cannot lock ref\b|fatal: Unable to create ['"].+\.lock['"]:|error: fetching ref \S+ failed: (?:incorrect old value provided|reference already exists)$)/i.test(
595 line.trim(),
596 ),
597 );
598}
599
594600interface Trace2Monitor {
595601 readonly env: NodeJS.ProcessEnv;
596602 readonly flush: Effect.Effect<void, never>;
......@@ -3713,15 +3719,26 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
37133719 env: STATUS_UPSTREAM_REFRESH_ENV,
37143720 fallbackErrorDetail: `git fetch ${input.remoteName} failed`,
37153721 };
3716 const fetchAll = executeGitWithStableDiagnostics(
3717 "GitVcsDriver.fetchRemote",
3718 input.cwd,
3719 args,
3720 {
3721 ...options,
3722 allowNonZeroExit: true,
3723 },
3724 ).pipe(
3722 // Another fetch in the same repository, such as the background status
3723 // poll, can win the race for a remote-tracking ref. The loser fails, but
3724 // the ref is free again by the time a rerun needs it, so retry once.
3725 const runFetch = (fetchArgs: readonly string[]) => {
3726 const run = executeGitWithStableDiagnostics(
3727 "GitVcsDriver.fetchRemote",
3728 input.cwd,
3729 fetchArgs,
3730 {
3731 ...options,
3732 allowNonZeroExit: true,
3733 },
3734 );
3735 return run.pipe(
3736 Effect.flatMap((result) =>
3737 result.exitCode !== 0 && isRefLockFailure(result.stderr) ? run : Effect.succeed(result),
3738 ),
3739 );
3740 };
3741 const fetchAll = runFetch(args).pipe(
37253742 Effect.flatMap((result) =>
37263743 result.exitCode === 0
37273744 ? Effect.void
......@@ -3750,12 +3767,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
37503767 ...args,
37513768 `+refs/heads/${branch}:refs/remotes/${input.remoteName}/${branch}`,
37523769 ];
3753 const result = yield* executeGitWithStableDiagnostics(
3754 "GitVcsDriver.fetchRemote",
3755 input.cwd,
3756 scopedArgs,
3757 { ...options, allowNonZeroExit: true },
3758 );
3770 const result = yield* runFetch(scopedArgs);
37593771 if (result.exitCode === 0) return;
37603772 if (
37613773 result.stderr