perf(worktrees): fetch the base while the host's create picks a name (#26007)

* perf(worktrees): look up a branch's PR only after a name collision on the host's create

The host's create (agent.launch, worktree.create, the phone and the CLI) called gh pr list for every
create, a 1-3 s network round trip, while the desktop create probes only after a branch collision. A
branch with an open PR exists on the remote, so the branch-conflict check already moves past it.

* test(worktrees): pin first-name PR lookup skip on the host's create

The runtime service specs still expected a PR lookup for a free first
name. Update them to the desktop-parity behavior: no lookup without a
branch collision, and a free push-target name is kept. Pin the exact
suffixed name, and that a later name with a PR is skipped.

* perf(worktrees): overlap the host create's base fetch with naming

Keep the first-name PR lookup (restores main's naming) and take it off
the critical path instead: start the base-ref refresh before naming and
await it after, as the desktop create does, so the lookup and the fetch
run concurrently. A refresh rejection while naming fails stays handled.

* test(worktrees): pin the overlapped origin fetch on the host's create

Cover the best-effort fetch origin path: it runs while the first-name PR
lookup is pending, and its rejection while naming fails stays handled.
This commit is contained in:
Brennan Benson
2026-10-13 21:27:34 -07:00
committed by GitHub
parent 04b4e962a7
commit 30909c77dd
4 changed files with 229 additions and 27 deletions
@@ -26,6 +26,54 @@ import {
} from '../worktree-name-retirement'
import type { WorktreeCreateTimingRecorder } from '../worktree-create-timing'
export type RuntimeLocalBaseRefRefresh = {
/** Awaits the deferred refresh started before naming, if any. */
finish: () => Promise<void>
}
// Why: the deferred refresh can change neither the base nor the name, so start it before naming
// and await it before the add, as the desktop create does; naming's PR lookup overlaps it.
export function startRuntimeLocalBaseRefRefresh(args: {
repoPath: string
base: RuntimeLocalWorktreeCreateBase
localWorktreeGitOptions: LocalGitExecOptions
refreshRemoteTrackingBase: (
repoPath: string,
base: RemoteTrackingBase,
options?: LocalGitExecOptions
) => Promise<RemoteFetchResult>
fetchRemote: (repoPath: string, remote: string, options?: LocalGitExecOptions) => Promise<void>
timing: WorktreeCreateTimingRecorder
}): RuntimeLocalBaseRefRefresh {
const { remoteTrackingBase, deferredRefresh } = args.base
if (remoteTrackingBase && deferredRefresh === 'tracking_ref') {
const refresh = args.refreshRemoteTrackingBase(
args.repoPath,
remoteTrackingBase,
args.localWorktreeGitOptions
)
// Why: naming can throw before finish() awaits the refresh.
void refresh.catch(() => undefined)
return {
finish: async () => {
await args.timing.time('refresh_base_ref', () => refresh)
}
}
}
if (deferredRefresh === 'origin') {
const originFetch = args.fetchRemote(args.repoPath, 'origin', args.localWorktreeGitOptions)
void originFetch.catch(() => undefined)
return {
finish: async () => {
try {
await args.timing.time('refresh_base_ref', () => originFetch)
} catch {}
}
}
}
return { finish: async () => {} }
}
export async function createRuntimeLocalGitWorktree(args: {
request: RuntimeManagedWorktreeCreateArgs
repo: Repo
@@ -43,12 +91,7 @@ export async function createRuntimeLocalGitWorktree(args: {
effectiveSanitizedName?: string
checkoutExistingBranch: boolean
localWorktreeGitOptions: LocalGitExecOptions
refreshRemoteTrackingBase: (
repoPath: string,
base: RemoteTrackingBase,
options?: LocalGitExecOptions
) => Promise<RemoteFetchResult>
fetchRemote: (repoPath: string, remote: string, options?: LocalGitExecOptions) => Promise<void>
baseRefRefresh: RuntimeLocalBaseRefRefresh
rearm: PreparationRearmHolder
timing: WorktreeCreateTimingRecorder
}): Promise<{
@@ -57,22 +100,8 @@ export async function createRuntimeLocalGitWorktree(args: {
created: GitWorktreeInfo
addResult: AddWorktreeResult
}> {
const { baseBranch, remoteTrackingBase, deferredRefresh } = args.base
if (remoteTrackingBase && deferredRefresh === 'tracking_ref') {
await args.timing.time('refresh_base_ref', () =>
args.refreshRemoteTrackingBase(
args.repo.path,
remoteTrackingBase,
args.localWorktreeGitOptions
)
)
} else if (deferredRefresh === 'origin') {
try {
await args.timing.time('refresh_base_ref', () =>
args.fetchRemote(args.repo.path, 'origin', args.localWorktreeGitOptions)
)
} catch {}
}
const { baseBranch, remoteTrackingBase } = args.base
await args.baseRefRefresh.finish()
const sparseDirectories = args.request.sparseCheckout
? normalizeSparseDirectories(args.request.sparseCheckout.directories)
: []