fix(worktree): run the create-base warm-up on the routed git host (#17794)

The speculative warm-up that runs while the create composer is open resolved
refs and fetched with host Git even when the project's runtime is a WSL distro,
while both the checkout preparation it feeds (`prepareWorktreeCreateForRepo`,
which already resolves `{ wslDistro }` itself) and the real create path run
inside the distro.

The concrete cost was a discarded fetch: `getCanonicalFetchKey` namespaces the
runtime's remote-fetch cache `wsl:<distro>` vs `local`, so the warm-up's fetch
landed in a namespace create never looks at, and create fetched again. On a
Windows host with no usable host-side Git the probes also failed outright, so
that cohort got no warm-up at all.

Thread the project's worktree Git options through the prefetch (resolved by a
non-throwing helper, because an optimistic warm-up must not surface a
repair-required runtime as a failure) so every probe and fetch runs where create
runs. `gitOptions` is a required argument, so a caller cannot drop the routing
silently. Host-routed calls keep their original arity, so macOS, Linux,
native-Windows-host projects, SSH repos and folder workspaces are unchanged.

Narrower than it looks: for a repo under \\wsl.localhost\<distro>\... the probes
were already routed by cwd, and for a repo on a Windows drive letter host Git
and WSL Git read the same on-disk repository, so the answers were already
correct there. What those cohorts gain is a fetch create can reuse; what they
pay is that the probes now run inside the distro (over /mnt/c for drive-letter
repos, which also newly arms the linked-worktree routing probe) and the
speculative fetch now shares create's per-remote fetch queue, as it always has
on native platforms.

Also collapse the three byte-equivalent copies of `hasLocalWorktreeBaseRef`
(create, prefetch, remote-repo create) into one in
git/worktree-base-ref-probe.ts, drop the host-only `hasLocalCommitObject` that
caused the routing bug, and add the first routing assertions on the create-path
consumers of the now-shared probe.
This commit is contained in:
Neil
2026-08-31 20:32:59 -07:00
committed by GitHub
parent 7f63db7d7a
commit abc099e4c7
17 changed files with 633 additions and 170 deletions
+2 -28
View File
@@ -1,22 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const gitExecFileAsyncMock = vi.hoisted(() => vi.fn())
vi.mock('./runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock
}))
import {
hasCommitObjectViaGitExec,
hasLocalCommitObject,
isFullGitObjectId
} from './commit-object-ref'
import { describe, expect, it, vi } from 'vitest'
import { hasCommitObjectViaGitExec, isFullGitObjectId } from './commit-object-ref'
describe('commit object refs', () => {
beforeEach(() => {
gitExecFileAsyncMock.mockReset()
})
it('recognizes only complete git object IDs', () => {
expect(isFullGitObjectId('a'.repeat(40))).toBe(true)
expect(isFullGitObjectId('A'.repeat(40))).toBe(true)
@@ -49,15 +34,4 @@ describe('commit object refs', () => {
expect(gitExec).not.toHaveBeenCalled()
})
it('checks local commit objects in the target repo path', async () => {
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'a'.repeat(40), stderr: '' })
await expect(hasLocalCommitObject('/repo', 'a'.repeat(40))).resolves.toBe(true)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['rev-parse', '--verify', '--quiet', `${'a'.repeat(40)}^{commit}`],
{ cwd: '/repo' }
)
})
})
-6
View File
@@ -1,5 +1,3 @@
import { gitExecFileAsync } from './runner'
type GitExec = (args: string[]) => Promise<unknown>
const FULL_GIT_OBJECT_ID_PATTERN = /^[0-9a-f]{40}$/i
@@ -20,7 +18,3 @@ export async function hasCommitObjectViaGitExec(gitExec: GitExec, ref: string):
return false
}
}
export function hasLocalCommitObject(repoPath: string, ref: string): Promise<boolean> {
return hasCommitObjectViaGitExec((args) => gitExecFileAsync(args, { cwd: repoPath }), ref)
}
+51 -2
View File
@@ -1,5 +1,10 @@
import { describe, expect, it, vi } from 'vitest'
import { probeWorktreeBaseRefPresence } from './worktree-base-ref-probe'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const gitExecFileAsync = vi.hoisted(() => vi.fn())
vi.mock('./runner', () => ({ gitExecFileAsync }))
import { hasLocalWorktreeBaseRef, probeWorktreeBaseRefPresence } from './worktree-base-ref-probe'
describe('probeWorktreeBaseRefPresence', () => {
it('uses an exact show-ref probe and reports a present ref', async () => {
@@ -52,3 +57,47 @@ describe('probeWorktreeBaseRefPresence', () => {
expect(runGit).not.toHaveBeenCalled()
})
})
describe('hasLocalWorktreeBaseRef', () => {
const repoPath = String.raw`C:\workspace\repo`
function resolveOnly(present: string[]): void {
gitExecFileAsync.mockImplementation(async (args: string[]) => ({
stdout: present.includes(args.at(-1)?.replace('^{commit}', '') ?? '') ? 'f'.repeat(40) : '',
stderr: ''
}))
}
beforeEach(() => {
gitExecFileAsync.mockReset()
})
it('prefers the remote namespace for a slashed short name', async () => {
resolveOnly(['refs/remotes/origin/main'])
await expect(hasLocalWorktreeBaseRef(repoPath, 'origin/main')).resolves.toBe(true)
expect(gitExecFileAsync).toHaveBeenCalledWith(
['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'],
{ cwd: repoPath }
)
})
it('probes a bare commit id as an object, not as a ref', async () => {
const sha = 'a'.repeat(40)
resolveOnly([sha])
await expect(hasLocalWorktreeBaseRef(repoPath, sha, { wslDistro: 'Ubuntu' })).resolves.toBe(
true
)
expect(gitExecFileAsync).toHaveBeenCalledWith(
['rev-parse', '--verify', '--quiet', `${sha}^{commit}`],
{ cwd: repoPath, wslDistro: 'Ubuntu' }
)
})
it('reports a base no namespace resolves as absent', async () => {
resolveOnly([])
await expect(hasLocalWorktreeBaseRef(repoPath, 'feature/topic')).resolves.toBe(false)
})
})
+30
View File
@@ -1,6 +1,8 @@
import { gitExecFileAsync } from './runner'
import { isShowRefNoMatchError } from './exact-ref-probe'
import { hasCommitObjectViaGitExec } from './commit-object-ref'
import { isSafeGitRefName } from '../../shared/git-status-upstream-ref'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
type GitExecOptions = {
wslDistro?: string
@@ -40,6 +42,34 @@ export async function hasWorktreeBaseCommitRef(
return (await resolveWorktreeBaseCommitOid(repoPath, qualifiedRef, options)) !== null
}
/**
* Whether a worktree base — a qualified ref, a short branch or remote name, or a
* full commit id — already resolves in this repo's own object/ref store.
*
* Single copy on purpose: the create path, the speculative create prefetch and
* the remote-repo create path must agree on what counts as a local base, or the
* warm-up prepares a checkout create then rejects.
*/
export async function hasLocalWorktreeBaseRef(
repoPath: string,
baseRef: string,
options: GitExecOptions = {}
): Promise<boolean> {
const refExists = (qualifiedRef: string) =>
hasWorktreeBaseCommitRef(repoPath, qualifiedRef, options)
const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists)
if (resolvedBaseRef !== baseRef) {
return true
}
if (baseRef.startsWith('refs/')) {
return refExists(baseRef)
}
return hasCommitObjectViaGitExec(
(gitArgs) => gitExecFileAsync(gitArgs, { cwd: repoPath, ...options }),
baseRef
)
}
export type WorktreeBaseRefPresence = 'present' | 'absent' | 'unknown'
/**
+10 -59
View File
@@ -38,7 +38,10 @@ import {
import { getBranchConflictKindViaExec } from '../git/repo-branch-conflict'
import { resolveLocalGitUsername, getSshGitUsername } from '../git/git-username'
import { hasCommitObjectViaGitExec } from '../git/commit-object-ref'
import { probeWorktreeBaseRefPresence } from '../git/worktree-base-ref-probe'
import {
hasLocalWorktreeBaseRef,
probeWorktreeBaseRefPresence
} from '../git/worktree-base-ref-probe'
import { resolveWorktreeCreateBase } from '../worktree-create-base'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { getHostedReviewForBranch } from '../source-control/hosted-review'
@@ -718,46 +721,6 @@ function hasLocalGitOptions(gitOptions: { wslDistro?: string }): boolean {
return Object.keys(gitOptions).length > 0
}
function hasLocalCommitObjectWithOptions(
repoPath: string,
ref: string,
gitOptions: { wslDistro?: string }
): Promise<boolean> {
return hasCommitObjectViaGitExec(
(gitArgs) => gitExecFileAsync(gitArgs, { cwd: repoPath, ...gitOptions }),
ref
)
}
async function hasLocalWorktreeBaseRefWithOptions(
repoPath: string,
baseRef: string,
gitOptions: { wslDistro?: string }
): Promise<boolean> {
const refExists = async (qualifiedRef: string) => {
try {
const { stdout } = await gitExecFileAsync(
['rev-parse', '--verify', '--quiet', `${qualifiedRef}^{commit}`],
{
cwd: repoPath,
...gitOptions
}
)
return stdout.trim().length > 0
} catch {
return false
}
}
const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists)
if (resolvedBaseRef !== baseRef) {
return true
}
if (baseRef.startsWith('refs/')) {
return refExists(baseRef)
}
return hasLocalCommitObjectWithOptions(repoPath, baseRef, gitOptions)
}
function getLocalGitHubPrForBranch(
repoPath: string,
branchName: string,
@@ -2066,14 +2029,10 @@ export async function createLocalWorktree(
) {
return true
}
return hasLocalWorktreeBaseRefWithOptions(
repo.path,
baseBranchCandidate,
localGitExecOptions
)
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate, localGitExecOptions)
}
}
return hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranchCandidate, localGitExecOptions)
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate, localGitExecOptions)
}
})
const [username, resolvedBaseBranch] = await Promise.all([usernamePromise, baseBranchPromise])
@@ -2103,15 +2062,11 @@ export async function createLocalWorktree(
if (remoteTrackingBase) {
const [hasRemoteTrackingBaseRef, hasNamedLocalBaseRef] = await Promise.all([
runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase, ...localWorktreeGitOptionArgs),
hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localGitExecOptions)
hasLocalWorktreeBaseRef(repo.path, baseBranch, localGitExecOptions)
])
const hasFallbackLocalBaseRef =
!hasNamedLocalBaseRef &&
(await hasLocalWorktreeBaseRefWithOptions(
repo.path,
remoteTrackingBase.branch,
localGitExecOptions
))
(await hasLocalWorktreeBaseRef(repo.path, remoteTrackingBase.branch, localGitExecOptions))
const hasLocalBaseRef =
hasRemoteTrackingBaseRef || hasNamedLocalBaseRef || hasFallbackLocalBaseRef
if (!hasRemoteTrackingBaseRef && hasLocalBaseRef) {
@@ -2136,9 +2091,7 @@ export async function createLocalWorktree(
)
}
}
} else if (
!(await hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localWorktreeGitOptions))
) {
} else if (!(await hasLocalWorktreeBaseRef(repo.path, baseBranch, localWorktreeGitOptions))) {
// Why: non-remote-prefix bases (plain main/master/local) keep the legacy best-effort fetch; verified PR SHA bases already have the object.
legacyFetchPromise = runtime
.fetchRemoteWithCache(repo.path, 'origin', ...localWorktreeGitOptionArgs)
@@ -2147,9 +2100,7 @@ export async function createLocalWorktree(
emitCreateWorktreeProgress(mainWindow, 'fetching', args.creationId)
}
} else {
if (
!(await hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localWorktreeGitOptions))
) {
if (!(await hasLocalWorktreeBaseRef(repo.path, baseBranch, localWorktreeGitOptions))) {
legacyFetchPromise = gitExecFileAsync(['fetch', 'origin'], {
...localGitExecOptions,
timeout: CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS
+11 -9
View File
@@ -76,6 +76,15 @@ export {
type HandlerMap
} from './worktrees-test-ipc-surface'
/** The single repo every worktree harness test resolves; exported so a test can vary one field. */
export const harnessRepo = {
id: 'repo-1',
path: '/workspace/repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0
}
/** Registers worktree IPC handlers against freshly reset shared mocks and returns the runtime stub. */
export function setupWorktreeHandlers(): WorktreeRuntimeStub {
delete (store as typeof store & { getAllWorktreeMetaForHost?: (...args: unknown[]) => unknown })
@@ -185,15 +194,8 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub {
handlers[channel] = handler
})
const repo = {
id: 'repo-1',
path: '/workspace/repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0
}
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue({ ...repo, worktreeBaseRef: null })
store.getRepos.mockReturnValue([harnessRepo])
store.getRepo.mockReturnValue({ ...harnessRepo, worktreeBaseRef: null })
store.getProjects.mockReturnValue([])
store.getSparsePresets.mockReturnValue([])
const settings = {
@@ -13,9 +13,11 @@ import {
createSetupRunnerScriptMock,
getEffectiveHooksFromConfigMock,
shouldRunSetupForCreateMock,
getBaseRefDefaultMock,
gitExecFileAsyncMock
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { handlers, harnessRepo, setupWorktreeHandlers, store } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import {
createdWorktreeList,
mockKnownFeatureWorktree,
@@ -104,9 +106,65 @@ vi.mock('../runtime/worktree-teardown', async () =>
)
vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock())
/** Every create-path git call, including the base-ref probe now shared with the
* speculative prefetch, must read the distro's ref store rather than host git's. */
function expectEveryGitCallRoutedTo(wslDistro: string): void {
const callDistros = new Set(
gitExecFileAsyncMock.mock.calls.map(
([, options]) => (options as { wslDistro?: string } | undefined)?.wslDistro
)
)
expect(callDistros).toEqual(new Set([wslDistro]))
}
describe('registerWorktreeHandlers', () => {
let runtimeStub: WorktreeRuntimeStub
beforeEach(() => {
setupWorktreeHandlers()
runtimeStub = setupWorktreeHandlers()
})
it('routes the speculative create-base prefetch through the selected WSL project runtime', async () => {
mockSelectedWslProjectRuntime()
const remoteTrackingBase = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteTrackingBase)
await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1' })
expect(getBaseRefDefaultMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' })
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'],
{ cwd: '/workspace/repo', wslDistro: 'Ubuntu' }
)
expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith(
'/workspace/repo',
'origin/main',
{ wslDistro: 'Ubuntu' }
)
expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith(
'/workspace/repo',
remoteTrackingBase,
{ wslDistro: 'Ubuntu' }
)
})
it('routes the prefetch remote-fetch fallback through the selected WSL project runtime', async () => {
mockSelectedWslProjectRuntime()
runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null)
await handlers['worktrees:prefetchCreateBase'](null, {
repoId: 'repo-1',
baseBranch: 'feature/topic'
})
expect(runtimeStub.fetchRemoteWithCache).toHaveBeenCalledWith('/workspace/repo', 'origin', {
wslDistro: 'Ubuntu'
})
})
it('routes local worktree creation through the selected WSL project runtime', async () => {
@@ -153,6 +211,36 @@ describe('registerWorktreeHandlers', () => {
{ wslDistro: 'Ubuntu' }
)
expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' })
expectEveryGitCallRoutedTo('Ubuntu')
})
it('routes local worktree creation with a remote tracking base through the selected WSL project runtime', async () => {
mockSelectedWslProjectRuntime()
runtimeStub.resolveRemoteTrackingBase.mockResolvedValue({
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
})
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'abc123\n', stderr: '' })
// A persisted base that differs from the detected default also drives the usability probe.
store.getRepo.mockReturnValue({ ...harnessRepo, worktreeBaseRef: 'custom-base' })
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expectEveryGitCallRoutedTo('Ubuntu')
})
it('routes fork push target setup through the selected WSL project runtime', async () => {
@@ -1,6 +1,7 @@
import { ipcMain } from 'electron'
import { prefetchWorktreeCreateBase } from '../../../worktree-create-base-prefetch'
import { prepareWorktreeCreateForRepo } from '../../../worktree-create-preparation'
import { getWorktreeCreatePrefetchGitOptions } from '../../../project-runtime-git-options'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
export function registerWorktreePrefetchHandler(context: WorktreeIpcContext): void {
@@ -17,7 +18,8 @@ export function registerWorktreePrefetchHandler(context: WorktreeIpcContext): vo
const baseBranch = await prefetchWorktreeCreateBase({
repo,
baseBranch: args.baseBranch,
runtime
runtime,
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo)
})
if (baseBranch) {
await prepareWorktreeCreateForRepo(store, repo, baseBranch)
+51 -1
View File
@@ -1,9 +1,10 @@
import { afterEach, describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Store } from './persistence'
import type { Project } from '../shared/project-types'
import type { Repo } from '../shared/repo-types'
import {
getLocalProjectGitExecOptions,
getWorktreeCreatePrefetchGitOptions,
getWorktreeMirrorDistro,
resolveLocalProjectRuntimeForRepo
} from './project-runtime-git-options'
@@ -176,6 +177,55 @@ describe('project runtime git options', () => {
})
})
describe('getWorktreeCreatePrefetchGitOptions', () => {
it('routes the warm-up through the distro a resolved WSL project runs in', () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
const project = makeProject({
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
expect(
withPlatform('win32', () =>
getWorktreeCreatePrefetchGitOptions(makeStore(project), makeRepo())
)
).toEqual({ wslDistro: 'Ubuntu' })
})
// A speculative warm-up must degrade to the host Git it used before routing
// existed, never surface the repair state git execution raises.
it('falls back to host git instead of throwing when the runtime needs repair', () => {
_setWslCachesForTests({ available: true, distros: ['Debian'] })
const project = makeProject({
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
expect(
withPlatform('win32', () =>
getWorktreeCreatePrefetchGitOptions(makeStore(project), makeRepo())
)
).toEqual({})
})
it('does not resolve a project runtime for folder workspaces', () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
const project = makeProject({
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
const store = makeStore(project)
const getProjects = vi.fn(store.getProjects)
expect(
withPlatform('win32', () =>
getWorktreeCreatePrefetchGitOptions(
{ ...store, getProjects } as unknown as Store,
makeRepo({ kind: 'folder' })
)
)
).toEqual({})
expect(getProjects).not.toHaveBeenCalled()
})
})
it('does not apply local Windows runtime routing to runtime-owned repos', () => {
const project = makeProject({
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
+23
View File
@@ -1,5 +1,6 @@
import type { Store } from './persistence'
import type { Repo } from '../shared/repo-types'
import { isFolderRepo } from '../shared/repo-kind'
import {
resolveLocalProjectRuntimeForRepo,
type ProjectRuntimeResolutionStore
@@ -58,6 +59,28 @@ export function getLocalProjectWorktreeGitOptions(
return wslDistro ? { wslDistro } : {}
}
/**
* Git routing for the speculative worktree-create warm-up.
*
* Deliberately non-throwing where `getLocalProjectWorktreeGitOptions` throws: an
* optimistic prefetch must not report a repair-required runtime as a failure, so
* an unresolved runtime falls back to the host Git the warm-up used before
* routing existed.
*/
export function getWorktreeCreatePrefetchGitOptions(
store: Store,
repo: Repo
): LocalProjectWorktreeGitOptions {
if (isFolderRepo(repo)) {
return {}
}
const projectRuntime = resolveLocalProjectRuntimeForRepo(store, repo)
if (!projectRuntime || projectRuntime.status !== 'resolved') {
return {}
}
return getLocalProjectWorktreeGitOptionsForRuntime(repo, projectRuntime)
}
export function getLocalProjectWorktreeGitOptionsForRuntime(
repo: Repo,
projectRuntime: ProjectExecutionRuntimeResolution | undefined
@@ -0,0 +1,121 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as WorktreeCreatePreparation from '../worktree-create-preparation'
import type { Project } from '../../shared/project-types'
import type { Repo } from '../../shared/repo-types'
import { _resetWslCachesForTests, _setWslCachesForTests } from '../wsl'
const mocks = vi.hoisted(() => ({
prefetchWorktreeCreateBase: vi.fn(),
prepareWorktreeCreateForRepo: vi.fn()
}))
vi.mock('../worktree-create-base-prefetch', () => ({
prefetchWorktreeCreateBase: mocks.prefetchWorktreeCreateBase
}))
vi.mock('../worktree-create-preparation', async (importOriginal) => ({
...(await importOriginal<typeof WorktreeCreatePreparation>()),
prepareWorktreeCreateForRepo: mocks.prepareWorktreeCreateForRepo
}))
import { OrcaRuntimeService } from './orca-runtime'
const repo: Repo = {
id: 'repo-1',
displayName: 'Repo',
path: String.raw`C:\workspace\repo`,
badgeColor: '#000000',
addedAt: 0
}
const project: Project = {
id: 'project-1',
displayName: 'Project',
badgeColor: '#000000',
sourceRepoIds: ['repo-1'],
createdAt: 0,
updatedAt: 0,
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
}
function makeStore(overrides: Partial<Project> = {}): unknown {
return {
getRepos: () => [repo],
getRepo: (id: string) => (id === repo.id ? repo : undefined),
getProjects: () => [{ ...project, ...overrides }],
getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'windows-host' } })
}
}
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
}
const hostPlatform = process.platform
beforeEach(() => {
mocks.prefetchWorktreeCreateBase.mockReset().mockResolvedValue(undefined)
mocks.prepareWorktreeCreateForRepo.mockReset().mockResolvedValue(undefined)
})
afterEach(() => {
setPlatform(hostPlatform)
_resetWslCachesForTests()
})
// The RPC/relay prefetch is the only warm-up a remote client reaches, so it has
// to resolve the same project runtime the IPC handler does. Constructed through
// the barrel because the split chain resolves selectors in a later subclass.
describe('prefetchManagedWorktreeCreateBase (orca-runtime-get-worktree-terminal-provisioning-host)', () => {
it('warms up in the distro a WSL-routed project runs in', async () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
setPlatform('win32')
const runtime = new OrcaRuntimeService(makeStore() as never)
await runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
expect(mocks.prefetchWorktreeCreateBase).toHaveBeenCalledWith(
expect.objectContaining({ gitOptions: { wslDistro: 'Ubuntu' } })
)
})
it('warms up on host git when no project runtime routes the repo', async () => {
setPlatform('darwin')
const runtime = new OrcaRuntimeService(makeStore() as never)
await runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
expect(mocks.prefetchWorktreeCreateBase).toHaveBeenCalledWith(
expect.objectContaining({ gitOptions: {} })
)
})
// A repair-required runtime must degrade to host git, not fail the warm-up.
it('does not surface a repair-required project runtime as a prefetch failure', async () => {
_setWslCachesForTests({ available: true, distros: ['Debian'] })
setPlatform('win32')
const runtime = new OrcaRuntimeService(makeStore() as never)
await expect(
runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
).resolves.toBeUndefined()
expect(mocks.prefetchWorktreeCreateBase).toHaveBeenCalledWith(
expect.objectContaining({ gitOptions: {} })
)
})
it('prepares the checkout the prefetch resolved', async () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
setPlatform('win32')
mocks.prefetchWorktreeCreateBase.mockResolvedValue('origin/main')
const runtime = new OrcaRuntimeService(makeStore() as never)
await runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
expect(mocks.prepareWorktreeCreateForRepo).toHaveBeenCalledWith(
expect.anything(),
repo,
'origin/main'
)
})
})
@@ -8,6 +8,7 @@ import type { TerminalCreateOptions } from './runtime-terminal-contracts'
import type { WorktreeStartupReadinessHost } from './runtime-worktree-startup-readiness'
import { prefetchWorktreeCreateBase } from '../worktree-create-base-prefetch'
import { prepareWorktreeCreateForRepo } from '../worktree-create-preparation'
import { getWorktreeCreatePrefetchGitOptions } from '../project-runtime-git-options'
export class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRuntimeWithActivateManagedWorktree {
protected getWorktreeTerminalProvisioningHost(): WorktreeTerminalProvisioningHost {
@@ -48,14 +49,16 @@ export class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRunt
}
const repo = await this.resolveRepoSelector(args.repoSelector)
const store = this.requireStore()
const baseBranch = await prefetchWorktreeCreateBase({
repo,
baseBranch: args.baseBranch,
runtime: this
runtime: this,
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo)
})
if (baseBranch) {
try {
await prepareWorktreeCreateForRepo(this.requireStore(), repo, baseBranch)
await prepareWorktreeCreateForRepo(store, repo, baseBranch)
} catch {
// Why: speculative preparation is an optimistic warm-up; the real create path reports failures.
}
@@ -15,7 +15,7 @@ import {
import type { RuntimeStore } from './runtime-store-contract'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
import type { RemoteFetchResult, RemoteTrackingBase } from './runtime-remote-fetch-controller'
import { hasLocalWorktreeBaseRef } from './runtime-worktree-create-git'
import { hasLocalWorktreeBaseRef } from '../git/worktree-base-ref-probe'
import { isGeneratedWorktreeCreateName } from '../worktree-create-candidates'
import { consumePreparedWorktreeCreate } from '../worktree-create-preparation'
import {
@@ -14,7 +14,7 @@ import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktre
import type { RemoteFetchResult, RemoteTrackingBase } from './runtime-remote-fetch-controller'
import type { HostedReviewExecutionOptions } from '../source-control/hosted-review-git-options'
import { hasLocalGitOptions } from './runtime-worktree-selection'
import { hasLocalWorktreeBaseRef } from './runtime-worktree-create-git'
import { hasLocalWorktreeBaseRef } from '../git/worktree-base-ref-probe'
import { resolveRuntimeLocalWorktreeCreateCandidate } from './runtime-local-worktree-create-candidate'
import { createRuntimeLocalGitWorktree } from './runtime-local-git-worktree-create'
import { materializeRuntimeLocalWorktree } from './runtime-local-worktree-materialization'
@@ -1,10 +1,7 @@
import type { BranchPrefixStrategy } from '../../shared/ui-chrome-types'
import type { Repo } from '../../shared/repo-types'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { getPRForBranch } from '../github/client'
import { hasCommitObjectViaGitExec } from '../git/commit-object-ref'
import { gitExecFileAsync } from '../git/runner'
import { hasWorktreeBaseCommitRef } from '../git/worktree-base-ref-probe'
import { listWorktrees } from '../git/worktree'
import { computeValidatedBranchName } from '../ipc/worktree-logic'
import { getHostedReviewForBranch } from '../source-control/hosted-review'
@@ -114,23 +111,3 @@ export async function getSelectedHostedReviewForBranch(
}
: null
}
export async function hasLocalWorktreeBaseRef(
repoPath: string,
baseRef: string,
options: { wslDistro?: string } = {}
): Promise<boolean> {
const refExists = (qualifiedRef: string) =>
hasWorktreeBaseCommitRef(repoPath, qualifiedRef, options)
const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists)
if (resolvedBaseRef !== baseRef) {
return true
}
if (baseRef.startsWith('refs/')) {
return refExists(baseRef)
}
return hasCommitObjectViaGitExec(
(gitArgs) => gitExecFileAsync(gitArgs, { cwd: repoPath, ...options }),
baseRef
)
}
@@ -0,0 +1,182 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getBaseRefDefault: vi.fn(),
gitExecFileAsync: vi.fn(),
getSshGitProvider: vi.fn(),
prefetchRemoteWorktreeCreateBase: vi.fn(),
resolveRemoteTrackingBase: vi.fn(),
hasRemoteTrackingRef: vi.fn(),
getOrStartRemoteTrackingBaseRefresh: vi.fn(),
fetchRemoteWithCache: vi.fn()
}))
vi.mock('./git/repo', () => ({ getBaseRefDefault: mocks.getBaseRefDefault }))
vi.mock('./git/runner', () => ({ gitExecFileAsync: mocks.gitExecFileAsync }))
vi.mock('./providers/ssh-git-dispatch', () => ({ getSshGitProvider: mocks.getSshGitProvider }))
vi.mock('./ipc/worktree-remote', () => ({
prefetchRemoteWorktreeCreateBase: mocks.prefetchRemoteWorktreeCreateBase
}))
import { prefetchWorktreeCreateBase } from './worktree-create-base-prefetch'
const repo = {
id: 'repo-1',
path: String.raw`C:\workspace\repo`,
displayName: 'repo',
badgeColor: '#000000',
addedAt: 0
}
const WSL = { wslDistro: 'Ubuntu' }
function runtime() {
return {
resolveRemoteTrackingBase: mocks.resolveRemoteTrackingBase,
hasRemoteTrackingRef: mocks.hasRemoteTrackingRef,
getOrStartRemoteTrackingBaseRefresh: mocks.getOrStartRemoteTrackingBaseRefresh,
fetchRemoteWithCache: mocks.fetchRemoteWithCache
}
}
/** Resolve only the named refs/objects; every other rev-parse answers "absent". */
function resolveOnly(present: string[]): void {
mocks.gitExecFileAsync.mockImplementation(async (args: string[]) => {
const rev = args.at(-1)?.replace('^{commit}', '') ?? ''
return present.includes(rev)
? { stdout: `${'f'.repeat(40)}\n`, stderr: '' }
: { stdout: '', stderr: '' }
})
}
function revParse(ref: string): string[] {
return ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]
}
beforeEach(() => {
for (const mock of Object.values(mocks)) {
mock.mockReset()
}
mocks.getBaseRefDefault.mockResolvedValue('origin/main')
resolveOnly([])
mocks.resolveRemoteTrackingBase.mockResolvedValue(null)
mocks.hasRemoteTrackingRef.mockResolvedValue(false)
mocks.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: true })
mocks.fetchRemoteWithCache.mockResolvedValue(undefined)
})
describe('prefetchWorktreeCreateBase local git routing', () => {
it('resolves the default base and its ref probes inside the selected WSL distro', async () => {
resolveOnly(['refs/remotes/origin/main'])
await expect(
prefetchWorktreeCreateBase({ repo, runtime: runtime(), gitOptions: WSL })
).resolves.toBe('origin/main')
expect(mocks.getBaseRefDefault).toHaveBeenCalledWith(repo.path, WSL)
expect(mocks.resolveRemoteTrackingBase).toHaveBeenCalledWith(repo.path, 'origin/main', WSL)
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse('refs/remotes/origin/main'), {
cwd: repo.path,
...WSL
})
// A base that is already local needs no fetch.
expect(mocks.fetchRemoteWithCache).not.toHaveBeenCalled()
})
it('probes a full commit object inside the selected WSL distro', async () => {
const sha = 'a'.repeat(40)
resolveOnly([sha])
await expect(
prefetchWorktreeCreateBase({ repo, baseBranch: sha, runtime: runtime(), gitOptions: WSL })
).resolves.toBe(sha)
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse(sha), {
cwd: repo.path,
...WSL
})
expect(mocks.fetchRemoteWithCache).not.toHaveBeenCalled()
})
it('routes the exact remote-base refresh through the selected WSL distro', async () => {
const remoteTrackingBase = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
mocks.resolveRemoteTrackingBase.mockResolvedValue(remoteTrackingBase)
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBe('origin/main')
expect(mocks.hasRemoteTrackingRef).toHaveBeenCalledWith(repo.path, remoteTrackingBase, WSL)
expect(mocks.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith(
repo.path,
remoteTrackingBase,
WSL
)
})
it('routes the broad remote-fetch fallback through the selected WSL distro', async () => {
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'feature/topic',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBe('feature/topic')
expect(mocks.fetchRemoteWithCache).toHaveBeenCalledWith(repo.path, 'origin', WSL)
})
it('leaves host-routed probes on the git host they used before routing existed', async () => {
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'feature/topic',
runtime: runtime(),
gitOptions: {}
})
).resolves.toBe('feature/topic')
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse('refs/remotes/feature/topic'), {
cwd: repo.path
})
for (const call of mocks.gitExecFileAsync.mock.calls) {
expect(call[1]).toEqual({ cwd: repo.path })
}
// Runtime calls keep their original arity so host repos stay on the runtime's own defaults.
expect(mocks.resolveRemoteTrackingBase).toHaveBeenCalledWith(repo.path, 'feature/topic')
expect(mocks.fetchRemoteWithCache).toHaveBeenCalledWith(repo.path, 'origin')
})
it('does not resolve a local base for SSH repos', async () => {
const provider = { exec: vi.fn() }
mocks.getSshGitProvider.mockReturnValue(provider)
await expect(
prefetchWorktreeCreateBase({
repo: { ...repo, connectionId: 'conn-1' },
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBeUndefined()
expect(mocks.prefetchRemoteWorktreeCreateBase).toHaveBeenCalledWith(
provider,
expect.objectContaining({ connectionId: 'conn-1' }),
{ baseBranch: 'origin/main' }
)
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
})
})
+52 -35
View File
@@ -1,12 +1,15 @@
import { isFolderRepo } from '../shared/repo-kind'
import type { Repo } from '../shared/repo-types'
import { hasLocalCommitObject, isFullGitObjectId } from './git/commit-object-ref'
import { hasWorktreeBaseCommitRef } from './git/worktree-base-ref-probe'
import { isFullGitObjectId } from './git/commit-object-ref'
import { hasLocalWorktreeBaseRef } from './git/worktree-base-ref-probe'
import { getBaseRefDefault } from './git/repo'
import { getSshGitProvider } from './providers/ssh-git-dispatch'
import { prefetchRemoteWorktreeCreateBase } from './ipc/worktree-remote'
import { resolveWorktreeCreateBase } from './worktree-create-base'
import { resolveWorktreeAddBaseRef } from '../shared/worktree/base-ref'
type WorktreeCreateBaseGitOptions = {
wslDistro?: string
}
type RemoteTrackingBaseForPrefetch = {
remote: string
@@ -18,49 +21,51 @@ type RemoteTrackingBaseForPrefetch = {
type WorktreeCreateBasePrefetchRuntime = {
resolveRemoteTrackingBase: (
repoPath: string,
baseBranch: string
baseBranch: string,
options?: WorktreeCreateBaseGitOptions
) => Promise<RemoteTrackingBaseForPrefetch | null>
hasRemoteTrackingRef: (repoPath: string, base: RemoteTrackingBaseForPrefetch) => Promise<boolean>
hasRemoteTrackingRef: (
repoPath: string,
base: RemoteTrackingBaseForPrefetch,
options?: WorktreeCreateBaseGitOptions
) => Promise<boolean>
getOrStartRemoteTrackingBaseRefresh: (
repoPath: string,
base: RemoteTrackingBaseForPrefetch
base: RemoteTrackingBaseForPrefetch,
options?: WorktreeCreateBaseGitOptions
) => Promise<unknown>
fetchRemoteWithCache: (repoPath: string, remote: string) => Promise<void>
}
async function hasLocalWorktreeBaseRef(repoPath: string, baseRef: string): Promise<boolean> {
const refExists = (qualifiedRef: string) => hasWorktreeBaseCommitRef(repoPath, qualifiedRef)
const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists)
if (resolvedBaseRef !== baseRef) {
return true
}
if (baseRef.startsWith('refs/')) {
return refExists(baseRef)
}
return hasLocalCommitObject(repoPath, baseRef)
fetchRemoteWithCache: (
repoPath: string,
remote: string,
options?: WorktreeCreateBaseGitOptions
) => Promise<void>
}
async function prefetchLocalWorktreeCreateBase(
repo: Repo,
baseBranch: string | undefined,
runtime: WorktreeCreateBasePrefetchRuntime
runtime: WorktreeCreateBasePrefetchRuntime,
options: WorktreeCreateBaseGitOptions
): Promise<string | undefined> {
// Keep host-routed calls at their original arity so they stay on the runtime's default options.
const optionArgs: [] | [WorktreeCreateBaseGitOptions] = options.wslDistro ? [options] : []
const resolvedBaseBranch = await resolveWorktreeCreateBase({
requestedBaseBranch: baseBranch,
repoWorktreeBaseRef: repo.worktreeBaseRef,
resolveDefaultBaseRef: () => getBaseRefDefault(repo.path),
resolveDefaultBaseRef: () => getBaseRefDefault(repo.path, ...optionArgs),
isBaseUsable: async (baseBranchCandidate) => {
const remoteTrackingBase = await runtime.resolveRemoteTrackingBase(
repo.path,
baseBranchCandidate
baseBranchCandidate,
...optionArgs
)
if (remoteTrackingBase) {
if (await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase)) {
if (await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase, ...optionArgs)) {
return true
}
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate)
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate, options)
}
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate)
return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate, options)
}
})
if (!resolvedBaseBranch) {
@@ -68,28 +73,37 @@ async function prefetchLocalWorktreeCreateBase(
}
if (
isFullGitObjectId(resolvedBaseBranch) &&
(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch))
(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch, options))
) {
return resolvedBaseBranch
}
const remoteTrackingBase = await runtime.resolveRemoteTrackingBase(repo.path, resolvedBaseBranch)
const remoteTrackingBase = await runtime.resolveRemoteTrackingBase(
repo.path,
resolvedBaseBranch,
...optionArgs
)
if (remoteTrackingBase) {
if (
(await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase)) ||
!(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch))
(await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase, ...optionArgs)) ||
!(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch, options))
) {
await runtime.getOrStartRemoteTrackingBaseRefresh(repo.path, remoteTrackingBase)
await runtime.getOrStartRemoteTrackingBaseRefresh(
repo.path,
remoteTrackingBase,
...optionArgs
)
return resolvedBaseBranch
}
}
if (await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch)) {
if (await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch, options)) {
// Why: hosted-review start points and local branch bases are already local; a broad remote fetch cannot make them fresher.
return resolvedBaseBranch
}
// Why: keep optimistic prefetch on the same best-effort fallback path as
// create so the real create can reuse the runtime's remote fetch cache.
await runtime.fetchRemoteWithCache(repo.path, 'origin')
// Why: same best-effort fallback create takes, on create's own fetch key, so a
// create that lands here reuses this fetch instead of repeating it. A create
// that instead resolves an exact remote base still queues behind it.
await runtime.fetchRemoteWithCache(repo.path, 'origin', ...optionArgs)
return resolvedBaseBranch
}
@@ -97,6 +111,9 @@ export async function prefetchWorktreeCreateBase(args: {
repo: Repo
baseBranch?: string
runtime: WorktreeCreateBasePrefetchRuntime
/** Routing for the project's Git host; required so a caller cannot silently
* warm up the wrong ref store — pass `{}` for host Git. */
gitOptions: WorktreeCreateBaseGitOptions
}): Promise<string | undefined> {
if (isFolderRepo(args.repo)) {
return undefined
@@ -109,5 +126,5 @@ export async function prefetchWorktreeCreateBase(args: {
await prefetchRemoteWorktreeCreateBase(provider, args.repo, { baseBranch: args.baseBranch })
return undefined
}
return prefetchLocalWorktreeCreateBase(args.repo, args.baseBranch, args.runtime)
return prefetchLocalWorktreeCreateBase(args.repo, args.baseBranch, args.runtime, args.gitOptions)
}