@pnpm/resolving.git-resolver 1100.1.16 → 1100.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/index.js +52 -4
- package/lib/lsRemote.d.ts +2 -0
- package/lib/lsRemote.js +21 -1
- package/lib/parseBareSpecifier.js +14 -3
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @pnpm/git-resolver
|
|
2
2
|
|
|
3
|
+
## 1100.1.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A git dependency whose `git ls-remote` fails now reports the `ERR_PNPM_GIT_RESOLVE_FAILED` code, naming the dependency instead of printing a bare `git` invocation, with credentials in the repository URL redacted. A specifier that does not ask for SSH resolves over HTTPS, because the URL recorded in the lockfile has to work on every machine that installs it, so the error explains how to substitute the transport on a machine that can only reach the host over SSH (`git config --global url."git@<host>:".insteadOf "https://<host>/"`) [#13743](https://github.com/pnpm/pnpm/issues/13743).
|
|
8
|
+
|
|
9
|
+
A missing `git` executable is reported as one, instead of surfacing the raw failure to start the process.
|
|
10
|
+
|
|
11
|
+
Credentials embedded in a git specifier are redacted from the "Could not resolve \<ref\> to a commit of \<repo\>" errors too.
|
|
12
|
+
|
|
13
|
+
Resolving a public repository makes one `git ls-remote` round-trip instead of two.
|
|
14
|
+
|
|
15
|
+
- An `ssh://` git dependency pointing at a bracketed IPv6 host, such as `ssh://[::1]/repo.git`, is resolved now. Its colons were read as an SCP-style path separator, which turned the address into `[:/1]` and left the specifier unresolvable. Applies to both the TypeScript CLI and pacquet.
|
|
16
|
+
|
|
17
|
+
In the TypeScript CLI, an `ssh://` git dependency written without user info — `ssh://git.example.com/team/repo.git`, `git+ssh://git.example.com:2222/team/repo.git` — no longer fails with `TypeError: Cannot read properties of undefined (reading 'includes')`. Only the `user@host` form worked before.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies:
|
|
20
|
+
- @pnpm/error@1100.1.2
|
|
21
|
+
- @pnpm/network.fetch@1100.1.12
|
|
22
|
+
|
|
3
23
|
## 1100.1.16
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import util from 'node:util';
|
|
3
|
+
import { PnpmError, redactAndSanitize } from '@pnpm/error';
|
|
2
4
|
import semver from 'semver';
|
|
3
5
|
import { createGitHostedPkgId } from './createGitHostedPkgId.js';
|
|
4
6
|
import { lsRemote } from './lsRemote.js';
|
|
@@ -33,7 +35,14 @@ export function createGitResolver(opts) {
|
|
|
33
35
|
const bareSpecifier = parsedSpec.gitCommittish == null || parsedSpec.gitCommittish === ''
|
|
34
36
|
? 'HEAD'
|
|
35
37
|
: parsedSpec.gitCommittish;
|
|
36
|
-
|
|
38
|
+
let commit;
|
|
39
|
+
try {
|
|
40
|
+
commit = await resolveRef(parsedSpec.fetchSpec, bareSpecifier, parsedSpec.gitRange);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
assert(util.types.isNativeError(err));
|
|
44
|
+
throw gitResolveError(err, wantedDependency.bareSpecifier, parsedSpec.fetchSpec);
|
|
45
|
+
}
|
|
37
46
|
let resolution;
|
|
38
47
|
if ((parsedSpec.hosted != null) && !isSsh(parsedSpec.fetchSpec)) {
|
|
39
48
|
// don't use tarball for ssh url, they are likely private repo
|
|
@@ -135,7 +144,7 @@ function resolveRefFromRefs(refs, repo, ref, committish, range) {
|
|
|
135
144
|
commitId = commits[0];
|
|
136
145
|
}
|
|
137
146
|
else {
|
|
138
|
-
throw new Error(`Could not resolve ${ref} to a commit of ${repo}.`);
|
|
147
|
+
throw new Error(`Could not resolve ${ref} to a commit of ${redactAndSanitize(repo)}.`);
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
return commitId;
|
|
@@ -155,11 +164,50 @@ function resolveRefFromRefs(refs, repo, ref, committish, range) {
|
|
|
155
164
|
(refs[`refs/tags/${refVTag}^{}`] || // prefer annotated tags
|
|
156
165
|
refs[`refs/tags/${refVTag}`]);
|
|
157
166
|
if (!commitId) {
|
|
158
|
-
throw new Error(`Could not resolve ${range} to a commit of ${repo}. Available versions are: ${vTags.join(', ')}`);
|
|
167
|
+
throw new Error(`Could not resolve ${range} to a commit of ${redactAndSanitize(repo)}. Available versions are: ${vTags.join(', ')}`);
|
|
159
168
|
}
|
|
160
169
|
return commitId;
|
|
161
170
|
}
|
|
162
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Restate a failed `git ls-remote` as `ERR_PNPM_GIT_RESOLVE_FAILED`, naming the
|
|
174
|
+
* dependency it was resolving. Errors that describe the refs the remote did
|
|
175
|
+
* return (an unknown ref, an ambiguous commit-ish) already say which repository
|
|
176
|
+
* they came from and are left alone.
|
|
177
|
+
*/
|
|
178
|
+
function gitResolveError(err, bareSpecifier, repo) {
|
|
179
|
+
if (err.code !== 'ERR_PNPM_GIT_LS_REMOTE_FAILED')
|
|
180
|
+
return err;
|
|
181
|
+
return new PnpmError('GIT_RESOLVE_FAILED', `Failed to resolve git dependency "${redactAndSanitize(bareSpecifier)}": ${err.message}`, { hint: httpsTransportHint(repo) });
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Guidance for a specifier that resolved over HTTPS on a machine whose git
|
|
185
|
+
* cannot use that transport, or `undefined` when the resolution already went
|
|
186
|
+
* over SSH — there, the transport that failed is the one the specifier asked
|
|
187
|
+
* for.
|
|
188
|
+
*
|
|
189
|
+
* Substituting the transport is git's job rather than pnpm's: the URL pnpm
|
|
190
|
+
* records has to work for every machine that installs the lockfile, while
|
|
191
|
+
* `insteadOf` rewrites it for this one only.
|
|
192
|
+
*/
|
|
193
|
+
function httpsTransportHint(repo) {
|
|
194
|
+
let url;
|
|
195
|
+
try {
|
|
196
|
+
url = new URL(repo);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
202
|
+
return undefined;
|
|
203
|
+
const host = redactAndSanitize(url.host);
|
|
204
|
+
const hostname = redactAndSanitize(url.hostname);
|
|
205
|
+
return `pnpm resolves this specifier over HTTPS because it does not ask for SSH, and the URL it records has to work on every machine that installs the lockfile.
|
|
206
|
+
|
|
207
|
+
If git can only reach ${hostname} over SSH here, substitute the transport locally, leaving the recorded URL alone:
|
|
208
|
+
|
|
209
|
+
git config --global url."git@${hostname}:".insteadOf "${url.protocol}//${host}/"`;
|
|
210
|
+
}
|
|
163
211
|
function isSsh(gitSpec) {
|
|
164
212
|
return gitSpec.slice(0, 10) === 'git+ssh://' ||
|
|
165
213
|
gitSpec.slice(0, 4) === 'git@';
|
package/lib/lsRemote.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* ls-remote invocations must go through this function to keep that guarantee.
|
|
5
5
|
*
|
|
6
6
|
* Failed runs are retried immediately, matching the Rust runner's policy.
|
|
7
|
+
* A run that fails every attempt throws `ERR_PNPM_GIT_LS_REMOTE_FAILED`,
|
|
8
|
+
* which the git resolver restates with the dependency it was resolving.
|
|
7
9
|
*/
|
|
8
10
|
export declare function lsRemote(args: string[], opts: {
|
|
9
11
|
retries: number;
|
package/lib/lsRemote.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PnpmError, redactAndSanitizeMultiline } from '@pnpm/error';
|
|
1
2
|
import { safeExeca as execa } from 'execa';
|
|
2
3
|
/**
|
|
3
4
|
* Runs `git ls-remote` with interactive credential prompts disabled, so it
|
|
@@ -5,6 +6,8 @@ import { safeExeca as execa } from 'execa';
|
|
|
5
6
|
* ls-remote invocations must go through this function to keep that guarantee.
|
|
6
7
|
*
|
|
7
8
|
* Failed runs are retried immediately, matching the Rust runner's policy.
|
|
9
|
+
* A run that fails every attempt throws `ERR_PNPM_GIT_LS_REMOTE_FAILED`,
|
|
10
|
+
* which the git resolver restates with the dependency it was resolving.
|
|
8
11
|
*/
|
|
9
12
|
export async function lsRemote(args, opts) {
|
|
10
13
|
let lastErr;
|
|
@@ -21,6 +24,23 @@ export async function lsRemote(args, opts) {
|
|
|
21
24
|
lastErr = err;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
|
-
throw lastErr;
|
|
27
|
+
throw lsRemoteError(lastErr);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* git's stderr is untrusted: the repository URL it echoes back can carry
|
|
31
|
+
* `user:pass@` credentials, so it goes through
|
|
32
|
+
* {@link redactAndSanitizeMultiline} rather than being restated verbatim.
|
|
33
|
+
*/
|
|
34
|
+
function lsRemoteError(err) {
|
|
35
|
+
return new PnpmError('GIT_LS_REMOTE_FAILED', `git ls-remote failed: ${redactAndSanitizeMultiline(lsRemoteFailureDetail(err))}`);
|
|
36
|
+
}
|
|
37
|
+
function lsRemoteFailureDetail(err) {
|
|
38
|
+
if (err.code === 'ENOENT') {
|
|
39
|
+
return '`git` executable not found on PATH. Install git to resolve git-hosted packages.';
|
|
40
|
+
}
|
|
41
|
+
const stderr = err.stderr?.trim();
|
|
42
|
+
if (stderr != null && stderr !== '')
|
|
43
|
+
return stderr;
|
|
44
|
+
return err.message ?? String(err);
|
|
25
45
|
}
|
|
26
46
|
//# sourceMappingURL=lsRemote.js.map
|
|
@@ -56,9 +56,16 @@ async function fromHostedGit(hosted, dispatcherOptions) {
|
|
|
56
56
|
// (`shortcut`, `https`, ...) an SSH remote was never asked for, and recording one
|
|
57
57
|
// in the lockfile breaks installs in environments without SSH keys, so every
|
|
58
58
|
// HTTPS transport is exhausted first.
|
|
59
|
+
//
|
|
60
|
+
// Such a specifier therefore resolves over HTTPS whether or not git can reach
|
|
61
|
+
// the host that way on this machine, and its HTTPS access is not probed at all:
|
|
62
|
+
// the probe could not change what is recorded, and a machine that reaches the
|
|
63
|
+
// host only over SSH substitutes the transport itself through git's
|
|
64
|
+
// `url.<base>.insteadOf`. Only an explicit SSH URL, which HTTPS may displace,
|
|
65
|
+
// is worth the round-trip.
|
|
59
66
|
const preferSsh = hosted.default === 'sshurl';
|
|
60
67
|
const repoIsPublic = httpsUrl != null && await isRepoPublic(httpsUrl, dispatcherOptions);
|
|
61
|
-
if (httpsUrl && repoIsPublic && await accessRepository(httpsUrl)) {
|
|
68
|
+
if (httpsUrl && repoIsPublic && (!preferSsh || await accessRepository(httpsUrl))) {
|
|
62
69
|
fetchSpec = httpsUrl;
|
|
63
70
|
}
|
|
64
71
|
if (!fetchSpec && preferSsh && sshUrl && await accessRepository(sshUrl)) {
|
|
@@ -172,8 +179,12 @@ function correctUrl(gitUrl) {
|
|
|
172
179
|
_gitUrl = _gitUrl.slice(0, hashIndex);
|
|
173
180
|
}
|
|
174
181
|
const [auth, ...pathname] = _gitUrl.slice(6).split('/');
|
|
175
|
-
const
|
|
176
|
-
|
|
182
|
+
const userInfoEnd = auth.lastIndexOf('@');
|
|
183
|
+
const host = userInfoEnd === -1 ? auth : auth.slice(userInfoEnd + 1);
|
|
184
|
+
// The colons of a bracketed IPv6 literal belong to the address.
|
|
185
|
+
const bracketEnd = host.startsWith('[') ? host.indexOf(']') : -1;
|
|
186
|
+
const afterHost = bracketEnd === -1 ? host : host.slice(bracketEnd + 1);
|
|
187
|
+
if (afterHost.includes(':') && !/:\d+$/.test(afterHost)) {
|
|
177
188
|
const authArr = auth.split(':');
|
|
178
189
|
const protocol = gitUrl.split('://')[0];
|
|
179
190
|
gitUrl = `${protocol}://${authArr.slice(0, -1).join(':') + '/' + authArr[authArr.length - 1]}${pathname.length ? '/' + pathname.join('/') : ''}${hash}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.git-resolver",
|
|
3
|
-
"version": "1100.1.
|
|
3
|
+
"version": "1100.1.17",
|
|
4
4
|
"description": "Resolver for git-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pnpm/error": "1100.1.
|
|
33
|
-
"@pnpm/network.fetch": "1100.1.
|
|
32
|
+
"@pnpm/error": "1100.1.2",
|
|
33
|
+
"@pnpm/network.fetch": "1100.1.12",
|
|
34
34
|
"@pnpm/resolving.resolver-base": "1101.1.0",
|
|
35
35
|
"execa": "npm:safe-execa@0.3.0",
|
|
36
36
|
"hosted-git-info": "npm:@pnpm/hosted-git-info@1.0.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@jest/globals": "30.4.1",
|
|
41
|
-
"@pnpm/resolving.git-resolver": "1100.1.
|
|
41
|
+
"@pnpm/resolving.git-resolver": "1100.1.17",
|
|
42
42
|
"@types/hosted-git-info": "^3.0.5",
|
|
43
43
|
"@types/is-windows": "^1.0.2",
|
|
44
44
|
"@types/semver": "7.8.0",
|