diff --git a/src/shared/git-worktree-operation-lock.ts b/src/shared/git-worktree-operation-lock.ts index 1c3f6b05e27..273fa74f947 100644 --- a/src/shared/git-worktree-operation-lock.ts +++ b/src/shared/git-worktree-operation-lock.ts @@ -1,4 +1,4 @@ -import { realpathSync } from 'node:fs' +import { realpath } from 'node:fs/promises' import { resolve } from 'node:path' import { runWithGitOperationLock } from './git-operation-lock' @@ -8,13 +8,17 @@ export async function runWithGitWorktreeOperationLock( signal: AbortSignal | undefined, run: () => Promise ): Promise { - // Why: the key must resolve synchronously so back-to-back callers (stage, then commit) - // join the lane in call order. An async realpath is not ordered and can run a commit first. - let key = resolve(worktreePath) - try { - key = realpathSync.native(worktreePath) || key - } catch { - // A missing or temporarily unreachable worktree still gets serialized. - } - return runWithGitOperationLock(key, signal, run) + const fallbackKey = resolve(worktreePath) + // Why: back-to-back callers (stage, then commit) must join the canonical lane in call order. + // realpath is async and unordered, so sequence it under a lane keyed by the raw path, which + // needs no I/O. realpath stays async because a hung 9P/UNC lookup must not block the process. + return runWithGitOperationLock(`order\0${fallbackKey}`, signal, async () => { + let key = fallbackKey + try { + key = (await realpath(worktreePath)) || fallbackKey + } catch { + // A missing or temporarily unreachable worktree still gets serialized. + } + return runWithGitOperationLock(key, signal, run) + }) }