// // Git remote URL parsing. Covers the four shapes Gitea hands out: HTTPS, SCP // style SSH, ssh:// URLs, and git:// URLs. // function stripCredentials(host) { const at = host.lastIndexOf("@"); return at === -1 ? host : host.slice(at + 1); } function splitPath(path) { const parts = path .replace(/^\/+/, "") .replace(/\.git$/i, "") .replace(/\/+$/, "") .split("/") .filter(Boolean); if (parts.length < 2) return null; // Sub-path installs ("https://example.com/gitea/owner/repo") still end with // owner/repo, so take the last two components. return { owner: parts[parts.length - 2], name: parts[parts.length - 1] }; } /** Parses a remote URL into { host, port, owner, name }, or null. */ function parse(url) { if (!url) return null; const trimmed = String(url).trim(); let match = trimmed.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)(\/.*)$/i); if (match) { let authority = stripCredentials(match[1]); let port = null; const portMatch = authority.match(/^(.+):(\d+)$/); if (portMatch) { authority = portMatch[1]; port = Number(portMatch[2]); } const path = splitPath(match[2]); if (!path) return null; return { host: authority.toLowerCase(), port: port, owner: path.owner, name: path.name }; } // SCP-like: git@host:owner/repo.git match = trimmed.match(/^([^/@]+@)?([^/:]+):(.+)$/); if (match) { const path = splitPath(match[3]); if (!path) return null; return { host: match[2].toLowerCase(), port: null, owner: path.owner, name: path.name, }; } return null; } /** The host of an instance base URL, for matching against a remote's host. */ function hostOf(baseUrl) { const match = String(baseUrl || "").match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)/i); if (!match) return null; return stripCredentials(match[1]).replace(/:\d+$/, "").toLowerCase(); } exports.parse = parse; exports.hostOf = hostOf;