mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-30 04:52:04 -04:00
fix: handle git+ssh with semver (#6239)
This commit is contained in:
6
.changeset/khaki-spoons-decide.md
Normal file
6
.changeset/khaki-spoons-decide.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/git-resolver": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix git-hosted dependencies referenced via `git+ssh` that use semver selectors [#6239](https://github.com/pnpm/pnpm/pull/6239).
|
||||
@@ -38,15 +38,9 @@ export async function parsePref (pref: string): Promise<HostedPackageSpec | null
|
||||
if (colonsPos === -1) return null
|
||||
const protocol = pref.slice(0, colonsPos)
|
||||
if (protocol && gitProtocols.has(protocol.toLocaleLowerCase())) {
|
||||
const urlparse = new URL(escapeColon(pref))
|
||||
const correctPref = correctUrl(pref)
|
||||
const urlparse = new URL(correctPref)
|
||||
if (!urlparse?.protocol) return null
|
||||
const match = urlparse.protocol === 'git+ssh:' && matchGitScp(pref)
|
||||
if (match) {
|
||||
return {
|
||||
...match,
|
||||
normalizedPref: pref,
|
||||
}
|
||||
}
|
||||
|
||||
const committish = (urlparse.hash?.length > 1) ? decodeURIComponent(urlparse.hash.slice(1)) : null
|
||||
return {
|
||||
@@ -58,13 +52,6 @@ export async function parsePref (pref: string): Promise<HostedPackageSpec | null
|
||||
return null
|
||||
}
|
||||
|
||||
function escapeColon (url: string) {
|
||||
if (!url.includes('@')) return url
|
||||
const [front, ...backs] = url.split('@')
|
||||
const escapedBacks = backs.map(e => e.replace(/:([^/\d]|\d+[^:/\d])/, ':/$1'))
|
||||
return [front, ...escapedBacks].join('@')
|
||||
}
|
||||
|
||||
function urlToFetchSpec (urlparse: URL) {
|
||||
urlparse.hash = ''
|
||||
const fetchSpec = url.format(urlparse)
|
||||
@@ -151,18 +138,19 @@ function setGitCommittish (committish: string | null) {
|
||||
return { gitCommittish: committish }
|
||||
}
|
||||
|
||||
function matchGitScp (spec: string) {
|
||||
// git ssh specifiers are overloaded to also use scp-style git
|
||||
// specifiers, so we have to parse those out and treat them special.
|
||||
// They are NOT true URIs, so we can't hand them to `url.parse`.
|
||||
//
|
||||
// This regex looks for things that look like:
|
||||
// git+ssh://git@my.custom.git.com:username/project.git#deadbeef
|
||||
//
|
||||
// ...and various combinations. The username in the beginning is *required*.
|
||||
const matched = spec.match(/^git\+ssh:\/\/([^:]+:[^#]+(?:\.git)?)(?:#(.*))$/i)
|
||||
return (matched != null) && (matched[1].match(/:[0-9]+\/?.*$/i) == null) && {
|
||||
fetchSpec: matched[1],
|
||||
gitCommittish: matched[2],
|
||||
// handle SCP-like URLs
|
||||
// see https://github.com/yarnpkg/yarn/blob/5682d55/src/util/git.js#L103
|
||||
function correctUrl (giturl: string) {
|
||||
const parsed = url.parse(giturl.replace(/^git\+/, '')) // eslint-disable-line n/no-deprecated-api
|
||||
|
||||
if (parsed.protocol === 'ssh:' &&
|
||||
parsed.hostname &&
|
||||
parsed.pathname &&
|
||||
parsed.pathname.startsWith('/:') &&
|
||||
parsed.port === null) {
|
||||
parsed.pathname = parsed.pathname.replace(/^\/:/, '')
|
||||
return url.format(parsed)
|
||||
}
|
||||
|
||||
return giturl
|
||||
}
|
||||
|
||||
@@ -423,3 +423,63 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/heads/master\
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolve an internal repository using SSH protocol with range semver', async () => {
|
||||
git.mockImplementation(async (args: string[]) => {
|
||||
if (!args.includes('ssh://git@example.com/org/repo.git')) throw new Error('')
|
||||
if (args.includes('--refs')) {
|
||||
return {
|
||||
stdout: '\
|
||||
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
|
||||
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39\
|
||||
',
|
||||
}
|
||||
}
|
||||
return {
|
||||
stdout: '0000000000000000000000000000000000000000\tHEAD\n\
|
||||
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
|
||||
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
|
||||
}
|
||||
})
|
||||
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38' })
|
||||
expect(resolveResult).toStrictEqual({
|
||||
id: 'example.com/org/repo/cba04669e621b85fbdb33371604de1a2898e68e9',
|
||||
normalizedPref: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38',
|
||||
resolution: {
|
||||
commit: 'cba04669e621b85fbdb33371604de1a2898e68e9',
|
||||
repo: 'ssh://git@example.com/org/repo.git',
|
||||
type: 'git',
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
})
|
||||
|
||||
test('resolve an internal repository using SSH protocol with range semver and SCP-like URL', async () => {
|
||||
git.mockImplementation(async (args: string[]) => {
|
||||
if (!args.includes('ssh://git@example.com/org/repo.git')) throw new Error('')
|
||||
if (args.includes('--refs')) {
|
||||
return {
|
||||
stdout: '\
|
||||
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
|
||||
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39\
|
||||
',
|
||||
}
|
||||
}
|
||||
return {
|
||||
stdout: '0000000000000000000000000000000000000000\tHEAD\n\
|
||||
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
|
||||
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
|
||||
}
|
||||
})
|
||||
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38' })
|
||||
expect(resolveResult).toStrictEqual({
|
||||
id: 'example.com/org/repo/cba04669e621b85fbdb33371604de1a2898e68e9',
|
||||
normalizedPref: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38',
|
||||
resolution: {
|
||||
commit: 'cba04669e621b85fbdb33371604de1a2898e68e9',
|
||||
repo: 'ssh://git@example.com/org/repo.git',
|
||||
type: 'git',
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user