mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
perf(worktree): overlap base refresh with prepared checkout
This commit is contained in:
@@ -278,6 +278,58 @@ describe('prepared worktree creation with real Git', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['before reset', 'after reset'])(
|
||||
'finalizes refreshed content when the base moves %s',
|
||||
async (when) => {
|
||||
const { repoPath, root } = await createRepo()
|
||||
const preparedPath = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY, 'fetch-overlap')
|
||||
const finalPath = join(root, 'final-overlap')
|
||||
await mkdir(join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY), { recursive: true })
|
||||
const original = git(repoPath, ['rev-parse', 'HEAD'])
|
||||
await writeFile(join(repoPath, 'version.txt'), 'refreshed\n')
|
||||
git(repoPath, ['commit', '-am', 'remote update'])
|
||||
const refreshed = git(repoPath, ['rev-parse', 'HEAD'])
|
||||
git(repoPath, ['update-ref', 'refs/remotes/origin/main', original])
|
||||
const exec = gitRunner.gitExecFileAsync
|
||||
let moved = false
|
||||
const spy = vi
|
||||
.spyOn(gitRunner, 'gitExecFileAsync')
|
||||
.mockImplementation(async (args, options) => {
|
||||
if (!moved && args.includes('reset') && when === 'before reset') {
|
||||
git(repoPath, ['update-ref', 'refs/remotes/origin/main', refreshed])
|
||||
moved = true
|
||||
}
|
||||
const result = await exec(args, options)
|
||||
if (!moved && args.includes('reset') && when === 'after reset') {
|
||||
git(repoPath, ['update-ref', 'refs/remotes/origin/main', refreshed])
|
||||
moved = true
|
||||
}
|
||||
return result
|
||||
})
|
||||
try {
|
||||
await prepareWorktreeCreateCheckout(
|
||||
repoPath,
|
||||
preparedPath,
|
||||
'refs/remotes/origin/main',
|
||||
createWorktreePreparationLockReason('fetch-overlap')
|
||||
)
|
||||
expect(moved).toBe(true)
|
||||
await finalizePreparedWorktree(
|
||||
repoPath,
|
||||
preparedPath,
|
||||
finalPath,
|
||||
'feature/overlap',
|
||||
'refs/remotes/origin/main'
|
||||
)
|
||||
expect(git(finalPath, ['rev-parse', 'HEAD'])).toBe(refreshed)
|
||||
expect(await readFile(join(finalPath, 'version.txt'), 'utf8')).toBe('refreshed\n')
|
||||
expect(git(finalPath, ['status', '--porcelain'])).toBe('')
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('hides the preparation, retargets an advanced base, and attaches the final branch', async () => {
|
||||
const { repoPath, root } = await createRepo()
|
||||
const preparationRoot = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY)
|
||||
|
||||
@@ -15,15 +15,13 @@ export function registerWorktreePrefetchHandler(context: WorktreeIpcContext): vo
|
||||
return
|
||||
}
|
||||
try {
|
||||
const baseBranch = await prefetchWorktreeCreateBase({
|
||||
await prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: args.baseBranch,
|
||||
runtime,
|
||||
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo)
|
||||
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo),
|
||||
prepareCheckout: (base) => prepareWorktreeCreateForRepo(store, repo, base)
|
||||
})
|
||||
if (baseBranch) {
|
||||
await prepareWorktreeCreateForRepo(store, repo, baseBranch)
|
||||
}
|
||||
} catch {
|
||||
// Why: optimistic warm-up; the real create path awaits the same refresh and reports failures there.
|
||||
}
|
||||
|
||||
@@ -107,7 +107,10 @@ describe('prefetchManagedWorktreeCreateBase (orca-runtime-get-worktree-terminal-
|
||||
it('prepares the checkout the prefetch resolved', async () => {
|
||||
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
|
||||
setPlatform('win32')
|
||||
mocks.prefetchWorktreeCreateBase.mockResolvedValue('origin/main')
|
||||
mocks.prefetchWorktreeCreateBase.mockImplementation(async ({ prepareCheckout }) => {
|
||||
await prepareCheckout('origin/main')
|
||||
return 'origin/main'
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
|
||||
await runtime.prefetchManagedWorktreeCreateBase({ repoSelector: 'repo-1' })
|
||||
|
||||
@@ -50,18 +50,12 @@ export class OrcaRuntimeWithGetWorktreeTerminalProvisioningHost extends OrcaRunt
|
||||
|
||||
const repo = await this.resolveRepoSelector(args.repoSelector)
|
||||
const store = this.requireStore()
|
||||
const baseBranch = await prefetchWorktreeCreateBase({
|
||||
await prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: args.baseBranch,
|
||||
runtime: this,
|
||||
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo)
|
||||
gitOptions: getWorktreeCreatePrefetchGitOptions(store, repo),
|
||||
prepareCheckout: (base) => prepareWorktreeCreateForRepo(store, repo, base)
|
||||
})
|
||||
if (baseBranch) {
|
||||
try {
|
||||
await prepareWorktreeCreateForRepo(store, repo, baseBranch)
|
||||
} catch {
|
||||
// Why: speculative preparation is an optimistic warm-up; the real create path reports failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,12 +160,14 @@ describe('prefetchWorktreeCreateBase local git routing', () => {
|
||||
})
|
||||
|
||||
it('does not resolve a local base for SSH repos', async () => {
|
||||
const prepareCheckout = vi.fn()
|
||||
const provider = { exec: vi.fn() }
|
||||
mocks.getSshGitProvider.mockReturnValue(provider)
|
||||
|
||||
await expect(
|
||||
prefetchWorktreeCreateBase({
|
||||
repo: { ...repo, connectionId: 'conn-1' },
|
||||
prepareCheckout,
|
||||
baseBranch: 'origin/main',
|
||||
runtime: runtime(),
|
||||
gitOptions: WSL
|
||||
@@ -178,5 +180,145 @@ describe('prefetchWorktreeCreateBase local git routing', () => {
|
||||
{ baseBranch: 'origin/main' }
|
||||
)
|
||||
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
|
||||
expect(prepareCheckout).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkout and refresh overlap', () => {
|
||||
it.each([{}, WSL])(
|
||||
'starts one checkout before a blocked refresh finishes on %j',
|
||||
async (gitOptions) => {
|
||||
const base = {
|
||||
remote: 'origin',
|
||||
branch: 'main',
|
||||
ref: 'refs/remotes/origin/main',
|
||||
base: 'origin/main'
|
||||
}
|
||||
mocks.resolveRemoteTrackingBase.mockResolvedValue(base)
|
||||
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
|
||||
let release!: () => void
|
||||
mocks.getOrStartRemoteTrackingBaseRefresh.mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
)
|
||||
const prepareCheckout = vi.fn().mockResolvedValue(undefined)
|
||||
let settled = false
|
||||
const result = prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: 'origin/main',
|
||||
runtime: runtime(),
|
||||
gitOptions,
|
||||
prepareCheckout
|
||||
}).finally(() => {
|
||||
settled = true
|
||||
})
|
||||
await vi.waitFor(() => expect(prepareCheckout).toHaveBeenCalledWith('origin/main'))
|
||||
expect(settled).toBe(false)
|
||||
release()
|
||||
await expect(result).resolves.toBe('origin/main')
|
||||
expect(prepareCheckout).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('waits for refresh when the selected base is not local', async () => {
|
||||
mocks.resolveRemoteTrackingBase.mockResolvedValue({
|
||||
remote: 'origin',
|
||||
branch: 'main',
|
||||
ref: 'refs/remotes/origin/main',
|
||||
base: 'origin/main'
|
||||
})
|
||||
let release!: () => void
|
||||
mocks.getOrStartRemoteTrackingBaseRefresh.mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
)
|
||||
const prepareCheckout = vi.fn().mockResolvedValue(undefined)
|
||||
const result = prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: 'origin/main',
|
||||
runtime: runtime(),
|
||||
gitOptions: {},
|
||||
prepareCheckout
|
||||
})
|
||||
await vi.waitFor(() =>
|
||||
expect(mocks.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledTimes(1)
|
||||
)
|
||||
expect(prepareCheckout).not.toHaveBeenCalled()
|
||||
release()
|
||||
await result
|
||||
expect(prepareCheckout).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not fail a successful refresh because preparation fails', async () => {
|
||||
mocks.resolveRemoteTrackingBase.mockResolvedValue({
|
||||
remote: 'origin',
|
||||
branch: 'main',
|
||||
ref: 'refs/remotes/origin/main',
|
||||
base: 'origin/main'
|
||||
})
|
||||
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
|
||||
const prepareCheckout = vi.fn().mockRejectedValue(new Error('checkout failed'))
|
||||
await expect(
|
||||
prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: 'origin/main',
|
||||
runtime: runtime(),
|
||||
gitOptions: {},
|
||||
prepareCheckout
|
||||
})
|
||||
).resolves.toBe('origin/main')
|
||||
expect(prepareCheckout).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('settles preparation before propagating refresh failure', async () => {
|
||||
mocks.resolveRemoteTrackingBase.mockResolvedValue({
|
||||
remote: 'origin',
|
||||
branch: 'main',
|
||||
ref: 'refs/remotes/origin/main',
|
||||
base: 'origin/main'
|
||||
})
|
||||
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
|
||||
const error = new Error('refresh failed')
|
||||
mocks.getOrStartRemoteTrackingBaseRefresh.mockRejectedValue(error)
|
||||
let release!: () => void
|
||||
const prepareCheckout = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
)
|
||||
let settled = false
|
||||
const result = prefetchWorktreeCreateBase({
|
||||
repo,
|
||||
baseBranch: 'origin/main',
|
||||
runtime: runtime(),
|
||||
gitOptions: {},
|
||||
prepareCheckout
|
||||
}).finally(() => {
|
||||
settled = true
|
||||
})
|
||||
const assertion = expect(result).rejects.toBe(error)
|
||||
await vi.waitFor(() => expect(prepareCheckout).toHaveBeenCalledTimes(1))
|
||||
expect(settled).toBe(false)
|
||||
release()
|
||||
await assertion
|
||||
})
|
||||
})
|
||||
|
||||
it('does not prepare folder repositories', async () => {
|
||||
const prepareCheckout = vi.fn()
|
||||
await expect(
|
||||
prefetchWorktreeCreateBase({
|
||||
repo: { ...repo, kind: 'folder' },
|
||||
runtime: runtime(),
|
||||
gitOptions: {},
|
||||
prepareCheckout
|
||||
})
|
||||
).resolves.toBeUndefined()
|
||||
expect(prepareCheckout).not.toHaveBeenCalled()
|
||||
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -45,7 +45,8 @@ async function prefetchLocalWorktreeCreateBase(
|
||||
repo: Repo,
|
||||
baseBranch: string | undefined,
|
||||
runtime: WorktreeCreateBasePrefetchRuntime,
|
||||
options: WorktreeCreateBaseGitOptions
|
||||
options: WorktreeCreateBaseGitOptions,
|
||||
prepareLocalCheckout: (base: string) => void
|
||||
): 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] : []
|
||||
@@ -83,8 +84,17 @@ async function prefetchLocalWorktreeCreateBase(
|
||||
...optionArgs
|
||||
)
|
||||
if (remoteTrackingBase) {
|
||||
const hasTrackingRef = await runtime.hasRemoteTrackingRef(
|
||||
repo.path,
|
||||
remoteTrackingBase,
|
||||
...optionArgs
|
||||
)
|
||||
if (hasTrackingRef) {
|
||||
// Finalization revalidates the refreshed commit before exposing the checkout.
|
||||
prepareLocalCheckout(resolvedBaseBranch)
|
||||
}
|
||||
if (
|
||||
(await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase, ...optionArgs)) ||
|
||||
hasTrackingRef ||
|
||||
!(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch, options))
|
||||
) {
|
||||
await runtime.getOrStartRemoteTrackingBaseRefresh(
|
||||
@@ -114,6 +124,7 @@ export async function prefetchWorktreeCreateBase(args: {
|
||||
/** 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
|
||||
prepareCheckout?: (base: string) => Promise<void>
|
||||
}): Promise<string | undefined> {
|
||||
if (isFolderRepo(args.repo)) {
|
||||
return undefined
|
||||
@@ -126,5 +137,29 @@ export async function prefetchWorktreeCreateBase(args: {
|
||||
await prefetchRemoteWorktreeCreateBase(provider, args.repo, { baseBranch: args.baseBranch })
|
||||
return undefined
|
||||
}
|
||||
return prefetchLocalWorktreeCreateBase(args.repo, args.baseBranch, args.runtime, args.gitOptions)
|
||||
const prepareCheckout = args.prepareCheckout
|
||||
let preparation: Promise<void> | undefined
|
||||
const prepare = (base: string): void => {
|
||||
if (!preparation && prepareCheckout) {
|
||||
preparation = Promise.resolve()
|
||||
.then(() => prepareCheckout(base))
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
try {
|
||||
const base = await prefetchLocalWorktreeCreateBase(
|
||||
args.repo,
|
||||
args.baseBranch,
|
||||
args.runtime,
|
||||
args.gitOptions,
|
||||
prepare
|
||||
)
|
||||
if (base) {
|
||||
prepare(base)
|
||||
}
|
||||
return base
|
||||
} finally {
|
||||
// Settle speculative work even if refresh fails; Create owns error reporting.
|
||||
await preparation
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user