@hominis/fireforge 0.40.0 → 0.42.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 +85 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-mozconfig.js +9 -1
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { buildPatchQueueContext, collectForwardImportEdges, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
6
6
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
7
|
+
export { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
|
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { buildPatchQueueContext, collectForwardImportEdges, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
|
|
7
7
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
8
|
+
export { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
|
|
@@ -4,6 +4,12 @@ import { type CommentStyle } from './license-headers.js';
|
|
|
4
4
|
import { lintPatchedCss } from './patch-lint-css.js';
|
|
5
5
|
export * from './patch-lint-reexports.js';
|
|
6
6
|
export { lintPatchedCss };
|
|
7
|
+
/**
|
|
8
|
+
* Canonical `[check] file: message` rendering of a patch-lint issue — the
|
|
9
|
+
* one string shape every reporter and refusal message uses, so operators
|
|
10
|
+
* can grep one format across lint output and refusal details.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatPatchLintIssue(issue: Pick<PatchLintIssue, 'check' | 'file' | 'message'>): string;
|
|
7
13
|
/**
|
|
8
14
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
9
15
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
@@ -24,6 +30,8 @@ export declare function countNonBinaryDiffLines(diffContent: string): {
|
|
|
24
30
|
* `browser_`, `test_`, or `xpcshell_` (all `.js`).
|
|
25
31
|
*/
|
|
26
32
|
export declare function isTestFile(file: string): boolean;
|
|
33
|
+
/** True when `strippedContent` contains at least one assertion call. */
|
|
34
|
+
export declare function hasBrowserChromeAssertion(strippedContent: string): boolean;
|
|
27
35
|
/**
|
|
28
36
|
* Detects comment style from file extension for license header checks.
|
|
29
37
|
*/
|
|
@@ -49,6 +49,14 @@ function countContentLines(content) {
|
|
|
49
49
|
lines.pop();
|
|
50
50
|
return lines.length;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Canonical `[check] file: message` rendering of a patch-lint issue — the
|
|
54
|
+
* one string shape every reporter and refusal message uses, so operators
|
|
55
|
+
* can grep one format across lint output and refusal details.
|
|
56
|
+
*/
|
|
57
|
+
export function formatPatchLintIssue(issue) {
|
|
58
|
+
return `[${issue.check}] ${issue.file}: ${issue.message}`;
|
|
59
|
+
}
|
|
52
60
|
/**
|
|
53
61
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
54
62
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
@@ -136,6 +144,16 @@ const PATCH_FILES_THRESHOLDS = {
|
|
|
136
144
|
test: 5,
|
|
137
145
|
branding: 60,
|
|
138
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* Shared remedy sentence for the over-budget findings (FORGE H4): the two
|
|
149
|
+
* places an operator stands when a patch outgrows its budget must name the
|
|
150
|
+
* sanctioned one-transaction way out, or they reach for the wrong tools
|
|
151
|
+
* first (the field report tried two before finding it). One constant, four
|
|
152
|
+
* emission sites — the wording cannot drift.
|
|
153
|
+
*/
|
|
154
|
+
const SPLIT_HINT = 'Consider splitting into smaller, focused patches — ' +
|
|
155
|
+
'"fireforge patch move-files <from> <to> --file <path> --create --order <n>" ' +
|
|
156
|
+
'moves files into a new patch in one transaction.';
|
|
139
157
|
/**
|
|
140
158
|
* Fixed allowlist of non-branding sibling paths that real-world Firefox
|
|
141
159
|
* branding patches legitimately need to touch to register the new
|
|
@@ -214,6 +232,67 @@ function isBrowserChromeTestFile(file) {
|
|
|
214
232
|
return false;
|
|
215
233
|
return basename.startsWith('browser_');
|
|
216
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Bare assertion-call detection for the `test-needs-assertion` floor.
|
|
237
|
+
*
|
|
238
|
+
* Each alternative must sit at a call boundary: preceded by something that is
|
|
239
|
+
* not an identifier character, `.`, `#`, or `-`. Until 0.41.0 this was a plain
|
|
240
|
+
* `strippedContent.includes(tok)` over the tokens `Assert.`, `ok(`, `is(`,
|
|
241
|
+
* `isnot(`, `isDeeply(`, so any identifier merely *ending* in one satisfied
|
|
242
|
+
* the gate — `this.axis(3)` contains `is(`, `book(` contains `ok(`,
|
|
243
|
+
* `promiseIsDeeply(` contains `isDeeply(`. An assertion-free smoke test using
|
|
244
|
+
* any such call passed the floor, which is exactly what the rule exists to
|
|
245
|
+
* catch.
|
|
246
|
+
*
|
|
247
|
+
* The namespaced arm must name a member and a call. Matching the namespace and
|
|
248
|
+
* its dot alone — as `(?:Assert|SimpleTest)\.` did — re-opened the same hole
|
|
249
|
+
* from the other side: `SimpleTest` is mostly *harness* API, so
|
|
250
|
+
* `SimpleTest.finish()`, `SimpleTest.waitForExplicitFinish()` and
|
|
251
|
+
* `SimpleTest.requestCompleteLog()` each cleared the floor on their own. Those
|
|
252
|
+
* are the exact calls an assertion-free smoke test contains. The enumeration
|
|
253
|
+
* below is the mochitest/xpcshell assertion surface both namespaces share, and
|
|
254
|
+
* it is what the user-facing message already names.
|
|
255
|
+
*
|
|
256
|
+
* Two qualified spellings of the bare four still count: `window.ok(...)` (the
|
|
257
|
+
* globals ARE window properties) and an optional-chained `win?.ok(...)`.
|
|
258
|
+
* Blocking those flagged tests whose only assertions are real as
|
|
259
|
+
* assertion-free. `SpecialPowers.ok(` stays excluded — that is API surface,
|
|
260
|
+
* not an assertion.
|
|
261
|
+
*
|
|
262
|
+
* Deliberately still textual, not AST-based: this runs over patch bodies
|
|
263
|
+
* rather than resolvable sources, and the surrounding rule is a floor, not a
|
|
264
|
+
* proof.
|
|
265
|
+
*/
|
|
266
|
+
const ASSERTION_MEMBERS = [
|
|
267
|
+
'ok',
|
|
268
|
+
'is',
|
|
269
|
+
'isnot',
|
|
270
|
+
'isDeeply',
|
|
271
|
+
'equal',
|
|
272
|
+
'notEqual',
|
|
273
|
+
'deepEqual',
|
|
274
|
+
'notDeepEqual',
|
|
275
|
+
'strictEqual',
|
|
276
|
+
'notStrictEqual',
|
|
277
|
+
'greater',
|
|
278
|
+
'greaterOrEqual',
|
|
279
|
+
'less',
|
|
280
|
+
'lessOrEqual',
|
|
281
|
+
'stringMatches',
|
|
282
|
+
'stringContains',
|
|
283
|
+
'throws',
|
|
284
|
+
'rejects',
|
|
285
|
+
// `todo`, `todo_is`, `todo_isnot`, `info`-free — todo* record an expected
|
|
286
|
+
// failure, which is still a recorded assertion outcome.
|
|
287
|
+
'todo',
|
|
288
|
+
'todo_is',
|
|
289
|
+
'todo_isnot',
|
|
290
|
+
].join('|');
|
|
291
|
+
const BROWSER_CHROME_ASSERTION = new RegExp(`(?<![\\w.#$-])(?:(?:Assert|SimpleTest)\\.(?:${ASSERTION_MEMBERS})\\s*\\(|(?:window\\.|[\\w$]+\\?\\.)?(?:ok|is|isnot|isDeeply)\\s*\\()`);
|
|
292
|
+
/** True when `strippedContent` contains at least one assertion call. */
|
|
293
|
+
export function hasBrowserChromeAssertion(strippedContent) {
|
|
294
|
+
return BROWSER_CHROME_ASSERTION.test(strippedContent);
|
|
295
|
+
}
|
|
217
296
|
/**
|
|
218
297
|
* Detects comment style from file extension for license header checks.
|
|
219
298
|
*/
|
|
@@ -406,8 +485,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
406
485
|
// browser_*.js silently passed under the old `isNew` gate).
|
|
407
486
|
const assertionFloor = config.patchLint?.testAssertionFloor;
|
|
408
487
|
if (assertionFloor && assertionFloor !== 'off' && isBrowserChromeTestFile(file)) {
|
|
409
|
-
const
|
|
410
|
-
const hasAssertion = ASSERTION_TOKENS.some((tok) => strippedContent.includes(tok));
|
|
488
|
+
const hasAssertion = hasBrowserChromeAssertion(strippedContent);
|
|
411
489
|
if (!hasAssertion) {
|
|
412
490
|
issues.push({
|
|
413
491
|
file,
|
|
@@ -535,7 +613,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
535
613
|
issues.push({
|
|
536
614
|
file: AGGREGATE_PATCH_FILE,
|
|
537
615
|
check: 'large-patch-files',
|
|
538
|
-
message: `Patch affects ${filesAffected.length} files (recommended: ≤${fileThreshold}).
|
|
616
|
+
message: `Patch affects ${filesAffected.length} files (recommended: ≤${fileThreshold}). ${SPLIT_HINT}`,
|
|
539
617
|
severity: 'warning',
|
|
540
618
|
});
|
|
541
619
|
}
|
|
@@ -543,7 +621,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
543
621
|
issues.push({
|
|
544
622
|
file: AGGREGATE_PATCH_FILE,
|
|
545
623
|
check: 'large-patch-lines',
|
|
546
|
-
message: `Patch is ${lineCount} lines (hard limit: ${lineThresholds.error}).
|
|
624
|
+
message: `Patch is ${lineCount} lines (hard limit: ${lineThresholds.error}). ${SPLIT_HINT}`,
|
|
547
625
|
severity: 'error',
|
|
548
626
|
});
|
|
549
627
|
}
|
|
@@ -551,7 +629,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
551
629
|
issues.push({
|
|
552
630
|
file: AGGREGATE_PATCH_FILE,
|
|
553
631
|
check: 'large-patch-lines',
|
|
554
|
-
message: `Patch is ${lineCount} lines (soft limit: ${lineThresholds.warning}, hard limit: ${lineThresholds.error}).
|
|
632
|
+
message: `Patch is ${lineCount} lines (soft limit: ${lineThresholds.warning}, hard limit: ${lineThresholds.error}). ${SPLIT_HINT}`,
|
|
555
633
|
severity: 'warning',
|
|
556
634
|
});
|
|
557
635
|
}
|
|
@@ -559,7 +637,7 @@ export function lintPatchSize(filesAffected, lineCount, patchTier) {
|
|
|
559
637
|
issues.push({
|
|
560
638
|
file: AGGREGATE_PATCH_FILE,
|
|
561
639
|
check: 'large-patch-lines',
|
|
562
|
-
message: `Patch is ${lineCount} lines (soft limit: ${lineThresholds.warning}, hard limit: ${lineThresholds.error}).
|
|
640
|
+
message: `Patch is ${lineCount} lines (soft limit: ${lineThresholds.warning}, hard limit: ${lineThresholds.error}). ${SPLIT_HINT}`,
|
|
563
641
|
severity: 'notice',
|
|
564
642
|
});
|
|
565
643
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem-based lock for serializing patch directory mutations.
|
|
3
3
|
*/
|
|
4
|
+
export interface PatchDirectoryLockOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Wait up to this many seconds for the patch directory lock instead of the
|
|
7
|
+
* default ~30 s budget. Enables exponential poll backoff (100 ms → 2 s)
|
|
8
|
+
* and a holder-identified progress line roughly every 5 s. `undefined`
|
|
9
|
+
* preserves the historical behavior exactly.
|
|
10
|
+
*/
|
|
11
|
+
waitLockSeconds?: number | undefined;
|
|
12
|
+
/** Command name recorded in the lock's owner metadata for holder diagnostics. */
|
|
13
|
+
command?: string | undefined;
|
|
14
|
+
}
|
|
4
15
|
/**
|
|
5
16
|
* Runs a patch directory mutation while holding an exclusive filesystem lock.
|
|
6
17
|
* This serializes filename allocation and manifest writes across parallel exports.
|
|
7
18
|
*/
|
|
8
|
-
export declare function withPatchDirectoryLock<T>(patchesDir: string, operation: () => Promise<T
|
|
19
|
+
export declare function withPatchDirectoryLock<T>(patchesDir: string, operation: () => Promise<T>, options?: PatchDirectoryLockOptions): Promise<T>;
|
|
@@ -6,18 +6,54 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { FireForgeError } from '../errors/base.js';
|
|
7
7
|
import { PatchError } from '../errors/patch.js';
|
|
8
8
|
import { toError } from '../utils/errors.js';
|
|
9
|
+
import { info } from '../utils/logger.js';
|
|
9
10
|
import { withFileLock } from './file-lock.js';
|
|
10
11
|
const PATCH_DIRECTORY_LOCK = '.fireforge-patches.lock';
|
|
12
|
+
/**
|
|
13
|
+
* Formats the periodic operator-facing waiting line printed while
|
|
14
|
+
* `--wait-lock` polls a contended patch directory lock. Mirrors the engine
|
|
15
|
+
* session lock's progress line so operators read one shape for both locks.
|
|
16
|
+
*/
|
|
17
|
+
function formatWaitProgressLine(waitedMs, timeoutMs, holder) {
|
|
18
|
+
const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
|
|
19
|
+
if (holder === undefined) {
|
|
20
|
+
return `Waiting for the FireForge patch directory lock — ${progress}.`;
|
|
21
|
+
}
|
|
22
|
+
const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
|
|
23
|
+
return `Waiting for the FireForge patch directory lock held by PID ${String(holder.pid)}${details} — ${progress}.`;
|
|
24
|
+
}
|
|
11
25
|
/**
|
|
12
26
|
* Runs a patch directory mutation while holding an exclusive filesystem lock.
|
|
13
27
|
* This serializes filename allocation and manifest writes across parallel exports.
|
|
14
28
|
*/
|
|
15
|
-
export async function withPatchDirectoryLock(patchesDir, operation) {
|
|
29
|
+
export async function withPatchDirectoryLock(patchesDir, operation, options = {}) {
|
|
16
30
|
const lockDir = join(patchesDir, PATCH_DIRECTORY_LOCK);
|
|
31
|
+
const { waitLockSeconds, command } = options;
|
|
17
32
|
return withFileLock(lockDir, operation, {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
...(waitLockSeconds !== undefined
|
|
34
|
+
? {
|
|
35
|
+
timeoutMs: waitLockSeconds * 1000,
|
|
36
|
+
pollMs: 100,
|
|
37
|
+
pollMaxMs: 2000,
|
|
38
|
+
waitProgressMs: 5000,
|
|
39
|
+
onWaitProgress: ({ waitedMs, timeoutMs, holder }) => {
|
|
40
|
+
info(formatWaitProgressLine(waitedMs, timeoutMs, holder));
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
...(command !== undefined
|
|
45
|
+
? { ownerMetadata: [`command=${command}`, `started=${new Date().toISOString()}`] }
|
|
46
|
+
: {}),
|
|
47
|
+
// Reason first, remedy second (FORGE H5). The remedy is waiting, not
|
|
48
|
+
// deleting: bulk exports/re-exports legitimately hold this lock for
|
|
49
|
+
// minutes, FireForge reaps genuinely stale locks on its own, and the
|
|
50
|
+
// old advice ("rm -rf" while a holder was alive) destroyed a live
|
|
51
|
+
// lock. The leading sentence is a message contract; extend, don't
|
|
52
|
+
// reword. withFileLock appends the holder identification.
|
|
53
|
+
onTimeoutMessage: `Timed out waiting for another FireForge command mutating ${patchesDir}.\n` +
|
|
54
|
+
`Pass --wait-lock [seconds] to wait longer (bare --wait-lock waits up to 60 seconds). ` +
|
|
55
|
+
`FireForge removes genuinely stale locks automatically; only if no other fireforge ` +
|
|
56
|
+
`process is running and the timeout persists, remove the lock manually:\n rm -rf "${lockDir}"`,
|
|
21
57
|
onStaleLockMessage: (ageMs) => `Removing stale patch lock (age: ${Math.round(ageMs / 1000)}s). ` +
|
|
22
58
|
'A previous fireforge process may have crashed.',
|
|
23
59
|
}).catch((error) => {
|
|
@@ -112,10 +112,22 @@ async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVers
|
|
|
112
112
|
return Number.isFinite(patch.order) ? Math.max(highest, patch.order) : highest;
|
|
113
113
|
}, 0);
|
|
114
114
|
let nextRecoveredOrder = highestFiniteOrder + 1;
|
|
115
|
-
|
|
115
|
+
// Prefetch the two independent per-patch reads concurrently. This runs
|
|
116
|
+
// while the patch-directory lock is held, and it was two serialised awaits
|
|
117
|
+
// per patch across the whole queue — 58 round-trips on a 29-patch queue.
|
|
118
|
+
// The fold below stays sequential because `nextRecoveredOrder` is
|
|
119
|
+
// loop-carried: parallelising the assignment would scramble the recovered
|
|
120
|
+
// ordinals.
|
|
121
|
+
const prefetched = await Promise.all(patches.map(async (patch) => ({
|
|
122
|
+
filesAffected: normalizeFiles(await getAllTargetFilesFromPatch(patch.path)),
|
|
123
|
+
patchStats: await stat(patch.path),
|
|
124
|
+
})));
|
|
125
|
+
for (const [index, patch] of patches.entries()) {
|
|
116
126
|
const existing = existingEntries.get(patch.filename);
|
|
117
|
-
const
|
|
118
|
-
|
|
127
|
+
const prefetch = prefetched[index];
|
|
128
|
+
if (!prefetch)
|
|
129
|
+
continue;
|
|
130
|
+
const { filesAffected, patchStats } = prefetch;
|
|
119
131
|
const inferred = inferPatchMetadataFromFilename(patch.filename);
|
|
120
132
|
const recoveredOrder = Number.isFinite(patch.order) ? patch.order : nextRecoveredOrder++;
|
|
121
133
|
if (!existing) {
|
|
@@ -60,6 +60,14 @@ export interface DiffSection {
|
|
|
60
60
|
* refuse them rather than treat base85 lines starting with `+` as content.
|
|
61
61
|
*/
|
|
62
62
|
isBinary: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Old-side blob hash from the section's `index <old>..<new>` line
|
|
65
|
+
* (possibly abbreviated). Undefined when the metadata zone carries no
|
|
66
|
+
* parseable index line (hand-written diffs, `Binary files … differ`).
|
|
67
|
+
*/
|
|
68
|
+
indexOldHash?: string;
|
|
69
|
+
/** New-side blob hash from the `index` line; see {@link indexOldHash}. */
|
|
70
|
+
indexNewHash?: string;
|
|
63
71
|
/** Parsed hunks, in file order. Empty for binary or metadata-only sections. */
|
|
64
72
|
hunks: ParsedHunk[];
|
|
65
73
|
}
|
|
@@ -169,6 +169,16 @@ export function parseDiffSections(diffContent) {
|
|
|
169
169
|
current.isBinary = true;
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
|
+
// `git diff --binary` always records full blob hashes here (git
|
|
173
|
+
// apply requires them); text sections carry abbreviated ones. The
|
|
174
|
+
// hashes let binary sections be compared by identity when their
|
|
175
|
+
// payload cannot be applied as text (FORGE J3).
|
|
176
|
+
const indexMatch = /^index ([0-9a-f]{7,64})\.\.([0-9a-f]{7,64})(?: \d{6})?$/.exec(line);
|
|
177
|
+
if (indexMatch?.[1] !== undefined && indexMatch[2] !== undefined) {
|
|
178
|
+
current.indexOldHash = indexMatch[1];
|
|
179
|
+
current.indexNewHash = indexMatch[2];
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
if (current.isBinary)
|
|
174
184
|
continue;
|
|
@@ -23,6 +23,13 @@ export interface PatchPolicyEnforcementInput {
|
|
|
23
23
|
manifest: PatchesManifest;
|
|
24
24
|
command: string;
|
|
25
25
|
forceUnsafe?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Per-issue-code remediation lines appended to matching details, so a
|
|
28
|
+
* refusal can name the exact flag or command that fixes it on the
|
|
29
|
+
* command it was raised from (e.g. `--description` on
|
|
30
|
+
* `patch move-files --create`).
|
|
31
|
+
*/
|
|
32
|
+
hints?: Partial<Record<PatchPolicyIssueCode, string>>;
|
|
26
33
|
}
|
|
27
34
|
/** Returns valid categories for prompts and CLI validation under the config. */
|
|
28
35
|
export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
|
|
@@ -341,7 +341,10 @@ export function enforcePatchPolicy(input) {
|
|
|
341
341
|
if (issues.length === 0)
|
|
342
342
|
return;
|
|
343
343
|
const mode = mutationMode(input.config);
|
|
344
|
-
const details = issues.map((issue) =>
|
|
344
|
+
const details = issues.map((issue) => {
|
|
345
|
+
const hint = input.hints?.[issue.code];
|
|
346
|
+
return ` [${issue.code}] ${issue.message}${hint ? `\n → ${hint}` : ''}`;
|
|
347
|
+
});
|
|
345
348
|
if (mode === 'warn') {
|
|
346
349
|
warn(`${input.command}: patch policy warning(s):`);
|
|
347
350
|
for (const detail of details)
|
|
@@ -35,3 +35,13 @@ export declare function extractNewFileContent(patchPath: string, targetFile?: st
|
|
|
35
35
|
* @returns Modified content
|
|
36
36
|
*/
|
|
37
37
|
export declare function applyPatchToContent(content: string | null, patchPath: string, targetFile: string): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Applies an already-read patch body to content. Batched callers that hold
|
|
40
|
+
* many (patch, file) pairs use this to read each patch body once instead of
|
|
41
|
+
* once per pair.
|
|
42
|
+
* @param content - Original content (null for new files)
|
|
43
|
+
* @param patchContent - Full text of the patch body
|
|
44
|
+
* @param targetFile - The file path within the patch
|
|
45
|
+
* @returns Modified content
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyPatchTextToContent(content: string | null, patchContent: string, targetFile: string): string;
|
|
@@ -71,11 +71,22 @@ export async function extractNewFileContent(patchPath, targetFile) {
|
|
|
71
71
|
* @returns Modified content
|
|
72
72
|
*/
|
|
73
73
|
export async function applyPatchToContent(content, patchPath, targetFile) {
|
|
74
|
-
|
|
74
|
+
return applyPatchTextToContent(content, await readText(patchPath), targetFile);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Applies an already-read patch body to content. Batched callers that hold
|
|
78
|
+
* many (patch, file) pairs use this to read each patch body once instead of
|
|
79
|
+
* once per pair.
|
|
80
|
+
* @param content - Original content (null for new files)
|
|
81
|
+
* @param patchContent - Full text of the patch body
|
|
82
|
+
* @param targetFile - The file path within the patch
|
|
83
|
+
* @returns Modified content
|
|
84
|
+
*/
|
|
85
|
+
export function applyPatchTextToContent(content, patchContent, targetFile) {
|
|
75
86
|
// Check if this is a new file patch for the target file specifically
|
|
76
87
|
if (content === null) {
|
|
77
88
|
if (isNewFileInPatch(patchContent, targetFile)) {
|
|
78
|
-
return
|
|
89
|
+
return extractNewFileContentFromDiff(patchContent, targetFile);
|
|
79
90
|
}
|
|
80
91
|
// If not a new file patch but content is null, return empty
|
|
81
92
|
return '';
|
|
@@ -35,7 +35,47 @@ export interface RebaseSession {
|
|
|
35
35
|
currentIndex: number;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Outcome of reading the session file, following the {@link LockOwner}
|
|
39
|
+
* discriminated-union form rather than collapsing three states into `null`.
|
|
40
|
+
*
|
|
41
|
+
* Distinguishing `present: false` from `present: true, valid: false` is
|
|
42
|
+
* load-bearing. Before 0.41.0 both returned `null` while
|
|
43
|
+
* {@link hasActiveRebaseSession} reported liveness from `pathExists` alone, so
|
|
44
|
+
* an invalid session file wedged the operator in a closed cycle: `rebase` said
|
|
45
|
+
* "already in progress — use --continue or --abort", and both of those said
|
|
46
|
+
* "no rebase session in progress". No CLI path deleted the file and no message
|
|
47
|
+
* named it, so the only escape was knowing to `rm` it by hand.
|
|
48
|
+
*/
|
|
49
|
+
export type RebaseSessionRead = {
|
|
50
|
+
present: false;
|
|
51
|
+
} | {
|
|
52
|
+
present: true;
|
|
53
|
+
valid: true;
|
|
54
|
+
session: RebaseSession;
|
|
55
|
+
} | {
|
|
56
|
+
present: true;
|
|
57
|
+
valid: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
/** Absolute path of the session file, for messages that must name it. */
|
|
61
|
+
export declare function getRebaseSessionPath(projectRoot: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Reads the session file, reporting absent, valid, and corrupt as three
|
|
64
|
+
* distinct outcomes. Never throws for a malformed file: `readJson` calls
|
|
65
|
+
* `JSON.parse` with no guard of its own, so an interrupted write surfaced a
|
|
66
|
+
* raw `SyntaxError` out of `--continue`/`--abort` before 0.41.0.
|
|
67
|
+
*
|
|
68
|
+
* A single read determines liveness and validity: only ENOENT/ENOTDIR from
|
|
69
|
+
* that read mean `absent`. A pathExists pre-probe both raced deletion (a file
|
|
70
|
+
* removed between probe and read misreported an absent session as corrupt)
|
|
71
|
+
* and swallowed EACCES (an unreadable `.fireforge/` misreported a session as
|
|
72
|
+
* absent) — the same failure `readTreeMarkerState` fixed for tree markers.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readRebaseSession(projectRoot: string): Promise<RebaseSessionRead>;
|
|
75
|
+
/**
|
|
76
|
+
* Loads an existing rebase session, or returns `null` when none exists or the
|
|
77
|
+
* file on disk is unusable. Callers that must tell those two apart — every
|
|
78
|
+
* command that reports to an operator — should use {@link readRebaseSession}.
|
|
39
79
|
*/
|
|
40
80
|
export declare function loadRebaseSession(projectRoot: string): Promise<RebaseSession | null>;
|
|
41
81
|
/**
|
|
@@ -47,6 +87,8 @@ export declare function saveRebaseSession(projectRoot: string, session: RebaseSe
|
|
|
47
87
|
*/
|
|
48
88
|
export declare function clearRebaseSession(projectRoot: string): Promise<void>;
|
|
49
89
|
/**
|
|
50
|
-
* Returns `true` when
|
|
90
|
+
* Returns `true` when a rebase session file exists on disk, valid or not.
|
|
91
|
+
* Test-facing convenience — production code reads `readRebaseSession` once
|
|
92
|
+
* instead, so liveness and validity come from the same read.
|
|
51
93
|
*/
|
|
52
94
|
export declare function hasActiveRebaseSession(projectRoot: string): Promise<boolean>;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* resume with `fireforge rebase --continue`.
|
|
7
7
|
*/
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
9
10
|
import { pathExists, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
10
|
-
import { isArray, isObject, isString } from '../utils/validation.js';
|
|
11
|
+
import { isArray, isNumber, isObject, isString, isValidFirefoxProduct, isValidFirefoxVersion, } from '../utils/validation.js';
|
|
11
12
|
import { getProjectPaths } from './config-paths.js';
|
|
12
13
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
13
14
|
// ── Helpers ──
|
|
@@ -15,28 +16,122 @@ const SESSION_FILENAME = 'rebase-session.json';
|
|
|
15
16
|
function sessionPath(projectRoot) {
|
|
16
17
|
return join(getProjectPaths(projectRoot).fireforgeDir, SESSION_FILENAME);
|
|
17
18
|
}
|
|
19
|
+
const REBASE_PATCH_STATUSES = [
|
|
20
|
+
'pending',
|
|
21
|
+
'applied-clean',
|
|
22
|
+
'applied-fuzz',
|
|
23
|
+
'failed',
|
|
24
|
+
'resolved',
|
|
25
|
+
'skipped',
|
|
26
|
+
];
|
|
27
|
+
/** Validates one `patches[]` element to its declared shape. */
|
|
28
|
+
function isValidPatchEntry(value) {
|
|
29
|
+
if (!isObject(value))
|
|
30
|
+
return false;
|
|
31
|
+
if (!isString(value['filename']) || value['filename'].length === 0)
|
|
32
|
+
return false;
|
|
33
|
+
if (!REBASE_PATCH_STATUSES.includes(value['status']))
|
|
34
|
+
return false;
|
|
35
|
+
if (value['fuzzFactor'] !== undefined && !isNumber(value['fuzzFactor']))
|
|
36
|
+
return false;
|
|
37
|
+
if (value['error'] !== undefined && !isString(value['error']))
|
|
38
|
+
return false;
|
|
39
|
+
const conflicting = value['conflictingFiles'];
|
|
40
|
+
if (conflicting !== undefined && (!isArray(conflicting) || !conflicting.every(isString))) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** Optional product field: absent is legal, present must be a real product. */
|
|
46
|
+
function isValidOptionalProduct(value) {
|
|
47
|
+
return value === undefined || (isString(value) && isValidFirefoxProduct(value));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validates a session file against the shape the rebase commands actually
|
|
51
|
+
* consume — not merely against broad field types.
|
|
52
|
+
*
|
|
53
|
+
* The looser predicate this replaces checked six `typeof`s and nothing else,
|
|
54
|
+
* which admitted sessions the resume path then acted on. `patches: [null]`
|
|
55
|
+
* reached `patch-loop.ts`, which reads `.filename` off each entry and hands it
|
|
56
|
+
* to `stampPatchVersions`. A `currentIndex` of `NaN` (legal for
|
|
57
|
+
* `typeof === 'number'`) made the resume loop run zero iterations and report
|
|
58
|
+
* success; a negative one indexed out of range in `continue.ts`. `toVersion:
|
|
59
|
+
* ""` passed `isString` and was stamped verbatim onto every Furnace override's
|
|
60
|
+
* `baseVersion`. `fromProduct`/`toProduct` were not checked at all despite
|
|
61
|
+
* being typed `FirefoxProduct`.
|
|
62
|
+
*
|
|
63
|
+
* The session file is written only by `rebase/index.ts` from validated CLI and
|
|
64
|
+
* config values, so every one of these is a corrupt-file or hand-edit case —
|
|
65
|
+
* which is exactly the case this predicate exists to catch.
|
|
66
|
+
*/
|
|
18
67
|
function isValidSession(data) {
|
|
19
68
|
if (!isObject(data))
|
|
20
69
|
return false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
70
|
+
if (!isString(data['startedAt']) || Number.isNaN(Date.parse(data['startedAt'])))
|
|
71
|
+
return false;
|
|
72
|
+
// 4 is git's own minimum abbreviation length; 40/64 are full SHA-1/SHA-256.
|
|
73
|
+
if (!isString(data['preRebaseCommit']) || !/^[0-9a-f]{4,64}$/i.test(data['preRebaseCommit'])) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (!isString(data['fromVersion']) || !isValidFirefoxVersion(data['fromVersion']))
|
|
77
|
+
return false;
|
|
78
|
+
if (!isString(data['toVersion']) || !isValidFirefoxVersion(data['toVersion']))
|
|
79
|
+
return false;
|
|
80
|
+
if (!isValidOptionalProduct(data['fromProduct']))
|
|
81
|
+
return false;
|
|
82
|
+
if (!isValidOptionalProduct(data['toProduct']))
|
|
83
|
+
return false;
|
|
84
|
+
const patches = data['patches'];
|
|
85
|
+
if (!isArray(patches) || !patches.every(isValidPatchEntry))
|
|
86
|
+
return false;
|
|
87
|
+
// `<= patches.length` because the resume point legally sits one past the
|
|
88
|
+
// last entry once every patch has been processed.
|
|
89
|
+
const currentIndex = data['currentIndex'];
|
|
90
|
+
if (!isNumber(currentIndex) || !Number.isInteger(currentIndex))
|
|
91
|
+
return false;
|
|
92
|
+
return currentIndex >= 0 && currentIndex <= patches.length;
|
|
93
|
+
}
|
|
94
|
+
/** Absolute path of the session file, for messages that must name it. */
|
|
95
|
+
export function getRebaseSessionPath(projectRoot) {
|
|
96
|
+
return sessionPath(projectRoot);
|
|
27
97
|
}
|
|
28
|
-
// ── Public API ──
|
|
29
98
|
/**
|
|
30
|
-
*
|
|
99
|
+
* Reads the session file, reporting absent, valid, and corrupt as three
|
|
100
|
+
* distinct outcomes. Never throws for a malformed file: `readJson` calls
|
|
101
|
+
* `JSON.parse` with no guard of its own, so an interrupted write surfaced a
|
|
102
|
+
* raw `SyntaxError` out of `--continue`/`--abort` before 0.41.0.
|
|
103
|
+
*
|
|
104
|
+
* A single read determines liveness and validity: only ENOENT/ENOTDIR from
|
|
105
|
+
* that read mean `absent`. A pathExists pre-probe both raced deletion (a file
|
|
106
|
+
* removed between probe and read misreported an absent session as corrupt)
|
|
107
|
+
* and swallowed EACCES (an unreadable `.fireforge/` misreported a session as
|
|
108
|
+
* absent) — the same failure `readTreeMarkerState` fixed for tree markers.
|
|
31
109
|
*/
|
|
32
|
-
export async function
|
|
110
|
+
export async function readRebaseSession(projectRoot) {
|
|
33
111
|
const path = sessionPath(projectRoot);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
112
|
+
let data;
|
|
113
|
+
try {
|
|
114
|
+
data = await readJson(path);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const code = getNodeErrorCode(error);
|
|
118
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
119
|
+
return { present: false };
|
|
120
|
+
return { present: true, valid: false, reason: toError(error).message };
|
|
121
|
+
}
|
|
122
|
+
if (!isValidSession(data)) {
|
|
123
|
+
return { present: true, valid: false, reason: 'the file is not a valid rebase session' };
|
|
124
|
+
}
|
|
125
|
+
return { present: true, valid: true, session: data };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Loads an existing rebase session, or returns `null` when none exists or the
|
|
129
|
+
* file on disk is unusable. Callers that must tell those two apart — every
|
|
130
|
+
* command that reports to an operator — should use {@link readRebaseSession}.
|
|
131
|
+
*/
|
|
132
|
+
export async function loadRebaseSession(projectRoot) {
|
|
133
|
+
const result = await readRebaseSession(projectRoot);
|
|
134
|
+
return result.present && result.valid ? result.session : null;
|
|
40
135
|
}
|
|
41
136
|
/**
|
|
42
137
|
* Persists a rebase session atomically.
|
|
@@ -57,7 +152,9 @@ export async function clearRebaseSession(projectRoot) {
|
|
|
57
152
|
}
|
|
58
153
|
}
|
|
59
154
|
/**
|
|
60
|
-
* Returns `true` when
|
|
155
|
+
* Returns `true` when a rebase session file exists on disk, valid or not.
|
|
156
|
+
* Test-facing convenience — production code reads `readRebaseSession` once
|
|
157
|
+
* instead, so liveness and validity come from the same read.
|
|
61
158
|
*/
|
|
62
159
|
export async function hasActiveRebaseSession(projectRoot) {
|
|
63
160
|
return pathExists(sessionPath(projectRoot));
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { GeneralError } from '../errors/base.js';
|
|
7
7
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
8
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
8
9
|
import { findAlphabeticalPosition, findAlphabeticalTokenPosition } from './manifest-helpers.js';
|
|
9
10
|
import { tokenizeJarMn } from './manifest-tokenizers.js';
|
|
10
11
|
import { withParserFallback } from './parser-fallback.js';
|
|
@@ -18,7 +19,7 @@ function registerBrowserContentTokenized(content, fileName, entry, after) {
|
|
|
18
19
|
let insertIndex;
|
|
19
20
|
let previousEntry;
|
|
20
21
|
if (after) {
|
|
21
|
-
const afterPattern = new RegExp(`(?:^|/)${after
|
|
22
|
+
const afterPattern = new RegExp(`(?:^|/)${escapeRegex(after)}(?:\\s|\\)|$)`);
|
|
22
23
|
const afterToken = tokens.find((t) => afterPattern.test(t.raw));
|
|
23
24
|
if (afterToken) {
|
|
24
25
|
insertIndex = afterToken.lineIndex + 1;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { GeneralError } from '../errors/base.js';
|
|
7
7
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
8
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
8
9
|
import { findAlphabeticalPosition, findAlphabeticalTokenPosition } from './manifest-helpers.js';
|
|
9
10
|
import { tokenizeJarMn } from './manifest-tokenizers.js';
|
|
10
11
|
import { withParserFallback } from './parser-fallback.js';
|
|
@@ -18,7 +19,7 @@ function registerSharedCSSTokenized(content, name, entry, after) {
|
|
|
18
19
|
let insertIndex;
|
|
19
20
|
let previousEntry;
|
|
20
21
|
if (after) {
|
|
21
|
-
const afterPattern = new RegExp(`(?:^|/)${after
|
|
22
|
+
const afterPattern = new RegExp(`(?:^|/)${escapeRegex(after)}(?:\\s|\\)|$)`);
|
|
22
23
|
const afterToken = tokens.find((t) => afterPattern.test(t.raw));
|
|
23
24
|
if (afterToken) {
|
|
24
25
|
insertIndex = afterToken.lineIndex + 1;
|