feat(git-resolver): fix private git repo (#6832)

fixes #6827
closes #6829
This commit is contained in:
Khải
2023-07-19 17:48:05 +07:00
committed by GitHub
parent 653e9104c0
commit 22bbe92554
4 changed files with 67 additions and 4 deletions

View File

@@ -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)

View File

@@ -0,0 +1,6 @@
module.exports = jest.createMockFromModule('@pnpm/fetch')
// default implementation
module.exports.fetch.mockImplementation(async (_url, _opts) => {
return { ok: true }
})

View File

@@ -64,9 +64,14 @@ function urlToFetchSpec (urlparse: URL) {
async function fromHostedGit (hosted: any): Promise<HostedPackageSpec> { // 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<HostedPackageSpec> { // 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<HostedPackageSpec> { // 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 })

View File

@@ -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>
(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',