diff --git a/src/__tests__/repository-url.test.ts b/src/__tests__/repository-url.test.ts new file mode 100644 index 0000000..5a24277 --- /dev/null +++ b/src/__tests__/repository-url.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { parseRepositoryReference } from "../../website/src/lib/repository-url.js"; + +describe("parseRepositoryReference", () => { + it.each([ + [ + "getsentry/coverage-action", + { owner: "getsentry", repo: "coverage-action" }, + ], + [ + "github.com/getsentry/coverage-action", + { owner: "getsentry", repo: "coverage-action" }, + ], + [ + "https://github.com/getsentry/coverage-action.git", + { owner: "getsentry", repo: "coverage-action" }, + ], + ])("parses supported repository references", (value, expected) => { + expect(parseRepositoryReference(value)).toEqual(expected); + }); + + it.each([ + "github.com/getsentry", + "https://example.com/github.com/getsentry/coverage-action", + "https://github.com.example.com/getsentry/coverage-action", + "https://github.com@evil.example/getsentry/coverage-action", + ])("rejects URLs that do not target github.com", (value) => { + expect(parseRepositoryReference(value)).toBeNull(); + }); +}); diff --git a/website/src/lib/repository-url.ts b/website/src/lib/repository-url.ts new file mode 100644 index 0000000..7a75486 --- /dev/null +++ b/website/src/lib/repository-url.ts @@ -0,0 +1,42 @@ +export interface RepositoryReference { + owner: string; + repo: string; +} + +/** + * Parse a repository shorthand or a URL hosted on github.com. + */ +export function parseRepositoryReference( + value: string, +): RepositoryReference | null { + const input = value.trim(); + if (!input) { + return null; + } + + const urlInput = input.startsWith("github.com/") ? `https://${input}` : input; + + try { + const url = new URL(urlInput); + if (url.hostname !== "github.com") { + return null; + } + + const [owner, repo] = url.pathname.split("/").filter(Boolean); + if (!owner || !repo) { + return null; + } + + return { owner, repo: repo.replace(/\.git$/, "") }; + } catch { + const shorthand = input.match(/^([^/\s]+)\/([^/\s]+)$/); + if (!shorthand) { + return null; + } + + return { + owner: shorthand[1], + repo: shorthand[2].replace(/\.git$/, ""), + }; + } +} diff --git a/website/src/pages/HomePage.tsx b/website/src/pages/HomePage.tsx index 0ea2c71..afb20ba 100644 --- a/website/src/pages/HomePage.tsx +++ b/website/src/pages/HomePage.tsx @@ -10,6 +10,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { parseRepositoryReference } from "@/lib/repository-url"; export default function HomePage() { const navigate = useNavigate(); @@ -18,23 +19,9 @@ export default function HomePage() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - // Parse repo URL or owner/repo format - let owner = ""; - let repo = ""; - - // Try to extract from various formats - if (repoUrl.includes("github.com/")) { - const match = repoUrl.match(/github\.com\/([^/]+)\/([^/\s]+)/); - if (match) { - owner = match[1]; - repo = match[2].replace(/\.git$/, ""); - } - } else if (repoUrl.includes("/")) { - [owner, repo] = repoUrl.split("/").map((s) => s.trim()); - } - - if (owner && repo) { - navigate(`/${owner}/${repo}`); + const repository = parseRepositoryReference(repoUrl); + if (repository) { + navigate(`/${repository.owner}/${repository.repo}`); } };