@@ -24,6 +24,22 @@ const HOST_ENUM: Record<string, RepoHost> = {
2424// same repo never produces two distinct repos.url keys.
2525const CASE_INSENSITIVE_HOSTS = new Set ( [ 'github.com' , 'gitlab.com' ] )
2626
27+ // Pre-2018 GitLab URLs (and shorthand copies of them still circulating) mark deep-links
28+ // the same way GitHub does — appended directly after the project path, with no `/-/`
29+ // separator. GitLab reserves these route words at the project-slug position precisely so
30+ // they can never collide with a real project name (docs: user/reserved_names), so treating
31+ // the first one as a deep-link boundary is safe even for arbitrarily nested subgroups.
32+ const GITLAB_LEGACY_ROUTE_SEGMENTS = new Set ( [
33+ 'tree' ,
34+ 'blob' ,
35+ 'commits' ,
36+ 'commit' ,
37+ 'compare' ,
38+ 'issues' ,
39+ 'merge_requests' ,
40+ 'wikis' ,
41+ ] )
42+
2743/**
2844 * Canonicalize a source-repository URL to `{ url, host }` where url is
2945 * `https://<host>/<owner>/<name>` and host is the coarse classification stored
@@ -35,8 +51,10 @@ const CASE_INSENSITIVE_HOSTS = new Set(['github.com', 'gitlab.com'])
3551 * `git+`, `git://`, `www.`, and monorepo deep-links: GitHub/Bitbucket's bare
3652 * `/tree/<branch>/<path>` (only the first two path segments are kept) and
3753 * GitLab's `/-/tree/<branch>/<path>` (kept segments run up to the `/-/`,
38- * preserving arbitrarily nested subgroups). Returns null when the input
39- * cannot be reduced to an owner/name pair.
54+ * preserving arbitrarily nested subgroups) — plus a legacy fallback for pre-2018
55+ * GitLab links with no `/-/` marker, cut at the first reserved route keyword
56+ * (`tree`, `blob`, `issues`, ...) instead. Returns null when the input cannot be
57+ * reduced to an owner/name pair.
4058 */
4159export function canonicalizeRepoUrl ( raw : string ) : CanonicalRepo | null {
4260 let s = raw . trim ( ) . replace ( / # .* $ / , '' )
@@ -89,7 +107,13 @@ export function canonicalizeRepoUrl(raw: string): CanonicalRepo | null {
89107 let pathSegments : string [ ]
90108 if ( hostname === 'gitlab.com' ) {
91109 const dashIdx = segments . indexOf ( '-' )
92- pathSegments = dashIdx === - 1 ? segments : segments . slice ( 0 , dashIdx )
110+ // Legacy fallback: a route keyword with no `/-/` ahead of it (index 2+, so at least
111+ // group + project survive) also marks the boundary — whichever comes first wins.
112+ const legacyIdx = segments . findIndex (
113+ ( seg , i ) => i >= 2 && GITLAB_LEGACY_ROUTE_SEGMENTS . has ( seg ) ,
114+ )
115+ const cutIdx = [ dashIdx , legacyIdx ] . filter ( ( i ) => i !== - 1 ) . sort ( ( a , b ) => a - b ) [ 0 ]
116+ pathSegments = cutIdx === undefined ? segments : segments . slice ( 0 , cutIdx )
93117 } else if ( isKnownHost ) {
94118 pathSegments = segments . slice ( 0 , 2 )
95119 } else {
0 commit comments