Skip to content

Commit ed269a3

Browse files
committed
fix: add apache normalize
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 7d6771d commit ed269a3

2 files changed

Lines changed: 82 additions & 11 deletions

File tree

services/apps/packages_worker/src/maven/__tests__/normalize.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,41 @@ describe('normalizeScmUrl', () => {
201201
expect(normalizeScmUrl('scm:git:https://github.com/${owner}/repo.git')).toBeNull()
202202
})
203203

204+
it('ignores an unresolved placeholder in a trailing suffix once owner/repo are valid', () => {
205+
// ${project.scm.tag} lands in the /tree/<ref> suffix, which is already ignored —
206+
// only segments[0]/[1] (owner/repo) are inspected.
207+
expect(
208+
normalizeScmUrl('https://github.com/apache/httpcomponents-client/tree/${project.scm.tag}'),
209+
).toBe('https://github.com/apache/httpcomponents-client')
210+
expect(normalizeScmUrl('https://github.com/apache/maven/tree/${project.scm.tag}')).toBe(
211+
'https://github.com/apache/maven',
212+
)
213+
})
214+
215+
it('recovers Apache gitweb hosts (gitbox/git-wip-us/git.apache.org) in path form', () => {
216+
expect(normalizeScmUrl('https://gitbox.apache.org/repos/asf/commons-lang.git')).toBe(
217+
'https://gitbox.apache.org/repos/asf/commons-lang',
218+
)
219+
expect(normalizeScmUrl('https://git.apache.org/repos/asf/ant.git')).toBe(
220+
'https://git.apache.org/repos/asf/ant',
221+
)
222+
})
223+
224+
it('recovers Apache gitweb hosts in classic ?p= query-string form', () => {
225+
expect(normalizeScmUrl('https://gitbox.apache.org/repos/asf?p=commons-io.git')).toBe(
226+
'https://gitbox.apache.org/repos/asf/commons-io',
227+
)
228+
expect(normalizeScmUrl('https://git-wip-us.apache.org/repos/asf?p=commons-math.git')).toBe(
229+
'https://git-wip-us.apache.org/repos/asf/commons-math',
230+
)
231+
})
232+
233+
it('returns null for Apache gitweb hosts with no recoverable repo name', () => {
234+
expect(normalizeScmUrl('https://gitbox.apache.org/')).toBeNull()
235+
expect(normalizeScmUrl('https://gitbox.apache.org/repos/asf/')).toBeNull()
236+
expect(normalizeScmUrl('https://gitbox.apache.org/some/other/path')).toBeNull()
237+
})
238+
204239
// SCP colon form: "host:owner/repo" where the colon is a path separator, not a port
205240
it('recovers bare host:owner/repo SCP colon form', () => {
206241
expect(normalizeScmUrl('github.com:japgolly/scalacss.git')).toBe(

services/apps/packages_worker/src/maven/extract.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -484,15 +484,17 @@ export async function extractArtifact(groupId: string, artifactId: string, versi
484484
*
485485
* Candidate additions found in Maven declared_repository_url (critical rows) but
486486
* intentionally NOT added yet, because they need more than a host allowlist:
487-
* - gitbox.apache.org / git.apache.org / git-wip-us.apache.org (~1.3k rows):
488-
* paths are `/repos/asf/<repo>`, so the generic first-two-segments logic would
489-
* collapse every Apache repo to `repos/asf`. Needs path-aware handling (skip
490-
* the `/repos/asf/` prefix) or mapping to the github.com/apache mirror.
491-
* - git.eclipse.org (~180 rows): Gerrit paths, likewise not owner/repo.
487+
* - git.eclipse.org (~180 rows): Gerrit `/c/<repo>` paths, not owner/repo.
488+
* - eclipse.gerrithub.io (~10 rows): Gerrit admin-UI paths (`/admin/repos/...`),
489+
* not a clone URL shape.
492490
* - android.googlesource.com, ec.europa.eu (Bitbucket-Server /projects/x/repos/y):
493491
* multi-segment paths, not owner/repo.
494492
* - ambiguous `git.*` hosts (git.iem.at, git.i-novus.ru, git.oschina.net) and the
495493
* internal git.corp.adobe.com: pending path/reachability confirmation.
494+
*
495+
* gitbox.apache.org / git.apache.org / git-wip-us.apache.org are handled separately
496+
* below (normalizeApacheGitwebUrl) rather than through this allowlist — their paths
497+
* are `/repos/asf/<repo>` or `?p=<repo>` (classic gitweb query form), not owner/repo.
496498
*/
497499
const SCM_HOSTS = new Set([
498500
'github.com',
@@ -522,6 +524,33 @@ const SCM_HOSTS = new Set([
522524
/** Hosts whose owner/repo path is case-insensitive and should be lower-cased. */
523525
const CASE_INSENSITIVE_HOSTS = new Set(['github.com', 'gitlab.com'])
524526

527+
/**
528+
* Apache's gitweb-based hosts serve every repo under a fixed /repos/asf/ prefix,
529+
* in two equivalent forms: /repos/asf/<repo>.git (path) or /repos/asf?p=<repo>.git
530+
* (classic gitweb query-string). Neither is an owner/repo shape — the generic path
531+
* logic would either reject the query form (no second segment) or collapse every
532+
* repo to the literal segments "repos"/"asf" for the path form. Handled on the same
533+
* host rather than mapped to a github.com/apache mirror, since that mapping isn't
534+
* guaranteed to hold for every repo.
535+
*/
536+
const APACHE_GITWEB_HOSTS = new Set([
537+
'gitbox.apache.org',
538+
'git.apache.org',
539+
'git-wip-us.apache.org',
540+
])
541+
542+
function normalizeApacheGitwebUrl(host: string, pathname: string, search: string): string | null {
543+
const queryRepo = new URLSearchParams(search).get('p')
544+
const raw =
545+
queryRepo ?? (pathname.startsWith('/repos/asf/') ? pathname.slice('/repos/asf/'.length) : null)
546+
if (!raw) return null
547+
548+
const name = raw.replace(/\.git$/, '').replace(/\/+$/, '')
549+
if (!name || name.includes('/') || /\$\{|%7B/i.test(name)) return null
550+
551+
return `https://${host}/repos/asf/${name}`
552+
}
553+
525554
/**
526555
* Converts the raw SCM URL from a POM (declared_repository_url) into a clean,
527556
* canonical `https://<host>/<owner>/<repo>` repository URL suitable for storage
@@ -547,12 +576,6 @@ export function normalizeScmUrl(raw: string | null): string | null {
547576
let s = raw.trim()
548577
if (!s) return null
549578

550-
// A leftover ${...} means best-effort interpolation upstream could not resolve it.
551-
// Reject outright: a placeholder embedded in the path (e.g. github.com/owner/${x})
552-
// otherwise survives URL parsing (percent-encoded to %7B…%7D) and would yield a junk
553-
// repository_url instead of null.
554-
if (s.includes('${')) return null
555-
556579
// Strip Maven scm:git: / scm: prefix
557580
s = s.replace(/^scm:git:/i, '').replace(/^scm:/i, '')
558581

@@ -593,6 +616,11 @@ export function normalizeScmUrl(raw: string | null): string | null {
593616
if (parsed.protocol !== 'https:') return null
594617

595618
const host = parsed.hostname.toLowerCase().replace(/^www\./, '')
619+
620+
if (APACHE_GITWEB_HOSTS.has(host)) {
621+
return normalizeApacheGitwebUrl(host, parsed.pathname, parsed.search)
622+
}
623+
596624
if (!SCM_HOSTS.has(host)) return null
597625

598626
// Require at least owner + repo path segments
@@ -603,6 +631,14 @@ export function normalizeScmUrl(raw: string | null): string | null {
603631
let name = segments[1].replace(/\.git$/, '')
604632
if (!owner || !name) return null
605633

634+
// A leftover ${...} in owner/repo means best-effort interpolation upstream could
635+
// not resolve it — reject rather than store a junk link. The URL parser
636+
// percent-encodes { and } in the path, so check both forms. Placeholders in a
637+
// trailing suffix (e.g. /tree/${project.scm.tag}) are irrelevant here since only
638+
// segments[0]/[1] are inspected — that suffix is simply never read.
639+
const hasPlaceholder = (seg: string) => /\$\{|%7B/i.test(seg)
640+
if (hasPlaceholder(owner) || hasPlaceholder(name)) return null
641+
606642
if (CASE_INSENSITIVE_HOSTS.has(host)) {
607643
owner = owner.toLowerCase()
608644
name = name.toLowerCase()

0 commit comments

Comments
 (0)