Files
orca/src/main/runtime/runtime-local-worktree-create-base.ts
T
Brennan Benson e5ade4b868 fix(worktrees): when the host can't fetch a remote base, create from the local branch and say so (#26009)
* fix(worktrees): create from the usable local base when there is no tracking ref, and say so, on the host's create

The host's create (agent.launch, worktree.create, the phone and the CLI) asked for a remote base like
origin/main with no tracking ref yet fetched it, and threw offline, even when the local branch existed;
it also never reported a base it fell back to. Like the desktop create, it now uses the local branch
without fetching and returns baseFallback, which the desktop window already turns into a notice.

* fix(worktrees): fetch the remote base first and fall back to the local branch only when the fetch fails

The host's create skipped the fetch whenever a local branch matched a
remote base with no tracking ref, so online phone/CLI/agent.launch creates
silently built on a possibly stale local branch, every time. Fetch first,
as main does; only when that fetch fails use the local branch the remote
names and report it with baseFallback. With no local branch the existing
error is unchanged.

Decide the base before naming so branch reuse and conflict checks, the add
and the persisted baseRef all see the base actually used, and return
baseFallback as its own field of the create result instead of riding on the
git add result.

* fix(worktrees): report the fallback for a local ref of the requested name, as the desktop create does

The tracking ref is still missing there; only the fetch is skipped, as before.

* test(worktrees): pin that the host create names only after the base fetch settles

Also pins the existing not-found-after-fetching error, now owned by the base decision.

* fix(worktrees): fall back to the local base only for creates a person asked for; keep the exact-name ref silent

Scheduled automations, orchestration workers and federation keep main's
"Check your network" error offline: nobody is watching to be told the
workspace was built on a possibly stale local branch. The fallback is a
host-side create option (allowLocalBaseFallback, default off, not on the
wire) that the worktree.create RPC and agent.launch set unless the request
carries automation provenance.

A local ref of the exact requested name goes back to silent, as on main's
host: reporting it showed "may not include the latest remote changes"
online for plain local branches.

* test(worktrees): pin the local-base opt-in at agent.launch and its absence for an automation's worktree.create
2026-10-07 00:55:21 -07:00

89 lines
3.8 KiB
TypeScript

import type { LocalGitExecOptions } from '../git/repo-default-base-ref'
import type { WorktreeCreateBaseFallback } from '../../shared/worktree/create-types'
import { hasLocalWorktreeBaseRef } from '../git/worktree-base-ref-probe'
import type { WorktreeCreateTimingRecorder } from '../worktree-create-timing'
import type { RemoteFetchResult, RemoteTrackingBase } from './runtime-remote-fetch-controller'
export type RuntimeLocalWorktreeCreateBase = {
/** The base the add uses: the request's, or the local branch it fell back to. */
baseBranch: string
remoteTrackingBase: RemoteTrackingBase | null
/**
* Best-effort refresh the git create runs after naming, since it cannot change the base: the
* existing tracking ref, or `fetch origin` for a plain base that is not local yet.
*/
deferredRefresh: 'tracking_ref' | 'origin' | null
baseFallback?: WorktreeCreateBaseFallback
}
/**
* Decides the base before naming, because branch reuse and conflict checks must see the base the
* add will use, including the local fallback.
*/
export async function resolveRuntimeLocalWorktreeCreateBase(args: {
repoPath: string
baseBranch: string
localWorktreeGitOptions: LocalGitExecOptions
allowLocalBaseFallback: boolean
resolveRemoteTrackingBase: (
repoPath: string,
baseBranch: string,
options?: LocalGitExecOptions
) => Promise<RemoteTrackingBase | null>
hasRemoteTrackingRef: (
repoPath: string,
base: RemoteTrackingBase,
options?: LocalGitExecOptions
) => Promise<boolean>
refreshRemoteTrackingBase: (
repoPath: string,
base: RemoteTrackingBase,
options?: LocalGitExecOptions
) => Promise<RemoteFetchResult>
timing: WorktreeCreateTimingRecorder
}): Promise<RuntimeLocalWorktreeCreateBase> {
const { repoPath, baseBranch, localWorktreeGitOptions: options } = args
const remoteTrackingBase = await args.resolveRemoteTrackingBase(repoPath, baseBranch, options)
if (!remoteTrackingBase) {
const hasBase = await hasLocalWorktreeBaseRef(repoPath, baseBranch, options)
return { baseBranch, remoteTrackingBase: null, deferredRefresh: hasBase ? null : 'origin' }
}
const [hadRemoteRef, hasNamedLocalBaseRef] = await Promise.all([
args.hasRemoteTrackingRef(repoPath, remoteTrackingBase, options),
hasLocalWorktreeBaseRef(repoPath, baseBranch, options)
])
if (hadRemoteRef) {
return { baseBranch, remoteTrackingBase, deferredRefresh: 'tracking_ref' }
}
if (hasNamedLocalBaseRef) {
// Why: a local ref of the requested name (often a plain local branch like `team/feature`) is
// the base asked for, so there is nothing to fetch and no fallback to report.
return { baseBranch, remoteTrackingBase: null, deferredRefresh: null }
}
const refresh = await args.timing.time('refresh_base_ref', () =>
args.refreshRemoteTrackingBase(repoPath, remoteTrackingBase, options)
)
if (!refresh.ok) {
// Why: fetch first so an online create still gets the fresh remote base; only when that fails
// (offline) does the local branch the remote names beat failing the create, and the result says so.
if (
args.allowLocalBaseFallback &&
(await hasLocalWorktreeBaseRef(repoPath, remoteTrackingBase.branch, options))
) {
return {
baseBranch: remoteTrackingBase.branch,
remoteTrackingBase: null,
deferredRefresh: null,
baseFallback: { requestedRef: remoteTrackingBase.base, localRef: remoteTrackingBase.branch }
}
}
throw new Error(
`Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.`
)
}
if (!(await args.hasRemoteTrackingRef(repoPath, remoteTrackingBase, options))) {
throw new Error(`Base ref "${baseBranch}" was not found after fetching.`)
}
return { baseBranch, remoteTrackingBase, deferredRefresh: null }
}