Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle git+ssh with semver #6239

Merged
merged 2 commits into from Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/khaki-spoons-decide.md
@@ -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).
44 changes: 16 additions & 28 deletions resolving/git-resolver/src/parsePref.ts
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
60 changes: 60 additions & 0 deletions resolving/git-resolver/test/index.ts
Expand Up @@ -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',
})
})