refactor: share github remote parsing (#843)

This commit is contained in:
Mohamed Boudra
2026-05-09 22:40:05 +08:00
committed by GitHub
parent 0079f2a875
commit f17fb77013
4 changed files with 101 additions and 118 deletions
+11 -85
View File
@@ -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);
}