Files
orca/src/shared/folder-workspace-execution-host.ts
T

140 lines
4.9 KiB
TypeScript

/**
* Resolves which host a folder workspace executes on.
*
* Main and the renderer both have to answer this — the list projects a record's
* host from it, dispatch targets one from it — so the answer lives here rather
* than being re-derived per side and drifting. The workspace's own
* `executionHostId` pin wins: a pinned workspace runs there whatever its repos
* say, which is exactly the case that used to read as local.
*
* `ambiguous` is a distinct answer, not a missing one: a scope that spans a
* local repo and an SSH one has no single host, and callers must fail closed
* with something visible rather than defaulting to local.
*/
import type { FolderWorkspace } from './folder-workspace-types'
import type { ProjectGroup } from './project-group-types'
import type { Repo } from './repo-types'
import { isPathInsideOrEqual } from './cross-platform-path'
import { getProjectGroupSubtreeIds } from './project-groups'
import { parseExecutionHostId } from './execution-host'
export type FolderWorkspaceHostState = {
folderWorkspaces: readonly FolderWorkspace[]
projectGroups: readonly ProjectGroup[]
repos: readonly Repo[]
}
export type FolderWorkspaceHost =
| { kind: 'missing' }
| { kind: 'local' }
| { kind: 'ssh'; targetId: string }
| { kind: 'ambiguous' }
/** Reads a stored connection id: blank is local, since no write path normalizes it. */
export function normalizeConnectionId(value: string | null | undefined): string | null {
return value?.trim() || null
}
function getFolderScopeCandidateRepos(args: {
folderPath: string
projectGroupId: string
connectionId: string | null
projectGroups: readonly ProjectGroup[]
repos: readonly Repo[]
}): Repo[] {
const groupIds = getProjectGroupSubtreeIds(args.projectGroups, args.projectGroupId)
// Classify each repo once. The previous pair of filters read every
// projectGroupId twice before applying the same path predicate.
const groupRepos: Repo[] = []
const pathRepos: Repo[] = []
for (const repo of args.repos) {
const projectGroupId = repo.projectGroupId
if (typeof projectGroupId === 'string' && groupIds.has(projectGroupId)) {
groupRepos.push(repo)
} else if (isPathInsideOrEqual(args.folderPath, repo.path)) {
pathRepos.push(repo)
}
}
if (args.connectionId) {
return [
...groupRepos,
...pathRepos.filter((repo) => normalizeConnectionId(repo.connectionId) === args.connectionId)
]
}
if (groupRepos.length === 0) {
return pathRepos
}
const groupConnectionIds = new Set(
groupRepos.map((repo) => normalizeConnectionId(repo.connectionId))
)
return [
...groupRepos,
...pathRepos.filter((repo) => groupConnectionIds.has(normalizeConnectionId(repo.connectionId)))
]
}
export function findFolderWorkspaceCandidateRepos(
state: FolderWorkspaceHostState,
folderWorkspaceId: string
): Repo[] {
const workspace = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
if (!workspace) {
return []
}
const group = state.projectGroups.find((entry) => entry.id === workspace.projectGroupId)
return getFolderScopeCandidateRepos({
folderPath: workspace.folderPath,
projectGroupId: workspace.projectGroupId,
connectionId: normalizeConnectionId(workspace.connectionId ?? group?.connectionId),
projectGroups: state.projectGroups,
repos: state.repos
})
}
export function resolveFolderWorkspaceHost(
state: FolderWorkspaceHostState,
folderWorkspaceId: string
): FolderWorkspaceHost {
const workspace = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
if (!workspace) {
return { kind: 'missing' }
}
const explicitHost = parseExecutionHostId(workspace.executionHostId)
if (explicitHost) {
return explicitHost.kind === 'ssh'
? { kind: 'ssh', targetId: explicitHost.targetId }
: { kind: 'local' }
}
const scopeConnectionId = normalizeConnectionId(
workspace.connectionId ??
state.projectGroups.find((entry) => entry.id === workspace.projectGroupId)?.connectionId
)
const candidateRepos = findFolderWorkspaceCandidateRepos(state, folderWorkspaceId)
let hasLocalRepo = false
const connectionIds = new Set<string>()
for (const repo of candidateRepos) {
const connectionId = normalizeConnectionId(repo.connectionId)
if (connectionId) {
connectionIds.add(connectionId)
} else {
hasLocalRepo = true
}
}
if (scopeConnectionId) {
const hasDifferentSshConnection = [...connectionIds].some(
(connectionId) => connectionId !== scopeConnectionId
)
return hasLocalRepo || hasDifferentSshConnection
? { kind: 'ambiguous' }
: { kind: 'ssh', targetId: scopeConnectionId }
}
if (candidateRepos.length === 0 || connectionIds.size === 0) {
return { kind: 'local' }
}
if (hasLocalRepo || connectionIds.size > 1) {
return { kind: 'ambiguous' }
}
return { kind: 'ssh', targetId: [...connectionIds][0] }
}