mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
feat: improve git URL detection to recognize plain HTTP/HTTPS URLs
Improve git URL detection to recognize plain HTTP/HTTPS URLs ending in `.git` and prioritize git resolver over tarball resolver. close #10468
This commit is contained in:
17
.changeset/fix-git-url-detection.md
Normal file
17
.changeset/fix-git-url-detection.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
"@pnpm/git-resolver": minor
|
||||
"@pnpm/tarball-resolver": minor
|
||||
"@pnpm/default-resolver": minor
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Support plain `http://` and `https://` URLs ending with `.git` as git repository dependencies.
|
||||
|
||||
Previously, URLs like `https://gitea.example.org/user/repo.git#commit` were not recognized as git repositories because they lacked the `git+` prefix (e.g., `git+https://`). This caused issues when installing dependencies from self-hosted git servers like Gitea or Forgejo that don't provide tarball downloads.
|
||||
|
||||
Changes:
|
||||
- The git resolver now runs before the tarball resolver, ensuring git URLs are handled by the correct resolver
|
||||
- The git resolver now recognizes plain `http://` and `https://` URLs ending in `.git` as git repositories
|
||||
- Removed the `isRepository` check from the tarball resolver since it's no longer needed with the new resolver order
|
||||
|
||||
Fixes #10468
|
||||
@@ -81,12 +81,14 @@
|
||||
"fnumber",
|
||||
"foobarqar",
|
||||
"foofoo",
|
||||
"forgejo",
|
||||
"fsevents",
|
||||
"gabor",
|
||||
"gcttmf",
|
||||
"getattr",
|
||||
"ghsa",
|
||||
"ghsas",
|
||||
"gitea",
|
||||
"globalconfig",
|
||||
"globstar",
|
||||
"gruntfile",
|
||||
|
||||
@@ -108,8 +108,8 @@ export function createResolver (
|
||||
await resolveFromNpm(wantedDependency, opts as ResolveFromNpmOptions) ??
|
||||
await resolveFromJsr(wantedDependency, opts as ResolveFromNpmOptions) ??
|
||||
(wantedDependency.bareSpecifier && (
|
||||
await resolveFromTarball(fetchFromRegistry, wantedDependency as { bareSpecifier: string }) ??
|
||||
await resolveFromGit(wantedDependency as { bareSpecifier: string }, opts) ??
|
||||
await resolveFromTarball(fetchFromRegistry, wantedDependency as { bareSpecifier: string }) ??
|
||||
await _resolveFromLocal(wantedDependency as { bareSpecifier: string }, opts)
|
||||
)) ??
|
||||
await _resolveNodeRuntime(wantedDependency, opts) ??
|
||||
|
||||
@@ -40,7 +40,11 @@ export function parseBareSpecifier (bareSpecifier: string, opts: AgentOptions):
|
||||
const colonsPos = bareSpecifier.indexOf(':')
|
||||
if (colonsPos === -1) return null
|
||||
const protocol = bareSpecifier.slice(0, colonsPos)
|
||||
if (protocol && gitProtocols.has(protocol.toLocaleLowerCase())) {
|
||||
|
||||
// Also detect http/https URLs ending in .git as git repositories
|
||||
const isGitUrl = gitProtocols.has(protocol.toLocaleLowerCase()) ||
|
||||
((protocol === 'http' || protocol === 'https') && /\.git(?:#|$)/.test(bareSpecifier))
|
||||
if (protocol && isGitUrl) {
|
||||
const correctBareSpecifier = correctUrl(bareSpecifier)
|
||||
const url = new URL(correctBareSpecifier)
|
||||
if (!url?.protocol) return null
|
||||
|
||||
@@ -52,3 +52,24 @@ test.each([
|
||||
const parsed = await parseBareSpecifier(input, {})?.()
|
||||
expect(parsed?.fetchSpec).toBe(output)
|
||||
})
|
||||
|
||||
// Test for https:// URLs ending in .git (issue #10468)
|
||||
test.each([
|
||||
['https://gitea.osmocom.org/ttcn3/highlightjs-ttcn3.git', 'https://gitea.osmocom.org/ttcn3/highlightjs-ttcn3.git'],
|
||||
['https://gitea.osmocom.org/ttcn3/highlightjs-ttcn3.git#6daccff309fca1e7561a43984d42fa4f829ce06d', 'https://gitea.osmocom.org/ttcn3/highlightjs-ttcn3.git'],
|
||||
['http://example.com/repo.git', 'http://example.com/repo.git'],
|
||||
['http://example.com/repo.git#main', 'http://example.com/repo.git'],
|
||||
])('plain http/https URLs ending in .git should be recognized: %s', async (input, output) => {
|
||||
const parsed = await parseBareSpecifier(input, {})?.()
|
||||
expect(parsed?.fetchSpec).toBe(output)
|
||||
})
|
||||
|
||||
// Ensure non-.git https URLs are not recognized as git repos
|
||||
test.each([
|
||||
['https://example.com/package.tar.gz'],
|
||||
['https://example.com/package.tgz'],
|
||||
['https://example.com/file'],
|
||||
])('plain http/https URLs not ending in .git should not be recognized: %s', async (input) => {
|
||||
const parsed = parseBareSpecifier(input, {})
|
||||
expect(parsed).toBeNull()
|
||||
})
|
||||
|
||||
@@ -15,8 +15,6 @@ export async function resolveFromTarball (
|
||||
return null
|
||||
}
|
||||
|
||||
if (isRepository(wantedDependency.bareSpecifier)) return null
|
||||
|
||||
// The URL is normalized to remove the port if it is the default port of the protocol.
|
||||
const normalizedBareSpecifier = new URL(wantedDependency.bareSpecifier).toString()
|
||||
let resolvedUrl: string
|
||||
@@ -38,22 +36,3 @@ export async function resolveFromTarball (
|
||||
resolvedVia: 'url',
|
||||
}
|
||||
}
|
||||
|
||||
const GIT_HOSTERS = new Set([
|
||||
'github.com',
|
||||
'gitlab.com',
|
||||
'bitbucket.org',
|
||||
])
|
||||
|
||||
function isRepository (bareSpecifier: string): boolean {
|
||||
const url = new URL(bareSpecifier)
|
||||
if (url.hash && url.hash.includes('/')) {
|
||||
url.hash = encodeURIComponent(url.hash.substring(1))
|
||||
bareSpecifier = url.href
|
||||
}
|
||||
if (bareSpecifier.endsWith('/')) {
|
||||
bareSpecifier = bareSpecifier.slice(0, -1)
|
||||
}
|
||||
const parts = bareSpecifier.split('/')
|
||||
return (parts.length === 5 && GIT_HOSTERS.has(parts[2]))
|
||||
}
|
||||
|
||||
@@ -107,20 +107,3 @@ test('tarballs from GitHub (is-negative)', async () => {
|
||||
resolvedVia: 'url',
|
||||
})
|
||||
})
|
||||
|
||||
test('ignore direct URLs to repositories', async () => {
|
||||
expect(await resolveFromTarball({ bareSpecifier: 'https://github.com/foo/bar' })).toBeNull()
|
||||
expect(await resolveFromTarball({ bareSpecifier: 'https://github.com/foo/bar/' })).toBeNull()
|
||||
expect(await resolveFromTarball({ bareSpecifier: 'https://gitlab.com/foo/bar' })).toBeNull()
|
||||
expect(await resolveFromTarball({ bareSpecifier: 'https://bitbucket.org/foo/bar' })).toBeNull()
|
||||
})
|
||||
|
||||
test('ignore slash in hash', async () => {
|
||||
// expect resolve from git.
|
||||
let hash = 'path:/packages/simple-react-app'
|
||||
expect(await resolveFromTarball({ bareSpecifier: `RexSkz/test-git-subdir-fetch#${hash}` })).toBeNull()
|
||||
expect(await resolveFromTarball({ bareSpecifier: `RexSkz/test-git-subdir-fetch#${encodeURIComponent(hash)}` })).toBeNull()
|
||||
hash = 'heads/canary'
|
||||
expect(await resolveFromTarball({ bareSpecifier: `zkochan/is-negative#${hash}` })).toBeNull()
|
||||
expect(await resolveFromTarball({ bareSpecifier: `zkochan/is-negative#${encodeURIComponent(hash)}` })).toBeNull()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user