@hominis/fireforge 0.47.1 → 0.48.0
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 +54 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +15 -0
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +2 -0
- package/dist/src/utils/process.js +2 -1
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +81 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +4 -3
- package/docs/patch-workflow.md +35 -1
- package/package.json +7 -3
|
@@ -77,6 +77,27 @@ export declare function hashObjectBatch(repoDir: string, fullPaths: string[]): P
|
|
|
77
77
|
* @returns File content or null if file doesn't exist at that ref
|
|
78
78
|
*/
|
|
79
79
|
export declare function getFileContentAtRef(repoDir: string, filePath: string, ref?: string): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Batched equivalent of {@link getFileContentAtRef}: reads every path at
|
|
82
|
+
* `ref` through one `git cat-file --batch` process instead of one
|
|
83
|
+
* `git show` per file. A Firefox-sized ownership scan paid 1,493 of those,
|
|
84
|
+
* about 18 ms each and almost all of it spawn overhead.
|
|
85
|
+
*
|
|
86
|
+
* The first request line is `ref` itself, so an unresolvable ref throws a
|
|
87
|
+
* {@link GitError} exactly as the per-file helper does, rather than being
|
|
88
|
+
* read as "every path missing". A path reported `missing` maps to `null`,
|
|
89
|
+
* as the per-file helper returns for a path absent at that ref. Anything
|
|
90
|
+
* the batch cannot answer as a plain blob (a path containing a newline,
|
|
91
|
+
* which the line protocol cannot carry, or an object that is not a blob)
|
|
92
|
+
* is fetched through {@link getFileContentAtRef}, so the result for every
|
|
93
|
+
* path is what that helper returns.
|
|
94
|
+
*
|
|
95
|
+
* @param repoDir - Repository directory
|
|
96
|
+
* @param filePaths - Paths relative to the repository root
|
|
97
|
+
* @param ref - Git ref to read from. Defaults to HEAD.
|
|
98
|
+
* @returns Content (or `null` when absent at `ref`) for every input path
|
|
99
|
+
*/
|
|
100
|
+
export declare function getFilesContentAtRef(repoDir: string, filePaths: readonly string[], ref?: string): Promise<Map<string, string | null>>;
|
|
80
101
|
/**
|
|
81
102
|
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
82
103
|
* Uses the same heuristic as git.
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { GitError } from '../errors/git.js';
|
|
5
5
|
import { removeFile } from '../utils/fs.js';
|
|
6
6
|
import { exec } from '../utils/process.js';
|
|
7
|
+
import { execWithInput } from '../utils/process-input.js';
|
|
7
8
|
import { chunkPathspecs, ensureGit, git } from './git-base.js';
|
|
8
9
|
/**
|
|
9
10
|
* Restores a tracked path from HEAD, including staged changes.
|
|
@@ -175,6 +176,80 @@ export async function getFileContentAtRef(repoDir, filePath, ref = 'HEAD') {
|
|
|
175
176
|
}
|
|
176
177
|
return result.stdout;
|
|
177
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Batched equivalent of {@link getFileContentAtRef}: reads every path at
|
|
181
|
+
* `ref` through one `git cat-file --batch` process instead of one
|
|
182
|
+
* `git show` per file. A Firefox-sized ownership scan paid 1,493 of those,
|
|
183
|
+
* about 18 ms each and almost all of it spawn overhead.
|
|
184
|
+
*
|
|
185
|
+
* The first request line is `ref` itself, so an unresolvable ref throws a
|
|
186
|
+
* {@link GitError} exactly as the per-file helper does, rather than being
|
|
187
|
+
* read as "every path missing". A path reported `missing` maps to `null`,
|
|
188
|
+
* as the per-file helper returns for a path absent at that ref. Anything
|
|
189
|
+
* the batch cannot answer as a plain blob (a path containing a newline,
|
|
190
|
+
* which the line protocol cannot carry, or an object that is not a blob)
|
|
191
|
+
* is fetched through {@link getFileContentAtRef}, so the result for every
|
|
192
|
+
* path is what that helper returns.
|
|
193
|
+
*
|
|
194
|
+
* @param repoDir - Repository directory
|
|
195
|
+
* @param filePaths - Paths relative to the repository root
|
|
196
|
+
* @param ref - Git ref to read from. Defaults to HEAD.
|
|
197
|
+
* @returns Content (or `null` when absent at `ref`) for every input path
|
|
198
|
+
*/
|
|
199
|
+
export async function getFilesContentAtRef(repoDir, filePaths, ref = 'HEAD') {
|
|
200
|
+
const contents = new Map();
|
|
201
|
+
const unique = [...new Set(filePaths)];
|
|
202
|
+
const batchable = unique.filter((path) => !path.includes('\n'));
|
|
203
|
+
const fallback = unique.filter((path) => path.includes('\n'));
|
|
204
|
+
if (batchable.length > 0) {
|
|
205
|
+
await ensureGit();
|
|
206
|
+
const requests = [ref, ...batchable.map((path) => `${ref}:${path}`)];
|
|
207
|
+
const result = await execWithInput('git', ['cat-file', '--batch'], requests.join('\n') + '\n', {
|
|
208
|
+
cwd: repoDir,
|
|
209
|
+
});
|
|
210
|
+
if (result.exitCode !== 0) {
|
|
211
|
+
throw new GitError(result.stderr.trim() || 'git cat-file --batch failed', 'cat-file --batch');
|
|
212
|
+
}
|
|
213
|
+
const out = result.stdout;
|
|
214
|
+
let offset = 0;
|
|
215
|
+
for (const [index, request] of requests.entries()) {
|
|
216
|
+
const newline = out.indexOf(0x0a, offset);
|
|
217
|
+
if (newline === -1) {
|
|
218
|
+
throw new GitError(`Truncated cat-file --batch output at ${request}`, 'cat-file --batch');
|
|
219
|
+
}
|
|
220
|
+
const header = out.toString('utf8', offset, newline);
|
|
221
|
+
offset = newline + 1;
|
|
222
|
+
const path = index === 0 ? undefined : batchable[index - 1];
|
|
223
|
+
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
|
|
224
|
+
if (path === undefined) {
|
|
225
|
+
throw new GitError(`Cannot resolve ${ref}: ${header}`, `cat-file --batch ${ref}`);
|
|
226
|
+
}
|
|
227
|
+
if (header.endsWith(' missing'))
|
|
228
|
+
contents.set(path, null);
|
|
229
|
+
else
|
|
230
|
+
fallback.push(path);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const match = /^[0-9a-f]+ (\S+) (\d+)$/.exec(header);
|
|
234
|
+
if (!match) {
|
|
235
|
+
throw new GitError(`Unexpected cat-file --batch header: ${header}`, 'cat-file --batch');
|
|
236
|
+
}
|
|
237
|
+
const size = Number(match[2]);
|
|
238
|
+
const body = out.subarray(offset, offset + size);
|
|
239
|
+
offset += size + 1;
|
|
240
|
+
if (path === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
if (match[1] === 'blob')
|
|
243
|
+
contents.set(path, body.toString('utf8'));
|
|
244
|
+
else
|
|
245
|
+
fallback.push(path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const path of fallback) {
|
|
249
|
+
contents.set(path, await getFileContentAtRef(repoDir, path, ref));
|
|
250
|
+
}
|
|
251
|
+
return contents;
|
|
252
|
+
}
|
|
178
253
|
/**
|
|
179
254
|
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
180
255
|
* Uses the same heuristic as git.
|
|
@@ -39,6 +39,20 @@ export declare function resolveMaxUntrackedFilesPerDir(): number;
|
|
|
39
39
|
* files
|
|
40
40
|
*/
|
|
41
41
|
export declare function expandUntrackedDirectoryEntries(repoDir: string, entries: GitStatusEntry[]): Promise<GitStatusEntry[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Lists the untracked files under each collapsed `dir/` status entry with
|
|
44
|
+
* one `git ls-files` per ARG_MAX chunk, instead of one spawn per directory
|
|
45
|
+
* (a Firefox-sized worktree scan paid 73 of them). The result maps every
|
|
46
|
+
* requested directory to its files in git's order, which is exactly what
|
|
47
|
+
* {@link getUntrackedFilesInDir} returns for that directory alone. `-z`
|
|
48
|
+
* keeps non-ASCII names literal: the newline form C-quotes them under
|
|
49
|
+
* git's default `core.quotePath`, and a quoted path names no file.
|
|
50
|
+
*
|
|
51
|
+
* @param repoDir - Repository directory
|
|
52
|
+
* @param dirs - Collapsed untracked directories, each ending in `/`
|
|
53
|
+
* @returns Files per directory (every input key present, possibly empty)
|
|
54
|
+
*/
|
|
55
|
+
export declare function listUntrackedFilesInDirs(repoDir: string, dirs: readonly string[]): Promise<Map<string, string[]>>;
|
|
42
56
|
/**
|
|
43
57
|
* Gets all untracked files (including files inside untracked directories).
|
|
44
58
|
* @param repoDir - Repository directory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { warn } from '../utils/logger.js';
|
|
3
|
-
import { git } from './git-base.js';
|
|
3
|
+
import { chunkPathspecs, git } from './git-base.js';
|
|
4
4
|
/**
|
|
5
5
|
* True when a porcelain status column carries a rename or copy, i.e. the
|
|
6
6
|
* record is followed by a second NUL-terminated field holding the original
|
|
@@ -95,12 +95,16 @@ export function resolveMaxUntrackedFilesPerDir() {
|
|
|
95
95
|
export async function expandUntrackedDirectoryEntries(repoDir, entries) {
|
|
96
96
|
const expanded = [];
|
|
97
97
|
const maxPerDir = resolveMaxUntrackedFilesPerDir();
|
|
98
|
+
const collapsed = entries
|
|
99
|
+
.filter((entry) => entry.isUntracked && entry.file.endsWith('/'))
|
|
100
|
+
.map((entry) => entry.file);
|
|
101
|
+
const untrackedByDir = await listUntrackedFilesInDirs(repoDir, collapsed);
|
|
98
102
|
for (const entry of entries) {
|
|
99
103
|
if (!entry.isUntracked || !entry.file.endsWith('/')) {
|
|
100
104
|
expanded.push(entry);
|
|
101
105
|
continue;
|
|
102
106
|
}
|
|
103
|
-
const individualFiles =
|
|
107
|
+
const individualFiles = untrackedByDir.get(entry.file) ?? [];
|
|
104
108
|
if (individualFiles.length > maxPerDir) {
|
|
105
109
|
warn(`Untracked directory ${entry.file} contains ${individualFiles.length} files — only the first ${maxPerDir} are listed. Add a .gitignore entry or clean the directory.`);
|
|
106
110
|
}
|
|
@@ -118,6 +122,41 @@ export async function expandUntrackedDirectoryEntries(repoDir, entries) {
|
|
|
118
122
|
}
|
|
119
123
|
return expanded;
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Lists the untracked files under each collapsed `dir/` status entry with
|
|
127
|
+
* one `git ls-files` per ARG_MAX chunk, instead of one spawn per directory
|
|
128
|
+
* (a Firefox-sized worktree scan paid 73 of them). The result maps every
|
|
129
|
+
* requested directory to its files in git's order, which is exactly what
|
|
130
|
+
* {@link getUntrackedFilesInDir} returns for that directory alone. `-z`
|
|
131
|
+
* keeps non-ASCII names literal: the newline form C-quotes them under
|
|
132
|
+
* git's default `core.quotePath`, and a quoted path names no file.
|
|
133
|
+
*
|
|
134
|
+
* @param repoDir - Repository directory
|
|
135
|
+
* @param dirs - Collapsed untracked directories, each ending in `/`
|
|
136
|
+
* @returns Files per directory (every input key present, possibly empty)
|
|
137
|
+
*/
|
|
138
|
+
export async function listUntrackedFilesInDirs(repoDir, dirs) {
|
|
139
|
+
const byDir = new Map(dirs.map((dir) => [dir, []]));
|
|
140
|
+
if (byDir.size === 0)
|
|
141
|
+
return byDir;
|
|
142
|
+
for (const chunk of chunkPathspecs([...byDir.keys()])) {
|
|
143
|
+
const output = await git(['ls-files', '--others', '--exclude-standard', '-z', '--', ...chunk], repoDir);
|
|
144
|
+
for (const file of output.split('\0')) {
|
|
145
|
+
if (file.length === 0)
|
|
146
|
+
continue;
|
|
147
|
+
// Collapsed status directories never nest, so the first matching
|
|
148
|
+
// ancestor is the only one.
|
|
149
|
+
for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) {
|
|
150
|
+
const bucket = byDir.get(file.slice(0, slash + 1));
|
|
151
|
+
if (bucket !== undefined) {
|
|
152
|
+
bucket.push(file);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return byDir;
|
|
159
|
+
}
|
|
121
160
|
/**
|
|
122
161
|
* Gets all untracked files (including files inside untracked directories).
|
|
123
162
|
* @param repoDir - Repository directory
|
|
@@ -52,5 +52,6 @@ export declare function assertBrandingMozconfigAgreement(engineDir: string, mozc
|
|
|
52
52
|
* @param configsDir - Path to the configs directory
|
|
53
53
|
* @param engineDir - Path to the engine directory
|
|
54
54
|
* @param config - FireForge configuration
|
|
55
|
+
* @returns Whether engine/mozconfig was rewritten (its content changed)
|
|
55
56
|
*/
|
|
56
|
-
export declare function generateMozconfig(configsDir: string, engineDir: string, config: FireForgeConfig): Promise<
|
|
57
|
+
export declare function generateMozconfig(configsDir: string, engineDir: string, config: FireForgeConfig): Promise<boolean>;
|
|
@@ -114,6 +114,7 @@ export async function assertBrandingMozconfigAgreement(engineDir, mozconfigPath,
|
|
|
114
114
|
* @param configsDir - Path to the configs directory
|
|
115
115
|
* @param engineDir - Path to the engine directory
|
|
116
116
|
* @param config - FireForge configuration
|
|
117
|
+
* @returns Whether engine/mozconfig was rewritten (its content changed)
|
|
117
118
|
*/
|
|
118
119
|
export async function generateMozconfig(configsDir, engineDir, config) {
|
|
119
120
|
const platform = getPlatform();
|
|
@@ -161,4 +162,5 @@ export async function generateMozconfig(configsDir, engineDir, config) {
|
|
|
161
162
|
// write, before anything consumes mozconfig) keeps `generateMozconfig`
|
|
162
163
|
// the single source of truth for both the render and the sanity-check.
|
|
163
164
|
await assertBrandingMozconfigAgreement(engineDir, outputPath, config);
|
|
165
|
+
return wrote;
|
|
164
166
|
}
|
|
@@ -26,6 +26,18 @@ export interface FuzzyApplyResult {
|
|
|
26
26
|
/** List of `.rej` files created (when falling through to --reject). */
|
|
27
27
|
rejectFiles?: string[];
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The context-reduction ladder shared by the real apply and the dry-run
|
|
31
|
+
* replay: step 0 is an exact apply, step k passes `-C(3-k)`.
|
|
32
|
+
* @param maxFuzz - Maximum context-reduction steps (validated by callers)
|
|
33
|
+
* @returns One `git apply` argument list per step, in order
|
|
34
|
+
*/
|
|
35
|
+
export declare function contextReductionSteps(maxFuzz: number): string[][];
|
|
36
|
+
/**
|
|
37
|
+
* Validates a `maxFuzz` value.
|
|
38
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertValidMaxFuzz(maxFuzz: number): void;
|
|
29
41
|
/**
|
|
30
42
|
* Attempts to apply a patch with escalating context reduction.
|
|
31
43
|
*
|
|
@@ -23,6 +23,29 @@ import { exec } from '../utils/process.js';
|
|
|
23
23
|
import { ensureGit } from './git-base.js';
|
|
24
24
|
/** Git's default diff context width, the ceiling for reduction steps. */
|
|
25
25
|
const GIT_DEFAULT_CONTEXT = 3;
|
|
26
|
+
/**
|
|
27
|
+
* The context-reduction ladder shared by the real apply and the dry-run
|
|
28
|
+
* replay: step 0 is an exact apply, step k passes `-C(3-k)`.
|
|
29
|
+
* @param maxFuzz - Maximum context-reduction steps (validated by callers)
|
|
30
|
+
* @returns One `git apply` argument list per step, in order
|
|
31
|
+
*/
|
|
32
|
+
export function contextReductionSteps(maxFuzz) {
|
|
33
|
+
const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
|
|
34
|
+
const steps = [];
|
|
35
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
36
|
+
steps.push(step > 0 ? [`-C${GIT_DEFAULT_CONTEXT - step}`] : []);
|
|
37
|
+
}
|
|
38
|
+
return steps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates a `maxFuzz` value.
|
|
42
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer
|
|
43
|
+
*/
|
|
44
|
+
export function assertValidMaxFuzz(maxFuzz) {
|
|
45
|
+
if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
|
|
46
|
+
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
26
49
|
// ── Implementation ──
|
|
27
50
|
/**
|
|
28
51
|
* Attempts to apply a patch with escalating context reduction.
|
|
@@ -41,14 +64,12 @@ const GIT_DEFAULT_CONTEXT = 3;
|
|
|
41
64
|
* (including the exact-match one) and fall straight through to `--reject`
|
|
42
65
|
*/
|
|
43
66
|
export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
|
|
44
|
-
|
|
45
|
-
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
|
|
46
|
-
}
|
|
67
|
+
assertValidMaxFuzz(maxFuzz);
|
|
47
68
|
await ensureGit();
|
|
48
69
|
// Try exact match first, then escalate context reduction.
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
const ladder = contextReductionSteps(maxFuzz);
|
|
71
|
+
const maxSteps = ladder.length - 1;
|
|
72
|
+
for (const [step, contextArgs] of ladder.entries()) {
|
|
52
73
|
const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
|
|
53
74
|
cwd: engineDir,
|
|
54
75
|
});
|
|
@@ -63,6 +63,19 @@ export interface PatchedContentContext {
|
|
|
63
63
|
getAffectingPatches: (filePath: string) => readonly PatchInfo[];
|
|
64
64
|
/** Patch body text, memoized per patch path across the whole batch. */
|
|
65
65
|
readPatchBody: (patch: PatchInfo) => Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Reads the HEAD content of every path in one `git cat-file --batch`
|
|
68
|
+
* and holds it for the next {@link computePatched} of each path, which
|
|
69
|
+
* otherwise spawns one `git show` per file. Call it before a loop.
|
|
70
|
+
*/
|
|
71
|
+
prefetchBase: (filePaths: readonly string[]) => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Hashes every live file in one `git hash-object` per ARG_MAX chunk and
|
|
74
|
+
* holds the hashes for {@link liveBlobHash}.
|
|
75
|
+
*/
|
|
76
|
+
prefetchLiveBlobHashes: (fullPaths: readonly string[]) => Promise<void>;
|
|
77
|
+
/** The live blob hash of a file, from the prefetch or a single hash-object. */
|
|
78
|
+
liveBlobHash: (fullPath: string) => Promise<string | undefined>;
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Builds a batched patched-content context that loads the manifest and
|
|
@@ -11,7 +11,7 @@ import { pathExists, readText, writeFileAtomic, writeText } from '../utils/fs.js
|
|
|
11
11
|
import { verbose } from '../utils/logger.js';
|
|
12
12
|
import { isContainedRelativePath } from '../utils/paths.js';
|
|
13
13
|
import { applyPatchIdempotent, reversePatch } from './git.js';
|
|
14
|
-
import { getFileContentAtRef } from './git-file-ops.js';
|
|
14
|
+
import { getFileContentAtRef, getFilesContentAtRef, hashObjectBatch } from './git-file-ops.js';
|
|
15
15
|
import { discoverPatches } from './patch-files.js';
|
|
16
16
|
import { loadPatchesManifest } from './patch-manifest.js';
|
|
17
17
|
import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch, parseDiffSections, } from './patch-parse.js';
|
|
@@ -395,12 +395,37 @@ export async function createPatchedContentContext(patchesDir, engineDir) {
|
|
|
395
395
|
return body;
|
|
396
396
|
};
|
|
397
397
|
const getAffectingPatches = (filePath) => affectingByFile.get(filePath) ?? [];
|
|
398
|
+
// Prefetched HEAD content, consumed on first use so a large scan does not
|
|
399
|
+
// hold every base file for the life of the context.
|
|
400
|
+
const baseContent = new Map();
|
|
401
|
+
const takeBase = async (filePath) => {
|
|
402
|
+
if (baseContent.has(filePath)) {
|
|
403
|
+
const content = baseContent.get(filePath) ?? null;
|
|
404
|
+
baseContent.delete(filePath);
|
|
405
|
+
return content;
|
|
406
|
+
}
|
|
407
|
+
return getFileContentAtRef(engineDir, filePath);
|
|
408
|
+
};
|
|
409
|
+
const liveHashes = new Map();
|
|
398
410
|
return {
|
|
399
411
|
manifestPatches: manifest?.patches ?? [],
|
|
400
412
|
getAffectingPatches,
|
|
401
413
|
readPatchBody,
|
|
414
|
+
prefetchBase: async (filePaths) => {
|
|
415
|
+
const wanted = filePaths.filter((file) => !baseContent.has(file));
|
|
416
|
+
for (const [file, content] of await getFilesContentAtRef(engineDir, wanted)) {
|
|
417
|
+
baseContent.set(file, content);
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
prefetchLiveBlobHashes: async (fullPaths) => {
|
|
421
|
+
const wanted = fullPaths.filter((path) => !liveHashes.has(path));
|
|
422
|
+
for (const [path, hash] of await hashObjectBatch(engineDir, wanted)) {
|
|
423
|
+
liveHashes.set(path, hash);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
liveBlobHash: async (fullPath) => liveHashes.get(fullPath) ?? (await hashObjectBatch(engineDir, [fullPath])).get(fullPath),
|
|
402
427
|
computePatched: async (filePath) => {
|
|
403
|
-
let content = await
|
|
428
|
+
let content = await takeBase(filePath);
|
|
404
429
|
for (const patch of getAffectingPatches(filePath)) {
|
|
405
430
|
content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
|
|
406
431
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed `SourceFile`s reused across programs, keyed by file name and the
|
|
3
|
+
* parse options TypeScript asked for. The language service shares files
|
|
4
|
+
* across programs the same way (its document registry): a `SourceFile` is
|
|
5
|
+
* parsed and bound once, and every program that includes it reuses both.
|
|
6
|
+
* Only valid while the files cannot change and every program uses the same
|
|
7
|
+
* compiler options, which holds within one lint run.
|
|
8
|
+
*/
|
|
9
|
+
export type CheckJsSourceFileCache = Map<string, import('typescript').SourceFile>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
12
|
+
* or `host` itself when not.
|
|
13
|
+
* @param host - The program's compiler host
|
|
14
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
15
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
16
|
+
* @param shimSource - The composed shim text
|
|
17
|
+
*/
|
|
18
|
+
export declare function withSharedSourceFiles(host: import('typescript').CompilerHost, cache: CheckJsSourceFileCache | undefined, shimPath: string, shimSource: string): import('typescript').CompilerHost;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Parsed-source sharing across checkJs programs.
|
|
4
|
+
*
|
|
5
|
+
* Its own module rather than part of `patch-lint-checkjs.ts` only because
|
|
6
|
+
* that file sits on a 500-line budget.
|
|
7
|
+
*/
|
|
8
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a host's `getSourceFile` so each file is parsed at most once per
|
|
11
|
+
* cache. The shim is keyed by its content as well, since the shim text is
|
|
12
|
+
* composed per call rather than read from disk.
|
|
13
|
+
*/
|
|
14
|
+
function cachingGetSourceFile(cache, shimPath, shimSource, getSourceFile) {
|
|
15
|
+
const shimDigest = sha256Hex(shimSource);
|
|
16
|
+
return (fileName, languageVersionOrOptions, onError, shouldCreate) => {
|
|
17
|
+
const parse = typeof languageVersionOrOptions === 'object'
|
|
18
|
+
? `${languageVersionOrOptions.languageVersion}|${String(languageVersionOrOptions.impliedNodeFormat)}`
|
|
19
|
+
: String(languageVersionOrOptions);
|
|
20
|
+
const key = fileName === shimPath ? `${fileName}\0${parse}\0${shimDigest}` : `${fileName}\0${parse}`;
|
|
21
|
+
const cached = cache.get(key);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const created = getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate);
|
|
25
|
+
if (created !== undefined)
|
|
26
|
+
cache.set(key, created);
|
|
27
|
+
return created;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
32
|
+
* or `host` itself when not.
|
|
33
|
+
* @param host - The program's compiler host
|
|
34
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
35
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
36
|
+
* @param shimSource - The composed shim text
|
|
37
|
+
*/
|
|
38
|
+
export function withSharedSourceFiles(host, cache, shimPath, shimSource) {
|
|
39
|
+
if (cache === undefined)
|
|
40
|
+
return host;
|
|
41
|
+
return {
|
|
42
|
+
...host,
|
|
43
|
+
getSourceFile: cachingGetSourceFile(cache, shimPath, shimSource, host.getSourceFile.bind(host)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
28
28
|
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
|
|
29
|
+
import { type CheckJsSourceFileCache } from './patch-lint-checkjs-cache.js';
|
|
29
30
|
/** How a checkJs run controls reporting and resolution. See module docs. */
|
|
30
31
|
export interface CheckJsMode {
|
|
31
32
|
strict: boolean;
|
|
@@ -76,6 +77,11 @@ export interface RunCheckJsGroupedInput {
|
|
|
76
77
|
* targets.
|
|
77
78
|
*/
|
|
78
79
|
rootScope?: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Parsed source files shared across several programs built with the same
|
|
82
|
+
* compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
|
|
83
|
+
*/
|
|
84
|
+
sourceFileCache?: CheckJsSourceFileCache;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds the checkJs program once over `resolutionOwned` and returns its
|
|
@@ -30,6 +30,7 @@ import { toError } from '../utils/errors.js';
|
|
|
30
30
|
import { pathExists } from '../utils/fs.js';
|
|
31
31
|
import { verbose } from '../utils/logger.js';
|
|
32
32
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
33
|
+
import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
|
|
33
34
|
import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
|
|
34
35
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -340,7 +341,7 @@ export async function runCheckJsGrouped(input) {
|
|
|
340
341
|
});
|
|
341
342
|
},
|
|
342
343
|
};
|
|
343
|
-
const program = ts.createProgram(rootFiles, options, host);
|
|
344
|
+
const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
|
|
344
345
|
const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning');
|
|
345
346
|
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
|
|
346
347
|
return { byFile, global: [] };
|
|
@@ -461,6 +462,11 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
461
462
|
const mode = modeFromPatchLintConfig(patchLint);
|
|
462
463
|
const seenGlobal = new Set();
|
|
463
464
|
const files = [...testFiles].sort((a, b) => a.localeCompare(b));
|
|
465
|
+
// Every test program below shares options and shim, and mostly the same
|
|
466
|
+
// files: TypeScript's lib declarations, the shim, and the directory's
|
|
467
|
+
// head.js helpers. Parsing them once instead of once per test file took
|
|
468
|
+
// a 467-test queue's pass from 66 s to a fraction of that.
|
|
469
|
+
const sourceFileCache = new Map();
|
|
464
470
|
const companions = await collectUnmanagedCompanions(repoDir, files, testFiles);
|
|
465
471
|
for (const file of files) {
|
|
466
472
|
// Under a rootScope only the scoped files get their own program, but
|
|
@@ -487,6 +493,7 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
487
493
|
projectRoot,
|
|
488
494
|
mode,
|
|
489
495
|
builtinShimSuffix: TEST_HARNESS_SHIM,
|
|
496
|
+
sourceFileCache,
|
|
490
497
|
});
|
|
491
498
|
const own = result.byFile.get(file);
|
|
492
499
|
if (own && own.length > 0) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
13
13
|
import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
|
|
14
|
+
import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
14
15
|
/**
|
|
15
16
|
* One patch's contribution to {@link PatchQueueContext}.
|
|
16
17
|
*
|
|
@@ -39,7 +40,7 @@ export interface PatchQueueEntry {
|
|
|
39
40
|
* reconstructable `GIT binary patch`, or a payload-less
|
|
40
41
|
* `Binary files … differ` stub. `newFiles` is text-only by design (a
|
|
41
42
|
* binary authors no imports, so the import rules gain nothing from it and
|
|
42
|
-
* `
|
|
43
|
+
* `extractNewFileContentFromDiff` correctly refuses to decode one), but "does a
|
|
43
44
|
* later patch create this path" is a question about existence after apply,
|
|
44
45
|
* not about content, and answering it from `newFiles` made every binary
|
|
45
46
|
* creation invisible to the staged-registration rules.
|
|
@@ -76,6 +77,11 @@ export interface PatchQueueContext {
|
|
|
76
77
|
* Absent = current behavior (hint always printed).
|
|
77
78
|
*/
|
|
78
79
|
patchPolicy?: PatchPolicyConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Which carriers the forward-registration rule checks, from
|
|
82
|
+
* `patchLint.forwardRegistration`. Absent = `support-files` only.
|
|
83
|
+
*/
|
|
84
|
+
forwardRegistration?: ForwardRegistrationScope;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* module.
|
|
12
12
|
*/
|
|
13
13
|
import { basename } from 'node:path';
|
|
14
|
-
import { toError } from '../utils/errors.js';
|
|
15
14
|
import { readText } from '../utils/fs.js';
|
|
16
15
|
import { verbose } from '../utils/logger.js';
|
|
17
16
|
import { stripJsComments } from '../utils/regex.js';
|
|
@@ -24,7 +23,7 @@ import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registrat
|
|
|
24
23
|
import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, } from './patch-lint-staged-registration.js';
|
|
25
24
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
26
25
|
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
27
|
-
import {
|
|
26
|
+
import { extractNewFileContentsFromDiff } from './patch-transform.js';
|
|
28
27
|
/**
|
|
29
28
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
30
29
|
* directory and extracting new-file content for each creation.
|
|
@@ -50,15 +49,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
50
49
|
for (const patch of patches) {
|
|
51
50
|
const diff = await readText(patch.path);
|
|
52
51
|
const newFilePaths = detectNewFilesInDiff(diff);
|
|
53
|
-
|
|
54
|
-
for
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: ${toError(error).message}`);
|
|
61
|
-
}
|
|
52
|
+
// One parse of the body already in hand. Extracting per file re-read
|
|
53
|
+
// the whole patch from disk for every file it creates.
|
|
54
|
+
const { contents: newFiles, refused } = extractNewFileContentsFromDiff(diff, newFilePaths);
|
|
55
|
+
for (const newFile of refused) {
|
|
56
|
+
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: binary section cannot be extracted as text`);
|
|
62
57
|
}
|
|
63
58
|
// Added-line content for every file the patch modifies but does not
|
|
64
59
|
// create. Fed to the forward-import rule so imports introduced into
|
|
@@ -87,7 +82,13 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
87
82
|
}
|
|
88
83
|
// Sort by order so rules can rely on entries being in apply order.
|
|
89
84
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
90
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
entries,
|
|
87
|
+
...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}),
|
|
88
|
+
...(config?.patchLint?.forwardRegistration !== undefined
|
|
89
|
+
? { forwardRegistration: config.patchLint.forwardRegistration }
|
|
90
|
+
: {}),
|
|
91
|
+
};
|
|
91
92
|
}
|
|
92
93
|
// The creators map (split out of `lintPatchQueueDuplicateCreations` so
|
|
93
94
|
// `status --ownership` can consume the structured data) lives in
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
|
|
2
|
+
/** Which carriers `forward-registration` checks. */
|
|
3
|
+
export type ForwardRegistrationScope = 'support-files' | 'extended';
|
|
4
|
+
/** The carrier kind a forward registration was found in. */
|
|
5
|
+
export type ForwardRegistrationKind = 'support-files' | 'jar-mn' | 'moz-build' | 'test-manifest' | 'custom-elements';
|
|
6
|
+
/** One undeclared forward registration: a line naming a file a later patch creates. */
|
|
7
|
+
export interface ForwardRegistration {
|
|
8
|
+
/** The patch that adds the registration line. */
|
|
9
|
+
patch: string;
|
|
10
|
+
/** Carrier kind. */
|
|
11
|
+
kind: ForwardRegistrationKind;
|
|
12
|
+
/** Engine-relative path of the carrier file (the manifest, jar.mn, …). */
|
|
13
|
+
file: string;
|
|
14
|
+
/** The added line, whitespace-trimmed, that names the target. */
|
|
15
|
+
line: string;
|
|
16
|
+
/** The token on that line that names the target, as written. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Engine-relative path of the file the later patch creates. */
|
|
19
|
+
creates: string;
|
|
20
|
+
/** The later patch that creates it. */
|
|
21
|
+
owner: string;
|
|
22
|
+
/** How many distinct added lines in the carrier name the same target. */
|
|
23
|
+
occurrences: number;
|
|
24
|
+
/** Paste-and-run declaration that records the stage as intentional. */
|
|
25
|
+
command: string;
|
|
26
|
+
}
|
|
27
|
+
/** The discharge command, in the exact shape the support-files arm prints. */
|
|
28
|
+
export declare function formatRegistrationDeclaration(patch: string, file: string, line: string, creates: string, owner: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Finds every undeclared forward registration in the extended carriers:
|
|
31
|
+
* a registration line a patch adds that names a file only a later-ordered
|
|
32
|
+
* patch creates. Creators come from `createdFiles` (the `new file mode`
|
|
33
|
+
* sections), never `filesAffected`.
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - Queue in application order
|
|
36
|
+
* @returns One record per (patch, carrier, created file), first line quoted
|
|
37
|
+
*/
|
|
38
|
+
export declare function findExtendedForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): ForwardRegistration[];
|