@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 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 { PnpmError } from '@pnpm/error';
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
- const commit = await resolveRef(parsedSpec.fetchSpec, bareSpecifier, parsedSpec.gitRange);
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 [, host] = auth.split('@');
176
- if (host.includes(':') && !/:\d+$/.test(host)) {
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.16",
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.1",
33
- "@pnpm/network.fetch": "1100.1.11",
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.16",
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",