@@ -458,13 +458,32 @@ export async function extractArtifact(groupId: string, artifactId: string, versi
458458 * placeholders) yields null so it is never stored as a repository link.
459459 *
460460 * TODO(CM): host list pending product confirmation before rollout.
461+ *
462+ * Candidate additions found in Maven declared_repository_url (critical rows) but
463+ * intentionally NOT added yet, because they need more than a host allowlist:
464+ * - gitbox.apache.org / git.apache.org / git-wip-us.apache.org (~1.3k rows):
465+ * paths are `/repos/asf/<repo>`, so the generic first-two-segments logic would
466+ * collapse every Apache repo to `repos/asf`. Needs path-aware handling (skip
467+ * the `/repos/asf/` prefix) or mapping to the github.com/apache mirror.
468+ * - git.eclipse.org (~180 rows): Gerrit paths, likewise not owner/repo.
469+ * - android.googlesource.com, ec.europa.eu (Bitbucket-Server /projects/x/repos/y):
470+ * multi-segment paths, not owner/repo.
471+ * - ambiguous `git.*` hosts (git.iem.at, git.i-novus.ru, git.oschina.net) and the
472+ * internal git.corp.adobe.com: pending path/reachability confirmation.
461473 */
462474const SCM_HOSTS = new Set ( [
463475 'github.com' ,
464476 'gitlab.com' ,
465477 'bitbucket.org' ,
466478 'gitee.com' ,
467479 'codeberg.org' ,
480+ // Self-hosted GitLab / Gitea instances seen in Maven POMs with clean
481+ // /<owner>/<repo> paths (same shape as gitlab.com — handled by the generic logic).
482+ 'gitlab.smartb.city' ,
483+ 'gitlab.ow2.org' ,
484+ 'gitlab.nuiton.org' ,
485+ 'gitlab.inria.fr' ,
486+ 'git.neckar.it' ,
468487] )
469488
470489/** Hosts whose owner/repo path is case-insensitive and should be lower-cased. */
@@ -483,6 +502,9 @@ const CASE_INSENSITIVE_HOSTS = new Set(['github.com', 'gitlab.com'])
483502 * github.com/owner/repo (no scheme) → https://github.com/owner/repo
484503 * git://github.com/owner/repo.git → https://github.com/owner/repo
485504 * http://github.com/owner/repo/tree/... → https://github.com/owner/repo
505+ * github.com:owner/repo.git (SCP colon) → https://github.com/owner/repo
506+ * https://github.com:owner/repo → https://github.com/owner/repo
507+ * ssh://git@github.com:owner/repo.git → https://github.com/owner/repo
486508 *
487509 * Rejected (→ null): website-only URLs (https://meson.ai/), non-SCM hosts
488510 * (svn://…, http://source.android.com), placeholders (Private, ${scm-url}).
@@ -501,14 +523,25 @@ export function normalizeScmUrl(raw: string | null): string | null {
501523 // SCP form git@host :owner/repo → https://host/owner/repo
502524 s = s . replace ( / ^ g i t @ ( [ ^ : / ] + ) : ( .+ ) $ / , 'https://$1/$2' )
503525
526+ // ssh://git@host :owner/repo → https://host/owner/repo (SCP colon under ssh)
527+ s = s . replace ( / ^ s s h : \/ \/ g i t @ ( [ ^ : / ] + ) : (? = \D ) / , 'https://$1/' )
528+
504529 // ssh://git@host /… → https://host/…
505530 s = s . replace ( / ^ s s h : \/ \/ g i t @ ( [ ^ / ] + ) \/ / , 'https://$1/' )
506531
532+ // scheme://host:owner/repo → scheme://host/owner/repo — the colon is an SCP path
533+ // separator, not a port (guarded by \D so real numeric ports are left intact).
534+ s = s . replace ( / ^ ( h t t p s ? ) : \/ \/ ( [ ^ : / ] + ) : (? = \D ) / , '$1://$2/' )
535+
507536 // git:// → https://, and upgrade http:// → https://
508537 s = s . replace ( / ^ g i t : \/ \/ / , 'https://' ) . replace ( / ^ h t t p : \/ \/ / , 'https://' )
509538
510539 // No scheme at all (e.g. "github.com/owner/repo") → assume https
511- if ( ! s . includes ( '://' ) ) s = `https://${ s } `
540+ if ( ! s . includes ( '://' ) ) {
541+ // Bare SCP form "host:owner/repo" → "host/owner/repo" before assuming https.
542+ s = s . replace ( / ^ ( [ ^ / : ] + ) : (? = \D ) / , '$1/' )
543+ s = `https://${ s } `
544+ }
512545
513546 let parsed : URL
514547 try {
0 commit comments