From 22bbe92554d85e1ad47f689c72f2c4779ef27220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=E1=BA=A3i?= Date: Wed, 19 Jul 2023 17:48:05 +0700 Subject: [PATCH] feat(git-resolver): fix private git repo (#6832) fixes #6827 closes #6829 --- .changeset/silly-dolls-grin.md | 6 +++ .../git-resolver/__mocks__/@pnpm/fetch.js | 6 +++ resolving/git-resolver/src/parsePref.ts | 22 +++++++++-- resolving/git-resolver/test/index.ts | 37 +++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 .changeset/silly-dolls-grin.md create mode 100644 resolving/git-resolver/__mocks__/@pnpm/fetch.js diff --git a/.changeset/silly-dolls-grin.md b/.changeset/silly-dolls-grin.md new file mode 100644 index 0000000000..343e9ca873 --- /dev/null +++ b/.changeset/silly-dolls-grin.md @@ -0,0 +1,6 @@ +--- +"pnpm": patch +"@pnpm/git-resolver": patch +--- + +Pass the right scheme to `git ls-remote` in order to prevent a fallback to `git+ssh` that would result in a 'host key verification failed' issue [#6806](https://github.com/pnpm/pnpm/issues/6806) diff --git a/resolving/git-resolver/__mocks__/@pnpm/fetch.js b/resolving/git-resolver/__mocks__/@pnpm/fetch.js new file mode 100644 index 0000000000..ac4188c807 --- /dev/null +++ b/resolving/git-resolver/__mocks__/@pnpm/fetch.js @@ -0,0 +1,6 @@ +module.exports = jest.createMockFromModule('@pnpm/fetch') + +// default implementation +module.exports.fetch.mockImplementation(async (_url, _opts) => { + return { ok: true } +}) diff --git a/resolving/git-resolver/src/parsePref.ts b/resolving/git-resolver/src/parsePref.ts index d4f799da8e..b25b91308e 100644 --- a/resolving/git-resolver/src/parsePref.ts +++ b/resolving/git-resolver/src/parsePref.ts @@ -64,9 +64,14 @@ function urlToFetchSpec (urlparse: URL) { async function fromHostedGit (hosted: any): Promise { // eslint-disable-line let fetchSpec: string | null = null // try git/https url before fallback to ssh url - const gitUrl = hosted.https({ noCommittish: true }) ?? hosted.ssh({ noCommittish: true }) - if (gitUrl && await accessRepository(gitUrl)) { - fetchSpec = gitUrl + const gitHttpsUrl = hosted.https({ noCommittish: true, noGitPlus: true }) + if (gitHttpsUrl && await isRepoPublic(gitHttpsUrl) && await accessRepository(gitHttpsUrl)) { + fetchSpec = gitHttpsUrl + } else { + const gitSshUrl = hosted.ssh({ noCommittish: true }) + if (gitSshUrl && await accessRepository(gitSshUrl)) { + fetchSpec = gitSshUrl + } } if (!fetchSpec) { @@ -91,7 +96,7 @@ async function fromHostedGit (hosted: any): Promise { // esli // npm instead tries git ls-remote directly which prompts user for login credentials. // HTTP HEAD on https://domain/user/repo, strip out ".git" - const response = await fetch(httpsUrl.slice(0, -4), { method: 'HEAD', follow: 0, retry: { retries: 0 } }) + const response = await fetch(httpsUrl.replace(/\.git$/, ''), { method: 'HEAD', follow: 0, retry: { retries: 0 } }) if (response.ok) { fetchSpec = httpsUrl } @@ -119,6 +124,15 @@ async function fromHostedGit (hosted: any): Promise { // esli } } +async function isRepoPublic (httpsUrl: string) { + try { + const response = await fetch(httpsUrl.replace(/\.git$/, ''), { method: 'HEAD', follow: 0, retry: { retries: 0 } }) + return response.ok + } catch (_err) { + return false + } +} + async function accessRepository (repository: string) { try { await git(['ls-remote', '--exit-code', repository, 'HEAD'], { retries: 0 }) diff --git a/resolving/git-resolver/test/index.ts b/resolving/git-resolver/test/index.ts index b0f7f8ee62..eb0265f77f 100644 --- a/resolving/git-resolver/test/index.ts +++ b/resolving/git-resolver/test/index.ts @@ -3,9 +3,18 @@ import path from 'path' import { createGitResolver } from '@pnpm/git-resolver' import git from 'graceful-git' import isWindows from 'is-windows' +import { fetch } from '@pnpm/fetch' const resolveFromGit = createGitResolver({}) +function mockFetchAsPrivate (): void { + type Fetch = typeof fetch + type MockedFetch = jest.MockedFunction + (fetch as MockedFetch).mockImplementation(async (_url, _opts) => { + return { ok: false } as any // eslint-disable-line @typescript-eslint/no-explicit-any + }) +} + test('resolveFromGit() with commit', async () => { const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99' }) expect(resolveResult).toStrictEqual({ @@ -386,6 +395,7 @@ test('resolveFromGit() normalizes full url (alternative form 2)', async () => { // This test relies on implementation detail. // current implementation does not try git ls-remote --refs on pref with full commit hash, this fake repo url will pass. test('resolveFromGit() private repo with commit hash', async () => { + mockFetchAsPrivate() const resolveResult = await resolveFromGit({ pref: 'fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5' }) expect(resolveResult).toStrictEqual({ id: 'github.com/fake/private-repo/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', @@ -399,6 +409,32 @@ test('resolveFromGit() private repo with commit hash', async () => { }) }) +test('resolve a private repository using the HTTPS protocol without auth token', async () => { + git.mockImplementation(async (args: string[]) => { + expect(args).toContain('git+ssh://git@github.com/foo/bar.git') + if (args.includes('--refs')) { + return { + stdout: `\n${'a'.repeat(40)}\trefs/heads/master\n`, + } + } + return { + stdout: '0'.repeat(40) + '\tHEAD', + } + }) + mockFetchAsPrivate() + const resolveResult = await resolveFromGit({ pref: 'git+https://github.com/foo/bar.git' }) + expect(resolveResult).toStrictEqual({ + id: 'github.com/foo/bar/0000000000000000000000000000000000000000', + normalizedPref: 'github:foo/bar', + resolution: { + commit: '0000000000000000000000000000000000000000', + repo: 'git+ssh://git@github.com/foo/bar.git', + type: 'git', + }, + resolvedVia: 'git-repository', + }) +}) + test('resolve a private repository using the HTTPS protocol and an auth token', async () => { git.mockImplementation(async (args: string[]) => { if (!args.includes('https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git')) throw new Error('') @@ -411,6 +447,7 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/heads/master\ } return { stdout: '0000000000000000000000000000000000000000\tHEAD' } }) + mockFetchAsPrivate() const resolveResult = await resolveFromGit({ pref: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git' }) expect(resolveResult).toStrictEqual({ id: '0000000000000000000000000000000000000000+x-oauth-basic@github.com/foo/bar/0000000000000000000000000000000000000000',