Files
orca/src/main/runtime/runtime-worktree-selection.ts
T
Neil 2d41a26a38 fix(runtime): new local projects never dedupe onto an SSH project at the same path (#31055)
* fix(runtime): project registration dedupes only against rows on this machine

Codex review 6 #7: create() matched paths across hosts, so creating a local
project at a path an SSH project used returned the SSH row and made no
directory. add() with no host had the same gap. One predicate,
runtimeRepoIsRegistrationCandidate, now backs add, create and clone.

* test(runtime): cover nested-SSH rows and clone reuse in host-qualified dedupe

Review follow-up (Opus P3s): runtime:<env> rows carrying a nested SSH
connectionId, add() for a runtime host, the clone controller, and a stricter
create() assertion.

* test(runtime): clone dedupe fixture commits without signing
2026-10-20 01:59:18 -07:00

108 lines
4.1 KiB
TypeScript

import type { Repo } from '../../shared/repo-types'
import {
getRepoExecutionHostId,
getRepoSshConnectionId,
type ExecutionHostId
} from '../../shared/execution-host'
import { splitWorktreeId } from '../../shared/worktree/id'
import type { GitPushTarget } from '../../shared/worktree/types'
export type RuntimeWorktreeRemovalTarget = {
id: string
repoId: string
path: string
pushTarget?: GitPushTarget
}
/**
* Options for `removeManagedWorktree`. Named rather than positional on purpose: three of the
* four are interchangeable booleans that each waive a different safety check on a destructive
* delete, so a transposition would silently delete a checkout the caller meant to protect.
*/
export type RemoveManagedWorktreeOptions = {
force?: boolean
runHooks?: boolean
/** Waives proof that every PTY stopped (#11960). Set by explicit Force Delete only. */
allowUnverifiedPtyStop?: boolean
/** Waives a FAILED archive hook (#19334). Never implied by `force`, never by `runHooks`. */
allowFailedArchiveHook?: boolean
hostId?: string
/** Reply with the finished delete's result; otherwise a background delete replies on acceptance. */
waitForBackgroundRemoval?: boolean
}
export function getRuntimeWorktreeRemovalOptionsKey(
options: Pick<
RemoveManagedWorktreeOptions,
'force' | 'runHooks' | 'allowUnverifiedPtyStop' | 'allowFailedArchiveHook'
>
): string {
// Why: a forced retry must not coalesce onto the in-flight attempt that just
// failed the PTY gate — it would inherit that failure instead of retrying.
const ptyKey = options.allowUnverifiedPtyStop ? 'allow-unverified-pty' : 'require-pty-stop'
// Same reason for the archive waiver: a retry that waives the failed hook must not coalesce
// onto the in-flight attempt that is about to refuse on it.
const archiveKey = options.allowFailedArchiveHook ? 'allow-failed-archive' : 'require-archive'
const hooksKey = options.runHooks ? 'run-hooks' : 'skip-hooks'
return `${options.force ? 'force' : 'normal'}:${hooksKey}:${ptyKey}:${archiveKey}`
}
// Null executionHostId means host-unaware: path-only callers match any repo, and the first runtime
// host can adopt a legacy (unstamped) repo. A repo that names a host in *either* spelling matches
// only that host — including its own ssh:<connectionId>, which an executionHostId-only comparison
// used to reject, so an unstamped SSH repo failed to dedupe against itself.
export function runtimeRepoMatchesExecutionHost(
repo: Pick<Repo, 'connectionId' | 'executionHostId'>,
executionHostId?: ExecutionHostId | null
): boolean {
if (executionHostId == null) {
return true
}
if (repo.executionHostId == null && repo.connectionId == null) {
return true
}
return getRepoExecutionHostId(repo) === executionHostId
}
/**
* Whether `repo` can be the row a registration in this process dedupes onto. Registration reads its
* path on this machine, so an SSH row at the same path string is another machine's project.
*/
export function runtimeRepoIsRegistrationCandidate(
repo: Pick<Repo, 'connectionId' | 'executionHostId'>,
executionHostId?: ExecutionHostId | null
): boolean {
return runtimeRepoMatchesExecutionHost(repo, executionHostId) && !getRepoSshConnectionId(repo)
}
export function parseExactWorktreeIdSelector(
selector: string
): RuntimeWorktreeRemovalTarget | null {
const worktreeId = selector.startsWith('id:') ? selector.slice(3) : selector
const parsed = splitWorktreeId(worktreeId)
if (!parsed || !parsed.repoId || !parsed.worktreePath) {
return null
}
return {
id: worktreeId,
repoId: parsed.repoId,
path: parsed.worktreePath
}
}
export function normalizeLocalBranchName(branchName: string | undefined): string {
return branchName?.replace(/^refs\/heads\//, '') ?? ''
}
export function getExplicitWorktreeIdSelector(selector: string | undefined): string | null {
if (!selector?.startsWith('id:')) {
return null
}
const id = selector.slice(3)
return id.length > 0 ? id : null
}
export function hasLocalGitOptions(gitOptions: { wslDistro?: string }): boolean {
return Object.keys(gitOptions).length > 0
}