Files
pnpm/resolving/git-resolver/test/index.ts
Zoltan Kochan 60fd20536d fix: pin integrity of git-hosted tarballs in lockfile (#11481)
For git-hosted tarballs (`codeload.github.com` / `gitlab.com` / `bitbucket.org`) the fetcher dropped the integrity it computed while downloading, so the lockfile only ever stored the URL. A compromised git host or man-in-the-middle could serve a substituted tarball on subsequent installs and pnpm would install it — the lockfile had no hash to compare against.

This pins the SHA-512 SRI of the raw tarball in the lockfile, in the same `sha512-<base64>` form npm-registry tarballs use. The only difference is the source: for npm we pass through `dist.integrity`, for git we compute it locally from the downloaded buffer. Subsequent installs validate the download against that integrity in the worker (`addTarballToStore` → `parseIntegrity` → hash compare), so a tampered tarball fails with `TarballIntegrityError`.

## Why git-hosted stays on `gitHostedStoreIndexKey`

The lockfile pins integrity for security, but the *store key* for git-hosted resolutions stays on `gitHostedStoreIndexKey(pkgId, { built })` rather than collapsing under the integrity-based key. Reason: git-hosted tarballs are post-processed (`preparePackage` / `packlist`), so the cached file set depends on whether build scripts ran during fetch. The integrity-only key would fold the built and not-built variants into a single slot, letting one overwrite the other and serving the wrong content if `ignoreScripts` was toggled between runs. Keeping git-hosted on the existing key shape preserves that dimension; the integrity is still validated on every fresh download.

## How the routing stays clean

The naive way to express "use gitHostedStoreIndexKey for git-hosted, integrity key for npm" is to call `isGitHostedPkgUrl(resolution.tarball)` everywhere a store key is computed — fragile, scattered, and easy to forget when adding new readers (Copilot caught two of those during review). Instead, a typed annotation: `TarballResolution` gets an optional `gitHosted: boolean` field. The git resolver sets it; the lockfile loader (`convertToLockfileObject`) backfills it for entries written by older pnpm versions; `toLockfileResolution` carries it through on serialize. Every consumer reads `resolution.gitHosted` directly. URL detection lives in exactly two places — the resolver and the loader — instead of seven.

## Changes

### Security fix
- `fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts` — return the `integrity` that the inner remote-tarball fetch already computed (was being silently dropped by the destructure).

### Lockfile schema (additive)
- `@pnpm/lockfile.types` and `@pnpm/resolving.resolver-base` — `TarballResolution` gains optional `gitHosted: boolean`.
- `@pnpm/resolving.git-resolver` — sets `gitHosted: true` on every git-hosted tarball it produces.
- `@pnpm/lockfile.fs` (`convertToLockfileObject`) — backfills the field on load for older lockfiles via inlined URL detection.
- `@pnpm/lockfile.utils` (`toLockfileResolution`, `pkgSnapshotToResolution`) — preserve / read the field.

### Store-key consumers (now one-line typed reads, dropped the URL-sniffing dep)
- `installing/package-requester` (`getFilesIndexFilePath`)
- `store/pkg-finder` (`readPackageFileMap`)
- `modules-mounter/daemon` (`createFuseHandlers`)
- `building/after-install` (side-effects-cache lookup + write)
- `store/commands/storeStatus`
- `installing/deps-installer` (agent-mode store-controller wrapper)

### Fetcher routing
- `fetching/pick-fetcher` — `pickFetcher` prefers `resolution.gitHosted`; URL fallback retained for ad-hoc resolutions.

### Tests
- New integrity-validation test in `tarball-fetcher` (mismatched `integrity` on the resolution must throw `TarballIntegrityError`).
- New git-hosted lookup test in `pkg-finder` asserting routing through `gitHostedStoreIndexKey` even when integrity is present.
- New `toLockfileResolution` test asserting `gitHosted: true` flows through serialization.
- `fromRepo.ts` lockfile snapshot updated for the now-pinned integrity + `gitHosted: true`.
- `git-resolver` tests updated to assert `gitHosted: true` in produced resolutions.
2026-05-06 13:22:25 +02:00

610 lines
27 KiB
TypeScript

/// <reference path="../../../__typings__/index.d.ts"/>
import path from 'node:path'
import { beforeEach, expect, jest, test } from '@jest/globals'
import isWindows from 'is-windows'
const { fetchWithDispatcher: fetchWithDispatcherOriginal } = await import('@pnpm/network.fetch')
jest.unstable_mockModule('@pnpm/network.fetch', () => ({
fetchWithDispatcher: jest.fn(),
}))
const { gracefulGit: gitOriginal } = await import('graceful-git')
jest.unstable_mockModule('graceful-git', () => ({
gracefulGit: jest.fn(),
}))
const { fetchWithDispatcher } = await import('@pnpm/network.fetch')
const { gracefulGit: git } = await import('graceful-git')
const { createGitResolver } = await import('@pnpm/resolving.git-resolver')
const resolveFromGit = createGitResolver({})
beforeEach(() => {
jest.mocked(git).mockImplementation(gitOriginal)
jest.mocked(fetchWithDispatcher).mockImplementation(fetchWithDispatcherOriginal)
})
function mockFetchAsPrivate (): void {
jest.mocked(fetchWithDispatcher).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({ bareSpecifier: 'zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
normalizedBareSpecifier: 'github:zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with no commit', async () => {
// This is repeated twice because there was a bug which caused the specifier
// to contain the commit hash on second call.
// The issue occurred because .hosted field (which is class from the 'hosted-git-info' package)
// was mutated. A 'committish' field was added to it.
for (let i = 0; i < 2; i++) {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative' }) // eslint-disable-line no-await-in-loop
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
normalizedBareSpecifier: 'github:zkochan/is-negative',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
}
})
test('resolveFromGit() with no commit, when main branch is not master', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zoli-forks/cmd-shim' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
normalizedBareSpecifier: 'github:zoli-forks/cmd-shim',
resolution: {
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with partial commit', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zoli-forks/cmd-shim#a00a83a' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
normalizedBareSpecifier: 'github:zoli-forks/cmd-shim#a00a83a',
resolution: {
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with partial commit that is a branch name', async () => {
await expect(
resolveFromGit({ bareSpecifier: 'pnpm-e2e/simple-pkg#deadbeef' })
).rejects.toThrow(/resolved commit [0-9a-f]{40} from commit-ish reference deadbeef/)
})
test('resolveFromGit() with branch', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative#canary' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
normalizedBareSpecifier: 'github:zkochan/is-negative#canary',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with branch relative to refs', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative#heads/canary' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
normalizedBareSpecifier: 'github:zkochan/is-negative#heads/canary',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with tag', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test.skip('resolveFromGit() with tag (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'andreineculau/npm-publish-git#v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with strict semver', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative#semver:1.0.0' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
normalizedBareSpecifier: 'github:zkochan/is-negative#semver:1.0.0',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test.skip('resolveFromGit() with strict semver (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'andreineculau/npm-publish-git#semver:v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#semver:v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with range semver', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'zkochan/is-negative#semver:^1.0.0' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3',
normalizedBareSpecifier: 'github:zkochan/is-negative#semver:^1.0.0',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test.skip('resolveFromGit() with range semver (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'andreineculau/npm-publish-git#semver:<=v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#semver:<=v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with sub folder', async () => {
const headCommit = '2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22'
jest.mocked(fetchWithDispatcher).mockImplementation(async (_url, _opts) => {
return { ok: true } as any // eslint-disable-line @typescript-eslint/no-explicit-any
})
jest.mocked(git).mockImplementation(async (args: string[]) => {
if (args.includes('--exit-code')) {
return { stdout: `${headCommit}\tHEAD` }
}
return { stdout: `${headCommit}\tHEAD` }
})
const resolveResult = await resolveFromGit({ bareSpecifier: 'github:RexSkz/test-git-subfolder-fetch.git#path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${headCommit}#path:/packages/simple-react-app`,
normalizedBareSpecifier: 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
resolution: {
tarball: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${headCommit}`,
path: '/packages/simple-react-app',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() with both sub folder and branch', async () => {
const betaCommit = '777e8a3e78cc89bbf41fb3fd9f6cf922d5463313'
jest.mocked(fetchWithDispatcher).mockImplementation(async (_url, _opts) => {
return { ok: true } as any // eslint-disable-line @typescript-eslint/no-explicit-any
})
jest.mocked(git).mockImplementation(async (args: string[]) => {
if (args.includes('--exit-code')) {
return { stdout: `${betaCommit}\tHEAD` }
}
return { stdout: `${betaCommit}\trefs/heads/beta` }
})
const resolveResult = await resolveFromGit({ bareSpecifier: 'github:RexSkz/test-git-subfolder-fetch.git#beta&path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${betaCommit}#path:/packages/simple-react-app`,
normalizedBareSpecifier: 'github:RexSkz/test-git-subfolder-fetch#beta&path:/packages/simple-react-app',
resolution: {
tarball: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${betaCommit}`,
path: '/packages/simple-react-app',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() fails when ref not found', async () => {
await expect(
resolveFromGit({ bareSpecifier: 'zkochan/is-negative#bad-ref' })
).rejects.toThrow(/Could not resolve bad-ref to a commit of (https|git):\/\/github.com\/zkochan\/is-negative.git./)
})
test('resolveFromGit() fails when semver ref not found', async () => {
await expect(
resolveFromGit({ bareSpecifier: 'zkochan/is-negative#semver:^100.0.0' })
).rejects.toThrow(/Could not resolve \^100.0.0 to a commit of (https|git):\/\/github.com\/zkochan\/is-negative.git. Available versions are: 1.0.0, 1.0.1, 2.0.0, 2.0.1, 2.0.2, 2.1.0/)
})
test('resolveFromGit() with commit from non-github repo', async () => {
// TODO: make it pass on Windows
if (isWindows()) {
return
}
const localPath = process.cwd()
const resolveResult = await resolveFromGit({ bareSpecifier: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc` })
expect(resolveResult).toStrictEqual({
id: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc`,
normalizedBareSpecifier: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc`,
resolution: {
commit: '988c61e11dc8d9ca0b5580cb15291951812549dc',
repo: `file://${localPath}`,
type: 'git',
},
resolvedVia: 'git-repository',
})
})
// TODO: make it pass on CI servers
test.skip('resolveFromGit() with commit from non-github repo with no commit', async () => {
const localPath = path.resolve('..', '..')
const result = await git(['rev-parse', 'origin/master'], { retries: 0 })
const hash: string = result.stdout.trim()
const resolveResult = await resolveFromGit({ bareSpecifier: `git+file://${localPath}` })
expect(resolveResult).toStrictEqual({
id: `git+file://${localPath}#${hash}`,
normalizedBareSpecifier: `git+file://${localPath}`,
resolution: {
commit: hash,
repo: `file://${localPath}`,
type: 'git',
},
resolvedVia: 'git-repository',
})
})
// Stopped working. Environmental issue.
test.skip('resolveFromGit() bitbucket with commit', async () => {
// TODO: make it pass on Windows
if (isWindows()) {
return
}
const resolveResult = await resolveFromGit({ bareSpecifier: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'https://bitbucket.org/pnpmjs/git-resolver/get/988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
// Stopped working. Environmental issue.
test.skip('resolveFromGit() bitbucket with no commit', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'bitbucket:pnpmjs/git-resolver' })
const result = await git(['ls-remote', '--refs', 'https://bitbucket.org/pnpmjs/git-resolver.git', 'master'], { retries: 0 })
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver',
resolution: {
tarball: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
},
resolvedVia: 'git-repository',
})
})
// Stopped working. Environmental issue.
test.skip('resolveFromGit() bitbucket with branch', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'bitbucket:pnpmjs/git-resolver#master' })
const result = await git(['ls-remote', '--refs', 'https://bitbucket.org/pnpmjs/git-resolver.git', 'master'], { retries: 0 })
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver#master',
resolution: {
tarball: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
},
resolvedVia: 'git-repository',
})
})
// Stopped working. Environmental issue.
test.skip('resolveFromGit() bitbucket with tag', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'bitbucket:pnpmjs/git-resolver#0.3.4' })
expect(resolveResult).toStrictEqual({
id: 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz',
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver#0.3.4',
resolution: {
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() gitlab with colon in the URL', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'ssh://git@gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@gitlab/pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
normalizedBareSpecifier: 'ssh://git@gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
commit: '988c61e11dc8d9ca0b5580cb15291951812549dc',
repo: 'ssh://git@gitlab/pnpm/git-resolver',
type: 'git',
},
resolvedVia: 'git-repository',
})
})
// This test stopped working. Probably an environmental issue.
test.skip('resolveFromGit() gitlab with commit', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=988c61e11dc8d9ca0b5580cb15291951812549dc',
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=988c61e11dc8d9ca0b5580cb15291951812549dc',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
// This test stopped working. Probably an environmental issue.
test.skip('resolveFromGit() gitlab with no commit', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'gitlab:pnpm/git-resolver' })
const result = await git(['ls-remote', '--refs', 'https://gitlab.com/pnpm/git-resolver.git', 'master'], { retries: 0 })
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver',
resolution: {
tarball: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
},
resolvedVia: 'git-repository',
})
})
// This test stopped working. Probably an environmental issue.
test.skip('resolveFromGit() gitlab with branch', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'gitlab:pnpm/git-resolver#master' })
const result = await git(['ls-remote', '--refs', 'https://gitlab.com/pnpm/git-resolver.git', 'master'], { retries: 0 })
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver#master',
resolution: {
tarball: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
},
resolvedVia: 'git-repository',
})
})
// This test stopped working. Probably an environmental issue.
test.skip('resolveFromGit() gitlab with tag', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'gitlab:pnpm/git-resolver#0.3.4' })
expect(resolveResult).toStrictEqual({
id: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=87cf6a67064d2ce56e8cd20624769a5512b83ff9',
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver#0.3.4',
resolution: {
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=87cf6a67064d2ce56e8cd20624769a5512b83ff9',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() normalizes full url', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+ssh://git@github.com:zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() normalizes full url with port', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+ssh://git@github.com:22:zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() normalizes full url (alternative form)', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+ssh://git@github.com/zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
test('resolveFromGit() normalizes full url (alternative form 2)', async () => {
const resolveResult = await resolveFromGit({ bareSpecifier: 'https://github.com/zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
gitHosted: true,
},
resolvedVia: 'git-repository',
})
})
// This test relies on implementation detail.
// current implementation does not try git ls-remote on bareSpecifier with full commit hash, this fake repo url will pass.
test('resolveFromGit() private repo with commit hash', async () => {
// parseBareSpecifier will try to access the repository with --exit-code
git.mockImplementation(() => {
throw new Error('private')
})
mockFetchAsPrivate()
const resolveResult = await resolveFromGit({ bareSpecifier: 'fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@github.com/fake/private-repo.git#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedBareSpecifier: 'github:fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
resolution: {
commit: '2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
repo: 'git+ssh://git@github.com/fake/private-repo.git',
type: 'git',
},
resolvedVia: 'git-repository',
})
})
test('resolve a private repository using the HTTPS protocol without auth token', async () => {
jest.mocked(git).mockImplementation(async (args: string[]) => {
expect(args).toContain('git+ssh://git@github.com/foo/bar.git')
return {
stdout: '0'.repeat(40) + '\tHEAD',
}
})
mockFetchAsPrivate()
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+https://github.com/foo/bar.git' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@github.com/foo/bar.git#0000000000000000000000000000000000000000',
normalizedBareSpecifier: '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 with a commit hash', async () => {
jest.mocked(git).mockImplementation(async (args: string[]) => {
expect(args).toContain('ls-remote')
expect(args).toContain('https://github.com/foo/bar.git')
return {
// cspell:ignore aabbccddeeff
stdout: 'aabbccddeeff\tHEAD',
}
})
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+https://github.com/foo/bar.git#aabbccddeeff' })
expect(resolveResult).toStrictEqual({
id: 'git+https://github.com/foo/bar.git#aabbccddeeff',
normalizedBareSpecifier: 'git+https://github.com/foo/bar.git',
resolution: {
// cspell:ignore aabbccddeeff
commit: 'aabbccddeeff',
repo: 'https://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('')
return { stdout: '0000000000000000000000000000000000000000\tHEAD' }
})
mockFetchAsPrivate()
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git' })
expect(resolveResult).toStrictEqual({
id: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git#0000000000000000000000000000000000000000',
normalizedBareSpecifier: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git',
resolution: {
commit: '0000000000000000000000000000000000000000',
repo: 'https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git',
type: 'git',
},
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('')
return {
stdout: '0000000000000000000000000000000000000000\tHEAD\n\
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
}
})
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@example.com/org/repo.git#cba04669e621b85fbdb33371604de1a2898e68e9',
normalizedBareSpecifier: '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('')
return {
stdout: '0000000000000000000000000000000000000000\tHEAD\n\
ed3de20970d980cf21a07fd8b8732c70d5182303\trefs/tags/v0.0.38\n\
cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
}
})
const resolveResult = await resolveFromGit({ bareSpecifier: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@example.com/org/repo.git#cba04669e621b85fbdb33371604de1a2898e68e9',
normalizedBareSpecifier: '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',
})
})