@hominis/fireforge 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/dist/bin/fireforge.js +11 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-harness-teardown.d.ts +62 -0
- package/dist/src/commands/test-harness-teardown.js +128 -0
- package/dist/src/commands/test-register.js +2 -1
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +36 -7
- package/dist/src/commands/test-verdict.d.ts +19 -4
- package/dist/src/commands/test-verdict.js +22 -4
- package/dist/src/commands/test.js +18 -10
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/file-lock.js +16 -9
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/harness-helper-pids.d.ts +98 -0
- package/dist/src/core/harness-helper-pids.js +240 -0
- package/dist/src/core/harness-orphans.d.ts +53 -8
- package/dist/src/core/harness-orphans.js +125 -44
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach.d.ts +16 -0
- package/dist/src/core/mach.js +6 -1
- package/dist/src/core/marionette-preflight.js +9 -3
- package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
- package/dist/src/core/parent-exit-watchdog.js +69 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +26 -1
- package/dist/src/types/config.d.ts +17 -0
- package/dist/src/utils/process-group.d.ts +5 -1
- package/dist/src/utils/process-group.js +7 -3
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +20 -0
- package/dist/src/utils/process.js +16 -3
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +88 -1
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +21 -4
- package/docs/machine-output.md +22 -9
- package/docs/patch-workflow.md +35 -1
- package/docs/testing.md +68 -4
- package/package.json +7 -3
|
@@ -11,17 +11,20 @@
|
|
|
11
11
|
* removing the declaration produced identical silence, so the gap was
|
|
12
12
|
* invisible rather than merely absent.
|
|
13
13
|
*
|
|
14
|
-
* Scope is narrow
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Scope is narrow by default: test-manifest `support-files` only. The
|
|
15
|
+
* carriers whose target resolves without the build system (jar.mn pair
|
|
16
|
+
* lines, moz.build path tokens, test-manifest sections, customElements.js
|
|
17
|
+
* chrome URLs) are opt-in through `patchLint.forwardRegistration:
|
|
18
|
+
* "extended"` (see `patch-lint-forward-registration-extended.ts`), since a
|
|
19
|
+
* queue that predates them can carry such edges on a ratchet and a new
|
|
20
|
+
* error on install would red its gate. The staged-registration declaration
|
|
21
|
+
* remains the documented escape hatch for a deliberate stage.
|
|
20
22
|
*/
|
|
21
23
|
import { basename, dirname, posix } from 'node:path';
|
|
22
24
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
23
25
|
import { escapeRegex } from '../utils/regex.js';
|
|
24
|
-
import {
|
|
26
|
+
import { findExtendedForwardRegistrations, formatRegistrationDeclaration, } from './patch-lint-forward-registration-extended.js';
|
|
27
|
+
import { isLaterOwner } from './patch-lint-staged-registration.js';
|
|
25
28
|
/**
|
|
26
29
|
* Manifest basenames whose `support-files` key registers auxiliary files
|
|
27
30
|
* for a test suite. Closed and documented on purpose: a pattern like
|
|
@@ -142,15 +145,15 @@ function isDeclared(entry, manifestPath, createdPath) {
|
|
|
142
145
|
normalizePathSlashes(registration.creates) === normalizePathSlashes(createdPath));
|
|
143
146
|
}
|
|
144
147
|
/**
|
|
145
|
-
*
|
|
148
|
+
* Finds every `support-files` registration a patch introduces that names a
|
|
146
149
|
* file only a later-ordered patch creates, unless the staged dependency is
|
|
147
150
|
* declared.
|
|
148
151
|
*
|
|
149
152
|
* @param ctx - Projected queue in application order
|
|
150
|
-
* @returns One
|
|
153
|
+
* @returns One record per (patch, manifest, entry, created file)
|
|
151
154
|
*/
|
|
152
|
-
|
|
153
|
-
const
|
|
155
|
+
function findSupportFileForwardRegistrations(ctx) {
|
|
156
|
+
const found = [];
|
|
154
157
|
for (const entry of ctx.entries) {
|
|
155
158
|
// Both arms: a manifest this patch creates and one it modifies can each
|
|
156
159
|
// introduce a registration line.
|
|
@@ -163,17 +166,6 @@ export function lintPatchQueueForwardRegistrations(ctx) {
|
|
|
163
166
|
const matches = buildSupportFileMatcher(manifestPath, raw);
|
|
164
167
|
if (matches === undefined)
|
|
165
168
|
continue;
|
|
166
|
-
// `--line` is the line as the patch adds it, which is what the
|
|
167
|
-
// declared arm compares and what `--remove` matches on. Synthesising
|
|
168
|
-
// `support-files = ["<entry>"]` instead only ever matched a
|
|
169
|
-
// single-line single-entry array, so the pasted command left the
|
|
170
|
-
// declaring patch red with staged-dependency-unused.
|
|
171
|
-
const declaredLine = quoteRegistrationLine(addedLine);
|
|
172
|
-
const ambiguity = occurrences > 1
|
|
173
|
-
? ` (the entry is spelled out on ${occurrences} added lines here; ` +
|
|
174
|
-
'the first is quoted, and one declaration covers the file either way ' +
|
|
175
|
-
'because the declared check keys on --file and --creates.)'
|
|
176
|
-
: '';
|
|
177
169
|
for (const later of ctx.entries) {
|
|
178
170
|
// Same predicate the declared-registration arm validates against,
|
|
179
171
|
// tiebreak included: two spellings of "later" would let one rule
|
|
@@ -185,26 +177,80 @@ export function lintPatchQueueForwardRegistrations(ctx) {
|
|
|
185
177
|
continue;
|
|
186
178
|
if (isDeclared(entry, manifestPath, created))
|
|
187
179
|
continue;
|
|
188
|
-
|
|
180
|
+
found.push({
|
|
181
|
+
patch: entry.filename,
|
|
182
|
+
kind: 'support-files',
|
|
189
183
|
file: manifestPath,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
severity: 'error',
|
|
184
|
+
// `--line` is the line as the patch adds it, which is what the
|
|
185
|
+
// declared arm compares and what `--remove` matches on.
|
|
186
|
+
// Synthesising `support-files = ["<entry>"]` instead only ever
|
|
187
|
+
// matched a single-line single-entry array, so the pasted
|
|
188
|
+
// command left the declaring patch red with
|
|
189
|
+
// staged-dependency-unused.
|
|
190
|
+
line: addedLine,
|
|
191
|
+
token: raw,
|
|
192
|
+
creates: created,
|
|
193
|
+
owner: later.filename,
|
|
194
|
+
occurrences,
|
|
195
|
+
command: formatRegistrationDeclaration(entry.filename, manifestPath, addedLine, created, later.filename),
|
|
203
196
|
});
|
|
204
197
|
}
|
|
205
198
|
}
|
|
206
199
|
}
|
|
207
200
|
}
|
|
208
201
|
}
|
|
209
|
-
return
|
|
202
|
+
return found;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Every undeclared forward registration in the queue, over the carriers
|
|
206
|
+
* `scope` selects: `support-files` (the default) or `extended` (adds
|
|
207
|
+
* jar.mn, moz.build, test-manifest sections and customElements.js; see
|
|
208
|
+
* `patch-lint-forward-registration-extended.ts`).
|
|
209
|
+
*
|
|
210
|
+
* @param ctx - Queue in application order
|
|
211
|
+
* @param scope - Which carriers to check
|
|
212
|
+
*/
|
|
213
|
+
export function collectForwardRegistrations(ctx, scope = 'support-files') {
|
|
214
|
+
const found = findSupportFileForwardRegistrations(ctx);
|
|
215
|
+
if (scope === 'extended')
|
|
216
|
+
found.push(...findExtendedForwardRegistrations(ctx));
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
const KIND_PHRASES = {
|
|
220
|
+
'support-files': 'as a support file',
|
|
221
|
+
'jar-mn': 'for packaging',
|
|
222
|
+
'moz-build': 'in the build',
|
|
223
|
+
'test-manifest': 'as a test',
|
|
224
|
+
'custom-elements': 'as a custom element',
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Flags every forward registration the queue's scope selects, unless the
|
|
228
|
+
* staged dependency is declared.
|
|
229
|
+
*
|
|
230
|
+
* @param ctx - Projected queue in application order, with its
|
|
231
|
+
* `forwardRegistration` scope (default `support-files`)
|
|
232
|
+
* @returns One issue per undeclared forward registration
|
|
233
|
+
*/
|
|
234
|
+
export function lintPatchQueueForwardRegistrations(ctx) {
|
|
235
|
+
return collectForwardRegistrations(ctx, ctx.forwardRegistration).map((found) => {
|
|
236
|
+
const ambiguity = found.occurrences > 1
|
|
237
|
+
? ` (the entry is spelled out on ${found.occurrences} added lines here; ` +
|
|
238
|
+
'the first is quoted, and one declaration covers the file either way ' +
|
|
239
|
+
'because the declared check keys on --file and --creates.)'
|
|
240
|
+
: '';
|
|
241
|
+
return {
|
|
242
|
+
file: found.file,
|
|
243
|
+
check: 'forward-registration',
|
|
244
|
+
patches: [found.patch],
|
|
245
|
+
fingerprint: `forward-registration|${found.patch}|${found.file}|${found.token}|${found.creates}|${found.owner}`,
|
|
246
|
+
message: `${found.file} in ${found.patch} registers "${found.token}" ${KIND_PHRASES[found.kind]}, ` +
|
|
247
|
+
`but ${found.creates} is created by the later patch ${found.owner}. ` +
|
|
248
|
+
'Applying the queue up to this patch leaves the registration dangling. ' +
|
|
249
|
+
'Reorder the patches so the file is created first, move the registration into ' +
|
|
250
|
+
'the later patch, or declare the intentional staged dependency with: ' +
|
|
251
|
+
found.command +
|
|
252
|
+
ambiguity,
|
|
253
|
+
severity: 'error',
|
|
254
|
+
};
|
|
255
|
+
});
|
|
210
256
|
}
|
|
@@ -25,6 +25,17 @@ export declare function countNonBinaryDiffLines(diffContent: string): {
|
|
|
25
25
|
total: number;
|
|
26
26
|
textLines: number;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* The files the `file-too-large` rule measures for a patch: the JS files
|
|
30
|
+
* (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
|
|
31
|
+
* never measured, and neither is a new file of another kind (CSS, FTL,
|
|
32
|
+
* markup), however long. This is the rule's own predicate, not a mirror
|
|
33
|
+
* of it: the rule selects its files through the same function.
|
|
34
|
+
*
|
|
35
|
+
* @param patchText - Unified-diff body of one patch
|
|
36
|
+
* @returns Engine-relative paths the rule would size-check
|
|
37
|
+
*/
|
|
38
|
+
export declare function filesMeasuredByFileTooLarge(patchText: string): Set<string>;
|
|
28
39
|
/**
|
|
29
40
|
* Returns true if the file path looks like a test file.
|
|
30
41
|
* Matches paths containing `/test/` or filenames starting with
|
|
@@ -189,6 +189,23 @@ function isBrandingOnlyPatch(files) {
|
|
|
189
189
|
function isJsFile(file) {
|
|
190
190
|
return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
191
191
|
}
|
|
192
|
+
/** The subset of a patch's new files that `file-too-large` measures. */
|
|
193
|
+
function measuredByFileTooLarge(newFiles) {
|
|
194
|
+
return new Set([...newFiles].filter(isJsFile));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The files the `file-too-large` rule measures for a patch: the JS files
|
|
198
|
+
* (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
|
|
199
|
+
* never measured, and neither is a new file of another kind (CSS, FTL,
|
|
200
|
+
* markup), however long. This is the rule's own predicate, not a mirror
|
|
201
|
+
* of it: the rule selects its files through the same function.
|
|
202
|
+
*
|
|
203
|
+
* @param patchText - Unified-diff body of one patch
|
|
204
|
+
* @returns Engine-relative paths the rule would size-check
|
|
205
|
+
*/
|
|
206
|
+
export function filesMeasuredByFileTooLarge(patchText) {
|
|
207
|
+
return measuredByFileTooLarge(detectNewFilesInDiff(patchText));
|
|
208
|
+
}
|
|
192
209
|
/**
|
|
193
210
|
* Returns true if the file path looks like a test file.
|
|
194
211
|
* Matches paths containing `/test/` or filenames starting with
|
|
@@ -387,6 +404,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
387
404
|
return [];
|
|
388
405
|
const issues = [];
|
|
389
406
|
const binaryName = config.binaryName.toLowerCase();
|
|
407
|
+
const sizeMeasured = measuredByFileTooLarge(newFiles);
|
|
390
408
|
for (const file of jsFiles) {
|
|
391
409
|
const filePath = join(repoDir, file);
|
|
392
410
|
if (!(await pathExists(filePath)))
|
|
@@ -404,8 +422,8 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
404
422
|
severity: 'error',
|
|
405
423
|
});
|
|
406
424
|
}
|
|
407
|
-
// 2. File size check (new files only)
|
|
408
|
-
if (
|
|
425
|
+
// 2. File size check (new JS files only; see filesMeasuredByFileTooLarge)
|
|
426
|
+
if (sizeMeasured.has(file)) {
|
|
409
427
|
const lineCount = countContentLines(content);
|
|
410
428
|
const isTest = isTestFile(file);
|
|
411
429
|
const thresholds = resolveFileSizeThresholds(config.patchLint?.fileSizeThresholds, isTest);
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Extracts the complete file content from a "new file" patch given a raw
|
|
7
|
-
* diff string already in memory.
|
|
8
|
-
*
|
|
9
|
-
* that already hold the diff (e.g. the in-flight export planner) and do
|
|
10
|
-
* not want to round-trip through the filesystem.
|
|
7
|
+
* diff string already in memory. A caller with only a patch file path
|
|
8
|
+
* reads it first; every caller holds the diff already.
|
|
11
9
|
*
|
|
12
10
|
* @param diff - Raw unified-diff content
|
|
13
11
|
* @param targetFile - Optional target file to scope extraction to
|
|
@@ -18,6 +16,18 @@
|
|
|
18
16
|
* binary new-file conflicts as unresolvable rather than auto-resolving.
|
|
19
17
|
*/
|
|
20
18
|
export declare function extractNewFileContentFromDiff(diff: string, targetFile?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* {@link extractNewFileContentFromDiff} for many targets over one parse of
|
|
21
|
+
* the diff. Each target gets exactly what the single-target call returns;
|
|
22
|
+
* a target whose sections include a binary one (which that call refuses)
|
|
23
|
+
* is left out of the map and named in `refused`.
|
|
24
|
+
* @param diff - Raw unified-diff content
|
|
25
|
+
* @param targetFiles - Files whose created content to extract
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractNewFileContentsFromDiff(diff: string, targetFiles: Iterable<string>): {
|
|
28
|
+
contents: Map<string, string>;
|
|
29
|
+
refused: string[];
|
|
30
|
+
};
|
|
21
31
|
/**
|
|
22
32
|
* Projects every text new file a diff creates into a path → content map, for
|
|
23
33
|
* the cross-patch lint projections that build a synthetic patch-queue entry.
|
|
@@ -39,15 +49,6 @@ export declare function extractNewFileContentFromDiff(diff: string, targetFile?:
|
|
|
39
49
|
* call sites previously did.
|
|
40
50
|
*/
|
|
41
51
|
export declare function buildNewFileTextProjection(diff: string): Map<string, string>;
|
|
42
|
-
/**
|
|
43
|
-
* Extracts the complete file content from a "new file" patch.
|
|
44
|
-
* When targetFile is provided, only extracts content for that file
|
|
45
|
-
* (required for multi-file patches).
|
|
46
|
-
* @param patchPath - Path to the patch file
|
|
47
|
-
* @param targetFile - Optional target file to scope extraction to
|
|
48
|
-
* @returns The file content that the patch would create
|
|
49
|
-
*/
|
|
50
|
-
export declare function extractNewFileContent(patchPath: string, targetFile?: string): Promise<string>;
|
|
51
52
|
/**
|
|
52
53
|
* Applies an already-read patch body to content. Batched callers that hold
|
|
53
54
|
* many (patch, file) pairs use this to read each patch body once instead of
|
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
* These operate on file content strings without filesystem side effects.
|
|
5
5
|
*/
|
|
6
6
|
import { PatchError } from '../errors/patch.js';
|
|
7
|
-
import { readText } from '../utils/fs.js';
|
|
8
7
|
import { isNewFileInPatch, parseDiffSections, parseHunksForFile } from './patch-parse.js';
|
|
9
8
|
/**
|
|
10
9
|
* Extracts the complete file content from a "new file" patch given a raw
|
|
11
|
-
* diff string already in memory.
|
|
12
|
-
*
|
|
13
|
-
* that already hold the diff (e.g. the in-flight export planner) and do
|
|
14
|
-
* not want to round-trip through the filesystem.
|
|
10
|
+
* diff string already in memory. A caller with only a patch file path
|
|
11
|
+
* reads it first; every caller holds the diff already.
|
|
15
12
|
*
|
|
16
13
|
* @param diff - Raw unified-diff content
|
|
17
14
|
* @param targetFile - Optional target file to scope extraction to
|
|
@@ -29,6 +26,33 @@ export function extractNewFileContentFromDiff(diff, targetFile) {
|
|
|
29
26
|
}
|
|
30
27
|
return contentFromSections(sections);
|
|
31
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* {@link extractNewFileContentFromDiff} for many targets over one parse of
|
|
31
|
+
* the diff. Each target gets exactly what the single-target call returns;
|
|
32
|
+
* a target whose sections include a binary one (which that call refuses)
|
|
33
|
+
* is left out of the map and named in `refused`.
|
|
34
|
+
* @param diff - Raw unified-diff content
|
|
35
|
+
* @param targetFiles - Files whose created content to extract
|
|
36
|
+
*/
|
|
37
|
+
export function extractNewFileContentsFromDiff(diff, targetFiles) {
|
|
38
|
+
const sectionsByTarget = new Map();
|
|
39
|
+
for (const section of parseDiffSections(diff)) {
|
|
40
|
+
const list = sectionsByTarget.get(section.targetPath) ?? [];
|
|
41
|
+
list.push(section);
|
|
42
|
+
sectionsByTarget.set(section.targetPath, list);
|
|
43
|
+
}
|
|
44
|
+
const contents = new Map();
|
|
45
|
+
const refused = [];
|
|
46
|
+
for (const target of targetFiles) {
|
|
47
|
+
const sections = sectionsByTarget.get(target) ?? [];
|
|
48
|
+
if (sections.some((section) => section.isBinary)) {
|
|
49
|
+
refused.push(target);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
contents.set(target, contentFromSections(sections));
|
|
53
|
+
}
|
|
54
|
+
return { contents, refused };
|
|
55
|
+
}
|
|
32
56
|
/**
|
|
33
57
|
* Walks the added (`+`) lines of already-parsed sections into file content.
|
|
34
58
|
* Shared by {@link extractNewFileContentFromDiff} and
|
|
@@ -89,18 +113,6 @@ export function buildNewFileTextProjection(diff) {
|
|
|
89
113
|
}
|
|
90
114
|
return newFiles;
|
|
91
115
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Extracts the complete file content from a "new file" patch.
|
|
94
|
-
* When targetFile is provided, only extracts content for that file
|
|
95
|
-
* (required for multi-file patches).
|
|
96
|
-
* @param patchPath - Path to the patch file
|
|
97
|
-
* @param targetFile - Optional target file to scope extraction to
|
|
98
|
-
* @returns The file content that the patch would create
|
|
99
|
-
*/
|
|
100
|
-
export async function extractNewFileContent(patchPath, targetFile) {
|
|
101
|
-
const content = await readText(patchPath);
|
|
102
|
-
return extractNewFileContentFromDiff(content, targetFile);
|
|
103
|
-
}
|
|
104
116
|
/**
|
|
105
117
|
* Applies an already-read patch body to content. Batched callers that hold
|
|
106
118
|
* many (patch, file) pairs use this to read each patch body once instead of
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A patch to replay, in queue order. */
|
|
2
|
+
export interface DryRunPatch {
|
|
3
|
+
filename: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
/** What the real rebase loop would do with one patch. */
|
|
7
|
+
export type DryRunVerdict = {
|
|
8
|
+
filename: string;
|
|
9
|
+
outcome: 'clean';
|
|
10
|
+
} | {
|
|
11
|
+
filename: string;
|
|
12
|
+
outcome: 'reduced-context';
|
|
13
|
+
step: number;
|
|
14
|
+
contextArg: string;
|
|
15
|
+
} | {
|
|
16
|
+
filename: string;
|
|
17
|
+
outcome: 'reject';
|
|
18
|
+
/** Files git named as not applying. Empty when stderr named none. */
|
|
19
|
+
files: string[];
|
|
20
|
+
/** git's own explanation, from the exact-context `--check`. */
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
/** Result of a full queue replay. */
|
|
24
|
+
export interface DryRunReplay {
|
|
25
|
+
verdicts: DryRunVerdict[];
|
|
26
|
+
/**
|
|
27
|
+
* Index of the first reject, or `undefined`. Verdicts after it were
|
|
28
|
+
* computed without that patch applied, so a reject there may cascade from
|
|
29
|
+
* the earlier one rather than be a conflict of its own.
|
|
30
|
+
*/
|
|
31
|
+
firstRejectIndex: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses the file names out of `git apply --check` failure output. Git
|
|
35
|
+
* reports a context mismatch as `error: patch failed: <file>:<line>` and
|
|
36
|
+
* `error: <file>: patch does not apply`, and a missing or pre-existing
|
|
37
|
+
* target as `error: <file>: does not exist in index` /
|
|
38
|
+
* `error: <file>: already exists in index`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseApplyCheckFailures(stderr: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Replays `patches` onto the engine's HEAD tree in a private index.
|
|
43
|
+
* @param engineDir - Engine repository root
|
|
44
|
+
* @param patches - Patches in queue order
|
|
45
|
+
* @param maxFuzz - Maximum context-reduction steps, as for the real rebase
|
|
46
|
+
* @returns One verdict per patch
|
|
47
|
+
*/
|
|
48
|
+
export declare function replayQueueIndexOnly(engineDir: string, patches: readonly DryRunPatch[], maxFuzz: number): Promise<DryRunReplay>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `rebase --dry-run`: replays the patch queue against the engine's base
|
|
4
|
+
* commit without touching `engine/`.
|
|
5
|
+
*
|
|
6
|
+
* The replay runs on a private index seeded from HEAD (`GIT_INDEX_FILE`
|
|
7
|
+
* plus `git read-tree HEAD`) and applies each patch there with
|
|
8
|
+
* `git apply --cached`, in queue order, so a later patch sees what the
|
|
9
|
+
* earlier ones did, exactly as the real loop's worktree does. Each patch
|
|
10
|
+
* walks the same context-reduction ladder as {@link applyPatchWithFuzz}.
|
|
11
|
+
* The worktree and the real index are never written. The private index is
|
|
12
|
+
* deleted when the replay ends.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { GitError } from '../errors/git.js';
|
|
18
|
+
import { exec } from '../utils/process.js';
|
|
19
|
+
import { ensureGit } from './git-base.js';
|
|
20
|
+
import { assertValidMaxFuzz, contextReductionSteps } from './patch-apply-fuzz.js';
|
|
21
|
+
/**
|
|
22
|
+
* Parses the file names out of `git apply --check` failure output. Git
|
|
23
|
+
* reports a context mismatch as `error: patch failed: <file>:<line>` and
|
|
24
|
+
* `error: <file>: patch does not apply`, and a missing or pre-existing
|
|
25
|
+
* target as `error: <file>: does not exist in index` /
|
|
26
|
+
* `error: <file>: already exists in index`.
|
|
27
|
+
*/
|
|
28
|
+
export function parseApplyCheckFailures(stderr) {
|
|
29
|
+
const files = new Set();
|
|
30
|
+
for (const line of stderr.split('\n')) {
|
|
31
|
+
const failed = /^error: patch failed: (.+):\d+$/.exec(line);
|
|
32
|
+
if (failed?.[1]) {
|
|
33
|
+
files.add(failed[1]);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const named = /^error: (.+?): (?:patch does not apply|does not exist in index|already exists in index)$/.exec(line);
|
|
37
|
+
if (named?.[1])
|
|
38
|
+
files.add(named[1]);
|
|
39
|
+
}
|
|
40
|
+
return [...files];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Replays `patches` onto the engine's HEAD tree in a private index.
|
|
44
|
+
* @param engineDir - Engine repository root
|
|
45
|
+
* @param patches - Patches in queue order
|
|
46
|
+
* @param maxFuzz - Maximum context-reduction steps, as for the real rebase
|
|
47
|
+
* @returns One verdict per patch
|
|
48
|
+
*/
|
|
49
|
+
export async function replayQueueIndexOnly(engineDir, patches, maxFuzz) {
|
|
50
|
+
assertValidMaxFuzz(maxFuzz);
|
|
51
|
+
await ensureGit();
|
|
52
|
+
const scratch = await mkdtemp(join(tmpdir(), 'fireforge-rebase-dry-run-'));
|
|
53
|
+
const env = { GIT_INDEX_FILE: join(scratch, 'index') };
|
|
54
|
+
try {
|
|
55
|
+
const seeded = await exec('git', ['read-tree', 'HEAD'], { cwd: engineDir, env });
|
|
56
|
+
if (seeded.exitCode !== 0) {
|
|
57
|
+
throw new GitError(seeded.stderr.trim() || 'Could not seed a private index from HEAD', 'read-tree HEAD');
|
|
58
|
+
}
|
|
59
|
+
const ladder = contextReductionSteps(maxFuzz);
|
|
60
|
+
const verdicts = [];
|
|
61
|
+
let firstRejectIndex;
|
|
62
|
+
for (const patch of patches) {
|
|
63
|
+
let verdict;
|
|
64
|
+
let exactFailure = '';
|
|
65
|
+
for (const [step, contextArgs] of ladder.entries()) {
|
|
66
|
+
const check = await exec('git', ['apply', '--cached', '--check', ...contextArgs, '--', patch.path], { cwd: engineDir, env });
|
|
67
|
+
if (check.exitCode !== 0) {
|
|
68
|
+
if (step === 0)
|
|
69
|
+
exactFailure = check.stderr;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const apply = await exec('git', ['apply', '--cached', ...contextArgs, '--', patch.path], {
|
|
73
|
+
cwd: engineDir,
|
|
74
|
+
env,
|
|
75
|
+
});
|
|
76
|
+
if (apply.exitCode !== 0)
|
|
77
|
+
continue;
|
|
78
|
+
verdict =
|
|
79
|
+
step === 0
|
|
80
|
+
? { filename: patch.filename, outcome: 'clean' }
|
|
81
|
+
: {
|
|
82
|
+
filename: patch.filename,
|
|
83
|
+
outcome: 'reduced-context',
|
|
84
|
+
step,
|
|
85
|
+
contextArg: contextArgs[0] ?? '',
|
|
86
|
+
};
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (verdict === undefined) {
|
|
90
|
+
verdict = {
|
|
91
|
+
filename: patch.filename,
|
|
92
|
+
outcome: 'reject',
|
|
93
|
+
files: parseApplyCheckFailures(exactFailure),
|
|
94
|
+
detail: exactFailure.trim(),
|
|
95
|
+
};
|
|
96
|
+
firstRejectIndex ??= verdicts.length;
|
|
97
|
+
}
|
|
98
|
+
verdicts.push(verdict);
|
|
99
|
+
}
|
|
100
|
+
return { verdicts, firstRejectIndex };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await rm(scratch, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Directory-name prefix of every FireForge-owned temporary run profile. */
|
|
2
|
+
export declare const RUN_PROFILE_PREFIX = "fireforge-run-profile-";
|
|
3
|
+
/** The profile a run launches with, and how to release it afterwards. */
|
|
4
|
+
export interface RunProfile {
|
|
5
|
+
/** Program parameters to append to `mach run` (empty for mach's default). */
|
|
6
|
+
args: string[];
|
|
7
|
+
/** Absolute profile directory, when one was chosen. */
|
|
8
|
+
dir?: string;
|
|
9
|
+
/** Whether FireForge created the directory and will delete it. */
|
|
10
|
+
temporary: boolean;
|
|
11
|
+
/** Removes a temporary profile. A no-op otherwise. Never throws. */
|
|
12
|
+
dispose: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** Inputs to {@link resolveRunProfile}. */
|
|
15
|
+
export interface RunProfileRequest {
|
|
16
|
+
/** `--profile <path>`: launch with this directory, left in place. */
|
|
17
|
+
profile?: string | undefined;
|
|
18
|
+
/** `--temp-profile`: launch with a fresh directory removed afterwards. */
|
|
19
|
+
tempProfile?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Whether this is a `--smoke-exit` run. A smoke run with neither flag gets
|
|
22
|
+
* a temporary profile: the step exists to prove what the build does on a
|
|
23
|
+
* clean start, and the developer's profile-default carries whatever state
|
|
24
|
+
* the last session left.
|
|
25
|
+
*/
|
|
26
|
+
smoke: boolean;
|
|
27
|
+
/** Base directory for temporary profiles (defaults to the OS tmpdir). */
|
|
28
|
+
tempRoot?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Removes every temporary run profile this process still holds. Called by
|
|
32
|
+
* the CLI entry point's signal handler after child shutdown. Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function removeActiveRunProfiles(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Chooses the profile for a run.
|
|
37
|
+
* @param request - The run's profile flags
|
|
38
|
+
* @returns The profile arguments and their release handle
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveRunProfile(request: RunProfileRequest): Promise<RunProfile>;
|
|
41
|
+
/**
|
|
42
|
+
* Removes temporary run profiles whose owning FireForge process is gone.
|
|
43
|
+
* The `finally` that releases a profile covers a clean exit, a failed run
|
|
44
|
+
* and a forwarded SIGINT/SIGTERM, but not a SIGKILL of FireForge itself;
|
|
45
|
+
* this sweep is the backstop for that case. A directory whose marker is
|
|
46
|
+
* missing or unreadable is left alone: nothing proves it is abandoned.
|
|
47
|
+
* @param tempRoot - Base directory to sweep (defaults to the OS tmpdir)
|
|
48
|
+
* @returns The number of directories removed
|
|
49
|
+
*/
|
|
50
|
+
export declare function sweepAbandonedRunProfiles(tempRoot?: string): Promise<number>;
|