refactor: share github remote parsing (#843)
This commit is contained in:
@@ -1,49 +1,32 @@
|
||||
import {
|
||||
isGitHubHost,
|
||||
normalizeHost,
|
||||
parseGitHubRemoteIdentity,
|
||||
parseGitRemoteLocation,
|
||||
type GitHubRemoteIdentity as ResolvedGitHubRemoteIdentity,
|
||||
} from "../shared/git-remote.js";
|
||||
import { findExecutable } from "./executable.js";
|
||||
import { execCommand } from "./spawn.js";
|
||||
|
||||
const GITHUB_HOSTS = new Set(["github.com", "ssh.github.com"]);
|
||||
|
||||
const TRANSPORT_BY_PROTOCOL: Record<string, GitRemoteLocation["transport"]> = {
|
||||
"https:": "https",
|
||||
"http:": "http",
|
||||
"ssh:": "ssh",
|
||||
};
|
||||
|
||||
let sshExecutableLookup: Promise<string | null> | null = null;
|
||||
const sshHostnameResolutionCache = new Map<string, Promise<string | null>>();
|
||||
|
||||
interface GitRemoteLocation {
|
||||
transport: "scp" | "ssh" | "http" | "https";
|
||||
host: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface GitHubRemoteIdentity {
|
||||
owner: string;
|
||||
name: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export type SshHostnameResolver = (host: string) => Promise<string | null>;
|
||||
|
||||
export function parseGitHubRemoteUrl(remoteUrl: string): GitHubRemoteIdentity | null {
|
||||
const location = parseGitRemoteLocation(remoteUrl);
|
||||
if (!location || !GITHUB_HOSTS.has(location.host)) return null;
|
||||
return parseGitHubRemoteIdentity(location.path);
|
||||
}
|
||||
export { parseGitHubRemoteUrl, type GitHubRemoteIdentity } from "../shared/git-remote.js";
|
||||
|
||||
export async function resolveGitHubRemote(input: {
|
||||
remoteUrl: string;
|
||||
resolveSshHostname?: SshHostnameResolver;
|
||||
}): Promise<GitHubRemoteIdentity | null> {
|
||||
}): Promise<ResolvedGitHubRemoteIdentity | null> {
|
||||
const location = parseGitRemoteLocation(input.remoteUrl);
|
||||
if (!location) return null;
|
||||
if (GITHUB_HOSTS.has(location.host)) return parseGitHubRemoteIdentity(location.path);
|
||||
if (isGitHubHost(location.host)) return parseGitHubRemoteIdentity(location.path);
|
||||
if (location.transport !== "scp" && location.transport !== "ssh") return null;
|
||||
|
||||
const resolve = input.resolveSshHostname ?? resolveSshHostname;
|
||||
const resolvedHost = await resolve(location.host);
|
||||
if (!resolvedHost || !GITHUB_HOSTS.has(resolvedHost)) return null;
|
||||
if (!resolvedHost || !isGitHubHost(resolvedHost)) return null;
|
||||
return parseGitHubRemoteIdentity(location.path);
|
||||
}
|
||||
|
||||
@@ -86,60 +69,3 @@ function parseSshHostname(stdout: string): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseGitRemoteLocation(remoteUrl: string): GitRemoteLocation | null {
|
||||
const trimmed = remoteUrl.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/u);
|
||||
if (scpLike) {
|
||||
const host = normalizeHost(scpLike[1] ?? "");
|
||||
const path = normalizeRemotePath(scpLike[2] ?? "");
|
||||
if (!isValidRemoteHost(host) || !path) return null;
|
||||
return { transport: "scp", host, path };
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transport = TRANSPORT_BY_PROTOCOL[parsed.protocol.toLowerCase()];
|
||||
if (!transport) return null;
|
||||
|
||||
const host = normalizeHost(parsed.hostname);
|
||||
let path: string;
|
||||
try {
|
||||
path = decodeURIComponent(parsed.pathname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const normalizedPath = normalizeRemotePath(path);
|
||||
if (!isValidRemoteHost(host) || !normalizedPath) return null;
|
||||
|
||||
return { transport, host, path: normalizedPath };
|
||||
}
|
||||
|
||||
function parseGitHubRemoteIdentity(path: string): GitHubRemoteIdentity | null {
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
if (segments.length !== 2) return null;
|
||||
const [owner, name] = segments;
|
||||
if (!owner || !name) return null;
|
||||
return { owner, name, repo: `${owner}/${name}` };
|
||||
}
|
||||
|
||||
function normalizeRemotePath(path: string): string | null {
|
||||
let normalized = path.trim().replace(/^\/+|\/+$/gu, "");
|
||||
if (normalized.endsWith(".git")) normalized = normalized.slice(0, -4);
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function normalizeHost(host: string): string {
|
||||
return host.trim().replace(/\.+$/u, "").toLowerCase();
|
||||
}
|
||||
|
||||
function isValidRemoteHost(host: string): boolean {
|
||||
return /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(host);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user