@hominis/fireforge 0.50.0 → 0.51.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 +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- 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-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
package/dist/src/core/mach.js
CHANGED
|
@@ -7,6 +7,7 @@ import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from
|
|
|
7
7
|
import { withBuildRunRecord } from './build-run-record.js';
|
|
8
8
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
9
9
|
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
10
|
+
import { createLinePrefixer } from './line-prefixer.js';
|
|
10
11
|
import { explainMachError } from './mach-error-hints.js';
|
|
11
12
|
import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, hasKnownTeardownNoise, KNOWN_TEARDOWN_NOISE_BUILD_NOTE, } from './mach-known-noise-filter.js';
|
|
12
13
|
import { getPython } from './mach-python.js';
|
|
@@ -89,6 +90,15 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
89
90
|
const noiseContext = options.annotateKnownTeardownNoise === true ? createTeardownNoiseContext() : undefined;
|
|
90
91
|
const stdoutFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
91
92
|
const stderrFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
93
|
+
const prefix = options.outputPrefix;
|
|
94
|
+
const prefixers = prefix === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: {
|
|
97
|
+
stdoutLog: createLinePrefixer(prefix),
|
|
98
|
+
stdoutEcho: createLinePrefixer(prefix),
|
|
99
|
+
stderrLog: createLinePrefixer(prefix),
|
|
100
|
+
stderrEcho: createLinePrefixer(prefix),
|
|
101
|
+
};
|
|
92
102
|
const exitCode = await execStream(python, [machPath, ...args], {
|
|
93
103
|
cwd: engineDir,
|
|
94
104
|
...(options.env ? { env: options.env } : {}),
|
|
@@ -110,8 +120,9 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
110
120
|
// echo filter only exists to shorten what a human scrolls past.
|
|
111
121
|
// Collapsing a traceback in the artifact would reintroduce the loss
|
|
112
122
|
// this log exists to prevent.
|
|
113
|
-
writeToActiveRunLog(data);
|
|
114
|
-
|
|
123
|
+
writeToActiveRunLog(prefixers ? prefixers.stdoutLog.transform(data) : data);
|
|
124
|
+
const echo = stdoutFilter ? stdoutFilter.transform(data) : data;
|
|
125
|
+
process.stdout.write(prefixers ? prefixers.stdoutEcho.transform(echo) : echo);
|
|
115
126
|
},
|
|
116
127
|
onStderr: (data) => {
|
|
117
128
|
options.onOutputChunk?.('stderr', data);
|
|
@@ -119,19 +130,22 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
119
130
|
if (stderr.length > CAPTURE_TAIL_LIMIT) {
|
|
120
131
|
stderr = stderr.slice(-CAPTURE_TAIL_LIMIT);
|
|
121
132
|
}
|
|
122
|
-
writeToActiveRunLog(data);
|
|
123
|
-
|
|
133
|
+
writeToActiveRunLog(prefixers ? prefixers.stderrLog.transform(data) : data);
|
|
134
|
+
const echo = stderrFilter ? stderrFilter.transform(data) : data;
|
|
135
|
+
process.stderr.write(prefixers ? prefixers.stderrEcho.transform(echo) : echo);
|
|
124
136
|
},
|
|
125
137
|
});
|
|
126
138
|
if (stdoutFilter) {
|
|
127
139
|
const residue = stdoutFilter.flush();
|
|
128
|
-
if (residue.length > 0)
|
|
129
|
-
process.stdout.write(residue);
|
|
140
|
+
if (residue.length > 0) {
|
|
141
|
+
process.stdout.write(prefixers ? prefixers.stdoutEcho.transform(residue) : residue);
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
if (stderrFilter) {
|
|
132
145
|
const residue = stderrFilter.flush();
|
|
133
|
-
if (residue.length > 0)
|
|
134
|
-
process.stderr.write(residue);
|
|
146
|
+
if (residue.length > 0) {
|
|
147
|
+
process.stderr.write(prefixers ? prefixers.stderrEcho.transform(residue) : residue);
|
|
148
|
+
}
|
|
135
149
|
}
|
|
136
150
|
return { stdout, stderr, exitCode };
|
|
137
151
|
}
|
|
@@ -418,13 +432,14 @@ function testVerbosityEnvUnset(fullOutput) {
|
|
|
418
432
|
* @param options - Engine checkout, test paths, extra args, env, verbosity
|
|
419
433
|
*/
|
|
420
434
|
export async function runMachTestSuite(kind, options) {
|
|
421
|
-
const { engineDir, testPaths = [], args = [], env, fullOutput, teardown } = options;
|
|
435
|
+
const { engineDir, testPaths = [], args = [], env, fullOutput, teardown, outputPrefix } = options;
|
|
422
436
|
const guard = await installMachResourceGuard(engineDir);
|
|
423
437
|
const envUnset = testVerbosityEnvUnset(fullOutput);
|
|
424
438
|
return runMachCapture([kind, ...testPaths, ...args], engineDir, {
|
|
425
439
|
env: { ...guard.env, ...env },
|
|
426
440
|
...(envUnset ? { envUnset } : {}),
|
|
427
441
|
...teardown,
|
|
442
|
+
...(outputPrefix === undefined ? {} : { outputPrefix }),
|
|
428
443
|
annotateKnownTeardownNoise: true,
|
|
429
444
|
});
|
|
430
445
|
}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* no owned file is an upstream Firefox module, which FireForge does not
|
|
19
19
|
* police and cannot cheaply enumerate. Policing only what the fork owns is
|
|
20
20
|
* both the actionable set and the one that produces real incidents.
|
|
21
|
+
*
|
|
22
|
+
* An unscoped `verify` widens that set with the engine worktree: changed
|
|
23
|
+
* files import, and untracked files are targets. A chrome script's lazy
|
|
24
|
+
* getter for a new, not yet exported `.sys.mjs` with no moz.build line
|
|
25
|
+
* SIGSEGVed every start of a shared objdir, and neither file was in the
|
|
26
|
+
* queue yet, so the queue-only scan could not see it.
|
|
21
27
|
*/
|
|
22
28
|
/** Why a queue-owned module import does not resolve to a packaged module. */
|
|
23
29
|
export type UnresolvedModuleReason = 'missing-file' | 'unregistered';
|
|
@@ -31,16 +37,26 @@ export interface UnresolvedModuleImport {
|
|
|
31
37
|
importers: string[];
|
|
32
38
|
reason: UnresolvedModuleReason;
|
|
33
39
|
}
|
|
40
|
+
/** Engine worktree files the preflight also judges, beside the queue's. */
|
|
41
|
+
export interface WorktreeModuleFiles {
|
|
42
|
+
/** Changed or untracked files: scanned for imports. */
|
|
43
|
+
changed: readonly string[];
|
|
44
|
+
/** Untracked files: new modules the fork has not exported yet. */
|
|
45
|
+
untracked: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** Lists the engine's changed and untracked files for the worktree scan. */
|
|
48
|
+
export declare function collectWorktreeModuleFiles(engineDir: string): Promise<WorktreeModuleFiles>;
|
|
34
49
|
/**
|
|
35
50
|
* Resolves every `resource://` system-module import made by the
|
|
36
|
-
* queue-owned files and
|
|
37
|
-
* load.
|
|
51
|
+
* queue-owned files (and, given `worktree`, by changed engine files) and
|
|
52
|
+
* reports the ones a packaged browser could not load.
|
|
38
53
|
*
|
|
39
54
|
* @param engineDir - Path to the engine checkout
|
|
40
55
|
* @param ownedFiles - Engine-relative paths the patch queue owns
|
|
56
|
+
* @param worktree - Changed and untracked engine files to judge as well
|
|
41
57
|
* @returns Findings sorted by module path. Empty when everything resolves
|
|
42
58
|
*/
|
|
43
|
-
export declare function findUnresolvedSystemModuleImports(engineDir: string, ownedFiles: readonly string[]): Promise<UnresolvedModuleImport[]>;
|
|
59
|
+
export declare function findUnresolvedSystemModuleImports(engineDir: string, ownedFiles: readonly string[], worktree?: WorktreeModuleFiles): Promise<UnresolvedModuleImport[]>;
|
|
44
60
|
/**
|
|
45
61
|
* Renders findings as operator-facing lines, naming the unregistered or
|
|
46
62
|
* unresolvable module, so the failure is a sentence instead of a bare
|
|
@@ -19,12 +19,19 @@
|
|
|
19
19
|
* no owned file is an upstream Firefox module, which FireForge does not
|
|
20
20
|
* police and cannot cheaply enumerate. Policing only what the fork owns is
|
|
21
21
|
* both the actionable set and the one that produces real incidents.
|
|
22
|
+
*
|
|
23
|
+
* An unscoped `verify` widens that set with the engine worktree: changed
|
|
24
|
+
* files import, and untracked files are targets. A chrome script's lazy
|
|
25
|
+
* getter for a new, not yet exported `.sys.mjs` with no moz.build line
|
|
26
|
+
* SIGSEGVed every start of a shared objdir, and neither file was in the
|
|
27
|
+
* queue yet, so the queue-only scan could not see it.
|
|
22
28
|
*/
|
|
23
29
|
import { dirname, join } from 'node:path';
|
|
24
30
|
import { toError } from '../utils/errors.js';
|
|
25
31
|
import { pathExists, readText } from '../utils/fs.js';
|
|
26
32
|
import { verbose } from '../utils/logger.js';
|
|
27
33
|
import { escapeRegex } from '../utils/regex.js';
|
|
34
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from './git-status.js';
|
|
28
35
|
import { extractResourceModuleSpecifiers, moduleSourceSuffix, } from './patch-lint-module-registration.js';
|
|
29
36
|
/** Extensions whose contents are scanned for resource-module imports. */
|
|
30
37
|
const IMPORTABLE_EXTENSIONS = ['.mjs', '.sys.mjs', '.js', '.jsm'];
|
|
@@ -79,20 +86,31 @@ async function isRegisteredInAncestorMozBuild(engineDir, moduleRelPath, mozBuild
|
|
|
79
86
|
dir = parent;
|
|
80
87
|
}
|
|
81
88
|
}
|
|
89
|
+
/** Lists the engine's changed and untracked files for the worktree scan. */
|
|
90
|
+
export async function collectWorktreeModuleFiles(engineDir) {
|
|
91
|
+
const entries = await expandUntrackedDirectoryEntries(engineDir, await getWorkingTreeStatus(engineDir));
|
|
92
|
+
const present = entries.filter((entry) => entry.worktreeStatus !== 'D');
|
|
93
|
+
return {
|
|
94
|
+
changed: present.map((entry) => entry.file),
|
|
95
|
+
untracked: present.filter((entry) => entry.status === '??').map((entry) => entry.file),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
82
98
|
/**
|
|
83
99
|
* Resolves every `resource://` system-module import made by the
|
|
84
|
-
* queue-owned files and
|
|
85
|
-
* load.
|
|
100
|
+
* queue-owned files (and, given `worktree`, by changed engine files) and
|
|
101
|
+
* reports the ones a packaged browser could not load.
|
|
86
102
|
*
|
|
87
103
|
* @param engineDir - Path to the engine checkout
|
|
88
104
|
* @param ownedFiles - Engine-relative paths the patch queue owns
|
|
105
|
+
* @param worktree - Changed and untracked engine files to judge as well
|
|
89
106
|
* @returns Findings sorted by module path. Empty when everything resolves
|
|
90
107
|
*/
|
|
91
|
-
export async function findUnresolvedSystemModuleImports(engineDir, ownedFiles) {
|
|
92
|
-
const owned = [...new Set(ownedFiles)];
|
|
93
|
-
|
|
108
|
+
export async function findUnresolvedSystemModuleImports(engineDir, ownedFiles, worktree) {
|
|
109
|
+
const owned = [...new Set([...ownedFiles, ...(worktree?.untracked ?? [])])];
|
|
110
|
+
const importing = [...new Set([...owned, ...(worktree?.changed ?? [])])];
|
|
111
|
+
/** specifier → importing files. */
|
|
94
112
|
const importersBySpecifier = new Map();
|
|
95
|
-
for (const file of
|
|
113
|
+
for (const file of importing) {
|
|
96
114
|
if (!IMPORTABLE_EXTENSIONS.some((extension) => file.endsWith(extension)))
|
|
97
115
|
continue;
|
|
98
116
|
const content = await readIfPresent(join(engineDir, file));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which negative-control targets only a build can deliver.
|
|
3
|
+
*
|
|
4
|
+
* A control is applied after the pre-test build and right before dispatch,
|
|
5
|
+
* which is right for a file the runtime reads from the source tree or the
|
|
6
|
+
* objdir links to. A C++ (or Rust, IDL, …) source edited at that point is
|
|
7
|
+
* never compiled: the run tests the unmutated binary and reports
|
|
8
|
+
* `control=missed`, which reads as a test that cannot fail. Such a target
|
|
9
|
+
* is now refused without `--build`, and with `--build` the control is
|
|
10
|
+
* applied before the build so the binary under test carries it.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The engine-relative targets that reach the runtime only through a
|
|
14
|
+
* compile: a compiled-source extension and no install-manifest entry that
|
|
15
|
+
* ships the file itself.
|
|
16
|
+
*
|
|
17
|
+
* @param engineDir - Absolute engine directory
|
|
18
|
+
* @param enginePaths - The control's engine-relative targets
|
|
19
|
+
*/
|
|
20
|
+
export declare function findCompiledControlTargets(engineDir: string, enginePaths: readonly string[]): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* The refusal for a compiled control without `--build`, naming the manual
|
|
23
|
+
* procedure for a run that wants to keep its build out of FireForge.
|
|
24
|
+
*
|
|
25
|
+
* @param compiled - Engine-relative compiled targets
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatCompiledControlRefusal(compiled: readonly string[]): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Which negative-control targets only a build can deliver.
|
|
4
|
+
*
|
|
5
|
+
* A control is applied after the pre-test build and right before dispatch,
|
|
6
|
+
* which is right for a file the runtime reads from the source tree or the
|
|
7
|
+
* objdir links to. A C++ (or Rust, IDL, …) source edited at that point is
|
|
8
|
+
* never compiled: the run tests the unmutated binary and reports
|
|
9
|
+
* `control=missed`, which reads as a test that cannot fail. Such a target
|
|
10
|
+
* is now refused without `--build`, and with `--build` the control is
|
|
11
|
+
* applied before the build so the binary under test carries it.
|
|
12
|
+
*/
|
|
13
|
+
import { extname } from 'node:path';
|
|
14
|
+
import { classifyRuntimeDelivery } from './objdir-install-manifests.js';
|
|
15
|
+
import { loadEngineInstallIndex } from './test-stale-exempt.js';
|
|
16
|
+
/** Extensions of sources the build compiles or code-generates into the binary. */
|
|
17
|
+
const COMPILED_EXTENSIONS = new Set([
|
|
18
|
+
'.c',
|
|
19
|
+
'.cc',
|
|
20
|
+
'.cpp',
|
|
21
|
+
'.cxx',
|
|
22
|
+
'.h',
|
|
23
|
+
'.hh',
|
|
24
|
+
'.hpp',
|
|
25
|
+
'.m',
|
|
26
|
+
'.mm',
|
|
27
|
+
'.rs',
|
|
28
|
+
'.idl',
|
|
29
|
+
'.webidl',
|
|
30
|
+
'.ipdl',
|
|
31
|
+
'.ipdlh',
|
|
32
|
+
'.yaml',
|
|
33
|
+
'.yml',
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* The engine-relative targets that reach the runtime only through a
|
|
37
|
+
* compile: a compiled-source extension and no install-manifest entry that
|
|
38
|
+
* ships the file itself.
|
|
39
|
+
*
|
|
40
|
+
* @param engineDir - Absolute engine directory
|
|
41
|
+
* @param enginePaths - The control's engine-relative targets
|
|
42
|
+
*/
|
|
43
|
+
export async function findCompiledControlTargets(engineDir, enginePaths) {
|
|
44
|
+
const candidates = enginePaths.filter((path) => COMPILED_EXTENSIONS.has(extname(path)));
|
|
45
|
+
if (candidates.length === 0)
|
|
46
|
+
return [];
|
|
47
|
+
const index = await loadEngineInstallIndex(engineDir);
|
|
48
|
+
if (index === undefined)
|
|
49
|
+
return candidates;
|
|
50
|
+
const compiled = [];
|
|
51
|
+
for (const path of candidates) {
|
|
52
|
+
if ((await classifyRuntimeDelivery(index, engineDir, path)) === 'unlisted') {
|
|
53
|
+
compiled.push(path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return compiled;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The refusal for a compiled control without `--build`, naming the manual
|
|
60
|
+
* procedure for a run that wants to keep its build out of FireForge.
|
|
61
|
+
*
|
|
62
|
+
* @param compiled - Engine-relative compiled targets
|
|
63
|
+
*/
|
|
64
|
+
export function formatCompiledControlRefusal(compiled) {
|
|
65
|
+
const files = compiled.map((path) => `engine/${path}`).join(', ');
|
|
66
|
+
return (`This negative control edits compiled source (${files}). Applied right before dispatch, ` +
|
|
67
|
+
'it would never be compiled: the run would test the unmutated binary and report ' +
|
|
68
|
+
'control=missed. Add --build: the control is then applied before the pre-test build, so ' +
|
|
69
|
+
'the binary under test carries it, and restored afterwards with a fresh mtime so the next ' +
|
|
70
|
+
'build recompiles the original. To run it by hand instead: copy the mutated file in, ' +
|
|
71
|
+
'`touch` it, `fireforge build`, `fireforge test`, restore the file byte-exact, `touch` it ' +
|
|
72
|
+
'again and rebuild.');
|
|
73
|
+
}
|
|
@@ -18,15 +18,27 @@ export interface ControlDiffFile {
|
|
|
18
18
|
/** The hunks to apply, in file order. */
|
|
19
19
|
hunks: ParsedHunk[];
|
|
20
20
|
}
|
|
21
|
+
/** Options for {@link parseControlDiff}. */
|
|
22
|
+
export interface ParseControlDiffOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Called for a section whose old and new paths differ. Returns the path
|
|
25
|
+
* the control targets (a `git diff --no-index <file> <edited copy>` names
|
|
26
|
+
* the copy on its new side), or undefined for a real rename, which is
|
|
27
|
+
* refused.
|
|
28
|
+
*/
|
|
29
|
+
resolveRename?: (sourcePath: string, targetPath: string) => string | undefined;
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Parses a control diff. Accepts `git diff` output (a `diff --git` header
|
|
23
33
|
* per file), which is what `git -C engine diff -- <file>` or a project-root
|
|
24
|
-
* `git diff` writes
|
|
34
|
+
* `git diff` writes, a plain unified diff (`diff -u`), and, through
|
|
35
|
+
* `resolveRename`, a `git diff --no-index` against an edited copy.
|
|
25
36
|
*
|
|
26
37
|
* @param content - The diff file's text
|
|
38
|
+
* @param options - See {@link ParseControlDiffOptions}
|
|
27
39
|
* @returns One entry per file, in diff order
|
|
28
40
|
*/
|
|
29
|
-
export declare function parseControlDiff(content: string): ControlDiffFile[];
|
|
41
|
+
export declare function parseControlDiff(content: string, options?: ParseControlDiffOptions): ControlDiffFile[];
|
|
30
42
|
/**
|
|
31
43
|
* Applies `hunks` to `original`, strictly.
|
|
32
44
|
*
|
|
@@ -17,30 +17,73 @@ import { parseDiffSections } from './patch-parse.js';
|
|
|
17
17
|
function refuse(message) {
|
|
18
18
|
throw new GeneralError(`Negative control diff refused: ${message}`);
|
|
19
19
|
}
|
|
20
|
+
/** A `---`/`+++` file name, without a trailing timestamp or git's `a/`/`b/`. */
|
|
21
|
+
function plainDiffPath(raw) {
|
|
22
|
+
const name = raw.split('\t')[0]?.trim() ?? '';
|
|
23
|
+
return name.startsWith('a/') || name.startsWith('b/') ? name.slice(2) : name;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Gives a plain unified diff (`diff -u`, no `diff --git` lines) the git
|
|
27
|
+
* header the section parser keys on, one per `---`/`+++` pair. A diff that
|
|
28
|
+
* already carries `diff --git` headers is returned unchanged.
|
|
29
|
+
*
|
|
30
|
+
* @param content - The diff file's text
|
|
31
|
+
*/
|
|
32
|
+
function withGitHeaders(content) {
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
if (lines.some((line) => line.startsWith('diff --git ')))
|
|
35
|
+
return content;
|
|
36
|
+
const out = [];
|
|
37
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
38
|
+
const line = lines[index] ?? '';
|
|
39
|
+
const next = lines[index + 1] ?? '';
|
|
40
|
+
if (line.startsWith('--- ') && next.startsWith('+++ ')) {
|
|
41
|
+
const oldPath = plainDiffPath(line.slice(4));
|
|
42
|
+
const newPath = plainDiffPath(next.slice(4));
|
|
43
|
+
index += 1;
|
|
44
|
+
if (oldPath === '/dev/null' || newPath === '/dev/null') {
|
|
45
|
+
// Kept as a create/delete, which the parser below refuses by name.
|
|
46
|
+
const path = oldPath === '/dev/null' ? newPath : oldPath;
|
|
47
|
+
out.push(`diff --git a/${path} b/${path}`, oldPath === '/dev/null' ? 'new file mode 100644' : 'deleted file mode 100644', oldPath === '/dev/null' ? '--- /dev/null' : `--- a/${path}`, newPath === '/dev/null' ? '+++ /dev/null' : `+++ b/${path}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
out.push(`diff --git a/${oldPath} b/${newPath}`, `--- a/${oldPath}`, `+++ b/${newPath}`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
out.push(line);
|
|
54
|
+
}
|
|
55
|
+
return out.join('\n');
|
|
56
|
+
}
|
|
20
57
|
/**
|
|
21
58
|
* Parses a control diff. Accepts `git diff` output (a `diff --git` header
|
|
22
59
|
* per file), which is what `git -C engine diff -- <file>` or a project-root
|
|
23
|
-
* `git diff` writes
|
|
60
|
+
* `git diff` writes, a plain unified diff (`diff -u`), and, through
|
|
61
|
+
* `resolveRename`, a `git diff --no-index` against an edited copy.
|
|
24
62
|
*
|
|
25
63
|
* @param content - The diff file's text
|
|
64
|
+
* @param options - See {@link ParseControlDiffOptions}
|
|
26
65
|
* @returns One entry per file, in diff order
|
|
27
66
|
*/
|
|
28
|
-
export function parseControlDiff(content) {
|
|
29
|
-
const sections = parseDiffSections(content);
|
|
67
|
+
export function parseControlDiff(content, options = {}) {
|
|
68
|
+
const sections = parseDiffSections(withGitHeaders(content));
|
|
30
69
|
if (sections.length === 0) {
|
|
31
|
-
refuse('no
|
|
70
|
+
refuse('no diff sections found. Write the control with git, e.g. ' +
|
|
32
71
|
'`git -C engine diff -- <file> > control.diff` after making the edit, then revert the edit.');
|
|
33
72
|
}
|
|
34
73
|
const seen = new Set();
|
|
35
74
|
return sections.map((section) => {
|
|
36
|
-
|
|
75
|
+
let path = normalizePathSlashes(section.targetPath);
|
|
37
76
|
if (section.isBinary)
|
|
38
77
|
refuse(`${path} is a binary section.`);
|
|
39
78
|
if (section.isNewFile || section.isDeletedFile) {
|
|
40
79
|
refuse(`${path} creates or deletes a file; a control may only modify existing text files.`);
|
|
41
80
|
}
|
|
42
81
|
if (section.sourcePath !== section.targetPath) {
|
|
43
|
-
|
|
82
|
+
const resolved = options.resolveRename?.(section.sourcePath, section.targetPath);
|
|
83
|
+
if (resolved === undefined) {
|
|
84
|
+
refuse(`${section.sourcePath} -> ${path} is a rename; a control may only modify files.`);
|
|
85
|
+
}
|
|
86
|
+
path = normalizePathSlashes(resolved);
|
|
44
87
|
}
|
|
45
88
|
if (!isContainedRelativePath(path)) {
|
|
46
89
|
refuse(`${path} is not a contained relative path.`);
|
|
@@ -40,6 +40,12 @@ export interface NegativeControlPlan {
|
|
|
40
40
|
/** The diff file, as the operator passed it. */
|
|
41
41
|
diffFile: string;
|
|
42
42
|
targets: NegativeControlTarget[];
|
|
43
|
+
/**
|
|
44
|
+
* Engine-relative targets only a build delivers (compiled sources). A
|
|
45
|
+
* control with any must be applied before the pre-test build, and is
|
|
46
|
+
* restored with a fresh mtime. See `negative-control-compiled.ts`.
|
|
47
|
+
*/
|
|
48
|
+
compiled?: string[];
|
|
43
49
|
}
|
|
44
50
|
/** Locations a control works against. */
|
|
45
51
|
export interface NegativeControlPaths {
|
|
@@ -55,6 +61,12 @@ interface JournalEntry {
|
|
|
55
61
|
mode: number;
|
|
56
62
|
atimeMs: number;
|
|
57
63
|
mtimeMs: number;
|
|
64
|
+
/**
|
|
65
|
+
* Restore with the current time as mtime, not the original: a compiled
|
|
66
|
+
* source restored with its old mtime is skipped by the incremental build,
|
|
67
|
+
* which would leave the mutated code in the binary.
|
|
68
|
+
*/
|
|
69
|
+
freshMtime?: boolean;
|
|
58
70
|
}
|
|
59
71
|
interface Journal {
|
|
60
72
|
version: 1;
|
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
* it releases the held locks.
|
|
26
26
|
*/
|
|
27
27
|
import { lstat, mkdir, readFile, rm, utimes } from 'node:fs/promises';
|
|
28
|
-
import { join, posix, resolve } from 'node:path';
|
|
28
|
+
import { isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
29
29
|
import { GeneralError, PreflightRefusalError } from '../errors/base.js';
|
|
30
30
|
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
31
31
|
import { writeFileAtomic } from '../utils/fs.js';
|
|
32
32
|
import { sha256Hex } from '../utils/hash.js';
|
|
33
33
|
import { notice } from '../utils/logger.js';
|
|
34
|
-
import { normalizePathSlashes, stripEnginePrefix } from '../utils/paths.js';
|
|
34
|
+
import { isContainedRelativePath, isPathInsideRoot, normalizePathSlashes, stripEnginePrefix, } from '../utils/paths.js';
|
|
35
35
|
import { loadFurnaceConfig } from './furnace-config.js';
|
|
36
|
+
import { findCompiledControlTargets } from './negative-control-compiled.js';
|
|
36
37
|
import { applyControlHunks, parseControlDiff, } from './negative-control-diff.js';
|
|
37
38
|
import { runInSignalCriticalSection } from './signal-critical.js';
|
|
38
39
|
/** Journal directory, relative to `.fireforge/`. */
|
|
@@ -159,7 +160,9 @@ export async function planNegativeControl(projectRoot, engineDir, diffFile, opti
|
|
|
159
160
|
}
|
|
160
161
|
let files;
|
|
161
162
|
try {
|
|
162
|
-
files = parseControlDiff(content
|
|
163
|
+
files = parseControlDiff(content, {
|
|
164
|
+
resolveRename: (source, target) => resolveEditedCopyRename(projectRoot, source, target),
|
|
165
|
+
});
|
|
163
166
|
}
|
|
164
167
|
catch (error) {
|
|
165
168
|
refuse(toError(error).message);
|
|
@@ -170,7 +173,40 @@ export async function planNegativeControl(projectRoot, engineDir, diffFile, opti
|
|
|
170
173
|
continue;
|
|
171
174
|
await prepareTarget(engineDir, target);
|
|
172
175
|
}
|
|
173
|
-
|
|
176
|
+
const compiled = await findCompiledControlTargets(engineDir, targets.filter((target) => !target.fromComponents).map((target) => target.enginePath));
|
|
177
|
+
return { diffFile, targets, ...(compiled.length > 0 ? { compiled } : {}) };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A diff path as a project-relative path: a contained relative path as
|
|
181
|
+
* is, and an absolute one (git strips the leading `/` into `a/Users/…`)
|
|
182
|
+
* when it lies inside the project. Undefined otherwise.
|
|
183
|
+
*/
|
|
184
|
+
function projectRelative(projectRoot, path) {
|
|
185
|
+
const normalized = normalizePathSlashes(path);
|
|
186
|
+
for (const candidate of [`/${normalized}`, normalized]) {
|
|
187
|
+
if (!isAbsolute(candidate))
|
|
188
|
+
continue;
|
|
189
|
+
if (isPathInsideRoot(projectRoot, candidate)) {
|
|
190
|
+
return normalizePathSlashes(relative(projectRoot, candidate));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return isContainedRelativePath(normalized) ? normalized : undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The control target of a `git diff --no-index <engine file> <edited copy>`
|
|
197
|
+
* section: its old side, when that names a project file and the new side
|
|
198
|
+
* names a copy outside `engine/` and `components/`. A section whose sides
|
|
199
|
+
* both live in those trees is a real rename and stays refused.
|
|
200
|
+
*/
|
|
201
|
+
function resolveEditedCopyRename(projectRoot, source, target) {
|
|
202
|
+
const old = projectRelative(projectRoot, source);
|
|
203
|
+
if (old === undefined)
|
|
204
|
+
return undefined;
|
|
205
|
+
const edited = projectRelative(projectRoot, target);
|
|
206
|
+
const tracked = (path) => path.startsWith('engine/') || path.startsWith('components/');
|
|
207
|
+
if (edited !== undefined && tracked(edited))
|
|
208
|
+
return undefined;
|
|
209
|
+
return old;
|
|
174
210
|
}
|
|
175
211
|
async function writeJournal(paths, journal) {
|
|
176
212
|
await writeFileAtomic(join(journalDir(paths), JOURNAL_FILE), `${JSON.stringify(journal, null, 2)}\n`);
|
|
@@ -203,6 +239,7 @@ export async function applyNegativeControl(paths, plan) {
|
|
|
203
239
|
mode: item.mode,
|
|
204
240
|
atimeMs: item.atimeMs,
|
|
205
241
|
mtimeMs: item.mtimeMs,
|
|
242
|
+
...(plan.compiled?.includes(item.target.enginePath) === true ? { freshMtime: true } : {}),
|
|
206
243
|
});
|
|
207
244
|
}
|
|
208
245
|
const journal = {
|
|
@@ -248,7 +285,13 @@ async function restoreEntry(paths, entry) {
|
|
|
248
285
|
`left the file as is. Inspect ${journalDir(paths)}.`);
|
|
249
286
|
}
|
|
250
287
|
await writeFileAtomic(absolutePath, original);
|
|
251
|
-
|
|
288
|
+
if (entry.freshMtime === true) {
|
|
289
|
+
const now = Date.now() / 1000;
|
|
290
|
+
await utimes(absolutePath, now, now);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
await utimes(absolutePath, entry.atimeMs / 1000, entry.mtimeMs / 1000);
|
|
294
|
+
}
|
|
252
295
|
const readBack = await readFile(absolutePath);
|
|
253
296
|
const stats = await lstat(absolutePath);
|
|
254
297
|
if (!readBack.equals(original) || (stats.mode & 0o7777) !== (entry.mode & 0o7777)) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** One parked file: both sides, null meaning absent. */
|
|
2
|
+
export interface ParkedFile {
|
|
3
|
+
/** Engine-relative path. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** The file as the session left it. */
|
|
6
|
+
before: string | null;
|
|
7
|
+
/** The file the park wrote in its place. */
|
|
8
|
+
after: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** The record of one park. */
|
|
11
|
+
export interface ParkRecord {
|
|
12
|
+
label: string;
|
|
13
|
+
/** The patch whose files were parked. */
|
|
14
|
+
patch: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
/** `baseline` parks every drifted hunk, `hunks` only those a diff named. */
|
|
17
|
+
mode: 'baseline' | 'hunks';
|
|
18
|
+
files: ParkedFile[];
|
|
19
|
+
}
|
|
20
|
+
/** What `park.json` holds: the record without file contents. */
|
|
21
|
+
interface ParkIndex extends Omit<ParkRecord, 'files'> {
|
|
22
|
+
files: {
|
|
23
|
+
path: string;
|
|
24
|
+
before: boolean;
|
|
25
|
+
after: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
/** The outcome of restoring one file. */
|
|
29
|
+
export type RestoreOutcome = 'restored' | 'unchanged' | 'merged' | 'conflict';
|
|
30
|
+
/** Writes a park; refuses to overwrite an existing label. */
|
|
31
|
+
export declare function writePark(projectRoot: string, record: ParkRecord): Promise<string>;
|
|
32
|
+
/** Reads a park back, contents included. */
|
|
33
|
+
export declare function readPark(projectRoot: string, label: string): Promise<ParkRecord>;
|
|
34
|
+
/** Every park of the project, oldest first. */
|
|
35
|
+
export declare function listParks(projectRoot: string): Promise<ParkIndex[]>;
|
|
36
|
+
/** Deletes a park. */
|
|
37
|
+
export declare function dropPark(projectRoot: string, label: string): Promise<void>;
|
|
38
|
+
/** Reads an engine file, null when absent. */
|
|
39
|
+
export declare function readEngineFile(engineDir: string, path: string): Promise<string | null>;
|
|
40
|
+
/** Writes an engine file, removing it for null. */
|
|
41
|
+
export declare function writeEngineFile(engineDir: string, path: string, content: string | null): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Three-way merges `ours` and `theirs` over `base` with `git merge-file`.
|
|
44
|
+
* Conflicts come back as marker blocks.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeThreeWay(ours: string, base: string, theirs: string, path: string): Promise<{
|
|
47
|
+
content: string;
|
|
48
|
+
conflicts: number;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* What restoring one parked file writes: `before` when the file is still
|
|
52
|
+
* as the park left it, nothing when it already matches `before`, and a
|
|
53
|
+
* three-way merge otherwise. Absence only merges with absence.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planRestore(file: ParkedFile, now: string | null): Promise<{
|
|
56
|
+
outcome: RestoreOutcome;
|
|
57
|
+
content: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
/** Message ids a Fluent file's lines declare (`id = …`, `-term = …`). */
|
|
60
|
+
export declare function fluentIds(lines: readonly string[]): string[];
|
|
61
|
+
/** Lines `before` has that `after` lacks, as a multiset difference. */
|
|
62
|
+
export declare function removedLines(before: string | null, after: string | null): string[];
|
|
63
|
+
export {};
|