mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
#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.
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import type { GitLabProjectRef } from '../../shared/gitlab-types'
|
|
|
|
export type ProjectRef = GitLabProjectRef
|
|
|
|
/**
|
|
* Hosts always treated as GitLab. Self-hosted instances are added at
|
|
* runtime via `getGlabKnownHosts()`, which inspects `glab auth status`.
|
|
*/
|
|
export const DEFAULT_GITLAB_HOSTS = ['gitlab.com'] as const
|
|
|
|
export function normalizeGitLabHost(value: string): string {
|
|
return value.trim().toLowerCase()
|
|
}
|
|
|
|
// Why: host recognition is port-aware so two services on the same hostname
|
|
// but different ports (e.g. a GitLab on :8080 and a Gitea on :3030) are not
|
|
// conflated. The hostname (port-less) part is kept for legacy known-host
|
|
// entries that were recorded without a port.
|
|
function hostnameOf(host: string): string {
|
|
// `host` may be `name` or `name:port`. Strip a trailing `:digits` port.
|
|
return host.replace(/:\d+$/, '')
|
|
}
|
|
|
|
function stripGitSuffix(path: string): string {
|
|
return path.replace(/\/+$/, '').replace(/\.git$/i, '')
|
|
}
|
|
|
|
// Why: the GitLab host identity is the web/API endpoint, which is what `glab
|
|
// --hostname` and the known-hosts list speak in terms of. For http(s)
|
|
// remotes the URL port IS that endpoint port (e.g. self-hosted on :8080),
|
|
// so it must be kept. For ssh/git remotes the port is a transport port
|
|
// (e.g. ssh on :2222) that does not identify the GitLab instance, so it is
|
|
// dropped and only the hostname is used.
|
|
function hostIdentityFromUrl(url: URL): string {
|
|
const protocol = url.protocol.toLowerCase()
|
|
if (protocol === 'http:' || protocol === 'https:') {
|
|
return url.host
|
|
}
|
|
return url.hostname
|
|
}
|
|
|
|
function makeProjectRefForTrustedHost(host: string, path: string): ProjectRef | null {
|
|
const normalizedHost = normalizeGitLabHost(host)
|
|
const normalizedPath = stripGitSuffix(path.replace(/^\/+/, '')).trim()
|
|
// Reject paths without at least one group segment — `gitlab.com:foo`
|
|
// alone is not a project reference.
|
|
if (!normalizedPath.includes('/')) {
|
|
return null
|
|
}
|
|
return { host: normalizedHost, path: normalizedPath }
|
|
}
|
|
|
|
/**
|
|
* Does `urlHost` (which may include a `:port`) match a known-host entry?
|
|
* - An exact match (including any port) always counts.
|
|
* - A known entry WITHOUT a port also matches a URL host on the same
|
|
* hostname regardless of the URL's port — this preserves recognition for
|
|
* legacy `gitlab.com` / bare-hostname known entries.
|
|
* - A known entry WITH a port only matches a URL host with the exact same
|
|
* port, so `gitlab.example.com:8443` does not accept a
|
|
* `gitea.example.com:3000` (or same-host different-port) remote.
|
|
*/
|
|
function knownHostMatches(urlHost: string, knownHost: string): boolean {
|
|
if (urlHost === knownHost) {
|
|
return true
|
|
}
|
|
if (hostnameOf(knownHost) === knownHost) {
|
|
// Known entry has no port — match on hostname alone.
|
|
return hostnameOf(urlHost) === knownHost
|
|
}
|
|
return false
|
|
}
|
|
|
|
function makeProjectRef(
|
|
host: string,
|
|
path: string,
|
|
knownHosts: readonly string[]
|
|
): ProjectRef | null {
|
|
const normalizedHost = normalizeGitLabHost(host)
|
|
const normalizedKnownHosts = knownHosts.map(normalizeGitLabHost)
|
|
if (!normalizedKnownHosts.some((knownHost) => knownHostMatches(normalizedHost, knownHost))) {
|
|
return null
|
|
}
|
|
return makeProjectRefForTrustedHost(normalizedHost, path)
|
|
}
|
|
|
|
export function parseRemoteProjectRefCandidate(remoteUrl: string): ProjectRef | null {
|
|
const trimmed = remoteUrl.trim()
|
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
const scpLike = trimmed.match(/^(?:[^@/:]+@)?([^:\s/]+):([^\s]+?)(?:\.git)?$/)
|
|
if (scpLike) {
|
|
return makeProjectRefForTrustedHost(scpLike[1], scpLike[2])
|
|
}
|
|
}
|
|
|
|
try {
|
|
const url = new URL(trimmed)
|
|
if (!['http:', 'https:', 'ssh:', 'git:', 'git+ssh:'].includes(url.protocol.toLowerCase())) {
|
|
return null
|
|
}
|
|
return makeProjectRefForTrustedHost(hostIdentityFromUrl(url), url.pathname)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function parseGitLabProjectRef(
|
|
remoteUrl: string,
|
|
knownHosts: readonly string[] = DEFAULT_GITLAB_HOSTS
|
|
): ProjectRef | null {
|
|
const trimmed = remoteUrl.trim()
|
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
const scpLike = trimmed.match(/^(?:[^@/:]+@)?([^:\s/]+):([^\s]+?)(?:\.git)?$/)
|
|
if (scpLike) {
|
|
return makeProjectRef(scpLike[1], scpLike[2], knownHosts)
|
|
}
|
|
}
|
|
|
|
try {
|
|
const url = new URL(trimmed)
|
|
if (!['http:', 'https:', 'ssh:', 'git:', 'git+ssh:'].includes(url.protocol.toLowerCase())) {
|
|
return null
|
|
}
|
|
return makeProjectRef(hostIdentityFromUrl(url), url.pathname, knownHosts)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|