@fern-api/replay 0.18.0 → 0.19.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/dist/cli.cjs +427 -120
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +372 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -3
- package/dist/index.d.ts +193 -3
- package/dist/index.js +355 -94
- 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
|
}
|
|
@@ -2671,16 +2743,25 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2671
2743
|
};
|
|
2672
2744
|
|
|
2673
2745
|
// src/ReplayService.ts
|
|
2674
|
-
var
|
|
2746
|
+
var import_node_fs3 = require("fs");
|
|
2675
2747
|
var import_promises6 = require("fs/promises");
|
|
2676
|
-
var
|
|
2748
|
+
var import_node_path9 = require("path");
|
|
2677
2749
|
var import_minimatch5 = require("minimatch");
|
|
2678
2750
|
init_GitClient();
|
|
2679
2751
|
|
|
2752
|
+
// src/shared/fernignore.ts
|
|
2753
|
+
var import_node_fs2 = require("fs");
|
|
2754
|
+
var import_node_path6 = require("path");
|
|
2755
|
+
function readFernignorePatterns(outputDir) {
|
|
2756
|
+
const fernignorePath = (0, import_node_path6.join)(outputDir, ".fernignore");
|
|
2757
|
+
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
2758
|
+
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2680
2761
|
// src/PatchRegionDiff.ts
|
|
2681
2762
|
var import_promises4 = require("fs/promises");
|
|
2682
2763
|
var import_node_os2 = require("os");
|
|
2683
|
-
var
|
|
2764
|
+
var import_node_path7 = require("path");
|
|
2684
2765
|
var import_node_child_process = require("child_process");
|
|
2685
2766
|
init_HybridReconstruction();
|
|
2686
2767
|
function normalizeHunkBody(hunk) {
|
|
@@ -2898,10 +2979,10 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
2898
2979
|
}
|
|
2899
2980
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
2900
2981
|
if (beforeContent === afterContent) return "";
|
|
2901
|
-
const tmp = await (0, import_promises4.mkdtemp)((0,
|
|
2982
|
+
const tmp = await (0, import_promises4.mkdtemp)((0, import_node_path7.join)((0, import_node_os2.tmpdir)(), "fern-replay-pp-"));
|
|
2902
2983
|
try {
|
|
2903
|
-
const beforePath = (0,
|
|
2904
|
-
const afterPath = (0,
|
|
2984
|
+
const beforePath = (0, import_node_path7.join)(tmp, "before");
|
|
2985
|
+
const afterPath = (0, import_node_path7.join)(tmp, "after");
|
|
2905
2986
|
await (0, import_promises4.writeFile)(beforePath, beforeContent, "utf-8");
|
|
2906
2987
|
await (0, import_promises4.writeFile)(afterPath, afterContent, "utf-8");
|
|
2907
2988
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
@@ -3077,6 +3158,7 @@ var FirstGenerationFlow = class {
|
|
|
3077
3158
|
patchesToApply: [],
|
|
3078
3159
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3079
3160
|
detectorWarnings: [],
|
|
3161
|
+
detectorDegraded: [],
|
|
3080
3162
|
revertedCount: 0,
|
|
3081
3163
|
genSha: genRecord.commit_sha,
|
|
3082
3164
|
optionsSnapshot: options
|
|
@@ -3136,6 +3218,7 @@ var SkipApplicationFlow = class {
|
|
|
3136
3218
|
patchesToApply: [],
|
|
3137
3219
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3138
3220
|
detectorWarnings: [],
|
|
3221
|
+
detectorDegraded: [],
|
|
3139
3222
|
revertedCount: 0,
|
|
3140
3223
|
genSha: genRecord.commit_sha,
|
|
3141
3224
|
optionsSnapshot: options
|
|
@@ -3173,8 +3256,10 @@ var NoPatchesFlow = class {
|
|
|
3173
3256
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
3174
3257
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3175
3258
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3259
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3176
3260
|
if (options?.dryRun) {
|
|
3177
3261
|
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
3262
|
+
const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
|
|
3178
3263
|
const preparedReport = {
|
|
3179
3264
|
flow: "no-patches",
|
|
3180
3265
|
patchesDetected: newPatches.length,
|
|
@@ -3184,7 +3269,9 @@ var NoPatchesFlow = class {
|
|
|
3184
3269
|
patchesReverted: revertedPatchIds.length,
|
|
3185
3270
|
conflicts: [],
|
|
3186
3271
|
wouldApply: newPatches,
|
|
3187
|
-
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
3272
|
+
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0,
|
|
3273
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
3274
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
3188
3275
|
};
|
|
3189
3276
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3190
3277
|
return {
|
|
@@ -3238,6 +3325,7 @@ var NoPatchesFlow = class {
|
|
|
3238
3325
|
patchesToApply: newPatches,
|
|
3239
3326
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3240
3327
|
detectorWarnings,
|
|
3328
|
+
detectorDegraded,
|
|
3241
3329
|
revertedCount: revertedPatchIds.length,
|
|
3242
3330
|
genSha: genRecord.commit_sha,
|
|
3243
3331
|
optionsSnapshot: options
|
|
@@ -3281,6 +3369,7 @@ var NoPatchesFlow = class {
|
|
|
3281
3369
|
}
|
|
3282
3370
|
}
|
|
3283
3371
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3372
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3284
3373
|
return this.service.buildReport(
|
|
3285
3374
|
"no-patches",
|
|
3286
3375
|
newPatches,
|
|
@@ -3289,7 +3378,8 @@ var NoPatchesFlow = class {
|
|
|
3289
3378
|
warnings,
|
|
3290
3379
|
rebaseCounts,
|
|
3291
3380
|
void 0,
|
|
3292
|
-
prep._prepared.revertedCount
|
|
3381
|
+
prep._prepared.revertedCount,
|
|
3382
|
+
degradedReasons
|
|
3293
3383
|
);
|
|
3294
3384
|
}
|
|
3295
3385
|
};
|
|
@@ -3306,6 +3396,10 @@ var NormalRegenerationFlow = class {
|
|
|
3306
3396
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
3307
3397
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
3308
3398
|
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
3399
|
+
const dryRunDegraded = dedupeDegradedReasons([
|
|
3400
|
+
...this.service.detector.degradedReasons,
|
|
3401
|
+
...this.service.degradedReasons
|
|
3402
|
+
]);
|
|
3309
3403
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
3310
3404
|
const preparedReport = {
|
|
3311
3405
|
flow: "normal-regeneration",
|
|
@@ -3316,7 +3410,9 @@ var NormalRegenerationFlow = class {
|
|
|
3316
3410
|
patchesReverted: dryRunReverted.length,
|
|
3317
3411
|
conflicts: [],
|
|
3318
3412
|
wouldApply: allPatches2,
|
|
3319
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
3413
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
3414
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
3415
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
3320
3416
|
};
|
|
3321
3417
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3322
3418
|
return {
|
|
@@ -3355,6 +3451,7 @@ var NormalRegenerationFlow = class {
|
|
|
3355
3451
|
existingPatches = this.service.lockManager.getPatches();
|
|
3356
3452
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3357
3453
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3454
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3358
3455
|
if (removedByPreRebase.length > 0) {
|
|
3359
3456
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
3360
3457
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
@@ -3395,6 +3492,7 @@ var NormalRegenerationFlow = class {
|
|
|
3395
3492
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3396
3493
|
preRebaseCounts,
|
|
3397
3494
|
detectorWarnings,
|
|
3495
|
+
detectorDegraded,
|
|
3398
3496
|
revertedCount: revertedPatchIds.length,
|
|
3399
3497
|
genSha: genRecord.commit_sha,
|
|
3400
3498
|
optionsSnapshot: options
|
|
@@ -3443,6 +3541,7 @@ var NormalRegenerationFlow = class {
|
|
|
3443
3541
|
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
3444
3542
|
}
|
|
3445
3543
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3544
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3446
3545
|
return this.service.buildReport(
|
|
3447
3546
|
"normal-regeneration",
|
|
3448
3547
|
allPatches,
|
|
@@ -3451,7 +3550,8 @@ var NormalRegenerationFlow = class {
|
|
|
3451
3550
|
warnings,
|
|
3452
3551
|
rebaseCounts,
|
|
3453
3552
|
prep._prepared.preRebaseCounts,
|
|
3454
|
-
prep._prepared.revertedCount
|
|
3553
|
+
prep._prepared.revertedCount,
|
|
3554
|
+
degradedReasons
|
|
3455
3555
|
);
|
|
3456
3556
|
}
|
|
3457
3557
|
};
|
|
@@ -3491,6 +3591,16 @@ var ReplayService = class {
|
|
|
3491
3591
|
* @internal Used by Flow implementations in `src/flows/`.
|
|
3492
3592
|
*/
|
|
3493
3593
|
warnings = [];
|
|
3594
|
+
/**
|
|
3595
|
+
* Service-level structured degraded reasons, mirroring `warnings`.
|
|
3596
|
+
* Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
|
|
3597
|
+
* patch's base-generation tree is unreachable). Merged with
|
|
3598
|
+
* `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
|
|
3599
|
+
* flow handler. Cleared at the top of `prepareReplay()`.
|
|
3600
|
+
*
|
|
3601
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
3602
|
+
*/
|
|
3603
|
+
degradedReasons = [];
|
|
3494
3604
|
/**
|
|
3495
3605
|
* @internal Test-only accessor.
|
|
3496
3606
|
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
@@ -3534,7 +3644,10 @@ var ReplayService = class {
|
|
|
3534
3644
|
async prepareReplay(options) {
|
|
3535
3645
|
this.warnings = [];
|
|
3536
3646
|
this.detector.warnings.length = 0;
|
|
3647
|
+
this.degradedReasons = [];
|
|
3648
|
+
this.detector.degradedReasons.length = 0;
|
|
3537
3649
|
this.cleanseLegacyFernignoreEntries();
|
|
3650
|
+
this.cleanseInfrastructureFileEntries();
|
|
3538
3651
|
return this.selectFlow(options).prepare(options);
|
|
3539
3652
|
}
|
|
3540
3653
|
/**
|
|
@@ -3588,6 +3701,50 @@ var ReplayService = class {
|
|
|
3588
3701
|
this.lockManager.save();
|
|
3589
3702
|
}
|
|
3590
3703
|
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
|
|
3706
|
+
* from existing patches' `patch_content`. These diffs were bundled by older
|
|
3707
|
+
* detection logic that did not scope `git diff` when only infrastructure files
|
|
3708
|
+
* were stripped. Patches that end up with no surviving diff sections are
|
|
3709
|
+
* removed entirely.
|
|
3710
|
+
*
|
|
3711
|
+
* Idempotent: a second run is a no-op because the first run already removed
|
|
3712
|
+
* all infrastructure sections. Per-patch try/catch ensures isolation.
|
|
3713
|
+
*/
|
|
3714
|
+
cleanseInfrastructureFileEntries() {
|
|
3715
|
+
if (!this.lockManager.exists()) return;
|
|
3716
|
+
this.lockManager.read();
|
|
3717
|
+
let lockfileMutated = false;
|
|
3718
|
+
const patches = this.lockManager.getPatches();
|
|
3719
|
+
for (const patch of patches) {
|
|
3720
|
+
try {
|
|
3721
|
+
const result = computeInfrastructureFileCleanse(
|
|
3722
|
+
patch,
|
|
3723
|
+
(content) => this.detector.computeContentHash(content)
|
|
3724
|
+
);
|
|
3725
|
+
if (result.unchanged) continue;
|
|
3726
|
+
if (result.remove) {
|
|
3727
|
+
this.lockManager.removePatch(patch.id);
|
|
3728
|
+
this.warnings.push(
|
|
3729
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: patch contained only infrastructure file changes (${result.strippedPaths.join(", ")}) \u2014 removed.`
|
|
3730
|
+
);
|
|
3731
|
+
} else {
|
|
3732
|
+
this.lockManager.updatePatch(patch.id, {
|
|
3733
|
+
patch_content: result.patch_content,
|
|
3734
|
+
content_hash: result.content_hash
|
|
3735
|
+
});
|
|
3736
|
+
this.warnings.push(
|
|
3737
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: removed bundled diffs for ${result.strippedPaths.join(", ")}.`
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
lockfileMutated = true;
|
|
3741
|
+
} catch {
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
if (lockfileMutated) {
|
|
3745
|
+
this.lockManager.save();
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3591
3748
|
/**
|
|
3592
3749
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3593
3750
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3843,7 +4000,7 @@ var ReplayService = class {
|
|
|
3843
4000
|
for (const file of files) {
|
|
3844
4001
|
if (isBinaryFile(file)) continue;
|
|
3845
4002
|
try {
|
|
3846
|
-
const content = await (0, import_promises6.readFile)((0,
|
|
4003
|
+
const content = await (0, import_promises6.readFile)((0, import_node_path9.join)(this.outputDir, file), "utf-8");
|
|
3847
4004
|
snapshot[file] = content;
|
|
3848
4005
|
} catch {
|
|
3849
4006
|
}
|
|
@@ -4360,6 +4517,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4360
4517
|
);
|
|
4361
4518
|
}
|
|
4362
4519
|
}
|
|
4520
|
+
if (warned.size > 0) {
|
|
4521
|
+
this.degradedReasons.push({
|
|
4522
|
+
code: "patch-base-unreachable",
|
|
4523
|
+
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.`
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4363
4526
|
}
|
|
4364
4527
|
/**
|
|
4365
4528
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
@@ -4371,11 +4534,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4371
4534
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
4372
4535
|
for (const fileResult of result.fileResults) {
|
|
4373
4536
|
if (fileResult.status !== "conflict") continue;
|
|
4374
|
-
const filePath = (0,
|
|
4537
|
+
const filePath = (0, import_node_path9.join)(this.outputDir, fileResult.file);
|
|
4375
4538
|
try {
|
|
4376
|
-
const content = (0,
|
|
4539
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
4377
4540
|
const stripped = stripConflictMarkers(content);
|
|
4378
|
-
(0,
|
|
4541
|
+
(0, import_node_fs3.writeFileSync)(filePath, stripped);
|
|
4379
4542
|
} catch {
|
|
4380
4543
|
}
|
|
4381
4544
|
}
|
|
@@ -4403,12 +4566,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4403
4566
|
}
|
|
4404
4567
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4405
4568
|
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("#"));
|
|
4569
|
+
return readFernignorePatterns(this.outputDir);
|
|
4409
4570
|
}
|
|
4410
4571
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4411
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
4572
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted, degradedReasons) {
|
|
4573
|
+
const degraded = dedupeDegradedReasons(degradedReasons ?? []);
|
|
4412
4574
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
4413
4575
|
const conflictDetails = conflictResults.map((r) => {
|
|
4414
4576
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -4448,10 +4610,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4448
4610
|
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
4449
4611
|
})) : void 0,
|
|
4450
4612
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
4451
|
-
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
4613
|
+
warnings: warnings && warnings.length > 0 ? warnings : void 0,
|
|
4614
|
+
degraded: degraded.length > 0 ? true : void 0,
|
|
4615
|
+
degradedReasons: degraded.length > 0 ? degraded : void 0
|
|
4452
4616
|
};
|
|
4453
4617
|
}
|
|
4454
4618
|
};
|
|
4619
|
+
function dedupeDegradedReasons(reasons) {
|
|
4620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4621
|
+
const out = [];
|
|
4622
|
+
for (const reason of reasons) {
|
|
4623
|
+
const key = `${reason.code}\0${reason.message}`;
|
|
4624
|
+
if (seen.has(key)) continue;
|
|
4625
|
+
seen.add(key);
|
|
4626
|
+
out.push(reason);
|
|
4627
|
+
}
|
|
4628
|
+
return out;
|
|
4629
|
+
}
|
|
4455
4630
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
4456
4631
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
4457
4632
|
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
@@ -4500,7 +4675,7 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4500
4675
|
}
|
|
4501
4676
|
}
|
|
4502
4677
|
}
|
|
4503
|
-
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } =
|
|
4678
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, matchesFernignore);
|
|
4504
4679
|
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
4505
4680
|
if (unchanged) {
|
|
4506
4681
|
return {
|
|
@@ -4535,7 +4710,38 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4535
4710
|
strippedPaths
|
|
4536
4711
|
};
|
|
4537
4712
|
}
|
|
4538
|
-
function
|
|
4713
|
+
function isInfrastructureFile(filePath) {
|
|
4714
|
+
return filePath === ".fernignore" || filePath.startsWith(".fern/");
|
|
4715
|
+
}
|
|
4716
|
+
function computeInfrastructureFileCleanse(patch, computeContentHash2) {
|
|
4717
|
+
const { stripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, isInfrastructureFile);
|
|
4718
|
+
if (!stripped) {
|
|
4719
|
+
return {
|
|
4720
|
+
unchanged: true,
|
|
4721
|
+
remove: false,
|
|
4722
|
+
patch_content: patch.patch_content,
|
|
4723
|
+
content_hash: patch.content_hash,
|
|
4724
|
+
strippedPaths: []
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
if (newPatchContent.trim() === "" || patch.files.length === 0) {
|
|
4728
|
+
return {
|
|
4729
|
+
unchanged: false,
|
|
4730
|
+
remove: true,
|
|
4731
|
+
patch_content: "",
|
|
4732
|
+
content_hash: "",
|
|
4733
|
+
strippedPaths: strippedSectionPaths
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
return {
|
|
4737
|
+
unchanged: false,
|
|
4738
|
+
remove: false,
|
|
4739
|
+
patch_content: newPatchContent,
|
|
4740
|
+
content_hash: computeContentHash2(newPatchContent),
|
|
4741
|
+
strippedPaths: strippedSectionPaths
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
function stripDiffSectionsByPredicate(patchContent, shouldStrip) {
|
|
4539
4745
|
const lines = patchContent.split("\n");
|
|
4540
4746
|
const sectionStarts = [];
|
|
4541
4747
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4557,7 +4763,7 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4557
4763
|
const section = sectionStarts[i];
|
|
4558
4764
|
const next = sectionStarts[i + 1];
|
|
4559
4765
|
const endIdx = next ? next.idx : lines.length;
|
|
4560
|
-
if (section.path !== null &&
|
|
4766
|
+
if (section.path !== null && shouldStrip(section.path)) {
|
|
4561
4767
|
stripped = true;
|
|
4562
4768
|
strippedSectionPaths.push(section.path);
|
|
4563
4769
|
continue;
|
|
@@ -4569,8 +4775,8 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4569
4775
|
|
|
4570
4776
|
// src/FernignoreMigrator.ts
|
|
4571
4777
|
var import_node_crypto2 = require("crypto");
|
|
4572
|
-
var
|
|
4573
|
-
var
|
|
4778
|
+
var import_node_fs4 = require("fs");
|
|
4779
|
+
var import_node_path10 = require("path");
|
|
4574
4780
|
var import_minimatch6 = require("minimatch");
|
|
4575
4781
|
var import_yaml2 = require("yaml");
|
|
4576
4782
|
var FernignoreMigrator = class {
|
|
@@ -4583,14 +4789,14 @@ var FernignoreMigrator = class {
|
|
|
4583
4789
|
this.outputDir = outputDir;
|
|
4584
4790
|
}
|
|
4585
4791
|
fernignoreExists() {
|
|
4586
|
-
return (0,
|
|
4792
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path10.join)(this.outputDir, ".fernignore"));
|
|
4587
4793
|
}
|
|
4588
4794
|
readFernignorePatterns() {
|
|
4589
|
-
const fernignorePath = (0,
|
|
4590
|
-
if (!(0,
|
|
4795
|
+
const fernignorePath = (0, import_node_path10.join)(this.outputDir, ".fernignore");
|
|
4796
|
+
if (!(0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
4591
4797
|
return [];
|
|
4592
4798
|
}
|
|
4593
|
-
const content = (0,
|
|
4799
|
+
const content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
4594
4800
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
4595
4801
|
}
|
|
4596
4802
|
/** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
|
|
@@ -4695,16 +4901,16 @@ var FernignoreMigrator = class {
|
|
|
4695
4901
|
return results;
|
|
4696
4902
|
}
|
|
4697
4903
|
readFileContent(filePath) {
|
|
4698
|
-
const fullPath = (0,
|
|
4699
|
-
if (!(0,
|
|
4904
|
+
const fullPath = (0, import_node_path10.join)(this.outputDir, filePath);
|
|
4905
|
+
if (!(0, import_node_fs4.existsSync)(fullPath)) {
|
|
4700
4906
|
return null;
|
|
4701
4907
|
}
|
|
4702
4908
|
try {
|
|
4703
|
-
const stat = (0,
|
|
4909
|
+
const stat = (0, import_node_fs4.statSync)(fullPath);
|
|
4704
4910
|
if (stat.isDirectory()) {
|
|
4705
4911
|
return null;
|
|
4706
4912
|
}
|
|
4707
|
-
return (0,
|
|
4913
|
+
return (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
4708
4914
|
} catch {
|
|
4709
4915
|
return null;
|
|
4710
4916
|
}
|
|
@@ -4760,27 +4966,27 @@ var FernignoreMigrator = class {
|
|
|
4760
4966
|
};
|
|
4761
4967
|
}
|
|
4762
4968
|
movePatternsToReplayYml(patterns) {
|
|
4763
|
-
const replayYmlPath = (0,
|
|
4969
|
+
const replayYmlPath = (0, import_node_path10.join)(this.outputDir, ".fern", "replay.yml");
|
|
4764
4970
|
let config = {};
|
|
4765
|
-
if ((0,
|
|
4766
|
-
const content = (0,
|
|
4971
|
+
if ((0, import_node_fs4.existsSync)(replayYmlPath)) {
|
|
4972
|
+
const content = (0, import_node_fs4.readFileSync)(replayYmlPath, "utf-8");
|
|
4767
4973
|
config = (0, import_yaml2.parse)(content) ?? {};
|
|
4768
4974
|
}
|
|
4769
4975
|
const existing = config.exclude ?? [];
|
|
4770
4976
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
4771
4977
|
config.exclude = merged;
|
|
4772
|
-
const dir = (0,
|
|
4773
|
-
if (!(0,
|
|
4774
|
-
(0,
|
|
4978
|
+
const dir = (0, import_node_path10.dirname)(replayYmlPath);
|
|
4979
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
4980
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
4775
4981
|
}
|
|
4776
|
-
(0,
|
|
4982
|
+
(0, import_node_fs4.writeFileSync)(replayYmlPath, (0, import_yaml2.stringify)(config, { lineWidth: 0 }), "utf-8");
|
|
4777
4983
|
}
|
|
4778
4984
|
};
|
|
4779
4985
|
|
|
4780
4986
|
// src/commands/bootstrap.ts
|
|
4781
4987
|
var import_node_crypto3 = require("crypto");
|
|
4782
|
-
var
|
|
4783
|
-
var
|
|
4988
|
+
var import_node_fs5 = require("fs");
|
|
4989
|
+
var import_node_path11 = require("path");
|
|
4784
4990
|
init_GitClient();
|
|
4785
4991
|
async function bootstrap(outputDir, options) {
|
|
4786
4992
|
const git = new GitClient(outputDir);
|
|
@@ -4947,10 +5153,10 @@ function parseGitLog(log) {
|
|
|
4947
5153
|
}
|
|
4948
5154
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
4949
5155
|
function ensureFernignoreEntries(outputDir) {
|
|
4950
|
-
const fernignorePath = (0,
|
|
5156
|
+
const fernignorePath = (0, import_node_path11.join)(outputDir, ".fernignore");
|
|
4951
5157
|
let content = "";
|
|
4952
|
-
if ((0,
|
|
4953
|
-
content = (0,
|
|
5158
|
+
if ((0, import_node_fs5.existsSync)(fernignorePath)) {
|
|
5159
|
+
content = (0, import_node_fs5.readFileSync)(fernignorePath, "utf-8");
|
|
4954
5160
|
}
|
|
4955
5161
|
const lines = content.split("\n");
|
|
4956
5162
|
const toAdd = [];
|
|
@@ -4966,15 +5172,15 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
4966
5172
|
content += "\n";
|
|
4967
5173
|
}
|
|
4968
5174
|
content += toAdd.join("\n") + "\n";
|
|
4969
|
-
(0,
|
|
5175
|
+
(0, import_node_fs5.writeFileSync)(fernignorePath, content, "utf-8");
|
|
4970
5176
|
return true;
|
|
4971
5177
|
}
|
|
4972
5178
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
4973
5179
|
function ensureGitattributesEntries(outputDir) {
|
|
4974
|
-
const gitattributesPath = (0,
|
|
5180
|
+
const gitattributesPath = (0, import_node_path11.join)(outputDir, ".gitattributes");
|
|
4975
5181
|
let content = "";
|
|
4976
|
-
if ((0,
|
|
4977
|
-
content = (0,
|
|
5182
|
+
if ((0, import_node_fs5.existsSync)(gitattributesPath)) {
|
|
5183
|
+
content = (0, import_node_fs5.readFileSync)(gitattributesPath, "utf-8");
|
|
4978
5184
|
}
|
|
4979
5185
|
const lines = content.split("\n");
|
|
4980
5186
|
const toAdd = [];
|
|
@@ -4990,7 +5196,7 @@ function ensureGitattributesEntries(outputDir) {
|
|
|
4990
5196
|
content += "\n";
|
|
4991
5197
|
}
|
|
4992
5198
|
content += toAdd.join("\n") + "\n";
|
|
4993
|
-
(0,
|
|
5199
|
+
(0, import_node_fs5.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
4994
5200
|
}
|
|
4995
5201
|
function computeContentHash(patchContent) {
|
|
4996
5202
|
const lines = patchContent.split("\n");
|
|
@@ -5002,6 +5208,7 @@ function computeContentHash(patchContent) {
|
|
|
5002
5208
|
|
|
5003
5209
|
// src/commands/forget.ts
|
|
5004
5210
|
var import_minimatch7 = require("minimatch");
|
|
5211
|
+
init_GitClient();
|
|
5005
5212
|
function parseDiffStat(patchContent) {
|
|
5006
5213
|
let additions = 0;
|
|
5007
5214
|
let deletions = 0;
|
|
@@ -5060,7 +5267,28 @@ var EMPTY_RESULT = {
|
|
|
5060
5267
|
totalPatches: 0,
|
|
5061
5268
|
warnings: []
|
|
5062
5269
|
};
|
|
5063
|
-
function
|
|
5270
|
+
async function dismissPatches(outputDir, lockManager, patches) {
|
|
5271
|
+
const git = new GitClient(outputDir);
|
|
5272
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
5273
|
+
for (const patch of patches) {
|
|
5274
|
+
const hashes = [patch.content_hash];
|
|
5275
|
+
if (patch.original_commit) {
|
|
5276
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
5277
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
5278
|
+
hashes.push(materialized.contentHash);
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
lockManager.addDismissal({
|
|
5282
|
+
patch_id: patch.id,
|
|
5283
|
+
original_commit: patch.original_commit,
|
|
5284
|
+
original_message: patch.original_message,
|
|
5285
|
+
content_hashes: hashes,
|
|
5286
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5287
|
+
via: "forget"
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
async function forget(outputDir, options) {
|
|
5064
5292
|
const lockManager = new LockfileManager(outputDir);
|
|
5065
5293
|
if (!lockManager.exists()) {
|
|
5066
5294
|
return { ...EMPTY_RESULT };
|
|
@@ -5071,9 +5299,7 @@ function forget(outputDir, options) {
|
|
|
5071
5299
|
const removed = lock.patches.map(toMatchedPatch);
|
|
5072
5300
|
const warnings = buildWarnings(lock.patches);
|
|
5073
5301
|
if (!options.dryRun) {
|
|
5074
|
-
|
|
5075
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5076
|
-
}
|
|
5302
|
+
await dismissPatches(outputDir, lockManager, lock.patches);
|
|
5077
5303
|
lockManager.clearPatches();
|
|
5078
5304
|
lockManager.save();
|
|
5079
5305
|
}
|
|
@@ -5102,8 +5328,8 @@ function forget(outputDir, options) {
|
|
|
5102
5328
|
}
|
|
5103
5329
|
const warnings = buildWarnings(patchesToRemove);
|
|
5104
5330
|
if (!options.dryRun) {
|
|
5331
|
+
await dismissPatches(outputDir, lockManager, patchesToRemove);
|
|
5105
5332
|
for (const patch of patchesToRemove) {
|
|
5106
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5107
5333
|
lockManager.removePatch(patch.id);
|
|
5108
5334
|
}
|
|
5109
5335
|
lockManager.save();
|
|
@@ -5144,7 +5370,7 @@ function forget(outputDir, options) {
|
|
|
5144
5370
|
}
|
|
5145
5371
|
|
|
5146
5372
|
// src/commands/reset.ts
|
|
5147
|
-
var
|
|
5373
|
+
var import_node_fs6 = require("fs");
|
|
5148
5374
|
function reset(outputDir, options) {
|
|
5149
5375
|
const lockManager = new LockfileManager(outputDir);
|
|
5150
5376
|
if (!lockManager.exists()) {
|
|
@@ -5158,7 +5384,7 @@ function reset(outputDir, options) {
|
|
|
5158
5384
|
const lock = lockManager.read();
|
|
5159
5385
|
const patchCount = lock.patches.length;
|
|
5160
5386
|
if (!options?.dryRun) {
|
|
5161
|
-
(0,
|
|
5387
|
+
(0, import_node_fs6.unlinkSync)(lockManager.lockfilePath);
|
|
5162
5388
|
}
|
|
5163
5389
|
return {
|
|
5164
5390
|
success: true,
|
|
@@ -5170,7 +5396,7 @@ function reset(outputDir, options) {
|
|
|
5170
5396
|
|
|
5171
5397
|
// src/commands/resolve.ts
|
|
5172
5398
|
var import_promises7 = require("fs/promises");
|
|
5173
|
-
var
|
|
5399
|
+
var import_node_path12 = require("path");
|
|
5174
5400
|
init_GitClient();
|
|
5175
5401
|
async function resolve(outputDir, options) {
|
|
5176
5402
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -5211,15 +5437,16 @@ async function resolve(outputDir, options) {
|
|
|
5211
5437
|
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
5212
5438
|
if (patchesToCommit.length > 0) {
|
|
5213
5439
|
const currentGen = lock.current_generation;
|
|
5214
|
-
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
5440
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
5215
5441
|
const warnings = [];
|
|
5442
|
+
const patchesDismissed = [];
|
|
5216
5443
|
let patchesResolved = 0;
|
|
5217
5444
|
const pass1 = [];
|
|
5218
5445
|
for (const patch of patchesToCommit) {
|
|
5219
5446
|
const result = await computePerPatchDiffWithFallback(
|
|
5220
5447
|
patch,
|
|
5221
5448
|
(f) => git.showFile(currentGen, f),
|
|
5222
|
-
(f) => (0, import_promises7.readFile)((0,
|
|
5449
|
+
(f) => (0, import_promises7.readFile)((0, import_node_path12.join)(outputDir, f), "utf-8").catch(() => null),
|
|
5223
5450
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
5224
5451
|
);
|
|
5225
5452
|
if (result === null) {
|
|
@@ -5253,7 +5480,27 @@ async function resolve(outputDir, options) {
|
|
|
5253
5480
|
const r = pass1[i];
|
|
5254
5481
|
if (r.kind === "skip") continue;
|
|
5255
5482
|
if (r.kind === "revert") {
|
|
5483
|
+
const hashes = [patch.content_hash];
|
|
5484
|
+
if (patch.original_commit) {
|
|
5485
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
5486
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
5487
|
+
hashes.push(materialized.contentHash);
|
|
5488
|
+
} else if (materialized.status === "unreachable") {
|
|
5489
|
+
warnings.push(
|
|
5490
|
+
`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.`
|
|
5491
|
+
);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
lockManager.addDismissal({
|
|
5495
|
+
patch_id: patch.id,
|
|
5496
|
+
original_commit: patch.original_commit,
|
|
5497
|
+
original_message: patch.original_message,
|
|
5498
|
+
content_hashes: hashes,
|
|
5499
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5500
|
+
via: "resolve"
|
|
5501
|
+
});
|
|
5256
5502
|
lockManager.removePatch(patch.id);
|
|
5503
|
+
patchesDismissed.push({ id: patch.id, message: patch.original_message });
|
|
5257
5504
|
continue;
|
|
5258
5505
|
}
|
|
5259
5506
|
if (lastIndexByHash.get(r.hash) !== i) {
|
|
@@ -5275,7 +5522,7 @@ async function resolve(outputDir, options) {
|
|
|
5275
5522
|
const theirsSnapshot = {};
|
|
5276
5523
|
for (const file of filesForSnapshot) {
|
|
5277
5524
|
if (isBinaryFile(file)) continue;
|
|
5278
|
-
const content = await (0, import_promises7.readFile)((0,
|
|
5525
|
+
const content = await (0, import_promises7.readFile)((0, import_node_path12.join)(outputDir, file), "utf-8").catch(() => null);
|
|
5279
5526
|
if (content != null) {
|
|
5280
5527
|
theirsSnapshot[file] = content;
|
|
5281
5528
|
}
|
|
@@ -5302,6 +5549,7 @@ async function resolve(outputDir, options) {
|
|
|
5302
5549
|
commitSha: commitSha2,
|
|
5303
5550
|
phase: "committed",
|
|
5304
5551
|
patchesResolved,
|
|
5552
|
+
patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
|
|
5305
5553
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
5306
5554
|
};
|
|
5307
5555
|
}
|
|
@@ -5349,7 +5597,9 @@ function status(outputDir) {
|
|
|
5349
5597
|
lastGeneration: void 0,
|
|
5350
5598
|
patches: [],
|
|
5351
5599
|
unresolvedCount: 0,
|
|
5352
|
-
excludePatterns: []
|
|
5600
|
+
excludePatterns: [],
|
|
5601
|
+
dismissed: [],
|
|
5602
|
+
legacyDismissedCount: 0
|
|
5353
5603
|
};
|
|
5354
5604
|
}
|
|
5355
5605
|
const lock = lockManager.read();
|
|
@@ -5378,13 +5628,24 @@ function status(outputDir) {
|
|
|
5378
5628
|
}
|
|
5379
5629
|
const config = lockManager.getCustomizationsConfig();
|
|
5380
5630
|
const excludePatterns = config.exclude ?? [];
|
|
5631
|
+
const dismissals = lock.dismissals ?? [];
|
|
5632
|
+
const dismissed = dismissals.map((d) => ({
|
|
5633
|
+
message: d.original_message,
|
|
5634
|
+
sha: d.original_commit.slice(0, 7),
|
|
5635
|
+
via: d.via,
|
|
5636
|
+
dismissedAt: d.dismissed_at
|
|
5637
|
+
}));
|
|
5638
|
+
const coveredHashes = new Set(dismissals.flatMap((d) => d.content_hashes));
|
|
5639
|
+
const legacyDismissedCount = (lock.forgotten_hashes ?? []).filter((h) => !coveredHashes.has(h)).length;
|
|
5381
5640
|
return {
|
|
5382
5641
|
initialized: true,
|
|
5383
5642
|
generationCount: lock.generations.length,
|
|
5384
5643
|
lastGeneration,
|
|
5385
5644
|
patches,
|
|
5386
5645
|
unresolvedCount,
|
|
5387
|
-
excludePatterns
|
|
5646
|
+
excludePatterns,
|
|
5647
|
+
dismissed,
|
|
5648
|
+
legacyDismissedCount
|
|
5388
5649
|
};
|
|
5389
5650
|
}
|
|
5390
5651
|
// Annotate the CommonJS export names for ESM import in node:
|