mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`detectRepoIcon`, `detectRepoIconAndUpstream`, `detectGitHubAvatarIcon`, `detectRepoFileIcon` and `probeGitRemoteIdentity` took a `connectionId`-shaped parameter threaded down from their callers. That shape spells "runtime host", "unresolved" and "genuinely local" all as one falsy value, and because it is a *parameter* each caller decided independently what to pass — a wrong answer was invisible at the boundary. Replace it with a required `ExecutionHostId` and route through #18296's `resolveGitRouteForHost` / `resolveFilesystemRouteForHost`. The parameter is removed rather than added beside, so every caller became a compile error. No new resolver, no wire change: nothing these modules return carries a host id. Fixed at the call sites: - `repo-git-remote-identity-enrichment` read `repo.connectionId` raw, so a row minted with only `executionHostId: ssh:<t>` ran `git remote -v` against this machine's copy of the path (#11163), and a `runtime:` row handed its *nested* SSH target to this client's dispatch table — a same-named box of ours. - Its location key had the same collapse, so two rows at one path on different hosts shared a probe, an abort controller and a backoff deadline. - `runtime-repository-fork-backfill` guarded on `repo.connectionId`, so an `executionHostId`-only SSH row had its upstream read off the client. `runtime:` is refused inside the modules (this process does not execute another environment's git or filesystem), but store-backed callers ask `getSshTargetIdForExecutionHost` — "what may this client dial" — so a `runtime:` row keeps the probe this process has always run for it. Registering and cloning stay `local` on purpose: those controllers do the filesystem work here, whatever host id is stamped on the row (see `assertCloneHostIsSupported`).
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { mkdir } from 'node:fs/promises'
|
|
import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants'
|
|
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/execution-host'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import { getGitCloneFailureMessage } from '../../shared/git-clone-failure-message'
|
|
import {
|
|
cleanupClaimedCloneTarget,
|
|
claimCloneTarget,
|
|
deriveValidatedClonePath,
|
|
getClonePathComparisonKey
|
|
} from '../git/repo-clone-path'
|
|
import { gitSpawnAfterWindowsEnvironmentReady, nonInteractiveGitEnv } from '../git/runner'
|
|
import { runWithGitReadCacheInvalidation } from '../git/status'
|
|
import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth'
|
|
import { isFolderRepo } from '../../shared/repo-kind'
|
|
import { detectRepoIconAndUpstream } from '../repo-icon-autodetect'
|
|
import { getRepoName } from '../git/repo'
|
|
import { prepareLocalWorktreeRootForRepo } from '../worktree-root-preparation'
|
|
import type { RuntimeStore } from './runtime-store-contract'
|
|
import { runtimeRepoMatchesExecutionHost } from './runtime-worktree-selection'
|
|
|
|
type RuntimeRepositoryCloneDependencies = {
|
|
getStore: () => RuntimeStore | null
|
|
invalidateResolvedWorktrees: () => void
|
|
invalidateWorktreeScan: (repoId: string) => void
|
|
notifyReposChanged: () => void
|
|
}
|
|
|
|
export class RuntimeRepositoryCloneController {
|
|
private readonly inFlightByPath = new Map<string, Promise<void>>()
|
|
|
|
constructor(private readonly deps: RuntimeRepositoryCloneDependencies) {}
|
|
|
|
async clone(
|
|
url: string,
|
|
destination: string,
|
|
executionHostId?: ExecutionHostId | null
|
|
): Promise<Repo> {
|
|
if (!this.deps.getStore()) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
const trimmedUrl = url.trim()
|
|
const trimmedDestination = destination.trim()
|
|
if (!trimmedDestination) {
|
|
throw new Error('Clone destination is required')
|
|
}
|
|
const clonePath = deriveValidatedClonePath({ url: trimmedUrl, destination: trimmedDestination })
|
|
const clonePathKey = getClonePathComparisonKey(clonePath)
|
|
const previous = this.inFlightByPath.get(clonePathKey) ?? Promise.resolve()
|
|
let release!: () => void
|
|
const current = new Promise<void>((resolve) => {
|
|
release = resolve
|
|
})
|
|
const tail = previous.then(
|
|
() => current,
|
|
() => current
|
|
)
|
|
this.inFlightByPath.set(clonePathKey, tail)
|
|
try {
|
|
await previous
|
|
return await runWithGitReadCacheInvalidation(() =>
|
|
this.cloneAfterPathLock(
|
|
trimmedUrl,
|
|
trimmedDestination,
|
|
clonePath,
|
|
clonePathKey,
|
|
executionHostId
|
|
)
|
|
)
|
|
} finally {
|
|
release()
|
|
if (this.inFlightByPath.get(clonePathKey) === tail) {
|
|
this.inFlightByPath.delete(clonePathKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async cloneAfterPathLock(
|
|
trimmedUrl: string,
|
|
trimmedDestination: string,
|
|
clonePath: string,
|
|
clonePathKey: string,
|
|
executionHostId?: ExecutionHostId | null
|
|
): Promise<Repo> {
|
|
const store = this.deps.getStore()
|
|
if (!store) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
const existingBeforeClone = store.getRepos().find((repo) => {
|
|
return (
|
|
getClonePathComparisonKey(repo.path) === clonePathKey &&
|
|
runtimeRepoMatchesExecutionHost(repo, executionHostId)
|
|
)
|
|
})
|
|
if (existingBeforeClone && !isFolderRepo(existingBeforeClone)) {
|
|
return existingBeforeClone
|
|
}
|
|
|
|
await mkdir(trimmedDestination, { recursive: true })
|
|
const claimedTarget = await claimCloneTarget(clonePath)
|
|
let proc: Awaited<ReturnType<typeof gitSpawnAfterWindowsEnvironmentReady>>
|
|
try {
|
|
proc = await gitSpawnAfterWindowsEnvironmentReady(
|
|
['clone', '--progress', '--', trimmedUrl, clonePath],
|
|
{
|
|
cwd: trimmedDestination,
|
|
admissionTier: 'interactive',
|
|
env: nonInteractiveGitEnv(),
|
|
stdio: ['ignore', 'ignore', 'pipe']
|
|
}
|
|
)
|
|
} catch (error) {
|
|
await cleanupClaimedCloneTarget(clonePath, claimedTarget)
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
throw new Error(`Clone failed: ${message}`)
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
let stderrTail = ''
|
|
let settled = false
|
|
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
stderrTail = (stderrTail + chunk.toString()).slice(-4096)
|
|
})
|
|
const finish = async (code: number | null, signal: NodeJS.Signals | null, error?: Error) => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
if (error || code !== 0 || signal) {
|
|
await cleanupClaimedCloneTarget(clonePath, claimedTarget)
|
|
}
|
|
if (error) {
|
|
reject(new Error(`Clone failed: ${error.message}`))
|
|
} else if (signal === 'SIGTERM') {
|
|
reject(new Error('Clone aborted'))
|
|
} else if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`Clone failed: ${getGitCloneFailureMessage(stderrTail, { clonePath })}`))
|
|
}
|
|
}
|
|
proc.on('error', (error) => void finish(null, null, error))
|
|
proc.on('close', (code, signal) => void finish(code, signal))
|
|
})
|
|
|
|
const existing = store.getRepos().find((repo) => {
|
|
return (
|
|
getClonePathComparisonKey(repo.path) === clonePathKey &&
|
|
runtimeRepoMatchesExecutionHost(repo, executionHostId)
|
|
)
|
|
})
|
|
if (existing) {
|
|
if (isFolderRepo(existing)) {
|
|
const updated = store.updateRepo(existing.id, { kind: 'git' })
|
|
if (updated) {
|
|
await prepareLocalWorktreeRootForRepo(store, updated)
|
|
invalidateAuthorizedRootsCache()
|
|
this.invalidate(updated.id)
|
|
return updated
|
|
}
|
|
}
|
|
return existing
|
|
}
|
|
// `cloneRepo` ran `git clone` in this process (see `assertCloneHostIsSupported`), so the
|
|
// checkout is here regardless of the host id stamped on the row.
|
|
const detected = await detectRepoIconAndUpstream({
|
|
repoPath: clonePath,
|
|
kind: 'git',
|
|
executionHostId: LOCAL_EXECUTION_HOST_ID
|
|
})
|
|
const repo: Repo = {
|
|
id: randomUUID(),
|
|
path: clonePath,
|
|
displayName: getRepoName(clonePath),
|
|
badgeColor: DEFAULT_REPO_BADGE_COLOR,
|
|
...(executionHostId != null ? { executionHostId } : {}),
|
|
...detected,
|
|
addedAt: Date.now(),
|
|
kind: 'git',
|
|
externalWorktreeVisibilityLegacy: false
|
|
}
|
|
store.addRepo(repo)
|
|
await prepareLocalWorktreeRootForRepo(store, repo)
|
|
invalidateAuthorizedRootsCache()
|
|
this.invalidate(repo.id)
|
|
return store.getRepo(repo.id) ?? repo
|
|
}
|
|
|
|
private invalidate(repoId: string): void {
|
|
this.deps.invalidateResolvedWorktrees()
|
|
this.deps.invalidateWorktreeScan(repoId)
|
|
this.deps.notifyReposChanged()
|
|
}
|
|
}
|