mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
`RuntimeGitTarget` carried `connectionId?: string` and no host id, so `undefined` spelled three different answers at once — "runtime: host", "unresolved", and "genuinely local". Its sole resolver read `store.getRepo(worktree.repoId)?.connectionId` and never looked at `worktree.hostId`, which outranks every repo row, so one arbitrarily chosen row decided the execution host for 36 downstream dispatches. The target now carries `executionHostId: ExecutionHostId` (never null, never optional), resolved through the shared rule that landed with #17909/#17919 and dispatched through the host-keyed routes from #18296. Dispatch sites call `requireRuntimeGitProvider`, where `null` means exactly one thing: the host is `local` and the command runs here as free functions. Four answers that used to collapse into one: - `ssh:x` with a rival row on `ssh:y` — routes to x. Previously the first row won, which is the reproduced cross-host leak. - `local` with a surviving `connectionId` — a row contradicting itself; no SSH connection is handed out. - `runtime:<env>` — throws `ExecutionHostNotDispatchableError`. Its repo row's connection names a target in the *server's* namespace; dialling it here reaches a same-named target on this client. - rival rows disagreeing with no worktree host — `worktree_execution_host_unresolved`, matching the launch path rather than guessing a row. An unreachable SSH host still throws `SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE`; loss of contact is never evidence of locality (docs/reference/ssh-execution-boundary.md). `resolveWorktreeLaunchHost` keeps its exact signature and now delegates to `resolveWorktreeHostRouting`, the same resolution answering "which host is this on" rather than "what may this client dial" — the git target needs the first question because `local` and `runtime:` are two different non-SSH answers. No wire change: `RuntimeGitTarget` is main-process internal, and the SSH and local model-discovery host keys are byte-identical to before. `RuntimeFileTarget` has the same defect in ~30 filesystem dispatches and is deliberately left for a follow-up.
251 lines
9.3 KiB
TypeScript
251 lines
9.3 KiB
TypeScript
import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync'
|
|
import type { GitUpstreamStatus } from '../../shared/git-status-types'
|
|
import type { GitPushTarget } from '../../shared/worktree/types'
|
|
import { gitSyncForkDefaultBranch } from '../git/fork-sync'
|
|
import { gitFastForward, gitFetch, gitPull, gitPullRebaseFromBase, gitPush } from '../git/remote'
|
|
import { abortMerge, abortRebase, commitChanges } from '../git/status'
|
|
import { getUpstreamStatus } from '../git/upstream'
|
|
import {
|
|
materializeWorktreePushTargetRemote,
|
|
materializeWorktreePushTargetRemoteSsh
|
|
} from '../ipc/worktree-remote'
|
|
import {
|
|
localGitOptionsForTarget,
|
|
requireRuntimeGitProvider,
|
|
type RuntimeGitCommandHost,
|
|
type RuntimeGitTarget
|
|
} from './runtime-git-command-target'
|
|
|
|
export class RuntimeGitSyncCommands {
|
|
constructor(private readonly host: RuntimeGitCommandHost) {}
|
|
|
|
// Why (#17828 review follow-up): this class deliberately materializes with no store (see
|
|
// the `undefined` args below) to avoid unrelated ownership-inheritance/refspec-migration
|
|
// side effects on the RPC path -- so persistence goes through the host callback instead,
|
|
// using `target.worktree.id` already resolved here rather than threading a store through.
|
|
private persistMaterializedPushTargetIfCreated(
|
|
target: RuntimeGitTarget,
|
|
materialized: GitPushTarget | undefined
|
|
): void {
|
|
if (materialized?.remoteCreated) {
|
|
this.host.persistMaterializedPushTarget?.(target.worktree.id, materialized)
|
|
}
|
|
}
|
|
|
|
async abortRuntimeGitMerge(worktreeSelector: string): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
await provider.abortMerge(target.worktree.path)
|
|
return { ok: true }
|
|
}
|
|
await abortMerge(target.worktree.path, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async abortRuntimeGitRebase(worktreeSelector: string): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
await provider.abortRebase(target.worktree.path)
|
|
return { ok: true }
|
|
}
|
|
await abortRebase(target.worktree.path, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async getRuntimeGitUpstreamStatus(
|
|
worktreeSelector: string,
|
|
pushTarget?: GitPushTarget
|
|
): Promise<GitUpstreamStatus> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
return provider.getUpstreamStatus(target.worktree.path, pushTarget)
|
|
}
|
|
return getUpstreamStatus(target.worktree.path, pushTarget, localGitOptionsForTarget(target))
|
|
}
|
|
|
|
async fetchRuntimeGit(
|
|
worktreeSelector: string,
|
|
pushTarget?: GitPushTarget
|
|
): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemoteSsh(provider, target.worktree.path, pushTarget)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await provider.fetchRemote(target.worktree.path, materializedPushTarget)
|
|
return { ok: true }
|
|
}
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemote(
|
|
target.worktree.path,
|
|
pushTarget,
|
|
undefined,
|
|
target.repo?.id,
|
|
localGitOptionsForTarget(target)
|
|
)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await gitFetch(target.worktree.path, materializedPushTarget, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async syncRuntimeGitForkDefaultBranch(
|
|
worktreeSelector: string,
|
|
expectedUpstream: GitForkSyncExpectedUpstream
|
|
): Promise<GitForkSyncResult> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
return provider.syncForkDefaultBranch(target.worktree.path, expectedUpstream)
|
|
}
|
|
return gitSyncForkDefaultBranch(target.worktree.path, expectedUpstream, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
}
|
|
|
|
async pullRuntimeGit(
|
|
worktreeSelector: string,
|
|
pushTarget?: GitPushTarget
|
|
): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemoteSsh(provider, target.worktree.path, pushTarget)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await provider.pullBranch(target.worktree.path, materializedPushTarget)
|
|
return { ok: true }
|
|
}
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemote(
|
|
target.worktree.path,
|
|
pushTarget,
|
|
undefined,
|
|
target.repo?.id,
|
|
localGitOptionsForTarget(target)
|
|
)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await gitPull(target.worktree.path, materializedPushTarget, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async fastForwardRuntimeGit(
|
|
worktreeSelector: string,
|
|
pushTarget?: GitPushTarget
|
|
): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemoteSsh(provider, target.worktree.path, pushTarget)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await provider.fastForwardBranch(target.worktree.path, materializedPushTarget)
|
|
return { ok: true }
|
|
}
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemote(
|
|
target.worktree.path,
|
|
pushTarget,
|
|
undefined,
|
|
target.repo?.id,
|
|
localGitOptionsForTarget(target)
|
|
)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await gitFastForward(target.worktree.path, materializedPushTarget, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async rebaseRuntimeGitFromBase(worktreeSelector: string, baseRef: string): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
await provider.rebaseFromBase(target.worktree.path, baseRef)
|
|
return { ok: true }
|
|
}
|
|
await gitPullRebaseFromBase(target.worktree.path, baseRef, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async pushRuntimeGit(
|
|
worktreeSelector: string,
|
|
publish?: boolean,
|
|
pushTarget?: GitPushTarget,
|
|
forceWithLease?: boolean
|
|
): Promise<{ ok: true }> {
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemoteSsh(provider, target.worktree.path, pushTarget)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await provider.pushBranch(target.worktree.path, publish === true, materializedPushTarget, {
|
|
forceWithLease: forceWithLease === true
|
|
})
|
|
return { ok: true }
|
|
}
|
|
const materializedPushTarget = pushTarget
|
|
? await materializeWorktreePushTargetRemote(
|
|
target.worktree.path,
|
|
pushTarget,
|
|
undefined,
|
|
target.repo?.id,
|
|
localGitOptionsForTarget(target)
|
|
)
|
|
: undefined
|
|
this.persistMaterializedPushTargetIfCreated(target, materializedPushTarget)
|
|
await gitPush(target.worktree.path, publish === true, materializedPushTarget, {
|
|
forceWithLease: forceWithLease === true,
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
return { ok: true }
|
|
}
|
|
|
|
async commitRuntimeGit(
|
|
worktreeSelector: string,
|
|
message: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
if (message.trim().length === 0) {
|
|
throw new Error('Commit message is required')
|
|
}
|
|
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
|
|
const provider = requireRuntimeGitProvider(target)
|
|
if (provider) {
|
|
return provider.commit(target.worktree.path, message)
|
|
}
|
|
return commitChanges(target.worktree.path, message, {
|
|
...localGitOptionsForTarget(target),
|
|
admissionTier: 'interactive'
|
|
})
|
|
}
|
|
}
|