@@ -103,6 +103,14 @@ describe('parseRepoUrl', () => {
103103 expect ( result ?. host ) . toBe ( 'other' )
104104 } )
105105
106+ it ( 'keeps the full path as name (no owner) for known SVN hosts' , ( ) => {
107+ expect ( parseRepoUrl ( 'https://svn.apache.org/repos/asf/geronimo/server' ) ) . toEqual ( {
108+ host : 'svn' ,
109+ owner : null ,
110+ name : 'repos/asf/geronimo/server' ,
111+ } )
112+ } )
113+
106114 it ( 'returns null for invalid URLs' , ( ) => {
107115 expect ( parseRepoUrl ( 'not-a-url' ) ) . toBeNull ( )
108116 } )
@@ -111,6 +119,22 @@ describe('parseRepoUrl', () => {
111119 const result = parseRepoUrl ( 'https://github.com/' )
112120 expect ( result ) . toEqual ( { host : 'github' , owner : null , name : null } )
113121 } )
122+
123+ // The canonical OpenDaylight link keeps repo identity in ?p=, not the (fixed)
124+ // pathname — a generic parts[0]/parts[1] split would collapse every OpenDaylight
125+ // repo to the same owner='gerrit', name='gitweb'.
126+ it ( 'derives owner=null and name from the ?p= query for OpenDaylight Gerrit links' , ( ) => {
127+ expect ( parseRepoUrl ( 'https://git.opendaylight.org/gerrit/gitweb?p=netconf.git' ) ) . toEqual ( {
128+ host : 'gerrit' ,
129+ owner : null ,
130+ name : 'netconf' ,
131+ } )
132+ expect ( parseRepoUrl ( 'https://git.opendaylight.org/gerrit/gitweb?p=aaa.git' ) ) . toEqual ( {
133+ host : 'gerrit' ,
134+ owner : null ,
135+ name : 'aaa' ,
136+ } )
137+ } )
114138} )
115139
116140describe ( 'normalizeScmUrl' , ( ) => {
@@ -160,8 +184,57 @@ describe('normalizeScmUrl', () => {
160184 )
161185 } )
162186
163- it ( 'returns null for non-https result' , ( ) => {
164- expect ( normalizeScmUrl ( 'svn://svn.apache.org/repos/commons-lang' ) ) . toBeNull ( )
187+ it ( 'returns null for non-https, non-SVN schemes' , ( ) => {
188+ expect ( normalizeScmUrl ( 'ftp://ftp.apache.org/repos/commons-lang' ) ) . toBeNull ( )
189+ } )
190+
191+ // SVN has no owner/repo shape and is mapped even though the hosts are largely
192+ // unreachable today — see normalizeSvnUrl / normalizeSvnScmUrl docstrings.
193+ it ( 'maps svn:// URLs on known SVN hosts to a canonical link' , ( ) => {
194+ expect ( normalizeScmUrl ( 'svn://svn.apache.org/repos/asf/commons-lang/trunk' ) ) . toBe (
195+ 'https://svn.apache.org/repos/asf/commons-lang' ,
196+ )
197+ } )
198+
199+ it ( 'maps scm:svn: connection strings, unifying viewvc/repos-asf to the same link' , ( ) => {
200+ expect (
201+ normalizeScmUrl (
202+ 'scm:svn:http://svn.apache.org/repos/asf/geronimo/server/tags/geronimo-3.0.1' ,
203+ ) ,
204+ ) . toBe ( 'https://svn.apache.org/repos/asf/geronimo/server' )
205+ expect (
206+ normalizeScmUrl ( 'http://svn.apache.org/viewvc/geronimo/server/tags/geronimo-3.0.1' ) ,
207+ ) . toBe ( 'https://svn.apache.org/repos/asf/geronimo/server' )
208+ } )
209+
210+ it ( 'maps svn+ssh: with an embedded user and SourceForge /p/.../code paths' , ( ) => {
211+ expect (
212+ normalizeScmUrl ( 'scm:svn:svn+ssh://albertil@svn.forge.objectweb.org/svnroot/myproj/trunk' ) ,
213+ ) . toBe ( 'https://svn.forge.objectweb.org/svnroot/myproj' )
214+ expect ( normalizeScmUrl ( 'http://svn.code.sf.net/p/myproj/code/trunk/foo' ) ) . toBe (
215+ 'https://svn.code.sf.net/p/myproj/code' ,
216+ )
217+ } )
218+
219+ it ( 'returns null for a marker-only SVN root with no project segment' , ( ) => {
220+ // No trailing slash after "repos/asf" — must not fall through to being
221+ // mis-parsed as project segments ["repos", "asf"] and duplicated.
222+ expect ( normalizeScmUrl ( 'https://svn.apache.org/repos/asf' ) ) . toBeNull ( )
223+ } )
224+
225+ // java.net requires an explicit SVN marker because the bare host is shared with
226+ // non-SVN content (forums, wikis, JIRA) — this must not accidentally widen to
227+ // treat every java.net URL as SVN.
228+ it ( 'treats java.net as SVN only with an explicit scm:svn:/svn:// marker' , ( ) => {
229+ expect ( normalizeScmUrl ( 'https://java.net/projects/foo/sources/svn' ) ) . toBeNull ( )
230+ expect ( normalizeScmUrl ( 'scm:svn:https://java.net/svn/foo/trunk' ) ) . toBe (
231+ 'https://java.net/svn/foo' ,
232+ )
233+ expect ( normalizeScmUrl ( 'svn://java.net/svn/foo/trunk' ) ) . toBe ( 'https://java.net/svn/foo' )
234+ } )
235+
236+ it ( 'does not map dead services that incidentally match an "svn" text filter' , ( ) => {
237+ expect ( normalizeScmUrl ( 'http://specs.googlecode.com/svn/trunk' ) ) . toBeNull ( )
165238 } )
166239
167240 // Gap B — recover repository_url from inputs that were previously dropped
@@ -362,6 +435,21 @@ describe('normalizeScmUrl', () => {
362435 expect ( normalizeScmUrl ( 'https://android.googlesource.com/' ) ) . toBeNull ( )
363436 } )
364437
438+ // cs.android.com is the Android Code Search UI, backed by the same Gitiles
439+ // repos as android.googlesource.com — same identity passthrough applies.
440+ it ( 'passes through cs.android.com urls unchanged, mirroring the googlesource identity form' , ( ) => {
441+ expect ( normalizeScmUrl ( 'https://cs.android.com/androidx/platform/frameworks/support' ) ) . toBe (
442+ 'https://cs.android.com/androidx/platform/frameworks/support' ,
443+ )
444+ } )
445+
446+ it ( 'returns null for a bare OpenDaylight Gerrit root with no repo path' , ( ) => {
447+ // /gerrit/ alone (as opposed to /gerrit/gitweb?p=...) has nothing to extract —
448+ // same "no derivable identity" case as the bare gerrit.onosproject.org root
449+ // tested above, confirmed against real declared_repository_url values.
450+ expect ( normalizeScmUrl ( 'https://git.opendaylight.org/gerrit/' ) ) . toBeNull ( )
451+ } )
452+
365453 it ( 'recovers GitHub Pages project-page urls (owner.github.io/repo)' , ( ) => {
366454 expect ( normalizeScmUrl ( 'https://silentbalanceyh.github.io/vertx-zero/' ) ) . toBe (
367455 'https://github.com/silentbalanceyh/vertx-zero' ,
@@ -480,9 +568,19 @@ describe('normalizeScmUrl', () => {
480568 expect ( normalizeScmUrl ( 'https://git.iem.at/owner/repo' ) ) . toBe ( 'https://git.iem.at/owner/repo' )
481569 } )
482570
483- it ( 'still returns null for internal or non-allowlisted hosts' , ( ) => {
484- expect ( normalizeScmUrl ( 'https://git.corp.adobe.com/team/project' ) ) . toBeNull ( )
485- expect ( normalizeScmUrl ( 'https://gitlab.alibaba-inc.com/team/project' ) ) . toBeNull ( )
571+ // Internal-only hosts are mapped despite being unreachable for external
572+ // consumers — see the SCM_HOSTS docstring.
573+ it ( 'maps internal-only hosts (git.corp.adobe.com, gitlab.alibaba-inc.com)' , ( ) => {
574+ expect ( normalizeScmUrl ( 'https://git.corp.adobe.com/team/project' ) ) . toBe (
575+ 'https://git.corp.adobe.com/team/project' ,
576+ )
577+ expect ( normalizeScmUrl ( 'https://gitlab.alibaba-inc.com/team/project' ) ) . toBe (
578+ 'https://gitlab.alibaba-inc.com/team/project' ,
579+ )
580+ } )
581+
582+ it ( 'still returns null for genuinely non-allowlisted hosts' , ( ) => {
583+ expect ( normalizeScmUrl ( 'https://git.some-random-unlisted-host.example/team/project' ) ) . toBeNull ( )
486584 } )
487585
488586 it ( 'preserves nested GitLab group namespaces instead of truncating to the group' , ( ) => {
@@ -515,6 +613,33 @@ describe('normalizeScmUrl', () => {
515613 it ( 'returns null for a GitLab namespace with no project segment' , ( ) => {
516614 expect ( normalizeScmUrl ( 'https://gitlab.com/onlygroup' ) ) . toBeNull ( )
517615 } )
616+
617+ // OpenDaylight's Gerrit: both the gitweb browse query and the SSH clone form
618+ // (port, no git@ user) unify to the same gitweb browse link.
619+ it ( 'maps OpenDaylight Gerrit gitweb query and SSH clone URLs to the same link' , ( ) => {
620+ expect (
621+ normalizeScmUrl ( 'https://git.opendaylight.org/gerrit/gitweb?p=netconf.git;a=summary' ) ,
622+ ) . toBe ( 'https://git.opendaylight.org/gerrit/gitweb?p=netconf.git' )
623+ expect ( normalizeScmUrl ( 'scm:git:ssh://git.opendaylight.org:29418/aaa.git' ) ) . toBe (
624+ 'https://git.opendaylight.org/gerrit/gitweb?p=aaa.git' ,
625+ )
626+ } )
627+
628+ it ( 'returns null for Gerrit shapes with no derivable repo identity' , ( ) => {
629+ // Bare Gerrit root — no path at all, nothing to extract.
630+ expect ( normalizeScmUrl ( 'http://gerrit.onosproject.org/' ) ) . toBeNull ( )
631+ // Gerrit admin-UI fragment path — not a clone shape.
632+ expect (
633+ normalizeScmUrl ( 'https://gerrit.wikimedia.org/r/#/admin/projects/wikidata/query/rdf' ) ,
634+ ) . toBeNull ( )
635+ } )
636+
637+ it ( 'returns null for non-SCM OpenDaylight URLs instead of mistaking them for a repo' , ( ) => {
638+ // A `p` query on some unrelated path is not a gitweb browse link.
639+ expect ( normalizeScmUrl ( 'https://git.opendaylight.org/somewhere?p=netconf.git' ) ) . toBeNull ( )
640+ // Single path segment with no .git suffix — e.g. a static asset, not a clone URL.
641+ expect ( normalizeScmUrl ( 'https://git.opendaylight.org/favicon.ico' ) ) . toBeNull ( )
642+ } )
518643} )
519644
520645describe ( 'interpolateProperties' , ( ) => {
0 commit comments