11import type { ProviderId , RepoRef } from '#shared/utils/git-providers'
2- import { GIT_PROVIDER_API_ORIGINS , parseRepoUrl , GITLAB_HOSTS } from '#shared/utils/git-providers'
2+ import { GIT_PROVIDER_API_ORIGINS , parseRepoUrl } from '#shared/utils/git-providers'
33
44// TTL for git repo metadata (10 minutes - repo stats don't change frequently)
55const REPO_META_TTL = 60 * 10
@@ -88,8 +88,6 @@ type RadicleProjectResponse = {
8888}
8989
9090type ProviderAdapter = {
91- id : ProviderId
92- parse ( url : URL ) : RepoRef | null
9391 links ( ref : RepoRef ) : RepoMetaLinks
9492 fetchMeta (
9593 cachedFetch : CachedFetchFunction ,
@@ -100,25 +98,6 @@ type ProviderAdapter = {
10098}
10199
102100const githubAdapter : ProviderAdapter = {
103- id : 'github' ,
104-
105- parse ( url ) {
106- const host = url . hostname . toLowerCase ( )
107- if ( host !== 'github.com' && host !== 'www.github.com' ) return null
108-
109- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
110- if ( parts . length < 2 ) return null
111-
112- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
113- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
114- . trim ( )
115- . replace ( / \. g i t $ / i, '' )
116-
117- if ( ! owner || ! repo ) return null
118-
119- return { provider : 'github' , owner, repo }
120- } ,
121-
122101 links ( ref ) {
123102 const base = `https://github.com/${ ref . owner } /${ ref . repo } `
124103 return {
@@ -160,30 +139,6 @@ const githubAdapter: ProviderAdapter = {
160139}
161140
162141const gitlabAdapter : ProviderAdapter = {
163- id : 'gitlab' ,
164-
165- parse ( url ) {
166- const host = url . hostname . toLowerCase ( )
167- const isGitLab = GITLAB_HOSTS . some ( h => host === h || host === `www.${ h } ` )
168- if ( ! isGitLab ) return null
169-
170- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
171- if ( parts . length < 2 ) return null
172-
173- // GitLab supports nested groups, so we join all parts except the last as owner
174- const repo = decodeURIComponent ( parts [ parts . length - 1 ] ?? '' )
175- . trim ( )
176- . replace ( / \. g i t $ / i, '' )
177- const owner = parts
178- . slice ( 0 , - 1 )
179- . map ( p => decodeURIComponent ( p ) . trim ( ) )
180- . join ( '/' )
181-
182- if ( ! owner || ! repo ) return null
183-
184- return { provider : 'gitlab' , owner, repo, host }
185- } ,
186-
187142 links ( ref ) {
188143 const baseHost = ref . host ?? 'gitlab.com'
189144 const base = `https://${ baseHost } /${ ref . owner } /${ ref . repo } `
@@ -224,25 +179,6 @@ const gitlabAdapter: ProviderAdapter = {
224179}
225180
226181const bitbucketAdapter : ProviderAdapter = {
227- id : 'bitbucket' ,
228-
229- parse ( url ) {
230- const host = url . hostname . toLowerCase ( )
231- if ( host !== 'bitbucket.org' && host !== 'www.bitbucket.org' ) return null
232-
233- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
234- if ( parts . length < 2 ) return null
235-
236- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
237- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
238- . trim ( )
239- . replace ( / \. g i t $ / i, '' )
240-
241- if ( ! owner || ! repo ) return null
242-
243- return { provider : 'bitbucket' , owner, repo }
244- } ,
245-
246182 links ( ref ) {
247183 const base = `https://bitbucket.org/${ ref . owner } /${ ref . repo } `
248184 return {
@@ -281,25 +217,6 @@ const bitbucketAdapter: ProviderAdapter = {
281217}
282218
283219const codebergAdapter : ProviderAdapter = {
284- id : 'codeberg' ,
285-
286- parse ( url ) {
287- const host = url . hostname . toLowerCase ( )
288- if ( host !== 'codeberg.org' && host !== 'www.codeberg.org' ) return null
289-
290- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
291- if ( parts . length < 2 ) return null
292-
293- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
294- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
295- . trim ( )
296- . replace ( / \. g i t $ / i, '' )
297-
298- if ( ! owner || ! repo ) return null
299-
300- return { provider : 'codeberg' , owner, repo, host : 'codeberg.org' }
301- } ,
302-
303220 links ( ref ) {
304221 const base = `https://codeberg.org/${ ref . owner } /${ ref . repo } `
305222 return {
@@ -339,25 +256,6 @@ const codebergAdapter: ProviderAdapter = {
339256}
340257
341258const giteeAdapter : ProviderAdapter = {
342- id : 'gitee' ,
343-
344- parse ( url ) {
345- const host = url . hostname . toLowerCase ( )
346- if ( host !== 'gitee.com' && host !== 'www.gitee.com' ) return null
347-
348- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
349- if ( parts . length < 2 ) return null
350-
351- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
352- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
353- . trim ( )
354- . replace ( / \. g i t $ / i, '' )
355-
356- if ( ! owner || ! repo ) return null
357-
358- return { provider : 'gitee' , owner, repo }
359- } ,
360-
361259 links ( ref ) {
362260 const base = `https://gitee.com/${ ref . owner } /${ ref . repo } `
363261 return {
@@ -398,53 +296,8 @@ const giteeAdapter: ProviderAdapter = {
398296
399297/**
400298 * Generic Gitea adapter for self-hosted instances.
401- * Matches common Gitea/Forgejo hosting patterns.
402299 */
403300const giteaAdapter : ProviderAdapter = {
404- id : 'gitea' ,
405-
406- parse ( url ) {
407- const host = url . hostname . toLowerCase ( )
408-
409- // Match common Gitea/Forgejo hosting patterns
410- const giteaPatterns = [
411- / ^ g i t \. / i, // git.example.com
412- / ^ g i t e a \. / i, // gitea.example.com
413- / ^ f o r g e j o \. / i, // forgejo.example.com
414- / ^ c o d e \. / i, // code.example.com
415- / ^ s r c \. / i, // src.example.com
416- / g i t e a \. i o $ / i, // *.gitea.io
417- ]
418-
419- // Skip if it matches other known providers
420- const skipHosts = [
421- 'github.com' ,
422- 'gitlab.com' ,
423- 'codeberg.org' ,
424- 'bitbucket.org' ,
425- 'gitee.com' ,
426- 'sr.ht' ,
427- 'git.sr.ht' ,
428- ...GITLAB_HOSTS ,
429- ]
430- if ( skipHosts . some ( h => host === h || host . endsWith ( `.${ h } ` ) ) ) return null
431-
432- // Check if matches Gitea patterns
433- if ( ! giteaPatterns . some ( p => p . test ( host ) ) ) return null
434-
435- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
436- if ( parts . length < 2 ) return null
437-
438- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
439- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
440- . trim ( )
441- . replace ( / \. g i t $ / i, '' )
442-
443- if ( ! owner || ! repo ) return null
444-
445- return { provider : 'gitea' , owner, repo, host }
446- } ,
447-
448301 links ( ref ) {
449302 const base = `https://${ ref . host } /${ ref . owner } /${ ref . repo } `
450303 return {
@@ -488,27 +341,8 @@ const giteaAdapter: ProviderAdapter = {
488341}
489342
490343const sourcehutAdapter : ProviderAdapter = {
491- id : 'sourcehut' ,
492-
493- parse ( url ) {
494- const host = url . hostname . toLowerCase ( )
495- if ( host !== 'sr.ht' && host !== 'git.sr.ht' ) return null
496-
497- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
498- if ( parts . length < 2 ) return null
499-
500- // Sourcehut uses ~username/repo format
501- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
502- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
503- . trim ( )
504- . replace ( / \. g i t $ / i, '' )
505-
506- if ( ! owner || ! repo ) return null
507-
508- return { provider : 'sourcehut' , owner, repo }
509- } ,
510-
511344 links ( ref ) {
345+ // Sourcehut uses ~username/repo format.
512346 const base = `https://git.sr.ht/${ ref . owner } /${ ref . repo } `
513347 return {
514348 repo : base ,
@@ -531,34 +365,8 @@ const sourcehutAdapter: ProviderAdapter = {
531365}
532366
533367const tangledAdapter : ProviderAdapter = {
534- id : 'tangled' ,
535-
536- parse ( url ) {
537- const host = url . hostname . toLowerCase ( )
538- if (
539- host !== 'tangled.sh' &&
540- host !== 'www.tangled.sh' &&
541- host !== 'tangled.org' &&
542- host !== 'www.tangled.org'
543- ) {
544- return null
545- }
546-
547- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
548- if ( parts . length < 2 ) return null
549-
550- // Tangled uses owner/repo format (owner is a domain-like identifier)
551- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
552- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
553- . trim ( )
554- . replace ( / \. g i t $ / i, '' )
555-
556- if ( ! owner || ! repo ) return null
557-
558- return { provider : 'tangled' , owner, repo }
559- } ,
560-
561368 links ( ref ) {
369+ // Tangled uses owner/repo format, where owner is a domain-like identifier.
562370 const base = `https://tangled.org/${ ref . owner } /${ ref . repo } `
563371 return {
564372 repo : base ,
@@ -595,24 +403,8 @@ const tangledAdapter: ProviderAdapter = {
595403}
596404
597405const radicleAdapter : ProviderAdapter = {
598- id : 'radicle' ,
599-
600- parse ( url ) {
601- const host = url . hostname . toLowerCase ( )
602- if ( host !== 'radicle.at' && host !== 'app.radicle.at' && host !== 'seed.radicle.at' ) {
603- return null
604- }
605-
606- // Radicle URLs: app.radicle.at/nodes/seed.radicle.at/rad:z3nP4yT1PE3m1PxLEzr173sZtJVnT
607- const path = url . pathname
608- const radMatch = path . match ( / r a d : [ a - z A - Z 0 - 9 ] + / )
609- if ( ! radMatch ?. [ 0 ] ) return null
610-
611- // Use empty owner, store full rad: ID as repo
612- return { provider : 'radicle' , owner : '' , repo : radMatch [ 0 ] , host }
613- } ,
614-
615406 links ( ref ) {
407+ // Radicle refs store the full rad: ID as repo with no owner.
616408 const base = `https://app.radicle.at/nodes/seed.radicle.at/${ ref . repo } `
617409 return {
618410 repo : base ,
@@ -649,32 +441,10 @@ const radicleAdapter: ProviderAdapter = {
649441 } ,
650442}
651443
444+ /**
445+ * Adapter for explicit Forgejo instances.
446+ */
652447const forgejoAdapter : ProviderAdapter = {
653- id : 'forgejo' ,
654-
655- parse ( url ) {
656- const host = url . hostname . toLowerCase ( )
657-
658- // Match explicit Forgejo instances
659- const forgejoPatterns = [ / ^ f o r g e j o \. / i, / \. f o r g e j o \. / i]
660- const knownInstances = [ 'next.forgejo.org' , 'try.next.forgejo.org' ]
661-
662- const isMatch = knownInstances . some ( h => host === h ) || forgejoPatterns . some ( p => p . test ( host ) )
663- if ( ! isMatch ) return null
664-
665- const parts = url . pathname . split ( '/' ) . filter ( Boolean )
666- if ( parts . length < 2 ) return null
667-
668- const owner = decodeURIComponent ( parts [ 0 ] ?? '' ) . trim ( )
669- const repo = decodeURIComponent ( parts [ 1 ] ?? '' )
670- . trim ( )
671- . replace ( / \. g i t $ / i, '' )
672-
673- if ( ! owner || ! repo ) return null
674-
675- return { provider : 'forgejo' , owner, repo, host }
676- } ,
677-
678448 links ( ref ) {
679449 const base = `https://${ ref . host } /${ ref . owner } /${ ref . repo } `
680450 return {
@@ -715,21 +485,18 @@ const forgejoAdapter: ProviderAdapter = {
715485 } ,
716486}
717487
718- // Order matters: more specific adapters should come before generic ones
719- const providers : readonly ProviderAdapter [ ] = [
720- githubAdapter ,
721- gitlabAdapter ,
722- bitbucketAdapter ,
723- codebergAdapter ,
724- giteeAdapter ,
725- sourcehutAdapter ,
726- tangledAdapter ,
727- radicleAdapter ,
728- forgejoAdapter ,
729- giteaAdapter , // Generic Gitea adapter last as fallback for self-hosted instances
730- ] as const
731-
732- const parseRepoFromUrl = parseRepoUrl
488+ const providers = {
489+ github : githubAdapter ,
490+ gitlab : gitlabAdapter ,
491+ bitbucket : bitbucketAdapter ,
492+ codeberg : codebergAdapter ,
493+ gitee : giteeAdapter ,
494+ sourcehut : sourcehutAdapter ,
495+ tangled : tangledAdapter ,
496+ radicle : radicleAdapter ,
497+ forgejo : forgejoAdapter ,
498+ gitea : giteaAdapter ,
499+ } satisfies Record < ProviderId , ProviderAdapter >
733500
734501export function useRepoMeta ( repositoryUrl : MaybeRefOrGetter < string | null | undefined > ) {
735502 // Get cachedFetch in setup context (outside async handler)
@@ -738,7 +505,7 @@ export function useRepoMeta(repositoryUrl: MaybeRefOrGetter<string | null | unde
738505 const repoRef = computed ( ( ) => {
739506 const url = toValue ( repositoryUrl )
740507 if ( ! url ) return null
741- return parseRepoFromUrl ( url )
508+ return parseRepoUrl ( url )
742509 } )
743510
744511 const { data, pending, error, refresh } = useLazyAsyncData < RepoMeta | null > (
@@ -750,9 +517,7 @@ export function useRepoMeta(repositoryUrl: MaybeRefOrGetter<string | null | unde
750517 const ref = repoRef . value
751518 if ( ! ref ) return null
752519
753- const adapter = providers . find ( provider => provider . id === ref . provider )
754- if ( ! adapter ) return null
755-
520+ const adapter = providers [ ref . provider ]
756521 const links = adapter . links ( ref )
757522 return await adapter . fetchMeta ( cachedFetch , ref , links , { signal } )
758523 } ,
0 commit comments