Skip to content

Commit

Permalink
fix: handle git+ssh with semver (#6239)
Browse files Browse the repository at this point in the history
  • Loading branch information
andreineculau authored and zkochan committed Mar 20, 2023
1 parent 14fb01c commit b05d9df
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 28 deletions.
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',
})
})

0 comments on commit b05d9df

Please sign in to comment.