@fern-api/replay 0.18.0 → 0.19.1
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/dist/cli.cjs +522 -125
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +467 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -3
- package/dist/index.d.ts +208 -3
- package/dist/index.js +450 -99
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -430,15 +430,15 @@ __export(PatchApplyTheirs_exports, {
|
|
|
430
430
|
async function applyPatchToContent(base, patchContent, filePath) {
|
|
431
431
|
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
432
432
|
if (!fileDiff) return null;
|
|
433
|
-
const tempDir = await (0, import_promises5.mkdtemp)((0,
|
|
433
|
+
const tempDir = await (0, import_promises5.mkdtemp)((0, import_node_path8.join)((0, import_node_os3.tmpdir)(), "replay-migrate-"));
|
|
434
434
|
const tempGit = new GitClient(tempDir);
|
|
435
435
|
try {
|
|
436
436
|
await tempGit.exec(["init"]);
|
|
437
437
|
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
438
438
|
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
439
439
|
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
440
|
-
const tempFilePath = (0,
|
|
441
|
-
await (0, import_promises5.mkdir)((0,
|
|
440
|
+
const tempFilePath = (0, import_node_path8.join)(tempDir, filePath);
|
|
441
|
+
await (0, import_promises5.mkdir)((0, import_node_path8.dirname)(tempFilePath), { recursive: true });
|
|
442
442
|
await (0, import_promises5.writeFile)(tempFilePath, base);
|
|
443
443
|
await tempGit.exec(["add", filePath]);
|
|
444
444
|
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
@@ -469,13 +469,13 @@ function extractFileDiff(patchContent, filePath) {
|
|
|
469
469
|
}
|
|
470
470
|
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
471
471
|
}
|
|
472
|
-
var import_promises5, import_node_os3,
|
|
472
|
+
var import_promises5, import_node_os3, import_node_path8;
|
|
473
473
|
var init_PatchApplyTheirs = __esm({
|
|
474
474
|
"src/PatchApplyTheirs.ts"() {
|
|
475
475
|
"use strict";
|
|
476
476
|
import_promises5 = require("fs/promises");
|
|
477
477
|
import_node_os3 = require("os");
|
|
478
|
-
|
|
478
|
+
import_node_path8 = require("path");
|
|
479
479
|
init_GitClient();
|
|
480
480
|
}
|
|
481
481
|
});
|
|
@@ -750,6 +750,22 @@ var LockfileManager = class {
|
|
|
750
750
|
this.lock.forgotten_hashes.push(hash);
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Record a dismissed customization: visibility metadata in `dismissals`
|
|
755
|
+
* plus a tombstone in `forgotten_hashes` for every content hash.
|
|
756
|
+
* `forgotten_hashes` stays the only list the detector consults; the
|
|
757
|
+
* metadata exists so `status` can show what was dismissed.
|
|
758
|
+
*/
|
|
759
|
+
addDismissal(record) {
|
|
760
|
+
this.ensureLoaded();
|
|
761
|
+
if (!this.lock.dismissals) {
|
|
762
|
+
this.lock.dismissals = [];
|
|
763
|
+
}
|
|
764
|
+
this.lock.dismissals.push(record);
|
|
765
|
+
for (const hash of record.content_hashes) {
|
|
766
|
+
this.addForgottenHash(hash);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
753
769
|
getUnresolvedPatches() {
|
|
754
770
|
this.ensureLoaded();
|
|
755
771
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -917,12 +933,14 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
917
933
|
return snapshot;
|
|
918
934
|
}
|
|
919
935
|
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
920
|
-
const
|
|
936
|
+
const rawFiles = rawFilesOutput.trim().split("\n").filter(Boolean);
|
|
937
|
+
const infrastructureFiles = rawFiles.filter((f) => INFRASTRUCTURE_FILES.has(f) || f.startsWith(".fern/"));
|
|
938
|
+
const allFiles = rawFiles.filter((f) => !INFRASTRUCTURE_FILES.has(f) && !f.startsWith(".fern/"));
|
|
921
939
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
922
940
|
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
923
941
|
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
924
942
|
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
925
|
-
return { files, sensitiveFiles, fernignoredFiles };
|
|
943
|
+
return { files, sensitiveFiles, fernignoredFiles, infrastructureFiles };
|
|
926
944
|
}
|
|
927
945
|
var ReplayDetector = class {
|
|
928
946
|
git;
|
|
@@ -930,6 +948,15 @@ var ReplayDetector = class {
|
|
|
930
948
|
sdkOutputDir;
|
|
931
949
|
fernignorePatterns;
|
|
932
950
|
warnings = [];
|
|
951
|
+
/**
|
|
952
|
+
* Structured degraded-state channel, mirroring `warnings`. Populated when
|
|
953
|
+
* detection cannot guarantee customizations were carried forward (baseline
|
|
954
|
+
* unreachable in this clone). Cleared per-run by `ReplayService.prepareReplay`
|
|
955
|
+
* alongside `warnings`. Merged into `ReplayReport.degradedReasons` by each
|
|
956
|
+
* flow handler. Degraded is observability only: detection results are
|
|
957
|
+
* unchanged.
|
|
958
|
+
*/
|
|
959
|
+
degradedReasons = [];
|
|
933
960
|
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
934
961
|
this.git = git;
|
|
935
962
|
this.lockManager = lockManager;
|
|
@@ -1000,6 +1027,10 @@ var ReplayDetector = class {
|
|
|
1000
1027
|
this.warnings.push(
|
|
1001
1028
|
`No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
|
|
1002
1029
|
);
|
|
1030
|
+
this.degradedReasons.push({
|
|
1031
|
+
code: "baseline-unresolvable",
|
|
1032
|
+
message: `The previous generation recorded in the lockfile (${lock.current_generation.slice(0, 7)}) could not be resolved to any reachable baseline during this run. Customizations may not have been carried forward \u2014 review the result before merging.`
|
|
1033
|
+
});
|
|
1003
1034
|
}
|
|
1004
1035
|
return { patches: [], revertedPatchIds: [] };
|
|
1005
1036
|
}
|
|
@@ -1008,6 +1039,12 @@ var ReplayDetector = class {
|
|
|
1008
1039
|
this.warnings.push(
|
|
1009
1040
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
1010
1041
|
);
|
|
1042
|
+
if (lock.patches.length > 0) {
|
|
1043
|
+
this.degradedReasons.push({
|
|
1044
|
+
code: "generation-anchor-unreachable",
|
|
1045
|
+
message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run, and ${lock.patches.length} tracked customization(s) could not be verified against it. Customizations may not have been carried forward \u2014 review the result before merging.`
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1011
1048
|
return this.detectPatchesViaTreeDiff(
|
|
1012
1049
|
lastGen,
|
|
1013
1050
|
/* commitKnownMissing */
|
|
@@ -1047,54 +1084,27 @@ var ReplayDetector = class {
|
|
|
1047
1084
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
1048
1085
|
continue;
|
|
1049
1086
|
}
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1054
|
-
} catch {
|
|
1087
|
+
const materialized = await this.materializeCommitPatch(commit.sha);
|
|
1088
|
+
if (materialized.sensitiveFiles.length > 0) {
|
|
1055
1089
|
this.warnings.push(
|
|
1056
|
-
`
|
|
1090
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1057
1091
|
);
|
|
1058
|
-
continue;
|
|
1059
1092
|
}
|
|
1060
|
-
|
|
1061
|
-
filesOutput,
|
|
1062
|
-
this.fernignorePatterns
|
|
1063
|
-
);
|
|
1064
|
-
if (sensitiveFiles.length > 0) {
|
|
1093
|
+
if (materialized.fernignoredFiles.length > 0) {
|
|
1065
1094
|
this.warnings.push(
|
|
1066
|
-
|
|
1095
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1067
1096
|
);
|
|
1068
1097
|
}
|
|
1069
|
-
if (
|
|
1098
|
+
if (materialized.status === "unreachable") {
|
|
1070
1099
|
this.warnings.push(
|
|
1071
|
-
|
|
1100
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
1072
1101
|
);
|
|
1073
|
-
}
|
|
1074
|
-
if (files.length === 0) {
|
|
1075
1102
|
continue;
|
|
1076
1103
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
let patchContent;
|
|
1080
|
-
if (wasFiltered && parentSha) {
|
|
1081
|
-
try {
|
|
1082
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
1083
|
-
} catch {
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
if (!patchContent.trim()) continue;
|
|
1087
|
-
} else {
|
|
1088
|
-
try {
|
|
1089
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
1090
|
-
} catch {
|
|
1091
|
-
this.warnings.push(
|
|
1092
|
-
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
1093
|
-
);
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1104
|
+
if (materialized.status === "empty") {
|
|
1105
|
+
continue;
|
|
1096
1106
|
}
|
|
1097
|
-
const contentHash =
|
|
1107
|
+
const { files, patchContent, contentHash } = materialized;
|
|
1098
1108
|
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
1099
1109
|
continue;
|
|
1100
1110
|
}
|
|
@@ -1171,6 +1181,63 @@ var ReplayDetector = class {
|
|
|
1171
1181
|
const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
1172
1182
|
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
1173
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Materialize a commit into patch content exactly as detection does.
|
|
1186
|
+
*
|
|
1187
|
+
* Pipeline: `diff-tree --name-only` → filter infrastructure / sensitive /
|
|
1188
|
+
* `.fernignore`d files → scoped `git diff` when files were stripped,
|
|
1189
|
+
* otherwise full `git format-patch` → content hash.
|
|
1190
|
+
*
|
|
1191
|
+
* This is THE definition of a commit's re-emission identity: the content
|
|
1192
|
+
* hash this method returns is what `detectPatchesInRange` will compute if
|
|
1193
|
+
* the commit re-enters the scan range (e.g. after a regen PR is merged
|
|
1194
|
+
* with a merge commit). Callers that tombstone a dismissed patch must use
|
|
1195
|
+
* this hash — the stored `content_hash` drifts every time the patch is
|
|
1196
|
+
* carried forward, so it does not identify the commit on re-detection.
|
|
1197
|
+
*
|
|
1198
|
+
* Never throws; failures are reported through the `status` discriminant.
|
|
1199
|
+
*/
|
|
1200
|
+
async materializeCommitPatch(sha) {
|
|
1201
|
+
let parents;
|
|
1202
|
+
let filesOutput;
|
|
1203
|
+
try {
|
|
1204
|
+
parents = await this.git.getCommitParents(sha);
|
|
1205
|
+
filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]);
|
|
1206
|
+
} catch {
|
|
1207
|
+
return { status: "unreachable", sensitiveFiles: [], fernignoredFiles: [] };
|
|
1208
|
+
}
|
|
1209
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
1210
|
+
if (files.length === 0) {
|
|
1211
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
1212
|
+
}
|
|
1213
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
1214
|
+
const parentSha = parents[0];
|
|
1215
|
+
let patchContent;
|
|
1216
|
+
if (wasFiltered && parentSha) {
|
|
1217
|
+
try {
|
|
1218
|
+
patchContent = await this.git.exec(["diff", parentSha, sha, "--", ...files]);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
1221
|
+
}
|
|
1222
|
+
if (!patchContent.trim()) {
|
|
1223
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
try {
|
|
1227
|
+
patchContent = await this.git.formatPatch(sha);
|
|
1228
|
+
} catch {
|
|
1229
|
+
return { status: "unreachable", sensitiveFiles, fernignoredFiles };
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
status: "ok",
|
|
1234
|
+
files,
|
|
1235
|
+
sensitiveFiles,
|
|
1236
|
+
fernignoredFiles,
|
|
1237
|
+
patchContent,
|
|
1238
|
+
contentHash: this.computeContentHash(patchContent)
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1174
1241
|
/**
|
|
1175
1242
|
* Compute content hash for deduplication.
|
|
1176
1243
|
*
|
|
@@ -1275,6 +1342,13 @@ var ReplayDetector = class {
|
|
|
1275
1342
|
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
1276
1343
|
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
1277
1344
|
if (!diffBase) {
|
|
1345
|
+
this.warnings.push(
|
|
1346
|
+
`Neither the previous generation's commit (${lastGen.commit_sha.slice(0, 7)}) nor its recorded tree was reachable during this run. Falling back to a bounded commit scan, which cannot see customizations older than the clone window. Widen the clone window (e.g. \`git fetch --unshallow\`) and re-run replay to verify customizations were carried forward.`
|
|
1347
|
+
);
|
|
1348
|
+
this.degradedReasons.push({
|
|
1349
|
+
code: "generation-tree-unreachable",
|
|
1350
|
+
message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run \u2014 neither its commit nor its recorded tree. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
|
|
1351
|
+
});
|
|
1278
1352
|
return this.detectPatchesViaCommitScan();
|
|
1279
1353
|
}
|
|
1280
1354
|
const lockForGuard = this.lockManager.read();
|
|
@@ -1387,10 +1461,7 @@ var ReplayDetector = class {
|
|
|
1387
1461
|
continue;
|
|
1388
1462
|
}
|
|
1389
1463
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1390
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1391
|
-
filesOutput,
|
|
1392
|
-
this.fernignorePatterns
|
|
1393
|
-
);
|
|
1464
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
1394
1465
|
if (sensitiveFiles.length > 0) {
|
|
1395
1466
|
this.warnings.push(
|
|
1396
1467
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
@@ -1401,7 +1472,8 @@ var ReplayDetector = class {
|
|
|
1401
1472
|
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1402
1473
|
);
|
|
1403
1474
|
}
|
|
1404
|
-
|
|
1475
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
1476
|
+
if (wasFiltered && files.length > 0) {
|
|
1405
1477
|
if (parents.length === 0) {
|
|
1406
1478
|
continue;
|
|
1407
1479
|
}
|
|
@@ -2010,6 +2082,51 @@ async function buildMergePlan(args) {
|
|
|
2010
2082
|
// src/applicator/runMergeAndRecord.ts
|
|
2011
2083
|
var import_promises2 = require("fs/promises");
|
|
2012
2084
|
var import_node_path4 = require("path");
|
|
2085
|
+
|
|
2086
|
+
// src/autoversion.ts
|
|
2087
|
+
var AUTOVERSION_PLACEHOLDERS = [
|
|
2088
|
+
"v0.0.0-fern-placeholder",
|
|
2089
|
+
// Go -- a superstring of the default; check first
|
|
2090
|
+
"0.0.0-fern-placeholder",
|
|
2091
|
+
// TS/Java/C#/Ruby/PHP/Rust/default
|
|
2092
|
+
"0.0.0.dev0"
|
|
2093
|
+
// Python (PEP 440 -- Poetry rejects hyphens)
|
|
2094
|
+
];
|
|
2095
|
+
var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
|
|
2096
|
+
var VERSION_MASK = "@@FERN_VERSION@@";
|
|
2097
|
+
function maskVersions(line) {
|
|
2098
|
+
return line.replace(VERSION_TOKEN, VERSION_MASK);
|
|
2099
|
+
}
|
|
2100
|
+
function containsPlaceholder(content) {
|
|
2101
|
+
if (!content) return false;
|
|
2102
|
+
return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
|
|
2103
|
+
}
|
|
2104
|
+
function remapManagedLines(genBase, target, theirs) {
|
|
2105
|
+
if (!containsPlaceholder(genBase)) return theirs;
|
|
2106
|
+
const managedMasks = /* @__PURE__ */ new Set();
|
|
2107
|
+
for (const line of genBase.split("\n")) {
|
|
2108
|
+
if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
|
|
2109
|
+
}
|
|
2110
|
+
if (managedMasks.size === 0) return theirs;
|
|
2111
|
+
const targetByMask = /* @__PURE__ */ new Map();
|
|
2112
|
+
for (const line of target.split("\n")) {
|
|
2113
|
+
const mask = maskVersions(line);
|
|
2114
|
+
if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
|
|
2115
|
+
}
|
|
2116
|
+
return theirs.split("\n").map((line) => {
|
|
2117
|
+
const mask = maskVersions(line);
|
|
2118
|
+
const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
|
|
2119
|
+
return replacement !== void 0 && replacement !== line ? replacement : line;
|
|
2120
|
+
}).join("\n");
|
|
2121
|
+
}
|
|
2122
|
+
function neutralizeAutoversionTheirs(genBase, theirs) {
|
|
2123
|
+
return remapManagedLines(genBase, genBase, theirs);
|
|
2124
|
+
}
|
|
2125
|
+
function alignAutoversionToOurs(genBase, ours, theirs) {
|
|
2126
|
+
return remapManagedLines(genBase, ours, theirs);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/applicator/runMergeAndRecord.ts
|
|
2013
2130
|
async function runMergeAndRecord(args) {
|
|
2014
2131
|
const { plan, inputs, patch, accumulator } = args;
|
|
2015
2132
|
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
@@ -2040,11 +2157,13 @@ async function runMergeAndRecord(args) {
|
|
|
2040
2157
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
2041
2158
|
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
2042
2159
|
}
|
|
2043
|
-
const
|
|
2160
|
+
const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
|
|
2161
|
+
const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
|
|
2162
|
+
const merged = threeWayMerge(mergeBase, ours, theirs);
|
|
2044
2163
|
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
|
|
2045
2164
|
await (0, import_promises2.writeFile)(oursPath, merged.content);
|
|
2046
2165
|
if (merged.hasConflicts) {
|
|
2047
|
-
accumulator.recordConflict(resolvedPath,
|
|
2166
|
+
accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
|
|
2048
2167
|
return {
|
|
2049
2168
|
file: resolvedPath,
|
|
2050
2169
|
status: "conflict",
|
|
@@ -2053,7 +2172,7 @@ async function runMergeAndRecord(args) {
|
|
|
2053
2172
|
conflictMetadata: metadata
|
|
2054
2173
|
};
|
|
2055
2174
|
}
|
|
2056
|
-
accumulator.recordMerge(resolvedPath,
|
|
2175
|
+
accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
|
|
2057
2176
|
return {
|
|
2058
2177
|
file: resolvedPath,
|
|
2059
2178
|
status: "merged",
|
|
@@ -2671,16 +2790,25 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2671
2790
|
};
|
|
2672
2791
|
|
|
2673
2792
|
// src/ReplayService.ts
|
|
2674
|
-
var
|
|
2793
|
+
var import_node_fs3 = require("fs");
|
|
2675
2794
|
var import_promises6 = require("fs/promises");
|
|
2676
|
-
var
|
|
2795
|
+
var import_node_path9 = require("path");
|
|
2677
2796
|
var import_minimatch5 = require("minimatch");
|
|
2678
2797
|
init_GitClient();
|
|
2679
2798
|
|
|
2799
|
+
// src/shared/fernignore.ts
|
|
2800
|
+
var import_node_fs2 = require("fs");
|
|
2801
|
+
var import_node_path6 = require("path");
|
|
2802
|
+
function readFernignorePatterns(outputDir) {
|
|
2803
|
+
const fernignorePath = (0, import_node_path6.join)(outputDir, ".fernignore");
|
|
2804
|
+
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
2805
|
+
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2680
2808
|
// src/PatchRegionDiff.ts
|
|
2681
2809
|
var import_promises4 = require("fs/promises");
|
|
2682
2810
|
var import_node_os2 = require("os");
|
|
2683
|
-
var
|
|
2811
|
+
var import_node_path7 = require("path");
|
|
2684
2812
|
var import_node_child_process = require("child_process");
|
|
2685
2813
|
init_HybridReconstruction();
|
|
2686
2814
|
function normalizeHunkBody(hunk) {
|
|
@@ -2898,10 +3026,10 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
2898
3026
|
}
|
|
2899
3027
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
2900
3028
|
if (beforeContent === afterContent) return "";
|
|
2901
|
-
const tmp = await (0, import_promises4.mkdtemp)((0,
|
|
3029
|
+
const tmp = await (0, import_promises4.mkdtemp)((0, import_node_path7.join)((0, import_node_os2.tmpdir)(), "fern-replay-pp-"));
|
|
2902
3030
|
try {
|
|
2903
|
-
const beforePath = (0,
|
|
2904
|
-
const afterPath = (0,
|
|
3031
|
+
const beforePath = (0, import_node_path7.join)(tmp, "before");
|
|
3032
|
+
const afterPath = (0, import_node_path7.join)(tmp, "after");
|
|
2905
3033
|
await (0, import_promises4.writeFile)(beforePath, beforeContent, "utf-8");
|
|
2906
3034
|
await (0, import_promises4.writeFile)(afterPath, afterContent, "utf-8");
|
|
2907
3035
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
@@ -3077,6 +3205,7 @@ var FirstGenerationFlow = class {
|
|
|
3077
3205
|
patchesToApply: [],
|
|
3078
3206
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3079
3207
|
detectorWarnings: [],
|
|
3208
|
+
detectorDegraded: [],
|
|
3080
3209
|
revertedCount: 0,
|
|
3081
3210
|
genSha: genRecord.commit_sha,
|
|
3082
3211
|
optionsSnapshot: options
|
|
@@ -3136,6 +3265,7 @@ var SkipApplicationFlow = class {
|
|
|
3136
3265
|
patchesToApply: [],
|
|
3137
3266
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3138
3267
|
detectorWarnings: [],
|
|
3268
|
+
detectorDegraded: [],
|
|
3139
3269
|
revertedCount: 0,
|
|
3140
3270
|
genSha: genRecord.commit_sha,
|
|
3141
3271
|
optionsSnapshot: options
|
|
@@ -3173,8 +3303,10 @@ var NoPatchesFlow = class {
|
|
|
3173
3303
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
3174
3304
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3175
3305
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3306
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3176
3307
|
if (options?.dryRun) {
|
|
3177
3308
|
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
3309
|
+
const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
|
|
3178
3310
|
const preparedReport = {
|
|
3179
3311
|
flow: "no-patches",
|
|
3180
3312
|
patchesDetected: newPatches.length,
|
|
@@ -3184,7 +3316,9 @@ var NoPatchesFlow = class {
|
|
|
3184
3316
|
patchesReverted: revertedPatchIds.length,
|
|
3185
3317
|
conflicts: [],
|
|
3186
3318
|
wouldApply: newPatches,
|
|
3187
|
-
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
3319
|
+
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0,
|
|
3320
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
3321
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
3188
3322
|
};
|
|
3189
3323
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3190
3324
|
return {
|
|
@@ -3238,6 +3372,7 @@ var NoPatchesFlow = class {
|
|
|
3238
3372
|
patchesToApply: newPatches,
|
|
3239
3373
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3240
3374
|
detectorWarnings,
|
|
3375
|
+
detectorDegraded,
|
|
3241
3376
|
revertedCount: revertedPatchIds.length,
|
|
3242
3377
|
genSha: genRecord.commit_sha,
|
|
3243
3378
|
optionsSnapshot: options
|
|
@@ -3281,6 +3416,7 @@ var NoPatchesFlow = class {
|
|
|
3281
3416
|
}
|
|
3282
3417
|
}
|
|
3283
3418
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3419
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3284
3420
|
return this.service.buildReport(
|
|
3285
3421
|
"no-patches",
|
|
3286
3422
|
newPatches,
|
|
@@ -3289,7 +3425,8 @@ var NoPatchesFlow = class {
|
|
|
3289
3425
|
warnings,
|
|
3290
3426
|
rebaseCounts,
|
|
3291
3427
|
void 0,
|
|
3292
|
-
prep._prepared.revertedCount
|
|
3428
|
+
prep._prepared.revertedCount,
|
|
3429
|
+
degradedReasons
|
|
3293
3430
|
);
|
|
3294
3431
|
}
|
|
3295
3432
|
};
|
|
@@ -3306,6 +3443,10 @@ var NormalRegenerationFlow = class {
|
|
|
3306
3443
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
3307
3444
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
3308
3445
|
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
3446
|
+
const dryRunDegraded = dedupeDegradedReasons([
|
|
3447
|
+
...this.service.detector.degradedReasons,
|
|
3448
|
+
...this.service.degradedReasons
|
|
3449
|
+
]);
|
|
3309
3450
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
3310
3451
|
const preparedReport = {
|
|
3311
3452
|
flow: "normal-regeneration",
|
|
@@ -3316,7 +3457,9 @@ var NormalRegenerationFlow = class {
|
|
|
3316
3457
|
patchesReverted: dryRunReverted.length,
|
|
3317
3458
|
conflicts: [],
|
|
3318
3459
|
wouldApply: allPatches2,
|
|
3319
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
3460
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
3461
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
3462
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
3320
3463
|
};
|
|
3321
3464
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3322
3465
|
return {
|
|
@@ -3355,6 +3498,7 @@ var NormalRegenerationFlow = class {
|
|
|
3355
3498
|
existingPatches = this.service.lockManager.getPatches();
|
|
3356
3499
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3357
3500
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3501
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3358
3502
|
if (removedByPreRebase.length > 0) {
|
|
3359
3503
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
3360
3504
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
@@ -3395,6 +3539,7 @@ var NormalRegenerationFlow = class {
|
|
|
3395
3539
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3396
3540
|
preRebaseCounts,
|
|
3397
3541
|
detectorWarnings,
|
|
3542
|
+
detectorDegraded,
|
|
3398
3543
|
revertedCount: revertedPatchIds.length,
|
|
3399
3544
|
genSha: genRecord.commit_sha,
|
|
3400
3545
|
optionsSnapshot: options
|
|
@@ -3443,6 +3588,7 @@ var NormalRegenerationFlow = class {
|
|
|
3443
3588
|
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
3444
3589
|
}
|
|
3445
3590
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3591
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3446
3592
|
return this.service.buildReport(
|
|
3447
3593
|
"normal-regeneration",
|
|
3448
3594
|
allPatches,
|
|
@@ -3451,7 +3597,8 @@ var NormalRegenerationFlow = class {
|
|
|
3451
3597
|
warnings,
|
|
3452
3598
|
rebaseCounts,
|
|
3453
3599
|
prep._prepared.preRebaseCounts,
|
|
3454
|
-
prep._prepared.revertedCount
|
|
3600
|
+
prep._prepared.revertedCount,
|
|
3601
|
+
degradedReasons
|
|
3455
3602
|
);
|
|
3456
3603
|
}
|
|
3457
3604
|
};
|
|
@@ -3491,6 +3638,16 @@ var ReplayService = class {
|
|
|
3491
3638
|
* @internal Used by Flow implementations in `src/flows/`.
|
|
3492
3639
|
*/
|
|
3493
3640
|
warnings = [];
|
|
3641
|
+
/**
|
|
3642
|
+
* Service-level structured degraded reasons, mirroring `warnings`.
|
|
3643
|
+
* Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
|
|
3644
|
+
* patch's base-generation tree is unreachable). Merged with
|
|
3645
|
+
* `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
|
|
3646
|
+
* flow handler. Cleared at the top of `prepareReplay()`.
|
|
3647
|
+
*
|
|
3648
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
3649
|
+
*/
|
|
3650
|
+
degradedReasons = [];
|
|
3494
3651
|
/**
|
|
3495
3652
|
* @internal Test-only accessor.
|
|
3496
3653
|
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
@@ -3534,7 +3691,10 @@ var ReplayService = class {
|
|
|
3534
3691
|
async prepareReplay(options) {
|
|
3535
3692
|
this.warnings = [];
|
|
3536
3693
|
this.detector.warnings.length = 0;
|
|
3694
|
+
this.degradedReasons = [];
|
|
3695
|
+
this.detector.degradedReasons.length = 0;
|
|
3537
3696
|
this.cleanseLegacyFernignoreEntries();
|
|
3697
|
+
this.cleanseInfrastructureFileEntries();
|
|
3538
3698
|
return this.selectFlow(options).prepare(options);
|
|
3539
3699
|
}
|
|
3540
3700
|
/**
|
|
@@ -3588,6 +3748,50 @@ var ReplayService = class {
|
|
|
3588
3748
|
this.lockManager.save();
|
|
3589
3749
|
}
|
|
3590
3750
|
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
|
|
3753
|
+
* from existing patches' `patch_content`. These diffs were bundled by older
|
|
3754
|
+
* detection logic that did not scope `git diff` when only infrastructure files
|
|
3755
|
+
* were stripped. Patches that end up with no surviving diff sections are
|
|
3756
|
+
* removed entirely.
|
|
3757
|
+
*
|
|
3758
|
+
* Idempotent: a second run is a no-op because the first run already removed
|
|
3759
|
+
* all infrastructure sections. Per-patch try/catch ensures isolation.
|
|
3760
|
+
*/
|
|
3761
|
+
cleanseInfrastructureFileEntries() {
|
|
3762
|
+
if (!this.lockManager.exists()) return;
|
|
3763
|
+
this.lockManager.read();
|
|
3764
|
+
let lockfileMutated = false;
|
|
3765
|
+
const patches = this.lockManager.getPatches();
|
|
3766
|
+
for (const patch of patches) {
|
|
3767
|
+
try {
|
|
3768
|
+
const result = computeInfrastructureFileCleanse(
|
|
3769
|
+
patch,
|
|
3770
|
+
(content) => this.detector.computeContentHash(content)
|
|
3771
|
+
);
|
|
3772
|
+
if (result.unchanged) continue;
|
|
3773
|
+
if (result.remove) {
|
|
3774
|
+
this.lockManager.removePatch(patch.id);
|
|
3775
|
+
this.warnings.push(
|
|
3776
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: patch contained only infrastructure file changes (${result.strippedPaths.join(", ")}) \u2014 removed.`
|
|
3777
|
+
);
|
|
3778
|
+
} else {
|
|
3779
|
+
this.lockManager.updatePatch(patch.id, {
|
|
3780
|
+
patch_content: result.patch_content,
|
|
3781
|
+
content_hash: result.content_hash
|
|
3782
|
+
});
|
|
3783
|
+
this.warnings.push(
|
|
3784
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: removed bundled diffs for ${result.strippedPaths.join(", ")}.`
|
|
3785
|
+
);
|
|
3786
|
+
}
|
|
3787
|
+
lockfileMutated = true;
|
|
3788
|
+
} catch {
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
if (lockfileMutated) {
|
|
3792
|
+
this.lockManager.save();
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3591
3795
|
/**
|
|
3592
3796
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3593
3797
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3754,7 +3958,9 @@ var ReplayService = class {
|
|
|
3754
3958
|
if (baseChanged) repointed++;
|
|
3755
3959
|
continue;
|
|
3756
3960
|
}
|
|
3757
|
-
const
|
|
3961
|
+
const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
3962
|
+
const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
|
|
3963
|
+
const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
|
|
3758
3964
|
if (diff === null) continue;
|
|
3759
3965
|
if (!diff.trim()) {
|
|
3760
3966
|
if (hasProtectedFiles) {
|
|
@@ -3810,7 +4016,7 @@ var ReplayService = class {
|
|
|
3810
4016
|
patch.base_generation = currentGenSha;
|
|
3811
4017
|
patch.patch_content = diff;
|
|
3812
4018
|
patch.content_hash = newContentHash;
|
|
3813
|
-
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
4019
|
+
const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
|
|
3814
4020
|
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
3815
4021
|
if (snapshotIsNonEmpty) {
|
|
3816
4022
|
patch.theirs_snapshot = snapshot;
|
|
@@ -3843,13 +4049,54 @@ var ReplayService = class {
|
|
|
3843
4049
|
for (const file of files) {
|
|
3844
4050
|
if (isBinaryFile(file)) continue;
|
|
3845
4051
|
try {
|
|
3846
|
-
const content = await (0, import_promises6.readFile)((0,
|
|
4052
|
+
const content = await (0, import_promises6.readFile)((0, import_node_path9.join)(this.outputDir, file), "utf-8");
|
|
3847
4053
|
snapshot[file] = content;
|
|
3848
4054
|
} catch {
|
|
3849
4055
|
}
|
|
3850
4056
|
}
|
|
3851
4057
|
return snapshot;
|
|
3852
4058
|
}
|
|
4059
|
+
/**
|
|
4060
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
4061
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
4062
|
+
*
|
|
4063
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
4064
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
4065
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
4066
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
4067
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
4068
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
4069
|
+
*
|
|
4070
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
4071
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
4072
|
+
*/
|
|
4073
|
+
async computeAutoversionNeutralizedRebase(files, currentGenSha) {
|
|
4074
|
+
const genByFile = /* @__PURE__ */ new Map();
|
|
4075
|
+
let anyPlaceholder = false;
|
|
4076
|
+
for (const file of files) {
|
|
4077
|
+
const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
|
|
4078
|
+
genByFile.set(file, gen);
|
|
4079
|
+
if (containsPlaceholder(gen)) anyPlaceholder = true;
|
|
4080
|
+
}
|
|
4081
|
+
if (!anyPlaceholder) return null;
|
|
4082
|
+
const snapshot = {};
|
|
4083
|
+
const fragments = [];
|
|
4084
|
+
for (const file of files) {
|
|
4085
|
+
if (isBinaryFile(file)) continue;
|
|
4086
|
+
const disk = await (0, import_promises6.readFile)((0, import_node_path9.join)(this.outputDir, file), "utf-8").catch(() => null);
|
|
4087
|
+
if (disk == null) continue;
|
|
4088
|
+
const gen = genByFile.get(file) ?? null;
|
|
4089
|
+
const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
|
|
4090
|
+
snapshot[file] = neutralized;
|
|
4091
|
+
if (gen == null) {
|
|
4092
|
+
fragments.push(synthesizeNewFileDiff(file, neutralized));
|
|
4093
|
+
} else if (gen !== neutralized) {
|
|
4094
|
+
const fileDiff = await unifiedDiff(gen, neutralized, file);
|
|
4095
|
+
if (fileDiff.trim()) fragments.push(fileDiff);
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
return { diff: fragments.join(""), snapshot };
|
|
4099
|
+
}
|
|
3853
4100
|
/**
|
|
3854
4101
|
* Detects autoversion contamination — returns true when a
|
|
3855
4102
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
|
@@ -4360,6 +4607,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4360
4607
|
);
|
|
4361
4608
|
}
|
|
4362
4609
|
}
|
|
4610
|
+
if (warned.size > 0) {
|
|
4611
|
+
this.degradedReasons.push({
|
|
4612
|
+
code: "patch-base-unreachable",
|
|
4613
|
+
message: `${warned.size} customization file(s) could not be checked against the generation they were made on because that generation's tree was not reachable during this run. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4363
4616
|
}
|
|
4364
4617
|
/**
|
|
4365
4618
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
@@ -4371,11 +4624,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4371
4624
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
4372
4625
|
for (const fileResult of result.fileResults) {
|
|
4373
4626
|
if (fileResult.status !== "conflict") continue;
|
|
4374
|
-
const filePath = (0,
|
|
4627
|
+
const filePath = (0, import_node_path9.join)(this.outputDir, fileResult.file);
|
|
4375
4628
|
try {
|
|
4376
|
-
const content = (0,
|
|
4629
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
4377
4630
|
const stripped = stripConflictMarkers(content);
|
|
4378
|
-
(0,
|
|
4631
|
+
(0, import_node_fs3.writeFileSync)(filePath, stripped);
|
|
4379
4632
|
} catch {
|
|
4380
4633
|
}
|
|
4381
4634
|
}
|
|
@@ -4403,12 +4656,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4403
4656
|
}
|
|
4404
4657
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4405
4658
|
readFernignorePatterns() {
|
|
4406
|
-
|
|
4407
|
-
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
4408
|
-
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
4659
|
+
return readFernignorePatterns(this.outputDir);
|
|
4409
4660
|
}
|
|
4410
4661
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4411
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
4662
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted, degradedReasons) {
|
|
4663
|
+
const degraded = dedupeDegradedReasons(degradedReasons ?? []);
|
|
4412
4664
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
4413
4665
|
const conflictDetails = conflictResults.map((r) => {
|
|
4414
4666
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -4448,10 +4700,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4448
4700
|
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
4449
4701
|
})) : void 0,
|
|
4450
4702
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
4451
|
-
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
4703
|
+
warnings: warnings && warnings.length > 0 ? warnings : void 0,
|
|
4704
|
+
degraded: degraded.length > 0 ? true : void 0,
|
|
4705
|
+
degradedReasons: degraded.length > 0 ? degraded : void 0
|
|
4452
4706
|
};
|
|
4453
4707
|
}
|
|
4454
4708
|
};
|
|
4709
|
+
function dedupeDegradedReasons(reasons) {
|
|
4710
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4711
|
+
const out = [];
|
|
4712
|
+
for (const reason of reasons) {
|
|
4713
|
+
const key = `${reason.code}\0${reason.message}`;
|
|
4714
|
+
if (seen.has(key)) continue;
|
|
4715
|
+
seen.add(key);
|
|
4716
|
+
out.push(reason);
|
|
4717
|
+
}
|
|
4718
|
+
return out;
|
|
4719
|
+
}
|
|
4455
4720
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
4456
4721
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
4457
4722
|
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
@@ -4500,7 +4765,7 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4500
4765
|
}
|
|
4501
4766
|
}
|
|
4502
4767
|
}
|
|
4503
|
-
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } =
|
|
4768
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, matchesFernignore);
|
|
4504
4769
|
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
4505
4770
|
if (unchanged) {
|
|
4506
4771
|
return {
|
|
@@ -4535,7 +4800,38 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4535
4800
|
strippedPaths
|
|
4536
4801
|
};
|
|
4537
4802
|
}
|
|
4538
|
-
function
|
|
4803
|
+
function isInfrastructureFile(filePath) {
|
|
4804
|
+
return filePath === ".fernignore" || filePath.startsWith(".fern/");
|
|
4805
|
+
}
|
|
4806
|
+
function computeInfrastructureFileCleanse(patch, computeContentHash2) {
|
|
4807
|
+
const { stripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, isInfrastructureFile);
|
|
4808
|
+
if (!stripped) {
|
|
4809
|
+
return {
|
|
4810
|
+
unchanged: true,
|
|
4811
|
+
remove: false,
|
|
4812
|
+
patch_content: patch.patch_content,
|
|
4813
|
+
content_hash: patch.content_hash,
|
|
4814
|
+
strippedPaths: []
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
if (newPatchContent.trim() === "" || patch.files.length === 0) {
|
|
4818
|
+
return {
|
|
4819
|
+
unchanged: false,
|
|
4820
|
+
remove: true,
|
|
4821
|
+
patch_content: "",
|
|
4822
|
+
content_hash: "",
|
|
4823
|
+
strippedPaths: strippedSectionPaths
|
|
4824
|
+
};
|
|
4825
|
+
}
|
|
4826
|
+
return {
|
|
4827
|
+
unchanged: false,
|
|
4828
|
+
remove: false,
|
|
4829
|
+
patch_content: newPatchContent,
|
|
4830
|
+
content_hash: computeContentHash2(newPatchContent),
|
|
4831
|
+
strippedPaths: strippedSectionPaths
|
|
4832
|
+
};
|
|
4833
|
+
}
|
|
4834
|
+
function stripDiffSectionsByPredicate(patchContent, shouldStrip) {
|
|
4539
4835
|
const lines = patchContent.split("\n");
|
|
4540
4836
|
const sectionStarts = [];
|
|
4541
4837
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4557,7 +4853,7 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4557
4853
|
const section = sectionStarts[i];
|
|
4558
4854
|
const next = sectionStarts[i + 1];
|
|
4559
4855
|
const endIdx = next ? next.idx : lines.length;
|
|
4560
|
-
if (section.path !== null &&
|
|
4856
|
+
if (section.path !== null && shouldStrip(section.path)) {
|
|
4561
4857
|
stripped = true;
|
|
4562
4858
|
strippedSectionPaths.push(section.path);
|
|
4563
4859
|
continue;
|
|
@@ -4569,8 +4865,8 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4569
4865
|
|
|
4570
4866
|
// src/FernignoreMigrator.ts
|
|
4571
4867
|
var import_node_crypto2 = require("crypto");
|
|
4572
|
-
var
|
|
4573
|
-
var
|
|
4868
|
+
var import_node_fs4 = require("fs");
|
|
4869
|
+
var import_node_path10 = require("path");
|
|
4574
4870
|
var import_minimatch6 = require("minimatch");
|
|
4575
4871
|
var import_yaml2 = require("yaml");
|
|
4576
4872
|
var FernignoreMigrator = class {
|
|
@@ -4583,14 +4879,14 @@ var FernignoreMigrator = class {
|
|
|
4583
4879
|
this.outputDir = outputDir;
|
|
4584
4880
|
}
|
|
4585
4881
|
fernignoreExists() {
|
|
4586
|
-
return (0,
|
|
4882
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path10.join)(this.outputDir, ".fernignore"));
|
|
4587
4883
|
}
|
|
4588
4884
|
readFernignorePatterns() {
|
|
4589
|
-
const fernignorePath = (0,
|
|
4590
|
-
if (!(0,
|
|
4885
|
+
const fernignorePath = (0, import_node_path10.join)(this.outputDir, ".fernignore");
|
|
4886
|
+
if (!(0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
4591
4887
|
return [];
|
|
4592
4888
|
}
|
|
4593
|
-
const content = (0,
|
|
4889
|
+
const content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
4594
4890
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
4595
4891
|
}
|
|
4596
4892
|
/** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
|
|
@@ -4695,16 +4991,16 @@ var FernignoreMigrator = class {
|
|
|
4695
4991
|
return results;
|
|
4696
4992
|
}
|
|
4697
4993
|
readFileContent(filePath) {
|
|
4698
|
-
const fullPath = (0,
|
|
4699
|
-
if (!(0,
|
|
4994
|
+
const fullPath = (0, import_node_path10.join)(this.outputDir, filePath);
|
|
4995
|
+
if (!(0, import_node_fs4.existsSync)(fullPath)) {
|
|
4700
4996
|
return null;
|
|
4701
4997
|
}
|
|
4702
4998
|
try {
|
|
4703
|
-
const stat = (0,
|
|
4999
|
+
const stat = (0, import_node_fs4.statSync)(fullPath);
|
|
4704
5000
|
if (stat.isDirectory()) {
|
|
4705
5001
|
return null;
|
|
4706
5002
|
}
|
|
4707
|
-
return (0,
|
|
5003
|
+
return (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
4708
5004
|
} catch {
|
|
4709
5005
|
return null;
|
|
4710
5006
|
}
|
|
@@ -4760,27 +5056,27 @@ var FernignoreMigrator = class {
|
|
|
4760
5056
|
};
|
|
4761
5057
|
}
|
|
4762
5058
|
movePatternsToReplayYml(patterns) {
|
|
4763
|
-
const replayYmlPath = (0,
|
|
5059
|
+
const replayYmlPath = (0, import_node_path10.join)(this.outputDir, ".fern", "replay.yml");
|
|
4764
5060
|
let config = {};
|
|
4765
|
-
if ((0,
|
|
4766
|
-
const content = (0,
|
|
5061
|
+
if ((0, import_node_fs4.existsSync)(replayYmlPath)) {
|
|
5062
|
+
const content = (0, import_node_fs4.readFileSync)(replayYmlPath, "utf-8");
|
|
4767
5063
|
config = (0, import_yaml2.parse)(content) ?? {};
|
|
4768
5064
|
}
|
|
4769
5065
|
const existing = config.exclude ?? [];
|
|
4770
5066
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
4771
5067
|
config.exclude = merged;
|
|
4772
|
-
const dir = (0,
|
|
4773
|
-
if (!(0,
|
|
4774
|
-
(0,
|
|
5068
|
+
const dir = (0, import_node_path10.dirname)(replayYmlPath);
|
|
5069
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
5070
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
4775
5071
|
}
|
|
4776
|
-
(0,
|
|
5072
|
+
(0, import_node_fs4.writeFileSync)(replayYmlPath, (0, import_yaml2.stringify)(config, { lineWidth: 0 }), "utf-8");
|
|
4777
5073
|
}
|
|
4778
5074
|
};
|
|
4779
5075
|
|
|
4780
5076
|
// src/commands/bootstrap.ts
|
|
4781
5077
|
var import_node_crypto3 = require("crypto");
|
|
4782
|
-
var
|
|
4783
|
-
var
|
|
5078
|
+
var import_node_fs5 = require("fs");
|
|
5079
|
+
var import_node_path11 = require("path");
|
|
4784
5080
|
init_GitClient();
|
|
4785
5081
|
async function bootstrap(outputDir, options) {
|
|
4786
5082
|
const git = new GitClient(outputDir);
|
|
@@ -4947,10 +5243,10 @@ function parseGitLog(log) {
|
|
|
4947
5243
|
}
|
|
4948
5244
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
4949
5245
|
function ensureFernignoreEntries(outputDir) {
|
|
4950
|
-
const fernignorePath = (0,
|
|
5246
|
+
const fernignorePath = (0, import_node_path11.join)(outputDir, ".fernignore");
|
|
4951
5247
|
let content = "";
|
|
4952
|
-
if ((0,
|
|
4953
|
-
content = (0,
|
|
5248
|
+
if ((0, import_node_fs5.existsSync)(fernignorePath)) {
|
|
5249
|
+
content = (0, import_node_fs5.readFileSync)(fernignorePath, "utf-8");
|
|
4954
5250
|
}
|
|
4955
5251
|
const lines = content.split("\n");
|
|
4956
5252
|
const toAdd = [];
|
|
@@ -4966,15 +5262,15 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
4966
5262
|
content += "\n";
|
|
4967
5263
|
}
|
|
4968
5264
|
content += toAdd.join("\n") + "\n";
|
|
4969
|
-
(0,
|
|
5265
|
+
(0, import_node_fs5.writeFileSync)(fernignorePath, content, "utf-8");
|
|
4970
5266
|
return true;
|
|
4971
5267
|
}
|
|
4972
5268
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
4973
5269
|
function ensureGitattributesEntries(outputDir) {
|
|
4974
|
-
const gitattributesPath = (0,
|
|
5270
|
+
const gitattributesPath = (0, import_node_path11.join)(outputDir, ".gitattributes");
|
|
4975
5271
|
let content = "";
|
|
4976
|
-
if ((0,
|
|
4977
|
-
content = (0,
|
|
5272
|
+
if ((0, import_node_fs5.existsSync)(gitattributesPath)) {
|
|
5273
|
+
content = (0, import_node_fs5.readFileSync)(gitattributesPath, "utf-8");
|
|
4978
5274
|
}
|
|
4979
5275
|
const lines = content.split("\n");
|
|
4980
5276
|
const toAdd = [];
|
|
@@ -4990,7 +5286,7 @@ function ensureGitattributesEntries(outputDir) {
|
|
|
4990
5286
|
content += "\n";
|
|
4991
5287
|
}
|
|
4992
5288
|
content += toAdd.join("\n") + "\n";
|
|
4993
|
-
(0,
|
|
5289
|
+
(0, import_node_fs5.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
4994
5290
|
}
|
|
4995
5291
|
function computeContentHash(patchContent) {
|
|
4996
5292
|
const lines = patchContent.split("\n");
|
|
@@ -5002,6 +5298,7 @@ function computeContentHash(patchContent) {
|
|
|
5002
5298
|
|
|
5003
5299
|
// src/commands/forget.ts
|
|
5004
5300
|
var import_minimatch7 = require("minimatch");
|
|
5301
|
+
init_GitClient();
|
|
5005
5302
|
function parseDiffStat(patchContent) {
|
|
5006
5303
|
let additions = 0;
|
|
5007
5304
|
let deletions = 0;
|
|
@@ -5060,7 +5357,28 @@ var EMPTY_RESULT = {
|
|
|
5060
5357
|
totalPatches: 0,
|
|
5061
5358
|
warnings: []
|
|
5062
5359
|
};
|
|
5063
|
-
function
|
|
5360
|
+
async function dismissPatches(outputDir, lockManager, patches) {
|
|
5361
|
+
const git = new GitClient(outputDir);
|
|
5362
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
5363
|
+
for (const patch of patches) {
|
|
5364
|
+
const hashes = [patch.content_hash];
|
|
5365
|
+
if (patch.original_commit) {
|
|
5366
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
5367
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
5368
|
+
hashes.push(materialized.contentHash);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
lockManager.addDismissal({
|
|
5372
|
+
patch_id: patch.id,
|
|
5373
|
+
original_commit: patch.original_commit,
|
|
5374
|
+
original_message: patch.original_message,
|
|
5375
|
+
content_hashes: hashes,
|
|
5376
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5377
|
+
via: "forget"
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
async function forget(outputDir, options) {
|
|
5064
5382
|
const lockManager = new LockfileManager(outputDir);
|
|
5065
5383
|
if (!lockManager.exists()) {
|
|
5066
5384
|
return { ...EMPTY_RESULT };
|
|
@@ -5071,9 +5389,7 @@ function forget(outputDir, options) {
|
|
|
5071
5389
|
const removed = lock.patches.map(toMatchedPatch);
|
|
5072
5390
|
const warnings = buildWarnings(lock.patches);
|
|
5073
5391
|
if (!options.dryRun) {
|
|
5074
|
-
|
|
5075
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5076
|
-
}
|
|
5392
|
+
await dismissPatches(outputDir, lockManager, lock.patches);
|
|
5077
5393
|
lockManager.clearPatches();
|
|
5078
5394
|
lockManager.save();
|
|
5079
5395
|
}
|
|
@@ -5102,8 +5418,8 @@ function forget(outputDir, options) {
|
|
|
5102
5418
|
}
|
|
5103
5419
|
const warnings = buildWarnings(patchesToRemove);
|
|
5104
5420
|
if (!options.dryRun) {
|
|
5421
|
+
await dismissPatches(outputDir, lockManager, patchesToRemove);
|
|
5105
5422
|
for (const patch of patchesToRemove) {
|
|
5106
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5107
5423
|
lockManager.removePatch(patch.id);
|
|
5108
5424
|
}
|
|
5109
5425
|
lockManager.save();
|
|
@@ -5144,7 +5460,7 @@ function forget(outputDir, options) {
|
|
|
5144
5460
|
}
|
|
5145
5461
|
|
|
5146
5462
|
// src/commands/reset.ts
|
|
5147
|
-
var
|
|
5463
|
+
var import_node_fs6 = require("fs");
|
|
5148
5464
|
function reset(outputDir, options) {
|
|
5149
5465
|
const lockManager = new LockfileManager(outputDir);
|
|
5150
5466
|
if (!lockManager.exists()) {
|
|
@@ -5158,7 +5474,7 @@ function reset(outputDir, options) {
|
|
|
5158
5474
|
const lock = lockManager.read();
|
|
5159
5475
|
const patchCount = lock.patches.length;
|
|
5160
5476
|
if (!options?.dryRun) {
|
|
5161
|
-
(0,
|
|
5477
|
+
(0, import_node_fs6.unlinkSync)(lockManager.lockfilePath);
|
|
5162
5478
|
}
|
|
5163
5479
|
return {
|
|
5164
5480
|
success: true,
|
|
@@ -5170,7 +5486,7 @@ function reset(outputDir, options) {
|
|
|
5170
5486
|
|
|
5171
5487
|
// src/commands/resolve.ts
|
|
5172
5488
|
var import_promises7 = require("fs/promises");
|
|
5173
|
-
var
|
|
5489
|
+
var import_node_path12 = require("path");
|
|
5174
5490
|
init_GitClient();
|
|
5175
5491
|
async function resolve(outputDir, options) {
|
|
5176
5492
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -5211,15 +5527,16 @@ async function resolve(outputDir, options) {
|
|
|
5211
5527
|
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
5212
5528
|
if (patchesToCommit.length > 0) {
|
|
5213
5529
|
const currentGen = lock.current_generation;
|
|
5214
|
-
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
5530
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
5215
5531
|
const warnings = [];
|
|
5532
|
+
const patchesDismissed = [];
|
|
5216
5533
|
let patchesResolved = 0;
|
|
5217
5534
|
const pass1 = [];
|
|
5218
5535
|
for (const patch of patchesToCommit) {
|
|
5219
5536
|
const result = await computePerPatchDiffWithFallback(
|
|
5220
5537
|
patch,
|
|
5221
5538
|
(f) => git.showFile(currentGen, f),
|
|
5222
|
-
(f) => (0, import_promises7.readFile)((0,
|
|
5539
|
+
(f) => (0, import_promises7.readFile)((0, import_node_path12.join)(outputDir, f), "utf-8").catch(() => null),
|
|
5223
5540
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
5224
5541
|
);
|
|
5225
5542
|
if (result === null) {
|
|
@@ -5253,7 +5570,27 @@ async function resolve(outputDir, options) {
|
|
|
5253
5570
|
const r = pass1[i];
|
|
5254
5571
|
if (r.kind === "skip") continue;
|
|
5255
5572
|
if (r.kind === "revert") {
|
|
5573
|
+
const hashes = [patch.content_hash];
|
|
5574
|
+
if (patch.original_commit) {
|
|
5575
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
5576
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
5577
|
+
hashes.push(materialized.contentHash);
|
|
5578
|
+
} else if (materialized.status === "unreachable") {
|
|
5579
|
+
warnings.push(
|
|
5580
|
+
`Patch ${patch.id}: original commit ${patch.original_commit.slice(0, 7)} is unreachable; dismissed using the stored content hash only. If this commit reappears in history, the customization may be re-detected once.`
|
|
5581
|
+
);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
lockManager.addDismissal({
|
|
5585
|
+
patch_id: patch.id,
|
|
5586
|
+
original_commit: patch.original_commit,
|
|
5587
|
+
original_message: patch.original_message,
|
|
5588
|
+
content_hashes: hashes,
|
|
5589
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5590
|
+
via: "resolve"
|
|
5591
|
+
});
|
|
5256
5592
|
lockManager.removePatch(patch.id);
|
|
5593
|
+
patchesDismissed.push({ id: patch.id, message: patch.original_message });
|
|
5257
5594
|
continue;
|
|
5258
5595
|
}
|
|
5259
5596
|
if (lastIndexByHash.get(r.hash) !== i) {
|
|
@@ -5275,7 +5612,7 @@ async function resolve(outputDir, options) {
|
|
|
5275
5612
|
const theirsSnapshot = {};
|
|
5276
5613
|
for (const file of filesForSnapshot) {
|
|
5277
5614
|
if (isBinaryFile(file)) continue;
|
|
5278
|
-
const content = await (0, import_promises7.readFile)((0,
|
|
5615
|
+
const content = await (0, import_promises7.readFile)((0, import_node_path12.join)(outputDir, file), "utf-8").catch(() => null);
|
|
5279
5616
|
if (content != null) {
|
|
5280
5617
|
theirsSnapshot[file] = content;
|
|
5281
5618
|
}
|
|
@@ -5302,6 +5639,7 @@ async function resolve(outputDir, options) {
|
|
|
5302
5639
|
commitSha: commitSha2,
|
|
5303
5640
|
phase: "committed",
|
|
5304
5641
|
patchesResolved,
|
|
5642
|
+
patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
|
|
5305
5643
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
5306
5644
|
};
|
|
5307
5645
|
}
|
|
@@ -5349,7 +5687,9 @@ function status(outputDir) {
|
|
|
5349
5687
|
lastGeneration: void 0,
|
|
5350
5688
|
patches: [],
|
|
5351
5689
|
unresolvedCount: 0,
|
|
5352
|
-
excludePatterns: []
|
|
5690
|
+
excludePatterns: [],
|
|
5691
|
+
dismissed: [],
|
|
5692
|
+
legacyDismissedCount: 0
|
|
5353
5693
|
};
|
|
5354
5694
|
}
|
|
5355
5695
|
const lock = lockManager.read();
|
|
@@ -5378,13 +5718,24 @@ function status(outputDir) {
|
|
|
5378
5718
|
}
|
|
5379
5719
|
const config = lockManager.getCustomizationsConfig();
|
|
5380
5720
|
const excludePatterns = config.exclude ?? [];
|
|
5721
|
+
const dismissals = lock.dismissals ?? [];
|
|
5722
|
+
const dismissed = dismissals.map((d) => ({
|
|
5723
|
+
message: d.original_message,
|
|
5724
|
+
sha: d.original_commit.slice(0, 7),
|
|
5725
|
+
via: d.via,
|
|
5726
|
+
dismissedAt: d.dismissed_at
|
|
5727
|
+
}));
|
|
5728
|
+
const coveredHashes = new Set(dismissals.flatMap((d) => d.content_hashes));
|
|
5729
|
+
const legacyDismissedCount = (lock.forgotten_hashes ?? []).filter((h) => !coveredHashes.has(h)).length;
|
|
5381
5730
|
return {
|
|
5382
5731
|
initialized: true,
|
|
5383
5732
|
generationCount: lock.generations.length,
|
|
5384
5733
|
lastGeneration,
|
|
5385
5734
|
patches,
|
|
5386
5735
|
unresolvedCount,
|
|
5387
|
-
excludePatterns
|
|
5736
|
+
excludePatterns,
|
|
5737
|
+
dismissed,
|
|
5738
|
+
legacyDismissedCount
|
|
5388
5739
|
};
|
|
5389
5740
|
}
|
|
5390
5741
|
// Annotate the CommonJS export names for ESM import in node:
|