| 226 | * into a monorepo subtree such as the recipe entries, non-URLs). |
| 227 | */ |
| 228 | export function parseRepository(url: string | undefined): RepoRef | null { |
| 229 | if (!url) return null; |
| 230 | let parsed: URL; |
| 231 | try { |
| 232 | parsed = new URL(url); |
| 233 | } catch { |
| 234 | return null; |
| 235 | } |
| 236 | const host = HOSTS[parsed.hostname]; |
| 237 | if (!host) return null; |
| 238 | |
| 239 | const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/"); |
| 240 | if (parts.length < 2) return null; |
| 241 | // `/owner/repo/tree/main/...` — a subtree link, not a repository of its own. |
| 242 | if (parts.length > 2 && ["tree", "blob", "-"].includes(parts[2])) return null; |
| 243 | if (parts.length > 2 && host !== "gitlab") return null; |
| 244 | |
| 245 | const owner = host === "gitlab" ? parts.slice(0, -1).join("/") : parts[0]; |
| 246 | const repo = parts[parts.length - 1].replace(/\.git$/, ""); |
| 247 | return { host, owner, repo, slug: `${owner}/${repo}` }; |
| 248 | } |
| 249 | |
| 250 | // --------------------------------------------------------------------------- |
| 251 | // Repository metadata |