diff --git a/.changeset/fix-annotated-git-tags.md b/.changeset/fix-annotated-git-tags.md new file mode 100644 index 0000000000..900257017c --- /dev/null +++ b/.changeset/fix-annotated-git-tags.md @@ -0,0 +1,8 @@ +--- +"@pnpm/git-resolver": patch +"pnpm": patch +--- + +Fix installation of Git dependencies using annotated tags [#10335](https://github.com/pnpm/pnpm/issues/10335). + +Previously, pnpm would store the annotated tag object's SHA in the lockfile instead of the actual commit SHA. This caused `ERR_PNPM_GIT_CHECKOUT_FAILED` errors because the checked-out commit hash didn't match the stored tag object hash. diff --git a/fetching/pick-fetcher/test/pickFetcher.ts b/fetching/pick-fetcher/test/pickFetcher.ts index 3ebfe3179d..1afc27c9fc 100644 --- a/fetching/pick-fetcher/test/pickFetcher.ts +++ b/fetching/pick-fetcher/test/pickFetcher.ts @@ -30,7 +30,7 @@ test('should pick remoteTarball fetcher', async () => { }) test.each([ - 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz', 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz', ])('should pick gitHostedTarball fetcher', async (tarball) => { diff --git a/pkg-manager/core/test/install/fromRepo.ts b/pkg-manager/core/test/install/fromRepo.ts index 12c133e932..29dc2c71ff 100644 --- a/pkg-manager/core/test/install/fromRepo.ts +++ b/pkg-manager/core/test/install/fromRepo.ts @@ -267,12 +267,12 @@ test('re-adding a git repo with a different tag', async () => { lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies?.['is-negative']).toEqual({ specifier: 'github:kevva/is-negative#1.0.1', - version: 'https://codeload.github.com/kevva/is-negative/tar.gz/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72', + version: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3', }) expect(lockfile.packages).toEqual( { - 'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72': { - resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72' }, + 'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3': { + resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3' }, version: '1.0.1', engines: { node: '>=0.10.0' }, }, @@ -348,5 +348,5 @@ test('no hash character for github subdirectory install', async () => { ], testDefaults()) expect(fs.readdirSync('./node_modules/.pnpm')) - .toContain('only-allow@https+++codeload.github.com+pnpm+only-allow+tar.gz+4d577a5a5862a43e752df37a1e8a0c71c3a0084a+path++') + .toContain('only-allow@https+++codeload.github.com+pnpm+only-allow+tar.gz+91ab41994c6a1b7319869fa8864163c9954f56ec+path++') }) diff --git a/resolving/git-resolver/src/index.ts b/resolving/git-resolver/src/index.ts index f76bce5a9f..f882c9f8bd 100644 --- a/resolving/git-resolver/src/index.ts +++ b/resolving/git-resolver/src/index.ts @@ -83,11 +83,11 @@ function resolveVTags (vTags: string[], range: string): string | null { async function getRepoRefs (repo: string, ref: string | null): Promise> { const gitArgs = [repo] - if (ref !== 'HEAD') { - gitArgs.unshift('--refs') - } if (ref) { gitArgs.push(ref) + // Also request the peeled ref for annotated tags (e.g., refs/tags/v1.0.0^{}) + // This is needed because annotated tags have their own SHA, and we need the commit SHA they point to + gitArgs.push(`${ref}^{}`) } // graceful-git by default retries 10 times, reduce to single retry const result = await git(['ls-remote', ...gitArgs], { retries: 1 }) @@ -123,7 +123,8 @@ function resolveRefFromRefs (refs: { [ref: string]: string }, repo: string, ref: if (!commitId) { // check for a partial commit - const commits = committish ? Object.values(refs).filter((value: string) => value.startsWith(ref)) : [] + // Use Set to deduplicate since multiple refs can point to the same commit + const commits = committish ? [...new Set(Object.values(refs).filter((value: string) => value.startsWith(ref)))] : [] if (commits.length === 1) { commitId = commits[0] } else { @@ -133,7 +134,7 @@ function resolveRefFromRefs (refs: { [ref: string]: string }, repo: string, ref: return commitId } else { - const vTags = + const vTags = [...new Set( Object.keys(refs) // using the same semantics of version tags as https://github.com/zkat/pacote .filter((key: string) => /^refs\/tags\/v?\d+\.\d+\.\d+(?:[-+].+)?(?:\^\{\})?$/.test(key)) @@ -143,10 +144,11 @@ function resolveRefFromRefs (refs: { [ref: string]: string }, repo: string, ref: .replace(/\^\{\}$/, '') // accept annotated tags }) .filter((key: string) => semver.valid(key, true)) + )] const refVTag = resolveVTags(vTags, range) const commitId = refVTag && (refs[`refs/tags/${refVTag}^{}`] || // prefer annotated tags - refs[`refs/tags/${refVTag}`]) + refs[`refs/tags/${refVTag}`]) if (!commitId) { throw new Error(`Could not resolve ${range} to a commit of ${repo}. Available versions are: ${vTags.join(', ')}`) diff --git a/resolving/git-resolver/test/index.ts b/resolving/git-resolver/test/index.ts index dbe92b8970..0a9ffd5572 100644 --- a/resolving/git-resolver/test/index.ts +++ b/resolving/git-resolver/test/index.ts @@ -115,10 +115,10 @@ test('resolveFromGit() with branch relative to refs', async () => { 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', }, resolvedVia: 'git-repository', }) @@ -163,10 +163,10 @@ test.skip('resolveFromGit() with strict semver (v-prefixed tag)', async () => { 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/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72', + 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/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3', }, resolvedVia: 'git-repository', }) @@ -392,10 +392,10 @@ test.skip('resolveFromGit() gitlab with tag', async () => { 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', }, resolvedVia: 'git-repository', }) @@ -404,10 +404,10 @@ test('resolveFromGit() normalizes full url', async () => { 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', }, resolvedVia: 'git-repository', }) @@ -416,10 +416,10 @@ test('resolveFromGit() normalizes full url with port', async () => { 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', }, resolvedVia: 'git-repository', }) @@ -428,17 +428,17 @@ test('resolveFromGit() normalizes full url (alternative form)', async () => { 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + 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/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5', + tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933', }, resolvedVia: 'git-repository', }) }) // This test relies on implementation detail. -// current implementation does not try git ls-remote --refs on bareSpecifier with full commit hash, this fake repo url will pass. +// 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(() => { @@ -461,11 +461,6 @@ test('resolveFromGit() private repo with commit hash', async () => { 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') - if (args.includes('--refs')) { - return { - stdout: `\n${'a'.repeat(40)}\trefs/heads/master\n`, - } - } return { stdout: '0'.repeat(40) + '\tHEAD', } @@ -510,13 +505,6 @@ test('resolve a private repository using the HTTPS protocol with a commit hash', 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('') - if (args.includes('--refs')) { - return { - stdout: '\ -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/heads/master\ -', - } - } return { stdout: '0000000000000000000000000000000000000000\tHEAD' } }) mockFetchAsPrivate() @@ -536,14 +524,6 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/heads/master\ 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\ @@ -566,14 +546,6 @@ cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39', 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\