@hominis/fireforge 0.38.0 → 0.39.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 +20 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +7 -4
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +17 -2
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +15 -5
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.js +27 -13
- package/dist/src/commands/patch/rename.js +4 -1
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +17 -3
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +8 -2
- package/dist/src/commands/test.js +38 -32
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +8 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/patch-export.d.ts +10 -1
- package/dist/src/core/patch-export.js +12 -2
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
- package/dist/src/core/patch-lint-checkjs.js +30 -14
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint.js +4 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-manager.js +39 -2
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/typecheck-shim.d.ts +13 -0
- package/dist/src/core/typecheck-shim.js +17 -2
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/types/commands/options.d.ts +12 -0
- package/dist/src/types/config.d.ts +15 -0
- package/package.json +1 -1
|
@@ -10,7 +10,6 @@ import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionette
|
|
|
10
10
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
11
11
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
12
12
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
13
|
-
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
14
13
|
import { GeneralError } from '../errors/base.js';
|
|
15
14
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
16
15
|
import { toError } from '../utils/errors.js';
|
|
@@ -18,7 +17,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
18
17
|
import { info, intro, outro, spinner, success, verbose } from '../utils/logger.js';
|
|
19
18
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
20
19
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
21
|
-
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
20
|
+
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
22
21
|
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
23
22
|
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
24
23
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
@@ -34,19 +33,6 @@ async function assertTestPathsExist(engineDir, testPaths) {
|
|
|
34
33
|
throw new GeneralError(`Test path${missingPaths.length === 1 ? '' : 's'} not found under engine/: ${missingPaths.join(', ')}\n\n` +
|
|
35
34
|
'If you expected these files to come from your patch stack, run "fireforge import" first.');
|
|
36
35
|
}
|
|
37
|
-
async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
38
|
-
const result = { xpcshell: [], nonXpcshell: [] };
|
|
39
|
-
for (const testPath of normalizedPaths) {
|
|
40
|
-
const manifest = await findNearestXpcshellManifest(engineDir, testPath);
|
|
41
|
-
if (manifest) {
|
|
42
|
-
result.xpcshell.push(testPath);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
result.nonXpcshell.push(testPath);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
36
|
/**
|
|
51
37
|
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
52
38
|
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
@@ -66,12 +52,6 @@ function resolveTestSuite(classification, forceGeneric) {
|
|
|
66
52
|
}
|
|
67
53
|
return 'generic';
|
|
68
54
|
}
|
|
69
|
-
function buildMixedHarnessMessage(classification) {
|
|
70
|
-
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
71
|
-
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
72
|
-
` - xpcshell: ${classification.xpcshell.join(', ')}\n` +
|
|
73
|
-
` - browser/mochitest: ${classification.nonXpcshell.join(', ')}`);
|
|
74
|
-
}
|
|
75
55
|
function filterRedundantXpcshellFlavorArgs(machArgs, classification) {
|
|
76
56
|
if (classification.xpcshell.length === 0 || classification.nonXpcshell.length > 0) {
|
|
77
57
|
return [...machArgs];
|
|
@@ -140,7 +120,9 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
140
120
|
// anchor forward (a `mach build faster` does not rebake
|
|
141
121
|
// components.conf into the compiled table).
|
|
142
122
|
try {
|
|
143
|
-
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline
|
|
123
|
+
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline, testPackagingCoverage === 'full'
|
|
124
|
+
? 'fireforge test --build'
|
|
125
|
+
: `fireforge test --build ${testPackagingCoverage.join(' ')}`);
|
|
144
126
|
}
|
|
145
127
|
catch (baselineError) {
|
|
146
128
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -239,7 +221,7 @@ async function runDoctorPreflight(args) {
|
|
|
239
221
|
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
240
222
|
* Mutates `extraArgs` in place.
|
|
241
223
|
*/
|
|
242
|
-
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
224
|
+
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly = false) {
|
|
243
225
|
// Auto-forward the Marionette port to mach when `--marionette-port` is
|
|
244
226
|
// set. `--setpref=marionette.port=<n>` configures where the browser
|
|
245
227
|
// listener binds; `--marionette=127.0.0.1:<n>` tells the mochitest harness
|
|
@@ -258,6 +240,14 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
258
240
|
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
259
241
|
if (options.marionettePort === undefined)
|
|
260
242
|
return;
|
|
243
|
+
if (xpcshellOnly) {
|
|
244
|
+
// Manifest classification says every requested path is xpcshell —
|
|
245
|
+
// xpcshell ignores the browser Marionette path entirely, and the
|
|
246
|
+
// mochitest client flags previously forwarded here made mach reject
|
|
247
|
+
// the dispatch (FORGE F10).
|
|
248
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
261
251
|
{
|
|
262
252
|
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
263
253
|
const machArgs = options.machArg ?? [];
|
|
@@ -276,8 +266,23 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
276
266
|
}
|
|
277
267
|
}
|
|
278
268
|
}
|
|
279
|
-
async function ensureTestMarionettePortAvailable(port, binaryName,
|
|
280
|
-
if (
|
|
269
|
+
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
270
|
+
if (skip.xpcshellOnly && !skip.doctor) {
|
|
271
|
+
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
272
|
+
// interactive browser holding 2828 must not kill an xpcshell run
|
|
273
|
+
// (FORGE F10). --doctor keeps the preflight: its probe launches a
|
|
274
|
+
// Marionette browser regardless of the requested harness.
|
|
275
|
+
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
276
|
+
'(xpcshell does not bind the browser Marionette port).';
|
|
277
|
+
if (options.marionettePort !== undefined || options.killStaleMarionette === true) {
|
|
278
|
+
info(message);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
verbose(message);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (options.killStaleMarionette === true) {
|
|
281
286
|
await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
|
|
282
287
|
return;
|
|
283
288
|
}
|
|
@@ -321,6 +326,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
321
326
|
// stale/coverage refusals keep precedence over missing-path errors.)
|
|
322
327
|
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
323
328
|
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
329
|
+
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths);
|
|
324
330
|
// Run incremental build if requested
|
|
325
331
|
if (options.build) {
|
|
326
332
|
// A path-less `test --build` runs (and packages for) the full suite;
|
|
@@ -361,7 +367,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
361
367
|
// generic bind failure. 2026-04-21 eval (Finding #20): a stale
|
|
362
368
|
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
363
369
|
// the sibling `mybrowser/` workspace.
|
|
364
|
-
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options
|
|
370
|
+
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
371
|
+
xpcshellOnly,
|
|
372
|
+
doctor: options.doctor === true,
|
|
373
|
+
});
|
|
365
374
|
if (options.doctor) {
|
|
366
375
|
const doctorOutcome = await runDoctorPreflight({
|
|
367
376
|
engineDir: paths.engine,
|
|
@@ -375,10 +384,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
375
384
|
return;
|
|
376
385
|
}
|
|
377
386
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
378
|
-
const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
|
|
379
|
-
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
380
|
-
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
381
|
-
}
|
|
382
387
|
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
383
388
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
384
389
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
@@ -401,7 +406,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
401
406
|
if (forwardedMachArgs.length > 0) {
|
|
402
407
|
extraArgs.push(...forwardedMachArgs);
|
|
403
408
|
}
|
|
404
|
-
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort);
|
|
409
|
+
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly);
|
|
405
410
|
// Directory arguments mean EXACTLY that directory: mozbuild's test
|
|
406
411
|
// resolver matches paths by string prefix, so a bare directory arg
|
|
407
412
|
// silently swept in prefix-named siblings (152.0b7 → 153.0b8 drill:
|
|
@@ -437,6 +442,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
437
442
|
suite,
|
|
438
443
|
baseExtraArgs: extraArgs,
|
|
439
444
|
harnessRetries,
|
|
445
|
+
headless: options.headless === true,
|
|
440
446
|
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
441
447
|
};
|
|
442
448
|
const postRebuildContext = options.build
|
|
@@ -482,7 +488,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
482
488
|
reportCanaryOutcome(outcome);
|
|
483
489
|
return;
|
|
484
490
|
}
|
|
485
|
-
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
|
|
491
|
+
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
|
|
486
492
|
}
|
|
487
493
|
/**
|
|
488
494
|
* Builds the perf-sample env contract for the harness run (field report
|
|
@@ -76,6 +76,13 @@ export interface BuildBaseline {
|
|
|
76
76
|
* static-components stale check degrades to "fresh" in that case.
|
|
77
77
|
*/
|
|
78
78
|
staticComponentsBaseline?: StaticComponentsBaseline;
|
|
79
|
+
/**
|
|
80
|
+
* Invocation shape that recorded this baseline (`'fireforge build'`,
|
|
81
|
+
* `'fireforge build --ui'`, `'fireforge test --build [paths]'`).
|
|
82
|
+
* Informational — surfaced by `fireforge status --test-coverage`.
|
|
83
|
+
* Missing on pre-0.39.0 baselines; render as "unknown".
|
|
84
|
+
*/
|
|
85
|
+
recordedBy?: string;
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* State of the engine at the last FULL build, as far as compiled-in XPCOM
|
|
@@ -48,4 +48,4 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
48
48
|
* does not rebake `components.conf` registrations, so the last FULL
|
|
49
49
|
* build stays the honest anchor for the compiled StaticComponents table.
|
|
50
50
|
*/
|
|
51
|
-
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline): Promise<void>;
|
|
51
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string): Promise<void>;
|
|
@@ -74,7 +74,7 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
74
74
|
* does not rebake `components.conf` registrations, so the last FULL
|
|
75
75
|
* build stays the honest anchor for the compiled StaticComponents table.
|
|
76
76
|
*/
|
|
77
|
-
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline) {
|
|
77
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy) {
|
|
78
78
|
let engineHeadSha = '';
|
|
79
79
|
try {
|
|
80
80
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -103,6 +103,7 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
103
103
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
104
104
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
105
105
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
106
|
+
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
106
107
|
};
|
|
107
108
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
108
109
|
}
|
|
@@ -24,6 +24,15 @@ export interface PrepareBuildOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
previousBaseline?: BuildBaseline | undefined;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
29
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
30
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
31
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
32
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
33
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function describeSignalShapedExit(exitCode: number): string | undefined;
|
|
27
36
|
/**
|
|
28
37
|
* Returns true when the file path matches a pattern that forces
|
|
29
38
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared pre-flight logic for build and package commands:
|
|
4
4
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
5
5
|
*/
|
|
6
|
+
import { constants as osConstants } from 'node:os';
|
|
6
7
|
import { BuildError } from '../errors/build.js';
|
|
7
8
|
import { FurnaceError } from '../errors/furnace.js';
|
|
8
9
|
import { toError } from '../utils/errors.js';
|
|
@@ -33,6 +34,27 @@ function extractMachConfigureError(result) {
|
|
|
33
34
|
return '';
|
|
34
35
|
return combined.split('\n').slice(-40).join('\n').trim();
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
39
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
40
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
41
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
42
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
43
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function describeSignalShapedExit(exitCode) {
|
|
46
|
+
if (exitCode <= 128 || exitCode > 192)
|
|
47
|
+
return undefined;
|
|
48
|
+
const signalNumber = exitCode - 128;
|
|
49
|
+
const names = Object.entries(osConstants.signals)
|
|
50
|
+
.filter(([, value]) => value === signalNumber)
|
|
51
|
+
.map(([name]) => name);
|
|
52
|
+
const label = names.length > 0 ? names.join('/') : `signal ${signalNumber}`;
|
|
53
|
+
return (`Exit ${exitCode} is signal-shaped (${exitCode} - 128 = ${signalNumber}, ${label} on this host): ` +
|
|
54
|
+
'the process was likely interrupted externally (OOM-killer, terminal disconnect, ' +
|
|
55
|
+
'display/session teardown) rather than failing on its own, and the log may be ' +
|
|
56
|
+
'truncated mid-step.');
|
|
57
|
+
}
|
|
36
58
|
/**
|
|
37
59
|
* Builds the `BuildError` for a non-zero auto-configure exit: the output
|
|
38
60
|
* tail (so the underlying mozbuild failure survives), any matched
|
|
@@ -43,8 +65,10 @@ function extractMachConfigureError(result) {
|
|
|
43
65
|
function buildConfigureFailureError(captured) {
|
|
44
66
|
const detail = extractMachConfigureError(captured);
|
|
45
67
|
const hints = explainMachError(`${captured.stderr}\n${captured.stdout}`);
|
|
68
|
+
const signalNote = describeSignalShapedExit(captured.exitCode);
|
|
46
69
|
return new BuildError(`Backend regeneration failed: mach configure exited with code ${captured.exitCode}.` +
|
|
47
70
|
(detail ? `\n\nmach configure output (tail):\n${detail}` : '') +
|
|
71
|
+
(signalNote ? `\n\n${signalNote}` : '') +
|
|
48
72
|
(hints.length > 0 ? `\n\n${hints.map((hint) => `Hint: ${hint}`).join('\n')}` : '') +
|
|
49
73
|
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
50
74
|
}
|
|
@@ -348,6 +348,10 @@ function parsePatchLintBlock(rec) {
|
|
|
348
348
|
if (chromeScriptJsDoc !== undefined) {
|
|
349
349
|
out.chromeScriptJsDoc = chromeScriptJsDoc;
|
|
350
350
|
}
|
|
351
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'patchLint.undefinedIdentifiers');
|
|
352
|
+
if (undefinedIdentifiers !== undefined) {
|
|
353
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
354
|
+
}
|
|
351
355
|
if (out.checkJsStrict === true && out.checkJs !== true) {
|
|
352
356
|
throw new ConfigError('Config field "patchLint.checkJsStrict" requires "patchLint.checkJs": true');
|
|
353
357
|
}
|
|
@@ -413,6 +417,10 @@ function parseTypecheckBlock(rec) {
|
|
|
413
417
|
if (overrides) {
|
|
414
418
|
out.projectOverrides = overrides;
|
|
415
419
|
}
|
|
420
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'typecheck.undefinedIdentifiers');
|
|
421
|
+
if (undefinedIdentifiers !== undefined) {
|
|
422
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
423
|
+
}
|
|
416
424
|
return out;
|
|
417
425
|
}
|
|
418
426
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch-aware restore planning for `fireforge discard` (FORGE F1, P0).
|
|
3
|
+
*
|
|
4
|
+
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
5
|
+
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
6
|
+
* (`git apply` without `--index` → patch-edited tracked files are unstaged
|
|
7
|
+
* modifications, patch-created files are untracked). The pre-0.39.0
|
|
8
|
+
* `discard` was purely git-mechanical — it restored to HEAD and deleted
|
|
9
|
+
* untracked files, silently reverting patch-backed files PAST their owning
|
|
10
|
+
* patch and deleting patch-created files outright.
|
|
11
|
+
*
|
|
12
|
+
* This module classifies each status entry against the patch manifest's
|
|
13
|
+
* ownership claims and plans the correct restore target: the PATCH-APPLIED
|
|
14
|
+
* baseline (HEAD content folded through every affecting patch in order) for
|
|
15
|
+
* claimed files, and the legacy git mechanics for unmanaged ones.
|
|
16
|
+
*/
|
|
17
|
+
import type { GitStatusEntry } from './git-base.js';
|
|
18
|
+
/** How one status entry's canonical path will be restored. */
|
|
19
|
+
export type DiscardKind = 'unmanaged' | 'patch-backed' | 'patch-created' | 'patch-deleted';
|
|
20
|
+
/** Restore plan for one git status entry. */
|
|
21
|
+
export interface DiscardBaselinePlan {
|
|
22
|
+
entry: GitStatusEntry;
|
|
23
|
+
kind: DiscardKind;
|
|
24
|
+
/** Claiming patch filenames for `entry.file` ∪ `entry.originalPath`. */
|
|
25
|
+
owners: string[];
|
|
26
|
+
/** True when more than one patch claims the path (ownership conflict). */
|
|
27
|
+
conflicted: boolean;
|
|
28
|
+
/** Baseline content for `entry.file`; null = absent at baseline. Unset for 'unmanaged'. */
|
|
29
|
+
expectedContent?: string | null;
|
|
30
|
+
/** Rename/copy only: baseline content for `originalPath` when that side is claimed. */
|
|
31
|
+
expectedOriginalContent?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** Builds an all-unmanaged plan set — the `--to-upstream` (legacy) shape. */
|
|
34
|
+
export declare function planUpstreamDiscards(entries: readonly GitStatusEntry[]): DiscardBaselinePlan[];
|
|
35
|
+
/**
|
|
36
|
+
* Plans the restore target for each status entry. Entries claimed by the
|
|
37
|
+
* patch manifest restore to the patch-applied baseline; unclaimed entries
|
|
38
|
+
* keep the legacy git mechanics. A missing patches dir or empty manifest
|
|
39
|
+
* degrades every plan to `unmanaged` (identical to pre-0.39.0 behavior).
|
|
40
|
+
*
|
|
41
|
+
* A corrupt manifest or an unreconstructible patch baseline REFUSES with a
|
|
42
|
+
* GeneralError naming `--to-upstream` — silently falling back to pristine
|
|
43
|
+
* HEAD is exactly the data-loss path this module exists to close.
|
|
44
|
+
*/
|
|
45
|
+
export declare function planDiscardBaselines(patchesDir: string, engineDir: string, entries: readonly GitStatusEntry[]): Promise<DiscardBaselinePlan[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Applies one restore plan. Patch-claimed paths end up exactly as `import`
|
|
48
|
+
* leaves them: patched content as an unstaged worktree state (tracked files
|
|
49
|
+
* read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
|
|
50
|
+
* to the legacy {@link discardStatusEntry} mechanics.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyDiscardBaseline(engineDir: string, plan: DiscardBaselinePlan): Promise<void>;
|
|
53
|
+
/** One-line annotation for dry-run listings and confirmation summaries. */
|
|
54
|
+
export declare function describeDiscardBaseline(plan: DiscardBaselinePlan): string;
|
|
55
|
+
/** Single-file outro naming the baseline that was restored. */
|
|
56
|
+
export declare function describeDiscardOutcome(plan: DiscardBaselinePlan, toUpstream: boolean): string;
|
|
57
|
+
/** Batch outro summarising how many files restored to which baseline. */
|
|
58
|
+
export declare function summarizeDiscardBaselines(plans: readonly DiscardBaselinePlan[], succeeded: number): string;
|
|
59
|
+
/** Warn text for a multi-owner (conflicted) restore. */
|
|
60
|
+
export declare function describeConflictWarning(plan: DiscardBaselinePlan): string;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Patch-aware restore planning for `fireforge discard` (FORGE F1, P0).
|
|
4
|
+
*
|
|
5
|
+
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
6
|
+
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
7
|
+
* (`git apply` without `--index` → patch-edited tracked files are unstaged
|
|
8
|
+
* modifications, patch-created files are untracked). The pre-0.39.0
|
|
9
|
+
* `discard` was purely git-mechanical — it restored to HEAD and deleted
|
|
10
|
+
* untracked files, silently reverting patch-backed files PAST their owning
|
|
11
|
+
* patch and deleting patch-created files outright.
|
|
12
|
+
*
|
|
13
|
+
* This module classifies each status entry against the patch manifest's
|
|
14
|
+
* ownership claims and plans the correct restore target: the PATCH-APPLIED
|
|
15
|
+
* baseline (HEAD content folded through every affecting patch in order) for
|
|
16
|
+
* claimed files, and the legacy git mechanics for unmanaged ones.
|
|
17
|
+
*/
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { GeneralError } from '../errors/base.js';
|
|
20
|
+
import { toError } from '../utils/errors.js';
|
|
21
|
+
import { readText, removeFile, writeText } from '../utils/fs.js';
|
|
22
|
+
import { discardStatusEntry, fileExistsInHead, listTrackedInHead, restoreTrackedPath, unstageFiles, } from './git-file-ops.js';
|
|
23
|
+
import { createPatchedContentComputer } from './patch-apply.js';
|
|
24
|
+
import { loadPatchesManifestState } from './patch-manifest-io.js';
|
|
25
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
26
|
+
import { buildPatchClaims } from './status-classify.js';
|
|
27
|
+
/** Builds an all-unmanaged plan set — the `--to-upstream` (legacy) shape. */
|
|
28
|
+
export function planUpstreamDiscards(entries) {
|
|
29
|
+
return entries.map((entry) => ({ entry, kind: 'unmanaged', owners: [], conflicted: false }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Plans the restore target for each status entry. Entries claimed by the
|
|
33
|
+
* patch manifest restore to the patch-applied baseline; unclaimed entries
|
|
34
|
+
* keep the legacy git mechanics. A missing patches dir or empty manifest
|
|
35
|
+
* degrades every plan to `unmanaged` (identical to pre-0.39.0 behavior).
|
|
36
|
+
*
|
|
37
|
+
* A corrupt manifest or an unreconstructible patch baseline REFUSES with a
|
|
38
|
+
* GeneralError naming `--to-upstream` — silently falling back to pristine
|
|
39
|
+
* HEAD is exactly the data-loss path this module exists to close.
|
|
40
|
+
*/
|
|
41
|
+
export async function planDiscardBaselines(patchesDir, engineDir, entries) {
|
|
42
|
+
const state = await loadPatchesManifestState(patchesDir);
|
|
43
|
+
if (state.exists && state.parseError) {
|
|
44
|
+
throw new GeneralError('patches/patches.json is unreadable — refusing to discard while patch ownership cannot be ' +
|
|
45
|
+
'determined. Fix the manifest, or pass --to-upstream to explicitly revert to pristine ' +
|
|
46
|
+
'upstream (HEAD).');
|
|
47
|
+
}
|
|
48
|
+
const claims = state.manifest
|
|
49
|
+
? buildPatchClaims(state.manifest.patches)
|
|
50
|
+
: new Map();
|
|
51
|
+
const plans = [];
|
|
52
|
+
const claimedEntries = entries.filter((entry) => claims.has(entry.file) || (entry.originalPath !== undefined && claims.has(entry.originalPath)));
|
|
53
|
+
if (claimedEntries.length === 0) {
|
|
54
|
+
return planUpstreamDiscards(entries);
|
|
55
|
+
}
|
|
56
|
+
const computePatched = await createPatchedContentComputer(patchesDir, engineDir);
|
|
57
|
+
const claimedFiles = [
|
|
58
|
+
...new Set(claimedEntries.flatMap((entry) => entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file])),
|
|
59
|
+
];
|
|
60
|
+
const trackedInHead = await listTrackedInHead(engineDir, claimedFiles);
|
|
61
|
+
const computeExpected = async (path) => {
|
|
62
|
+
try {
|
|
63
|
+
return await computePatched(path);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new GeneralError(`Cannot reconstruct the patch baseline for ${path} (${toError(error).message}). ` +
|
|
67
|
+
'Fix the owning patch, or pass --to-upstream to explicitly revert to pristine ' +
|
|
68
|
+
'upstream (HEAD).');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// `computePatchedContent` folds a deletion into an empty string (an
|
|
72
|
+
// apply-to-content limitation), which is indistinguishable from a
|
|
73
|
+
// legitimately empty file. Consult the owning patches' diff sections
|
|
74
|
+
// directly: the LAST section (highest-ordered owner) naming the path
|
|
75
|
+
// decides whether the baseline is "absent".
|
|
76
|
+
const sectionCache = new Map();
|
|
77
|
+
const isDeletedAtBaseline = async (path, owners) => {
|
|
78
|
+
for (let i = owners.length - 1; i >= 0; i--) {
|
|
79
|
+
const owner = owners[i];
|
|
80
|
+
if (owner === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
let sections = sectionCache.get(owner);
|
|
83
|
+
if (!sections) {
|
|
84
|
+
try {
|
|
85
|
+
sections = parseDiffSections(await readText(join(patchesDir, owner)));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
sections = [];
|
|
89
|
+
}
|
|
90
|
+
sectionCache.set(owner, sections);
|
|
91
|
+
}
|
|
92
|
+
for (const section of sections) {
|
|
93
|
+
if (section.targetPath === path || section.sourcePath === path) {
|
|
94
|
+
return section.isDeletedFile;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const owners = [
|
|
102
|
+
...new Set([
|
|
103
|
+
...(claims.get(entry.file) ?? []),
|
|
104
|
+
...(entry.originalPath !== undefined ? (claims.get(entry.originalPath) ?? []) : []),
|
|
105
|
+
]),
|
|
106
|
+
];
|
|
107
|
+
if (owners.length === 0) {
|
|
108
|
+
plans.push({ entry, kind: 'unmanaged', owners, conflicted: false });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let expectedContent = claims.has(entry.file) ? await computeExpected(entry.file) : null;
|
|
112
|
+
if (expectedContent !== null &&
|
|
113
|
+
(await isDeletedAtBaseline(entry.file, claims.get(entry.file) ?? []))) {
|
|
114
|
+
expectedContent = null;
|
|
115
|
+
}
|
|
116
|
+
const expectedOriginalContent = entry.originalPath !== undefined && claims.has(entry.originalPath)
|
|
117
|
+
? await computeExpected(entry.originalPath)
|
|
118
|
+
: undefined;
|
|
119
|
+
const kind = expectedContent === null
|
|
120
|
+
? 'patch-deleted'
|
|
121
|
+
: trackedInHead.has(entry.file)
|
|
122
|
+
? 'patch-backed'
|
|
123
|
+
: 'patch-created';
|
|
124
|
+
plans.push({
|
|
125
|
+
entry,
|
|
126
|
+
kind,
|
|
127
|
+
owners,
|
|
128
|
+
conflicted: owners.length > 1,
|
|
129
|
+
expectedContent,
|
|
130
|
+
...(expectedOriginalContent !== undefined ? { expectedOriginalContent } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return plans;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Applies one restore plan. Patch-claimed paths end up exactly as `import`
|
|
137
|
+
* leaves them: patched content as an unstaged worktree state (tracked files
|
|
138
|
+
* read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
|
|
139
|
+
* to the legacy {@link discardStatusEntry} mechanics.
|
|
140
|
+
*/
|
|
141
|
+
export async function applyDiscardBaseline(engineDir, plan) {
|
|
142
|
+
if (plan.kind === 'unmanaged') {
|
|
143
|
+
await discardStatusEntry(engineDir, plan.entry);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const { entry } = plan;
|
|
147
|
+
// Drop any staged edits/adds/renames first so the rewritten content is a
|
|
148
|
+
// plain worktree state (harmless when nothing is staged; skipped for
|
|
149
|
+
// purely-untracked entries which have no index footprint).
|
|
150
|
+
if (!entry.isUntracked) {
|
|
151
|
+
const indexPaths = entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file];
|
|
152
|
+
await unstageFiles(engineDir, indexPaths);
|
|
153
|
+
}
|
|
154
|
+
if (plan.expectedContent === null || plan.expectedContent === undefined) {
|
|
155
|
+
await removeFile(join(engineDir, entry.file));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await writeText(join(engineDir, entry.file), plan.expectedContent);
|
|
159
|
+
}
|
|
160
|
+
// A rename/copy's original side restores to ITS baseline too: the patch
|
|
161
|
+
// baseline when claimed, else the legacy HEAD restore (matching what
|
|
162
|
+
// discardStatusEntry does for that side).
|
|
163
|
+
if (entry.originalPath !== undefined) {
|
|
164
|
+
if (plan.expectedOriginalContent !== undefined) {
|
|
165
|
+
if (plan.expectedOriginalContent === null) {
|
|
166
|
+
await removeFile(join(engineDir, entry.originalPath));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await writeText(join(engineDir, entry.originalPath), plan.expectedOriginalContent);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (await fileExistsInHead(engineDir, entry.originalPath)) {
|
|
173
|
+
await restoreTrackedPath(engineDir, entry.originalPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** One-line annotation for dry-run listings and confirmation summaries. */
|
|
178
|
+
export function describeDiscardBaseline(plan) {
|
|
179
|
+
const owners = plan.owners.join(' + ');
|
|
180
|
+
switch (plan.kind) {
|
|
181
|
+
case 'unmanaged':
|
|
182
|
+
return plan.entry.isUntracked ? 'unmanaged — delete' : 'unmanaged — revert to upstream';
|
|
183
|
+
case 'patch-backed':
|
|
184
|
+
return `patch baseline: ${owners}${plan.conflicted ? ' (conflict)' : ''}`;
|
|
185
|
+
case 'patch-created':
|
|
186
|
+
return `re-materialize from ${owners}${plan.conflicted ? ' (conflict)' : ''}`;
|
|
187
|
+
case 'patch-deleted':
|
|
188
|
+
return `delete — ${owners} removes it`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Single-file outro naming the baseline that was restored. */
|
|
192
|
+
export function describeDiscardOutcome(plan, toUpstream) {
|
|
193
|
+
if (toUpstream)
|
|
194
|
+
return 'File restored to pristine upstream (HEAD)';
|
|
195
|
+
const owners = plan.owners.join(' + ');
|
|
196
|
+
switch (plan.kind) {
|
|
197
|
+
case 'unmanaged':
|
|
198
|
+
return 'File restored to original state';
|
|
199
|
+
case 'patch-backed':
|
|
200
|
+
return `File restored to patch baseline (${owners})`;
|
|
201
|
+
case 'patch-created':
|
|
202
|
+
return `File re-materialized from patch baseline (${owners})`;
|
|
203
|
+
case 'patch-deleted':
|
|
204
|
+
return `File removed to match patch baseline (${owners} deletes it)`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Batch outro summarising how many files restored to which baseline. */
|
|
208
|
+
export function summarizeDiscardBaselines(plans, succeeded) {
|
|
209
|
+
const patchBacked = plans.filter((p) => p.kind === 'patch-backed').length;
|
|
210
|
+
const created = plans.filter((p) => p.kind === 'patch-created').length;
|
|
211
|
+
const deleted = plans.filter((p) => p.kind === 'patch-deleted').length;
|
|
212
|
+
const unmanaged = plans.filter((p) => p.kind === 'unmanaged').length;
|
|
213
|
+
if (patchBacked + created + deleted === 0) {
|
|
214
|
+
return `${String(succeeded)} file(s) restored to upstream state`;
|
|
215
|
+
}
|
|
216
|
+
const parts = [];
|
|
217
|
+
if (patchBacked > 0)
|
|
218
|
+
parts.push(`${String(patchBacked)} to patch baseline`);
|
|
219
|
+
if (created > 0)
|
|
220
|
+
parts.push(`${String(created)} re-materialized`);
|
|
221
|
+
if (deleted > 0)
|
|
222
|
+
parts.push(`${String(deleted)} removed per patch baseline`);
|
|
223
|
+
if (unmanaged > 0)
|
|
224
|
+
parts.push(`${String(unmanaged)} to upstream state`);
|
|
225
|
+
return `${String(succeeded)} file(s) restored: ${parts.join(', ')}`;
|
|
226
|
+
}
|
|
227
|
+
/** Warn text for a multi-owner (conflicted) restore. */
|
|
228
|
+
export function describeConflictWarning(plan) {
|
|
229
|
+
return (`${plan.entry.file} is claimed by ${String(plan.owners.length)} patches ` +
|
|
230
|
+
`(${plan.owners.join(', ')}). Restored the cumulative baseline; run ` +
|
|
231
|
+
'"fireforge status --ownership" to resolve ownership.');
|
|
232
|
+
}
|
|
@@ -81,6 +81,18 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
|
81
81
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
82
82
|
*/
|
|
83
83
|
export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
86
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
87
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
88
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
89
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
90
|
+
*
|
|
91
|
+
* @param content - File content to check
|
|
92
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
93
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
94
|
+
*/
|
|
95
|
+
export declare function hasThirdPartyPermissiveBanner(content: string, maxLines?: number): boolean;
|
|
84
96
|
/**
|
|
85
97
|
* Prepends the license header to a file on disk if it is not already present.
|
|
86
98
|
*
|
|
@@ -252,6 +252,33 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
252
252
|
];
|
|
253
253
|
return markers.some((marker) => scanText.includes(marker));
|
|
254
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
257
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
258
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
259
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
260
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
261
|
+
*
|
|
262
|
+
* @param content - File content to check
|
|
263
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
264
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
265
|
+
*/
|
|
266
|
+
export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
|
|
267
|
+
const head = content.split('\n').slice(0, maxLines).join('\n');
|
|
268
|
+
const scanText = normalizeLicenseHeadForScan(head).toLowerCase();
|
|
269
|
+
const markers = [
|
|
270
|
+
'mit license',
|
|
271
|
+
'permission is hereby granted, free of charge',
|
|
272
|
+
'isc license',
|
|
273
|
+
'permission to use, copy, modify',
|
|
274
|
+
'redistribution and use in source and binary forms',
|
|
275
|
+
'apache license',
|
|
276
|
+
'licensed under the apache license',
|
|
277
|
+
];
|
|
278
|
+
if (markers.some((marker) => scanText.includes(marker)))
|
|
279
|
+
return true;
|
|
280
|
+
return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0)\b/.test(scanText);
|
|
281
|
+
}
|
|
255
282
|
/**
|
|
256
283
|
* Prepends the license header to a file on disk if it is not already present.
|
|
257
284
|
*
|
|
@@ -30,7 +30,16 @@ export declare function sanitizeName(name: string): string;
|
|
|
30
30
|
* double-prefix incident) also collapses. Exported for direct testing.
|
|
31
31
|
*/
|
|
32
32
|
export declare function stripRedundantCategoryPrefix(sanitizedName: string, category: string): string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Sanitizes a patch name and drops redundant category/full-stem prefixes.
|
|
35
|
+
*
|
|
36
|
+
* A single trailing `.patch` extension is stripped before sanitisation:
|
|
37
|
+
* operators frequently pass a full patch FILENAME where a name is expected
|
|
38
|
+
* (`move-files <from> 348-ui-foo.patch --create`, `rename --to foo.patch`),
|
|
39
|
+
* and 0.38.0 slugged the extension into `-patch`, producing double-suffixed
|
|
40
|
+
* `...-patch.patch` files. A name that deliberately ends in a literal
|
|
41
|
+
* `-patch` slug segment must now be written without the dot form.
|
|
42
|
+
*/
|
|
34
43
|
export declare function patchNameSlug(name: string, category: string): string;
|
|
35
44
|
/**
|
|
36
45
|
* Generates the next patch filename with category.
|