fix(git-resolver): avoid encoded slash in GitLab tarball URL (#11551)

* fix(git-resolver): avoid encoded slash in GitLab tarball URL

hosted-git-info's default GitLab tarball URL routes through
`/api/v4/projects/<user>%2F<project>/...`. The `%2F` survives into the
virtual store directory name (depPathToFilename only escapes raw `/`,
not `%`), and Node refuses to import any module whose path contains an
encoded slash. The same URL is also intermittently rejected by GitLab
with a 406.

Override the GitLab tarballtemplate to the `/-/archive/` URL, which works
for both public and private repos and contains no encoded slashes.

Closes #11533

* test: avoid cspell-flagged words

* test: keep existing gitlab assertions, only add new ones

Restore the skipped tests' original API-URL assertions; they document the
old expected shape and weren't running anyway. Add the new `/-/archive/`
URL to the pick-fetcher fixture as an additional case so both shapes are
exercised.
This commit is contained in:
Zoltan Kochan
2026-05-08 22:29:27 +02:00
committed by GitHub
parent 219854fab8
commit 39e42665b3
5 changed files with 38 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/resolving.git-resolver": patch
"pnpm": patch
---
Fixed installation of GitLab-hosted dependencies. pnpm now downloads the tarball from `https://gitlab.com/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz` instead of the GitLab API endpoint that contained an encoded slash (`%2F`) between user and project. The encoded slash both triggered `406 Not Acceptable` responses from GitLab and produced virtual store directory names that Node refused to import (`ERR_INVALID_MODULE_SPECIFIER`) [#11533](https://github.com/pnpm/pnpm/issues/11533).

View File

@@ -330,6 +330,7 @@
"szia",
"tabtab",
"taffydb",
"tarballtemplate",
"teambit",
"tempy",
"testcase",

View File

@@ -33,6 +33,7 @@ test.each([
'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',
'https://gitlab.com/pnpm/git-resolver/-/archive/988c61e11dc8d9ca0b5580cb15291951812549dc/git-resolver-988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
])('should pick gitHostedTarball fetcher', async (tarball) => {
const gitHostedTarball = jest.fn() as FetchFunction
const fetcher = await pickFetcher(createMockFetchers({ gitHostedTarball }), { tarball })

View File

@@ -122,6 +122,7 @@ async function fromHostedGit (hosted: any, dispatcherOptions: DispatcherOptions)
fetchSpec: fetchSpec!,
hosted: {
...hosted,
tarballtemplate: hosted.type === 'gitlab' ? gitlabTarballTemplate : hosted.tarballtemplate,
_fill: hosted._fill,
tarball: hosted.tarball,
},
@@ -130,6 +131,14 @@ async function fromHostedGit (hosted: any, dispatcherOptions: DispatcherOptions)
}
}
// hosted-git-info's default GitLab tarball URL contains an encoded slash
// (`%2F`) which survives into the virtual store directory name and makes
// Node refuse to import the package (ERR_INVALID_MODULE_SPECIFIER).
function gitlabTarballTemplate ({ domain, user, project, committish }: { domain: string, user: string, project: string, committish: string | null }): string {
const ref = committish ? encodeURIComponent(committish) : 'HEAD'
return `https://${domain}/${user}/${project}/-/archive/${ref}/${project}-${ref}.tar.gz`
}
async function isRepoPublic (httpsUrl: string, dispatcherOptions: DispatcherOptions): Promise<boolean> {
try {
const response = await fetchWithDispatcher(httpsUrl.replace(/\.git$/, ''), { method: 'HEAD', redirect: 'manual', retry: { retries: 0 }, dispatcherOptions })

View File

@@ -370,6 +370,27 @@ test('resolveFromGit() gitlab with colon in the URL', async () => {
})
})
// Regression test for #11533: the tarball URL must not contain `%2F`,
// otherwise GitLab returns 406 and Node refuses to import the package
// (the encoded slash ends up in the virtual store directory name).
test('resolveFromGit() gitlab tarball uses /-/archive/ URL without encoded slash', async () => {
const headCommit = '988c61e11dc8d9ca0b5580cb15291951812549dc'
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 () => ({ stdout: `${headCommit}\tHEAD` }))
const resolveResult = await resolveFromGit({ bareSpecifier: 'https://gitlab.com/pnpmjs/git-resolver' })
expect(resolveResult).toStrictEqual({
id: `https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz`,
normalizedBareSpecifier: 'gitlab:pnpmjs/git-resolver',
resolution: {
tarball: `https://gitlab.com/pnpmjs/git-resolver/-/archive/${headCommit}/git-resolver-${headCommit}.tar.gz`,
gitHosted: true,
},
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' })