Files
orca/src/main/github/github-ssh-host-alias-resolution.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00

193 lines
6.1 KiB
TypeScript

import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types'
import { commandExecFileAsync } from '../git/runner'
import { getSshGitProvider, getSshGitProviderGeneration } from '../providers/ssh-git-dispatch'
import { parseWslPath } from '../wsl'
import { resolveWithSshG } from '../ssh/ssh-g-config-resolution'
import {
gitHubSshConfigHostAlias,
parseGitHubOwnerRepo,
parseGitHubOwnerRepoWithResolvedSshHostname
} from './github-remote-identity-parsing'
/** `indeterminate` means SSH alias expansion failed and must remain retryable. */
export type GitHubOwnerRepoResolution =
| { kind: 'github'; ownerRepo: GitHubOwnerRepo }
| { kind: 'not-github'; cacheWithGitConfigSignature: boolean }
| { kind: 'indeterminate' }
const SSH_HOSTNAME_CACHE_TTL_MS = 60_000
const SSH_HOSTNAME_FAILURE_CACHE_TTL_MS = 5_000
const SSH_HOSTNAME_CACHE_MAX = 256
const SSH_G_TIMEOUT_MS = 5_000
export type SshConfigResolutionContext = {
repoPath: string
connectionId?: string | null
wslDistro?: string
}
type SshHostnameCacheEntry = {
hostname: string | null
resolved: boolean
expiresAt: number
}
const sshHostnameCache = new Map<string, SshHostnameCacheEntry>()
const sshHostnameInFlight = new Map<string, Promise<SshHostnameCacheEntry>>()
/** @internal - tests only */
export function _resetSshHostnameResolutionCache(): void {
sshHostnameCache.clear()
sshHostnameInFlight.clear()
}
function pruneSshHostnameCache(now: number): void {
for (const [key, entry] of sshHostnameCache) {
if (entry.expiresAt <= now) {
sshHostnameCache.delete(key)
}
}
while (sshHostnameCache.size > SSH_HOSTNAME_CACHE_MAX) {
const oldest = sshHostnameCache.keys().next().value
if (oldest === undefined) {
return
}
sshHostnameCache.delete(oldest)
}
}
function sshRuntimeCacheKey(context: SshConfigResolutionContext): string {
if (context.connectionId) {
const generation = getSshGitProviderGeneration(context.connectionId)
return `ssh:${context.connectionId}:${generation}`
}
const distro = context.wslDistro ?? parseWslPath(context.repoPath)?.distro
return `local:${distro?.toLowerCase() ?? 'host'}`
}
function parseSshGHostname(stdout: string): string | null {
for (const line of stdout.split(/\r?\n/)) {
const match = line.match(/^hostname\s+(.+)$/i)
if (match?.[1].trim()) {
return match[1].trim()
}
}
return null
}
async function resolveSshHostnameInRuntime(
host: string,
context: SshConfigResolutionContext
): Promise<string | null> {
if (context.connectionId) {
const provider = getSshGitProvider(context.connectionId)
if (!provider) {
return null
}
try {
const result = await provider.execNonInteractive(
'ssh',
['-G', '--', host],
context.repoPath,
SSH_G_TIMEOUT_MS
)
return result.exitCode === 0 && !result.timedOut && !result.canceled
? parseSshGHostname(result.stdout)
: null
} catch {
return null
}
}
const wslDistro = context.wslDistro ?? parseWslPath(context.repoPath)?.distro
if (!wslDistro) {
return (await resolveWithSshG(host))?.hostname?.trim() || null
}
try {
const { stdout } = await commandExecFileAsync('ssh', ['-G', '--', host], {
cwd: context.repoPath,
timeout: SSH_G_TIMEOUT_MS,
wslDistro
})
return parseSshGHostname(stdout)
} catch {
return null
}
}
/** Resolve OpenSSH Host → HostName in the repository runtime. */
export async function resolveSshConfigHostname(
host: string,
context: SshConfigResolutionContext = { repoPath: '' }
): Promise<{
hostname: string | null
resolved: boolean
}> {
const cacheKey = `${sshRuntimeCacheKey(context)}\0${host}`
const now = Date.now()
pruneSshHostnameCache(now)
const cached = sshHostnameCache.get(cacheKey)
if (cached && cached.expiresAt > now) {
return { hostname: cached.hostname, resolved: cached.resolved }
}
const inFlight = sshHostnameInFlight.get(cacheKey)
if (inFlight) {
const entry = await inFlight
return { hostname: entry.hostname, resolved: entry.resolved }
}
const probe = (async (): Promise<SshHostnameCacheEntry> => {
const hostname = await resolveSshHostnameInRuntime(host, context)
const resolved = hostname != null && hostname.length > 0
const entry: SshHostnameCacheEntry = {
hostname: resolved ? hostname : null,
resolved,
expiresAt:
Date.now() + (resolved ? SSH_HOSTNAME_CACHE_TTL_MS : SSH_HOSTNAME_FAILURE_CACHE_TTL_MS)
}
sshHostnameCache.set(cacheKey, entry)
pruneSshHostnameCache(Date.now())
return entry
})()
sshHostnameInFlight.set(cacheKey, probe)
try {
const entry = await probe
return { hostname: entry.hostname, resolved: entry.resolved }
} finally {
if (sshHostnameInFlight.get(cacheKey) === probe) {
sshHostnameInFlight.delete(cacheKey)
}
}
}
/** Resolve github.com identity without rewriting the Git transport URL. */
export async function classifyGitHubOwnerRepoFromRemoteUrl(
remoteUrl: string,
context: SshConfigResolutionContext = { repoPath: '' }
): Promise<GitHubOwnerRepoResolution> {
const direct = parseGitHubOwnerRepo(remoteUrl)
if (direct) {
return { kind: 'github', ownerRepo: direct }
}
const aliasHost = gitHubSshConfigHostAlias(remoteUrl)
if (!aliasHost) {
return { kind: 'not-github', cacheWithGitConfigSignature: true }
}
const { hostname, resolved } = await resolveSshConfigHostname(aliasHost, context)
if (!resolved || !hostname) {
return { kind: 'indeterminate' }
}
const ownerRepo = parseGitHubOwnerRepoWithResolvedSshHostname(remoteUrl, hostname)
return ownerRepo
? { kind: 'github', ownerRepo }
: { kind: 'not-github', cacheWithGitConfigSignature: false }
}
/** Convenience wrapper for callers that only need owner/repo or null. */
export async function resolveGitHubOwnerRepoFromRemoteUrl(
remoteUrl: string,
context: SshConfigResolutionContext = { repoPath: '' }
): Promise<GitHubOwnerRepo | null> {
const result = await classifyGitHubOwnerRepoFromRemoteUrl(remoteUrl, context)
return result.kind === 'github' ? result.ownerRepo : null
}