mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* fix(worktrees): create from the usable local base when there is no tracking ref, and say so, on the host's create The host's create (agent.launch, worktree.create, the phone and the CLI) asked for a remote base like origin/main with no tracking ref yet fetched it, and threw offline, even when the local branch existed; it also never reported a base it fell back to. Like the desktop create, it now uses the local branch without fetching and returns baseFallback, which the desktop window already turns into a notice. * fix(worktrees): fetch the remote base first and fall back to the local branch only when the fetch fails The host's create skipped the fetch whenever a local branch matched a remote base with no tracking ref, so online phone/CLI/agent.launch creates silently built on a possibly stale local branch, every time. Fetch first, as main does; only when that fetch fails use the local branch the remote names and report it with baseFallback. With no local branch the existing error is unchanged. Decide the base before naming so branch reuse and conflict checks, the add and the persisted baseRef all see the base actually used, and return baseFallback as its own field of the create result instead of riding on the git add result. * fix(worktrees): report the fallback for a local ref of the requested name, as the desktop create does The tracking ref is still missing there; only the fetch is skipped, as before. * test(worktrees): pin that the host create names only after the base fetch settles Also pins the existing not-found-after-fetching error, now owned by the base decision. * fix(worktrees): fall back to the local base only for creates a person asked for; keep the exact-name ref silent Scheduled automations, orchestration workers and federation keep main's "Check your network" error offline: nobody is watching to be told the workspace was built on a possibly stale local branch. The fallback is a host-side create option (allowLocalBaseFallback, default off, not on the wire) that the worktree.create RPC and agent.launch set unless the request carries automation provenance. A local ref of the exact requested name goes back to silent, as on main's host: reporting it showed "may not include the latest remote changes" online for plain local branches. * test(worktrees): pin the local-base opt-in at agent.launch and its absence for an automation's worktree.create
212 lines
7.8 KiB
TypeScript
212 lines
7.8 KiB
TypeScript
import type { LocalGitExecOptions } from '../git/repo-default-base-ref'
|
|
import { runWorktreeChangeInvalidators } from '../ipc/worktree-change-invalidators'
|
|
import type { GitPushTarget, GitWorktreeInfo } from '../../shared/worktree/types'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import { resolveCreatedWorktree } from '../ipc/created-worktree-reconciliation'
|
|
import { normalizeSparseDirectories } from '../ipc/sparse-checkout-directories'
|
|
import { configureCreatedWorktreePushTarget } from '../ipc/worktree-remote'
|
|
import {
|
|
addSparseWorktree,
|
|
addWorktree,
|
|
type AddWorktreeOptions,
|
|
type AddWorktreeResult
|
|
} from '../git/worktree'
|
|
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 type { RuntimeLocalWorktreeCreateBase } from './runtime-local-worktree-create-base'
|
|
import { isGeneratedWorktreeCreateName } from '../worktree-create-candidates'
|
|
import {
|
|
consumePreparedWorktreeCreate,
|
|
type PreparationRearmHolder
|
|
} from '../worktree-create-preparation'
|
|
import {
|
|
failedWorktreeCreationNeedsRetirement,
|
|
retireGeneratedWorktreeName
|
|
} from '../worktree-name-retirement'
|
|
import type { WorktreeCreateTimingRecorder } from '../worktree-create-timing'
|
|
|
|
export async function createRuntimeLocalGitWorktree(args: {
|
|
request: RuntimeManagedWorktreeCreateArgs
|
|
repo: Repo
|
|
store: RuntimeStore
|
|
settings: {
|
|
workspaceDir: string
|
|
nestWorkspaces: boolean
|
|
refreshLocalBaseRefOnWorktreeCreate: boolean
|
|
localBaseRefSuggestionDismissed?: boolean
|
|
}
|
|
base: RuntimeLocalWorktreeCreateBase
|
|
workspaceRoot: string
|
|
branchName: string
|
|
worktreePath: string
|
|
effectiveSanitizedName?: string
|
|
checkoutExistingBranch: boolean
|
|
localWorktreeGitOptions: LocalGitExecOptions
|
|
refreshRemoteTrackingBase: (
|
|
repoPath: string,
|
|
base: RemoteTrackingBase,
|
|
options?: LocalGitExecOptions
|
|
) => Promise<RemoteFetchResult>
|
|
fetchRemote: (repoPath: string, remote: string, options?: LocalGitExecOptions) => Promise<void>
|
|
rearm: PreparationRearmHolder
|
|
timing: WorktreeCreateTimingRecorder
|
|
}): Promise<{
|
|
sparseDirectories: string[]
|
|
configuredPushTarget?: GitPushTarget
|
|
created: GitWorktreeInfo
|
|
addResult: AddWorktreeResult
|
|
}> {
|
|
const { baseBranch, remoteTrackingBase, deferredRefresh } = args.base
|
|
if (remoteTrackingBase && deferredRefresh === 'tracking_ref') {
|
|
await args.timing.time('refresh_base_ref', () =>
|
|
args.refreshRemoteTrackingBase(
|
|
args.repo.path,
|
|
remoteTrackingBase,
|
|
args.localWorktreeGitOptions
|
|
)
|
|
)
|
|
} else if (deferredRefresh === 'origin') {
|
|
try {
|
|
await args.timing.time('refresh_base_ref', () =>
|
|
args.fetchRemote(args.repo.path, 'origin', args.localWorktreeGitOptions)
|
|
)
|
|
} catch {}
|
|
}
|
|
const sparseDirectories = args.request.sparseCheckout
|
|
? normalizeSparseDirectories(args.request.sparseCheckout.directories)
|
|
: []
|
|
if (args.request.sparseCheckout && sparseDirectories.length === 0) {
|
|
throw new Error('Sparse checkout requires at least one repo-relative directory.')
|
|
}
|
|
// Why: defer the remote add + fetch (fork case) or the redundant re-fetch
|
|
// (same-repo case, already fetched while resolving the PR start point) to
|
|
// first use -- push/pull/fetch/fast-forward materialize it on demand
|
|
// (#17828). Metadata is persisted untouched; only the git mutation defers.
|
|
const preparedPushTarget = args.request.pushTarget
|
|
const suggestLocalBaseRefUpdate =
|
|
!args.settings.refreshLocalBaseRefOnWorktreeCreate &&
|
|
!args.settings.localBaseRefSuggestionDismissed &&
|
|
Boolean(remoteTrackingBase)
|
|
const remoteOption = remoteTrackingBase ? { remoteTrackingBase } : undefined
|
|
const preparedWorktreeOptions: AddWorktreeOptions = {
|
|
...remoteOption,
|
|
...(suggestLocalBaseRefUpdate ? { suggestLocalBaseRefUpdate } : {}),
|
|
...args.localWorktreeGitOptions
|
|
}
|
|
const addOptions: AddWorktreeOptions = {
|
|
...preparedWorktreeOptions,
|
|
...(args.checkoutExistingBranch ? { checkoutExistingBranch: true } : {})
|
|
}
|
|
const shouldRetireGeneratedName =
|
|
args.request.nameWasGenerated === true &&
|
|
Boolean(args.effectiveSanitizedName) &&
|
|
isGeneratedWorktreeCreateName(args.effectiveSanitizedName!)
|
|
let addResult: AddWorktreeResult
|
|
try {
|
|
addResult = await args.timing.time('git_worktree_add', async () => {
|
|
if (sparseDirectories.length > 0 || args.checkoutExistingBranch) {
|
|
args.timing.recordPreparedCheckout({
|
|
status: 'miss',
|
|
reason: sparseDirectories.length > 0 ? 'sparse_checkout' : 'checkout_existing_branch'
|
|
})
|
|
} else {
|
|
const preparedAttempt = await consumePreparedWorktreeCreate({
|
|
repoPath: args.repo.path,
|
|
workspaceRoot: args.workspaceRoot,
|
|
worktreePath: args.worktreePath,
|
|
branch: args.branchName,
|
|
baseBranch,
|
|
refreshLocalBaseRef: args.settings.refreshLocalBaseRefOnWorktreeCreate,
|
|
options: preparedWorktreeOptions,
|
|
timing: args.timing
|
|
})
|
|
if (preparedAttempt.status === 'hit') {
|
|
// Deferred, not fired: re-arming is a full `reset --hard`, and the caller still has
|
|
// materialization probes and terminals ahead of it.
|
|
args.rearm.fire = preparedAttempt.rearm
|
|
return preparedAttempt.result
|
|
}
|
|
if (preparedAttempt.rearm) {
|
|
args.rearm.fire = preparedAttempt.rearm
|
|
}
|
|
}
|
|
if (sparseDirectories.length > 0) {
|
|
return (
|
|
(await addSparseWorktree(
|
|
args.repo.path,
|
|
args.worktreePath,
|
|
args.branchName,
|
|
sparseDirectories,
|
|
baseBranch,
|
|
args.settings.refreshLocalBaseRefOnWorktreeCreate,
|
|
addOptions
|
|
)) ?? {}
|
|
)
|
|
}
|
|
return (
|
|
(await addWorktree(
|
|
args.repo.path,
|
|
args.worktreePath,
|
|
args.branchName,
|
|
baseBranch,
|
|
args.settings.refreshLocalBaseRefOnWorktreeCreate,
|
|
false,
|
|
addOptions
|
|
)) ?? {}
|
|
)
|
|
})
|
|
} catch (error) {
|
|
if (shouldRetireGeneratedName && failedWorktreeCreationNeedsRetirement(error)) {
|
|
await retireGeneratedWorktreeName(
|
|
args.store as Parameters<typeof retireGeneratedWorktreeName>[0],
|
|
args.repo,
|
|
args.settings,
|
|
args.effectiveSanitizedName!
|
|
)
|
|
}
|
|
throw error
|
|
}
|
|
// Why: the worktree is listable from here on; scans that began before it appeared are stale.
|
|
runWorktreeChangeInvalidators(args.repo.id)
|
|
if (shouldRetireGeneratedName) {
|
|
await retireGeneratedWorktreeName(
|
|
args.store as Parameters<typeof retireGeneratedWorktreeName>[0],
|
|
args.repo,
|
|
args.settings,
|
|
args.effectiveSanitizedName!
|
|
)
|
|
}
|
|
// Why: `--set-upstream-to` requires the remote to already exist -- safe for a
|
|
// same-repo target (its remote, e.g. `origin`, always exists) but not for a
|
|
// deferred fork remote, which is materialized lazily at first push/pull/fetch.
|
|
const configuredPushTarget =
|
|
preparedPushTarget && !preparedPushTarget.remoteUrl
|
|
? await configureCreatedWorktreePushTarget(
|
|
args.worktreePath,
|
|
args.branchName,
|
|
preparedPushTarget,
|
|
args.localWorktreeGitOptions
|
|
)
|
|
: preparedPushTarget
|
|
const { created, worktrees, listingComplete } = await args.timing.time(
|
|
'list_created_worktree',
|
|
() =>
|
|
resolveCreatedWorktree(
|
|
args.repo.path,
|
|
args.worktreePath,
|
|
args.branchName,
|
|
args.localWorktreeGitOptions
|
|
)
|
|
)
|
|
if (listingComplete) {
|
|
args.timing.recordWorktreeCount(worktrees.length)
|
|
}
|
|
return {
|
|
sparseDirectories,
|
|
...(configuredPushTarget ? { configuredPushTarget } : {}),
|
|
created,
|
|
addResult
|
|
}
|
|
}
|