@hominis/fireforge 0.31.0 → 0.32.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 +11 -0
- package/dist/src/commands/export-all.js +4 -1
- package/dist/src/commands/export-shared.js +10 -1
- package/dist/src/commands/export.js +5 -1
- package/dist/src/commands/lint-per-patch.d.ts +2 -0
- package/dist/src/commands/lint-per-patch.js +206 -44
- package/dist/src/commands/lint.js +100 -7
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/test-run.d.ts +10 -0
- package/dist/src/commands/test-run.js +13 -4
- package/dist/src/commands/test.js +46 -7
- package/dist/src/core/config-validate.js +26 -0
- package/dist/src/core/furnace-jsconfig.js +22 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -0
- package/dist/src/core/git-file-ops.js +82 -1
- package/dist/src/core/mach.d.ts +17 -0
- package/dist/src/core/mach.js +21 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +213 -67
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +19 -163
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +9 -4
- package/dist/src/core/typecheck-shim.d.ts +3 -1
- package/dist/src/core/typecheck-shim.js +43 -3
- package/dist/src/types/commands/options.d.ts +17 -0
- package/dist/src/types/config.d.ts +11 -2
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* harness runs cost startup time but make results reproducible; the
|
|
12
12
|
* combined invocation stays available via `--no-shard`.
|
|
13
13
|
*/
|
|
14
|
-
import { testWithOutput } from '../core/mach.js';
|
|
14
|
+
import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
|
|
15
15
|
import { buildHarnessCrashMessage, classifyHarnessRun, } from '../core/test-harness-crash.js';
|
|
16
16
|
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
17
17
|
import { BuildError } from '../errors/build.js';
|
|
@@ -19,6 +19,14 @@ import { info, note, warn } from '../utils/logger.js';
|
|
|
19
19
|
import { maybeInjectAppdirArg } from './test-appdir.js';
|
|
20
20
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
21
21
|
export const DEFAULT_HARNESS_RETRIES = 2;
|
|
22
|
+
/** Resolves the capturing mach dispatcher for a suite. */
|
|
23
|
+
function dispatchForSuite(suite) {
|
|
24
|
+
if (suite === 'xpcshell')
|
|
25
|
+
return xpcshellTestWithOutput;
|
|
26
|
+
if (suite === 'mochitest')
|
|
27
|
+
return mochitestWithOutput;
|
|
28
|
+
return testWithOutput;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
24
32
|
* crashes up to the configured budget. Every attempt goes through the
|
|
@@ -27,6 +35,7 @@ export const DEFAULT_HARNESS_RETRIES = 2;
|
|
|
27
35
|
export async function runTestsWithRetries(ctx, paths) {
|
|
28
36
|
const extraArgs = [...ctx.baseExtraArgs];
|
|
29
37
|
const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
|
|
38
|
+
const dispatch = dispatchForSuite(ctx.suite);
|
|
30
39
|
const maxAttempts = Math.max(1, ctx.harnessRetries + 1);
|
|
31
40
|
let attempts = 0;
|
|
32
41
|
let result;
|
|
@@ -34,9 +43,9 @@ export async function runTestsWithRetries(ctx, paths) {
|
|
|
34
43
|
for (;;) {
|
|
35
44
|
attempts += 1;
|
|
36
45
|
result = ctx.env
|
|
37
|
-
? await
|
|
38
|
-
: await
|
|
39
|
-
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, ctx.env);
|
|
46
|
+
? await dispatch(ctx.engineDir, paths, extraArgs, ctx.env)
|
|
47
|
+
: await dispatch(ctx.engineDir, paths, extraArgs);
|
|
48
|
+
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, ctx.env, dispatch);
|
|
40
49
|
const combined = `${result.stdout}\n${result.stderr}`;
|
|
41
50
|
verdict = classifyHarnessRun(result.exitCode, combined, paths);
|
|
42
51
|
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
@@ -5,6 +5,7 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, withBuildLock, } from '../core/mach.js';
|
|
6
6
|
import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
7
7
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
|
+
import { buildHarnessCrashMessage, detectHarnessCrashSignature, } from '../core/test-harness-crash.js';
|
|
8
9
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
9
10
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
10
11
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
@@ -42,6 +43,25 @@ async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
|
42
43
|
}
|
|
43
44
|
return result;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
48
|
+
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
49
|
+
* `mach mochitest`), which degrades a broken host resource monitor to a
|
|
50
|
+
* warning instead of crashing generic `mach test` at startup (E1). Mixed runs
|
|
51
|
+
* are rejected before this point; a path-less "run all" or an explicit
|
|
52
|
+
* `--generic-mach-test` opt-out stays on the generic command.
|
|
53
|
+
*/
|
|
54
|
+
function resolveTestSuite(classification, forceGeneric) {
|
|
55
|
+
if (forceGeneric)
|
|
56
|
+
return 'generic';
|
|
57
|
+
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length === 0) {
|
|
58
|
+
return 'xpcshell';
|
|
59
|
+
}
|
|
60
|
+
if (classification.nonXpcshell.length > 0 && classification.xpcshell.length === 0) {
|
|
61
|
+
return 'mochitest';
|
|
62
|
+
}
|
|
63
|
+
return 'generic';
|
|
64
|
+
}
|
|
45
65
|
function buildMixedHarnessMessage(classification) {
|
|
46
66
|
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
47
67
|
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
@@ -80,16 +100,31 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
80
100
|
}
|
|
81
101
|
return bundleCheck.expectedPath;
|
|
82
102
|
}
|
|
83
|
-
async function runPreTestBuild(projectRoot, paths, projectConfig) {
|
|
103
|
+
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
|
|
84
104
|
await withBuildLock(projectRoot, async () => {
|
|
85
105
|
await prepareBuildEnvironment(projectRoot, paths, projectConfig);
|
|
86
106
|
const s = spinner('Running incremental build...');
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
// The pre-test build runs through mach too, so the same resource-monitor
|
|
108
|
+
// startup crash that aborts `mach test` can abort `mach build faster`.
|
|
109
|
+
// Run the harness-crash classifier over the build output and retry within
|
|
110
|
+
// the same `--harness-retries` budget rather than hard-failing with a
|
|
111
|
+
// bare "Pre-test build failed" (field report E2).
|
|
112
|
+
const maxAttempts = Math.max(1, harnessRetries + 1);
|
|
113
|
+
for (let attempt = 1;; attempt += 1) {
|
|
114
|
+
const buildResult = await buildUI(paths.engine);
|
|
115
|
+
if (buildResult.exitCode === 0) {
|
|
116
|
+
s.stop('Build complete');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const signature = detectHarnessCrashSignature(`${buildResult.stdout}\n${buildResult.stderr}`);
|
|
120
|
+
if (signature && attempt < maxAttempts) {
|
|
121
|
+
s.message(`Pre-test build hit a harness crash (${signature.reason}); ` +
|
|
122
|
+
`retrying (attempt ${attempt + 1} of ${maxAttempts})...`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
89
125
|
s.error('Pre-test build failed');
|
|
90
|
-
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
126
|
+
throw new BuildError(signature ? buildHarnessCrashMessage(signature, attempt) : 'Pre-test build failed', 'mach build faster');
|
|
91
127
|
}
|
|
92
|
-
s.stop('Build complete');
|
|
93
128
|
});
|
|
94
129
|
}
|
|
95
130
|
function logTestSelection(normalizedPaths) {
|
|
@@ -239,9 +274,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
239
274
|
// missing-bundle path instead of a cryptic `Browser process exited
|
|
240
275
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
241
276
|
const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
277
|
+
const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
|
|
242
278
|
// Run incremental build if requested
|
|
243
279
|
if (options.build) {
|
|
244
|
-
await runPreTestBuild(projectRoot, paths, projectConfig);
|
|
280
|
+
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries);
|
|
245
281
|
info('');
|
|
246
282
|
}
|
|
247
283
|
else {
|
|
@@ -299,6 +335,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
299
335
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
300
336
|
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
301
337
|
}
|
|
338
|
+
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
302
339
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
303
340
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
304
341
|
: [];
|
|
@@ -325,8 +362,9 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
325
362
|
engineDir: paths.engine,
|
|
326
363
|
objDir: buildCheck.objDir,
|
|
327
364
|
classification,
|
|
365
|
+
suite,
|
|
328
366
|
baseExtraArgs: extraArgs,
|
|
329
|
-
harnessRetries
|
|
367
|
+
harnessRetries,
|
|
330
368
|
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
331
369
|
};
|
|
332
370
|
const postRebuildContext = options.build
|
|
@@ -381,6 +419,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
381
419
|
}
|
|
382
420
|
return n;
|
|
383
421
|
})
|
|
422
|
+
.option('--generic-mach-test', 'Force dispatch through generic `mach test` instead of the suite-specific `mach xpcshell-test` / `mach mochitest` a single-suite run auto-selects (the suite-specific commands skip the mozlog resource monitor that crashes `mach test` on some hosts).')
|
|
384
423
|
.option('--no-shard', 'Run multiple test paths in one combined mach invocation instead of sequential per-file shards')
|
|
385
424
|
.option('--perf-samples <path>', 'Publish a perf-sample artifact path to the harness via <BINARYNAME>_PERF_SAMPLE_JSON (resolved against the project root)')
|
|
386
425
|
.option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and (unless --mach-arg includes --flavor=xpcshell) auto-forwarded mach args: --setpref=marionette.port=<n> (browser listener) and --marionette=127.0.0.1:<n> (mochitest client). Omits the client flag when --mach-arg already sets --marionette. Use when 2828 is busy or CI assigns another port.', (raw) => {
|
|
@@ -243,6 +243,28 @@ const PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS = [
|
|
|
243
243
|
'noUnusedLocals',
|
|
244
244
|
'noUnusedParameters',
|
|
245
245
|
];
|
|
246
|
+
/**
|
|
247
|
+
* Validates the reviewed `paths` mapping: an object of pattern → string[]
|
|
248
|
+
* targets, each pattern carrying at most one `*`. Lets patch-owned modules
|
|
249
|
+
* be typed from their real sources without an ambient stub shim.
|
|
250
|
+
*/
|
|
251
|
+
function parseCheckJsPathsMapping(raw) {
|
|
252
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
253
|
+
throw new ConfigError('Config field "patchLint.checkJsCompilerOptions.paths" must be a plain object');
|
|
254
|
+
}
|
|
255
|
+
const rec = raw;
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [pattern, targets] of Object.entries(rec)) {
|
|
258
|
+
if ((pattern.match(/\*/g) ?? []).length > 1) {
|
|
259
|
+
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths" key "${pattern}" may contain at most one "*"`);
|
|
260
|
+
}
|
|
261
|
+
if (!Array.isArray(targets) || targets.some((t) => typeof t !== 'string')) {
|
|
262
|
+
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions.paths.${pattern}" must be an array of strings`);
|
|
263
|
+
}
|
|
264
|
+
out[pattern] = targets;
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
246
268
|
function parsePatchLintCheckJsCompilerOptions(raw) {
|
|
247
269
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
248
270
|
throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" must be a plain object');
|
|
@@ -251,6 +273,10 @@ function parsePatchLintCheckJsCompilerOptions(raw) {
|
|
|
251
273
|
const allowed = new Set(PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS);
|
|
252
274
|
const out = {};
|
|
253
275
|
for (const key of Object.keys(rec)) {
|
|
276
|
+
if (key === 'paths') {
|
|
277
|
+
out.paths = parseCheckJsPathsMapping(rec[key]);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
254
280
|
if (!allowed.has(key)) {
|
|
255
281
|
throw new ConfigError(`Config field "patchLint.checkJsCompilerOptions" has unknown key "${key}"`);
|
|
256
282
|
}
|
|
@@ -51,12 +51,28 @@ async function computeDesiredChromePathEntries(config, customDir, jsconfigAbsPat
|
|
|
51
51
|
for (const file of files.sort()) {
|
|
52
52
|
if (!file.endsWith('.mjs'))
|
|
53
53
|
continue;
|
|
54
|
-
|
|
54
|
+
// Emit a `./`-prefixed relative value. TypeScript treats a bare
|
|
55
|
+
// `paths` value (`moz-widget/moz-widget.mjs`) as non-relative and
|
|
56
|
+
// rejects it without `baseUrl` (TS5090); a `./`-prefixed value
|
|
57
|
+
// resolves against the jsconfig directory with no `baseUrl` (which
|
|
58
|
+
// TS6 deprecates, TS5101). `../`-prefixed paths are already relative
|
|
59
|
+
// and left untouched.
|
|
60
|
+
const rel = normalizePathSlashes(relative(jsconfigDir, join(componentDir, file)));
|
|
61
|
+
const sourcePath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
55
62
|
entries[`${CHROME_ELEMENTS_URL_PREFIX}${file}`] = [sourcePath];
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
return entries;
|
|
59
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Compares two `paths` values treating a leading `./` as insignificant, so
|
|
69
|
+
* the reconciler does not churn between `./x` and bare `x` forms (either
|
|
70
|
+
* direction). Used to decide whether a managed entry is stale.
|
|
71
|
+
*/
|
|
72
|
+
function samePathValue(a, b) {
|
|
73
|
+
const strip = (p) => (p.startsWith('./') ? p.slice(2) : p);
|
|
74
|
+
return strip(a) === strip(b);
|
|
75
|
+
}
|
|
60
76
|
/** True when `key`/`value` is a Furnace-managed chrome-elements mapping. */
|
|
61
77
|
function isManagedEntry(key, value, jsconfigDir, customDir) {
|
|
62
78
|
if (!key.startsWith(CHROME_ELEMENTS_URL_PREFIX))
|
|
@@ -119,7 +135,11 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
|
|
|
119
135
|
result.pruned.push(key);
|
|
120
136
|
continue;
|
|
121
137
|
}
|
|
122
|
-
|
|
138
|
+
// Treat `./x` and bare `x` as equal so a previously-synced bare value (or
|
|
139
|
+
// a hand-written `./` prefix) is not rewritten as "stale" on every run.
|
|
140
|
+
// The existing value is kept verbatim when equivalent — no churn either
|
|
141
|
+
// way; only a genuinely different target updates (to the `./` form).
|
|
142
|
+
if (!samePathValue(value[0] ?? '', want[0] ?? '')) {
|
|
123
143
|
result.updated.push(key);
|
|
124
144
|
nextPaths[key] = want;
|
|
125
145
|
}
|
|
@@ -63,6 +63,21 @@ export declare function git(args: string[], cwd: string, options?: {
|
|
|
63
63
|
timeout?: number;
|
|
64
64
|
env?: Record<string, string>;
|
|
65
65
|
}): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
68
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
69
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
70
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
71
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
72
|
+
*
|
|
73
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
74
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
75
|
+
* how the paths are grouped across invocations never affects the result.
|
|
76
|
+
* @param paths - Pathspecs to chunk
|
|
77
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
78
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
79
|
+
*/
|
|
80
|
+
export declare function chunkPathspecs(paths: string[], budgetBytes?: number): string[][];
|
|
66
81
|
/**
|
|
67
82
|
* Configures git performance settings for large trees.
|
|
68
83
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -72,6 +72,38 @@ export async function git(args, cwd, options) {
|
|
|
72
72
|
}
|
|
73
73
|
return result.stdout;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Splits a pathspec list into chunks whose joined byte length stays well under
|
|
77
|
+
* the OS `ARG_MAX` limit, so a single batched `git` invocation over hundreds of
|
|
78
|
+
* Mozilla-length paths cannot fail with `E2BIG`. The 96 KB budget is
|
|
79
|
+
* deliberately conservative — even the smallest historical `ARG_MAX` (256 KB)
|
|
80
|
+
* leaves room for the fixed git arguments plus the inherited environment.
|
|
81
|
+
*
|
|
82
|
+
* Chunk boundaries are output-neutral for every batched caller here: each
|
|
83
|
+
* caller merges the per-chunk results into a single Set/Map keyed by path, so
|
|
84
|
+
* how the paths are grouped across invocations never affects the result.
|
|
85
|
+
* @param paths - Pathspecs to chunk
|
|
86
|
+
* @param budgetBytes - Maximum joined byte length per chunk
|
|
87
|
+
* @returns Path chunks, each safe to pass as a single argv tail
|
|
88
|
+
*/
|
|
89
|
+
export function chunkPathspecs(paths, budgetBytes = 96_000) {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let current = [];
|
|
92
|
+
let used = 0;
|
|
93
|
+
for (const path of paths) {
|
|
94
|
+
const cost = Buffer.byteLength(path) + 1;
|
|
95
|
+
if (current.length > 0 && used + cost > budgetBytes) {
|
|
96
|
+
chunks.push(current);
|
|
97
|
+
current = [];
|
|
98
|
+
used = 0;
|
|
99
|
+
}
|
|
100
|
+
current.push(path);
|
|
101
|
+
used += cost;
|
|
102
|
+
}
|
|
103
|
+
if (current.length > 0)
|
|
104
|
+
chunks.push(current);
|
|
105
|
+
return chunks;
|
|
106
|
+
}
|
|
75
107
|
/**
|
|
76
108
|
* Configures git performance settings for large trees.
|
|
77
109
|
* Enables index preloading, untracked cache, and the manyFiles feature
|
|
@@ -40,6 +40,14 @@ export declare function getAllDiff(repoDir: string): Promise<string>;
|
|
|
40
40
|
* Builds a combined diff against HEAD for the provided files without touching
|
|
41
41
|
* the real git index. Tracked files use `git diff HEAD`; untracked files use
|
|
42
42
|
* synthesized new-file diffs.
|
|
43
|
+
*
|
|
44
|
+
* Performance: the work is batched into a handful of `git` invocations
|
|
45
|
+
* (one `ls-tree` to classify, one `diff` over all tracked files, one
|
|
46
|
+
* `hash-object` over all new text files) rather than the ~2 spawns per file the
|
|
47
|
+
* previous per-file loop issued — that fan-out dominated the cold-run cost on a
|
|
48
|
+
* Firefox-sized checkout (~700 serial spawns, ~99s). Binary, directory, and
|
|
49
|
+
* recursion paths stay per-file because they are rare and (for binary) mutate
|
|
50
|
+
* the index.
|
|
43
51
|
* @param repoDir - Repository directory
|
|
44
52
|
* @param files - File paths to diff (relative to repo root)
|
|
45
53
|
* @returns Combined diff content
|