mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
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:
@@ -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' }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user