Plugins: treat self-hosted repository paths that differ in case as different repositories (#35640)

* fix(plugins): compare self-hosted repository paths case-sensitively

A self-hosted Git server can serve team/Plugin and team/plugin as two
repositories, so treating them as one let a catalog switch repositories
without the moved-repository review. Case is still ignored on GitHub,
GitLab.com and Bitbucket, which route paths case-insensitively.

* refactor(plugins): keep the marketplace module under the line limit
This commit is contained in:
Neil
2026-10-28 21:40:29 -07:00
committed by GitHub
parent 4a5e10223d
commit 0fd31726e1
2 changed files with 18 additions and 4 deletions
@@ -232,6 +232,13 @@ describe('isSameGitRepository', () => {
'git@gitlab.com:Acme/Team-A/Tool'
)
).toBe(true)
// A self-hosted server may serve these as two repositories, so an approval must not carry.
expect(
isSameGitRepository(
'https://git.example.com/team/Plugin.git',
'https://git.example.com/team/plugin.git'
)
).toBe(false)
expect(
isSameGitRepository(
'https://gitlab.com/acme/team-a/tool',
@@ -270,6 +270,9 @@ type GitRepositoryIdentity = {
path: string
}
// Why only these: a self-hosted server may serve `team/Plugin` and `team/plugin` as two repositories.
const CASE_INSENSITIVE_GIT_HOSTS = new Set(['github.com', 'gitlab.com', 'bitbucket.org'])
/** Extracts the host and first owner segment from the HTTPS/SSH Git forms the
* installer accepts. It is intentionally only a provenance parser. */
export function parseGitRepositoryIdentity(url: string): GitRepositoryIdentity | null {
@@ -302,17 +305,21 @@ function repositoryIdentity(host: string, repositoryPath: string): GitRepository
if (!repository) {
return null
}
const path = [...segments.slice(0, -1), repository].join('/').toLowerCase()
return { host: host.toLowerCase(), owner: segments[0]!, repository, path }
const lowerHost = host.toLowerCase()
const exactPath = [...segments.slice(0, -1), repository].join('/')
const path = CASE_INSENSITIVE_GIT_HOSTS.has(lowerHost) ? exactPath.toLowerCase() : exactPath
return { host: lowerHost, owner: segments[0]!, repository, path }
}
/** `host/owner` of a repository, with every GitLab subgroup (`gitlab.com/acme/tools`). */
export function gitRepositoryOwnerLabel(url: string): string | null {
const identity = parseGitRepositoryIdentity(url)
return identity ? [identity.host, ...identity.path.split('/').slice(0, -1)].join('/') : null
return identity
? [identity.host, ...identity.path.split('/').slice(0, -1)].join('/').toLowerCase()
: null
}
/** Same repository regardless of URL spelling (https vs scp, `.git`, case). */
/** Same repository regardless of URL spelling (https vs scp, `.git`, case where the host ignores it). */
export function isSameGitRepository(left: string, right: string): boolean {
const a = parseGitRepositoryIdentity(left)
const b = parseGitRepositoryIdentity(right)