@pnpm/fetching.git-fetcher 1102.0.13 → 1102.0.15

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/index.js +84 -7
  3. package/package.json +11 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @pnpm/git-fetcher
2
2
 
3
+ ## 1102.0.15
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies:
8
+ - @pnpm/error@1100.1.3
9
+ - @pnpm/exec.prepare-package@1100.0.33
10
+ - @pnpm/fetching.fetcher-base@1100.2.8
11
+ - @pnpm/resolving.git-resolver@1100.1.18
12
+ - @pnpm/store.index@1100.2.5
13
+
14
+ ## 1102.0.14
15
+
16
+ ### Patch Changes
17
+
18
+ - A git dependency whose clone (or shallow fetch) fails now reports which package it belongs to, under the `ERR_PNPM_GIT_FETCH_FAILED` code, with credentials in the repository URL redacted. When the lockfile records an SSH remote, the error also explains that fetching it needs an SSH key for that host, and that a lockfile entry written before pnpm v11.21 can be re-recorded over HTTPS with `pnpm update <package>` [#13743](https://github.com/pnpm/pnpm/issues/13743).
19
+
20
+ - Updated dependencies:
21
+ - @pnpm/error@1100.1.2
22
+ - @pnpm/exec.prepare-package@1100.0.32
23
+ - @pnpm/resolving.git-resolver@1100.1.17
24
+ - @pnpm/store.index@1100.2.4
25
+
3
26
  ## 1102.0.13
4
27
 
5
28
  ### Patch Changes
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import assert from 'node:assert';
2
2
  import path from 'node:path';
3
3
  import { URL } from 'node:url';
4
4
  import util from 'node:util';
5
- import { PnpmError } from '@pnpm/error';
5
+ import { PnpmError, redactAndSanitize, redactAndSanitizeMultiline } from '@pnpm/error';
6
6
  import { preparePackage } from '@pnpm/exec.prepare-package';
7
7
  import { packlist } from '@pnpm/fs.packlist';
8
8
  import { globalWarn } from '@pnpm/logger';
@@ -18,13 +18,19 @@ export function createGitFetcher(createOpts) {
18
18
  throw new PnpmError('INVALID_GIT_COMMIT', `Invalid git commit hash "${resolution.commit}" for repository "${resolution.repo}". Expected a 40-character hexadecimal SHA.`);
19
19
  }
20
20
  const tempLocation = await cafs.tempDir();
21
- if (allowedHosts.size > 0 && shouldUseShallow(resolution.repo, allowedHosts)) {
22
- await execGit(['init'], { cwd: tempLocation });
23
- await execGit(['remote', 'add', 'origin', resolution.repo], { cwd: tempLocation });
24
- await execGit(['fetch', '--depth', '1', 'origin', resolution.commit], { cwd: tempLocation });
21
+ try {
22
+ if (allowedHosts.size > 0 && shouldUseShallow(resolution.repo, allowedHosts)) {
23
+ await execGit(['init'], { cwd: tempLocation });
24
+ await execGit(['remote', 'add', 'origin', resolution.repo], { cwd: tempLocation });
25
+ await execGit(['fetch', '--depth', '1', 'origin', resolution.commit], { cwd: tempLocation });
26
+ }
27
+ else {
28
+ await execGit(['clone', resolution.repo, tempLocation]);
29
+ }
25
30
  }
26
- else {
27
- await execGit(['clone', resolution.repo, tempLocation]);
31
+ catch (err) {
32
+ assert(util.types.isNativeError(err));
33
+ throw gitFetchError(err, resolution.repo, opts.pkg?.name);
28
34
  }
29
35
  await execGit(['checkout', resolution.commit], { cwd: tempLocation });
30
36
  const receivedCommit = await execGit(['rev-parse', 'HEAD'], { cwd: tempLocation });
@@ -73,6 +79,77 @@ export function createGitFetcher(createOpts) {
73
79
  function isValidCommitHash(commit) {
74
80
  return /^[0-9a-f]{40}$/i.test(commit);
75
81
  }
82
+ /**
83
+ * Restate a failure of the transport-touching git invocations, naming the
84
+ * package the resolution belongs to.
85
+ *
86
+ * Every interpolated value is untrusted: a lockfile URL can carry `user:pass@`
87
+ * credentials, and git echoes it back through stderr. Only the values go
88
+ * through {@link redactAndSanitize} — it strips control characters, which would
89
+ * collapse the deliberately multi-line hint.
90
+ */
91
+ function gitFetchError(err, repo, pkgName) {
92
+ if (err.code === 'ENOENT') {
93
+ return new PnpmError('GIT_FETCHER_GIT_NOT_FOUND', '`git` executable not found on PATH. Install git to fetch git-hosted packages.');
94
+ }
95
+ const safePkgName = pkgName == null ? undefined : redactAndSanitize(pkgName);
96
+ const subject = safePkgName == null ? '' : `"${safePkgName}" `;
97
+ return new PnpmError('GIT_FETCH_FAILED', `Failed to fetch ${subject}from the git repository "${redactAndSanitize(repo)}": ${redactAndSanitizeMultiline(gitFailureDetail(err))}`, { hint: sshRemediationHint(repo, safePkgName) });
98
+ }
99
+ // git appends the child's stderr to its own message, which repeats the repository
100
+ // and leaks the store's temp directory. The stderr alone is what the user needs.
101
+ function gitFailureDetail(err) {
102
+ const stderr = err.stderr?.trim();
103
+ return stderr == null || stderr === '' ? err.message : stderr;
104
+ }
105
+ /**
106
+ * Guidance for a git dependency locked to an SSH remote, or `undefined` when the
107
+ * lockfile records a transport that needs no key.
108
+ *
109
+ * A lockfile written before pnpm v11.21 could record an SSH URL for a dependency
110
+ * whose specifier never asked for SSH, and resolution is skipped while that
111
+ * lockfile stays up to date — so the entry survives the upgrade that fixed it and
112
+ * the install keeps failing wherever no SSH key is configured.
113
+ */
114
+ function sshRemediationHint(repo, pkgName) {
115
+ const host = sshRepoHost(repo);
116
+ if (host == null)
117
+ return undefined;
118
+ return `The lockfile records an SSH remote for this dependency, so fetching it needs an SSH key for ${redactAndSanitize(host)}.
119
+
120
+ If its specifier does not ask for SSH (for example "github:owner/repo"), the lockfile entry was written before pnpm v11.21 and can be re-recorded over HTTPS:
121
+
122
+ pnpm update ${pkgName ?? '<package>'}
123
+
124
+ "pnpm install --force" and "pnpm install --resolution-only" do not re-resolve git dependencies, so neither clears it.`;
125
+ }
126
+ /**
127
+ * The host an SSH git reference points at, or `undefined` if `repo` is not one.
128
+ *
129
+ * Covers the URL form (`[git+]ssh://[user@]host[:port]/path`) and the scp-style
130
+ * shorthand (`[user@]host:path`) that carries no scheme. The `user@` is mandatory
131
+ * in the shorthand, which is what keeps a Windows drive path (`C:\repo`) from
132
+ * being read as a host.
133
+ */
134
+ function sshRepoHost(repo) {
135
+ const sshUrl = repo.replace(/^git\+/, '');
136
+ if (sshUrl.startsWith('ssh://')) {
137
+ try {
138
+ return new URL(sshUrl).hostname || undefined;
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ if (repo.includes('://'))
145
+ return undefined;
146
+ const colonPos = repo.indexOf(':');
147
+ if (colonPos === -1)
148
+ return undefined;
149
+ const authority = repo.slice(0, colonPos);
150
+ const atPos = authority.lastIndexOf('@');
151
+ return atPos === -1 ? undefined : authority.slice(atPos + 1) || undefined;
152
+ }
76
153
  function shouldUseShallow(repoUrl, allowedHosts) {
77
154
  try {
78
155
  const { host } = new URL(repoUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/fetching.git-fetcher",
3
- "version": "1102.0.13",
3
+ "version": "1102.0.15",
4
4
  "description": "A fetcher for git-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -28,27 +28,27 @@
28
28
  "!*.map"
29
29
  ],
30
30
  "dependencies": {
31
- "@pnpm/error": "1100.1.1",
32
- "@pnpm/exec.prepare-package": "1100.0.31",
33
- "@pnpm/fetching.fetcher-base": "1100.2.7",
31
+ "@pnpm/error": "1100.1.3",
32
+ "@pnpm/exec.prepare-package": "1100.0.33",
33
+ "@pnpm/fetching.fetcher-base": "1100.2.8",
34
34
  "@pnpm/fs.packlist": "1100.0.4",
35
- "@pnpm/resolving.git-resolver": "1100.1.16",
36
- "@pnpm/store.index": "1100.2.3",
35
+ "@pnpm/resolving.git-resolver": "1100.1.18",
36
+ "@pnpm/store.index": "1100.2.5",
37
37
  "@zkochan/rimraf": "^4.0.0",
38
38
  "execa": "npm:safe-execa@0.3.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@pnpm/logger": "^1100.0.0",
42
- "@pnpm/worker": "^1100.2.10"
42
+ "@pnpm/worker": "^1100.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@jest/globals": "30.4.1",
46
- "@pnpm/fetching.git-fetcher": "1102.0.13",
46
+ "@pnpm/fetching.git-fetcher": "1102.0.15",
47
47
  "@pnpm/logger": "1100.0.0",
48
- "@pnpm/store.cafs": "1100.1.18",
49
- "@pnpm/store.create-cafs-store": "1100.0.24",
48
+ "@pnpm/store.cafs": "1100.2.0",
49
+ "@pnpm/store.create-cafs-store": "1100.0.26",
50
50
  "@pnpm/text.ordinal-comparator": "1100.0.0",
51
- "@pnpm/types": "1101.9.0",
51
+ "@pnpm/types": "1102.0.0",
52
52
  "tempy": "3.0.0"
53
53
  },
54
54
  "engines": {