From 28796377cbfe09aaf809a87c1b06868be7da2e8a Mon Sep 17 00:00:00 2001 From: Andrei Neculau Date: Mon, 20 Mar 2023 23:40:26 +0100 Subject: [PATCH] fix: handle git+ssh with semver (#6239) --- .changeset/khaki-spoons-decide.md | 6 +++ resolving/git-resolver/src/parsePref.ts | 44 +++++++----------- resolving/git-resolver/test/index.ts | 60 +++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 .changeset/khaki-spoons-decide.md diff --git a/.changeset/khaki-spoons-decide.md b/.changeset/khaki-spoons-decide.md new file mode 100644 index 0000000000..fabc8958f1 --- /dev/null +++ b/.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). diff --git a/resolving/git-resolver/src/parsePref.ts b/resolving/git-resolver/src/parsePref.ts index 8f66058a74..d4f799da8e 100644 --- a/resolving/git-resolver/src/parsePref.ts +++ b/resolving/git-resolver/src/parsePref.ts @@ -38,15 +38,9 @@ export async function parsePref (pref: string): Promise 1) ? decodeURIComponent(urlparse.hash.slice(1)) : null return { @@ -58,13 +52,6 @@ export async function parsePref (pref: string): Promise 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 } diff --git a/resolving/git-resolver/test/index.ts b/resolving/git-resolver/test/index.ts index 50913f4e8f..9f1afe818c 100644 --- a/resolving/git-resolver/test/index.ts +++ b/resolving/git-resolver/test/index.ts @@ -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', + }) +})