@fern-api/replay 0.17.4 → 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 +436 -116
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +381 -107
- 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 +364 -90
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -62,6 +62,11 @@ var init_GitClient = __esm({
|
|
|
62
62
|
proc.stderr.on("data", (data) => {
|
|
63
63
|
stderr += data.toString();
|
|
64
64
|
});
|
|
65
|
+
proc.on("error", (err) => {
|
|
66
|
+
reject(new Error(`git ${args.join(" ")} failed to spawn: ${err.message}`));
|
|
67
|
+
});
|
|
68
|
+
proc.stdin.on("error", () => {
|
|
69
|
+
});
|
|
65
70
|
proc.on("close", (code) => {
|
|
66
71
|
if (code === 0) {
|
|
67
72
|
resolve2(stdout);
|
|
@@ -425,15 +430,15 @@ __export(PatchApplyTheirs_exports, {
|
|
|
425
430
|
async function applyPatchToContent(base, patchContent, filePath) {
|
|
426
431
|
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
427
432
|
if (!fileDiff) return null;
|
|
428
|
-
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-"));
|
|
429
434
|
const tempGit = new GitClient(tempDir);
|
|
430
435
|
try {
|
|
431
436
|
await tempGit.exec(["init"]);
|
|
432
437
|
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
433
438
|
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
434
439
|
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
435
|
-
const tempFilePath = (0,
|
|
436
|
-
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 });
|
|
437
442
|
await (0, import_promises5.writeFile)(tempFilePath, base);
|
|
438
443
|
await tempGit.exec(["add", filePath]);
|
|
439
444
|
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
@@ -464,13 +469,13 @@ function extractFileDiff(patchContent, filePath) {
|
|
|
464
469
|
}
|
|
465
470
|
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
466
471
|
}
|
|
467
|
-
var import_promises5, import_node_os3,
|
|
472
|
+
var import_promises5, import_node_os3, import_node_path8;
|
|
468
473
|
var init_PatchApplyTheirs = __esm({
|
|
469
474
|
"src/PatchApplyTheirs.ts"() {
|
|
470
475
|
"use strict";
|
|
471
476
|
import_promises5 = require("fs/promises");
|
|
472
477
|
import_node_os3 = require("os");
|
|
473
|
-
|
|
478
|
+
import_node_path8 = require("path");
|
|
474
479
|
init_GitClient();
|
|
475
480
|
}
|
|
476
481
|
});
|
|
@@ -745,6 +750,22 @@ var LockfileManager = class {
|
|
|
745
750
|
this.lock.forgotten_hashes.push(hash);
|
|
746
751
|
}
|
|
747
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
|
+
}
|
|
748
769
|
getUnresolvedPatches() {
|
|
749
770
|
this.ensureLoaded();
|
|
750
771
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -863,7 +884,8 @@ function isBinaryFile(filePath) {
|
|
|
863
884
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
864
885
|
function matchesFernignorePattern(filePath, patterns) {
|
|
865
886
|
if (patterns.length === 0) return false;
|
|
866
|
-
return patterns.some((
|
|
887
|
+
return patterns.some((rawPattern) => {
|
|
888
|
+
const p = rawPattern.endsWith("/") ? rawPattern.slice(0, -1) : rawPattern;
|
|
867
889
|
if (filePath === p) return true;
|
|
868
890
|
if ((0, import_minimatch2.minimatch)(filePath, p)) return true;
|
|
869
891
|
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
@@ -911,12 +933,14 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
911
933
|
return snapshot;
|
|
912
934
|
}
|
|
913
935
|
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
914
|
-
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/"));
|
|
915
939
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
916
940
|
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
917
941
|
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
918
942
|
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
919
|
-
return { files, sensitiveFiles, fernignoredFiles };
|
|
943
|
+
return { files, sensitiveFiles, fernignoredFiles, infrastructureFiles };
|
|
920
944
|
}
|
|
921
945
|
var ReplayDetector = class {
|
|
922
946
|
git;
|
|
@@ -924,6 +948,15 @@ var ReplayDetector = class {
|
|
|
924
948
|
sdkOutputDir;
|
|
925
949
|
fernignorePatterns;
|
|
926
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 = [];
|
|
927
960
|
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
928
961
|
this.git = git;
|
|
929
962
|
this.lockManager = lockManager;
|
|
@@ -994,6 +1027,10 @@ var ReplayDetector = class {
|
|
|
994
1027
|
this.warnings.push(
|
|
995
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.`
|
|
996
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
|
+
});
|
|
997
1034
|
}
|
|
998
1035
|
return { patches: [], revertedPatchIds: [] };
|
|
999
1036
|
}
|
|
@@ -1002,6 +1039,12 @@ var ReplayDetector = class {
|
|
|
1002
1039
|
this.warnings.push(
|
|
1003
1040
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
1004
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
|
+
}
|
|
1005
1048
|
return this.detectPatchesViaTreeDiff(
|
|
1006
1049
|
lastGen,
|
|
1007
1050
|
/* commitKnownMissing */
|
|
@@ -1041,48 +1084,28 @@ var ReplayDetector = class {
|
|
|
1041
1084
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
1042
1085
|
continue;
|
|
1043
1086
|
}
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
try {
|
|
1047
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
1048
|
-
} catch {
|
|
1087
|
+
const materialized = await this.materializeCommitPatch(commit.sha);
|
|
1088
|
+
if (materialized.sensitiveFiles.length > 0) {
|
|
1049
1089
|
this.warnings.push(
|
|
1050
|
-
`
|
|
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.`
|
|
1051
1091
|
);
|
|
1052
|
-
continue;
|
|
1053
1092
|
}
|
|
1054
|
-
|
|
1055
|
-
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1059
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1060
|
-
filesOutput,
|
|
1061
|
-
this.fernignorePatterns
|
|
1062
|
-
);
|
|
1063
|
-
if (sensitiveFiles.length > 0) {
|
|
1093
|
+
if (materialized.fernignoredFiles.length > 0) {
|
|
1064
1094
|
this.warnings.push(
|
|
1065
|
-
|
|
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.`
|
|
1066
1096
|
);
|
|
1067
1097
|
}
|
|
1068
|
-
if (
|
|
1098
|
+
if (materialized.status === "unreachable") {
|
|
1069
1099
|
this.warnings.push(
|
|
1070
|
-
|
|
1100
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
1071
1101
|
);
|
|
1102
|
+
continue;
|
|
1072
1103
|
}
|
|
1073
|
-
if (
|
|
1074
|
-
|
|
1075
|
-
if (parentSha) {
|
|
1076
|
-
try {
|
|
1077
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
1078
|
-
} catch {
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
if (!patchContent.trim()) continue;
|
|
1082
|
-
contentHash = this.computeContentHash(patchContent);
|
|
1083
|
-
}
|
|
1104
|
+
if (materialized.status === "empty") {
|
|
1105
|
+
continue;
|
|
1084
1106
|
}
|
|
1085
|
-
|
|
1107
|
+
const { files, patchContent, contentHash } = materialized;
|
|
1108
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
1086
1109
|
continue;
|
|
1087
1110
|
}
|
|
1088
1111
|
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
@@ -1158,6 +1181,63 @@ var ReplayDetector = class {
|
|
|
1158
1181
|
const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
1159
1182
|
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
1160
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
|
+
}
|
|
1161
1241
|
/**
|
|
1162
1242
|
* Compute content hash for deduplication.
|
|
1163
1243
|
*
|
|
@@ -1262,6 +1342,13 @@ var ReplayDetector = class {
|
|
|
1262
1342
|
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
1263
1343
|
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
1264
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
|
+
});
|
|
1265
1352
|
return this.detectPatchesViaCommitScan();
|
|
1266
1353
|
}
|
|
1267
1354
|
const lockForGuard = this.lockManager.read();
|
|
@@ -1374,10 +1461,7 @@ var ReplayDetector = class {
|
|
|
1374
1461
|
continue;
|
|
1375
1462
|
}
|
|
1376
1463
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1377
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1378
|
-
filesOutput,
|
|
1379
|
-
this.fernignorePatterns
|
|
1380
|
-
);
|
|
1464
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
1381
1465
|
if (sensitiveFiles.length > 0) {
|
|
1382
1466
|
this.warnings.push(
|
|
1383
1467
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
@@ -1388,7 +1472,8 @@ var ReplayDetector = class {
|
|
|
1388
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.`
|
|
1389
1473
|
);
|
|
1390
1474
|
}
|
|
1391
|
-
|
|
1475
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
1476
|
+
if (wasFiltered && files.length > 0) {
|
|
1392
1477
|
if (parents.length === 0) {
|
|
1393
1478
|
continue;
|
|
1394
1479
|
}
|
|
@@ -2658,16 +2743,25 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2658
2743
|
};
|
|
2659
2744
|
|
|
2660
2745
|
// src/ReplayService.ts
|
|
2661
|
-
var
|
|
2746
|
+
var import_node_fs3 = require("fs");
|
|
2662
2747
|
var import_promises6 = require("fs/promises");
|
|
2663
|
-
var
|
|
2748
|
+
var import_node_path9 = require("path");
|
|
2664
2749
|
var import_minimatch5 = require("minimatch");
|
|
2665
2750
|
init_GitClient();
|
|
2666
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
|
+
|
|
2667
2761
|
// src/PatchRegionDiff.ts
|
|
2668
2762
|
var import_promises4 = require("fs/promises");
|
|
2669
2763
|
var import_node_os2 = require("os");
|
|
2670
|
-
var
|
|
2764
|
+
var import_node_path7 = require("path");
|
|
2671
2765
|
var import_node_child_process = require("child_process");
|
|
2672
2766
|
init_HybridReconstruction();
|
|
2673
2767
|
function normalizeHunkBody(hunk) {
|
|
@@ -2885,10 +2979,10 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
2885
2979
|
}
|
|
2886
2980
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
2887
2981
|
if (beforeContent === afterContent) return "";
|
|
2888
|
-
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-"));
|
|
2889
2983
|
try {
|
|
2890
|
-
const beforePath = (0,
|
|
2891
|
-
const afterPath = (0,
|
|
2984
|
+
const beforePath = (0, import_node_path7.join)(tmp, "before");
|
|
2985
|
+
const afterPath = (0, import_node_path7.join)(tmp, "after");
|
|
2892
2986
|
await (0, import_promises4.writeFile)(beforePath, beforeContent, "utf-8");
|
|
2893
2987
|
await (0, import_promises4.writeFile)(afterPath, afterContent, "utf-8");
|
|
2894
2988
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
@@ -3064,6 +3158,7 @@ var FirstGenerationFlow = class {
|
|
|
3064
3158
|
patchesToApply: [],
|
|
3065
3159
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3066
3160
|
detectorWarnings: [],
|
|
3161
|
+
detectorDegraded: [],
|
|
3067
3162
|
revertedCount: 0,
|
|
3068
3163
|
genSha: genRecord.commit_sha,
|
|
3069
3164
|
optionsSnapshot: options
|
|
@@ -3123,6 +3218,7 @@ var SkipApplicationFlow = class {
|
|
|
3123
3218
|
patchesToApply: [],
|
|
3124
3219
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
3125
3220
|
detectorWarnings: [],
|
|
3221
|
+
detectorDegraded: [],
|
|
3126
3222
|
revertedCount: 0,
|
|
3127
3223
|
genSha: genRecord.commit_sha,
|
|
3128
3224
|
optionsSnapshot: options
|
|
@@ -3160,8 +3256,10 @@ var NoPatchesFlow = class {
|
|
|
3160
3256
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
3161
3257
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3162
3258
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3259
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3163
3260
|
if (options?.dryRun) {
|
|
3164
3261
|
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
3262
|
+
const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
|
|
3165
3263
|
const preparedReport = {
|
|
3166
3264
|
flow: "no-patches",
|
|
3167
3265
|
patchesDetected: newPatches.length,
|
|
@@ -3171,7 +3269,9 @@ var NoPatchesFlow = class {
|
|
|
3171
3269
|
patchesReverted: revertedPatchIds.length,
|
|
3172
3270
|
conflicts: [],
|
|
3173
3271
|
wouldApply: newPatches,
|
|
3174
|
-
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
|
|
3175
3275
|
};
|
|
3176
3276
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3177
3277
|
return {
|
|
@@ -3225,6 +3325,7 @@ var NoPatchesFlow = class {
|
|
|
3225
3325
|
patchesToApply: newPatches,
|
|
3226
3326
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3227
3327
|
detectorWarnings,
|
|
3328
|
+
detectorDegraded,
|
|
3228
3329
|
revertedCount: revertedPatchIds.length,
|
|
3229
3330
|
genSha: genRecord.commit_sha,
|
|
3230
3331
|
optionsSnapshot: options
|
|
@@ -3268,6 +3369,7 @@ var NoPatchesFlow = class {
|
|
|
3268
3369
|
}
|
|
3269
3370
|
}
|
|
3270
3371
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3372
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3271
3373
|
return this.service.buildReport(
|
|
3272
3374
|
"no-patches",
|
|
3273
3375
|
newPatches,
|
|
@@ -3276,7 +3378,8 @@ var NoPatchesFlow = class {
|
|
|
3276
3378
|
warnings,
|
|
3277
3379
|
rebaseCounts,
|
|
3278
3380
|
void 0,
|
|
3279
|
-
prep._prepared.revertedCount
|
|
3381
|
+
prep._prepared.revertedCount,
|
|
3382
|
+
degradedReasons
|
|
3280
3383
|
);
|
|
3281
3384
|
}
|
|
3282
3385
|
};
|
|
@@ -3293,6 +3396,10 @@ var NormalRegenerationFlow = class {
|
|
|
3293
3396
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
3294
3397
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
3295
3398
|
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
3399
|
+
const dryRunDegraded = dedupeDegradedReasons([
|
|
3400
|
+
...this.service.detector.degradedReasons,
|
|
3401
|
+
...this.service.degradedReasons
|
|
3402
|
+
]);
|
|
3296
3403
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
3297
3404
|
const preparedReport = {
|
|
3298
3405
|
flow: "normal-regeneration",
|
|
@@ -3303,7 +3410,9 @@ var NormalRegenerationFlow = class {
|
|
|
3303
3410
|
patchesReverted: dryRunReverted.length,
|
|
3304
3411
|
conflicts: [],
|
|
3305
3412
|
wouldApply: allPatches2,
|
|
3306
|
-
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
|
|
3307
3416
|
};
|
|
3308
3417
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
3309
3418
|
return {
|
|
@@ -3342,6 +3451,7 @@ var NormalRegenerationFlow = class {
|
|
|
3342
3451
|
existingPatches = this.service.lockManager.getPatches();
|
|
3343
3452
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
3344
3453
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
3454
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
3345
3455
|
if (removedByPreRebase.length > 0) {
|
|
3346
3456
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
3347
3457
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
@@ -3382,6 +3492,7 @@ var NormalRegenerationFlow = class {
|
|
|
3382
3492
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
3383
3493
|
preRebaseCounts,
|
|
3384
3494
|
detectorWarnings,
|
|
3495
|
+
detectorDegraded,
|
|
3385
3496
|
revertedCount: revertedPatchIds.length,
|
|
3386
3497
|
genSha: genRecord.commit_sha,
|
|
3387
3498
|
optionsSnapshot: options
|
|
@@ -3430,6 +3541,7 @@ var NormalRegenerationFlow = class {
|
|
|
3430
3541
|
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
3431
3542
|
}
|
|
3432
3543
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
3544
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
3433
3545
|
return this.service.buildReport(
|
|
3434
3546
|
"normal-regeneration",
|
|
3435
3547
|
allPatches,
|
|
@@ -3438,7 +3550,8 @@ var NormalRegenerationFlow = class {
|
|
|
3438
3550
|
warnings,
|
|
3439
3551
|
rebaseCounts,
|
|
3440
3552
|
prep._prepared.preRebaseCounts,
|
|
3441
|
-
prep._prepared.revertedCount
|
|
3553
|
+
prep._prepared.revertedCount,
|
|
3554
|
+
degradedReasons
|
|
3442
3555
|
);
|
|
3443
3556
|
}
|
|
3444
3557
|
};
|
|
@@ -3478,6 +3591,16 @@ var ReplayService = class {
|
|
|
3478
3591
|
* @internal Used by Flow implementations in `src/flows/`.
|
|
3479
3592
|
*/
|
|
3480
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 = [];
|
|
3481
3604
|
/**
|
|
3482
3605
|
* @internal Test-only accessor.
|
|
3483
3606
|
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
@@ -3521,7 +3644,10 @@ var ReplayService = class {
|
|
|
3521
3644
|
async prepareReplay(options) {
|
|
3522
3645
|
this.warnings = [];
|
|
3523
3646
|
this.detector.warnings.length = 0;
|
|
3647
|
+
this.degradedReasons = [];
|
|
3648
|
+
this.detector.degradedReasons.length = 0;
|
|
3524
3649
|
this.cleanseLegacyFernignoreEntries();
|
|
3650
|
+
this.cleanseInfrastructureFileEntries();
|
|
3525
3651
|
return this.selectFlow(options).prepare(options);
|
|
3526
3652
|
}
|
|
3527
3653
|
/**
|
|
@@ -3575,6 +3701,50 @@ var ReplayService = class {
|
|
|
3575
3701
|
this.lockManager.save();
|
|
3576
3702
|
}
|
|
3577
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
|
+
}
|
|
3578
3748
|
/**
|
|
3579
3749
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3580
3750
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3830,7 +4000,7 @@ var ReplayService = class {
|
|
|
3830
4000
|
for (const file of files) {
|
|
3831
4001
|
if (isBinaryFile(file)) continue;
|
|
3832
4002
|
try {
|
|
3833
|
-
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");
|
|
3834
4004
|
snapshot[file] = content;
|
|
3835
4005
|
} catch {
|
|
3836
4006
|
}
|
|
@@ -4347,6 +4517,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4347
4517
|
);
|
|
4348
4518
|
}
|
|
4349
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
|
+
}
|
|
4350
4526
|
}
|
|
4351
4527
|
/**
|
|
4352
4528
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
@@ -4358,11 +4534,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4358
4534
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
4359
4535
|
for (const fileResult of result.fileResults) {
|
|
4360
4536
|
if (fileResult.status !== "conflict") continue;
|
|
4361
|
-
const filePath = (0,
|
|
4537
|
+
const filePath = (0, import_node_path9.join)(this.outputDir, fileResult.file);
|
|
4362
4538
|
try {
|
|
4363
|
-
const content = (0,
|
|
4539
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
4364
4540
|
const stripped = stripConflictMarkers(content);
|
|
4365
|
-
(0,
|
|
4541
|
+
(0, import_node_fs3.writeFileSync)(filePath, stripped);
|
|
4366
4542
|
} catch {
|
|
4367
4543
|
}
|
|
4368
4544
|
}
|
|
@@ -4390,12 +4566,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4390
4566
|
}
|
|
4391
4567
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4392
4568
|
readFernignorePatterns() {
|
|
4393
|
-
|
|
4394
|
-
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
4395
|
-
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);
|
|
4396
4570
|
}
|
|
4397
4571
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4398
|
-
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 ?? []);
|
|
4399
4574
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
4400
4575
|
const conflictDetails = conflictResults.map((r) => {
|
|
4401
4576
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -4435,10 +4610,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4435
4610
|
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
4436
4611
|
})) : void 0,
|
|
4437
4612
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
4438
|
-
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
|
|
4439
4616
|
};
|
|
4440
4617
|
}
|
|
4441
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
|
+
}
|
|
4442
4630
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
4443
4631
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
4444
4632
|
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
@@ -4487,7 +4675,7 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4487
4675
|
}
|
|
4488
4676
|
}
|
|
4489
4677
|
}
|
|
4490
|
-
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } =
|
|
4678
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, matchesFernignore);
|
|
4491
4679
|
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
4492
4680
|
if (unchanged) {
|
|
4493
4681
|
return {
|
|
@@ -4522,7 +4710,38 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
4522
4710
|
strippedPaths
|
|
4523
4711
|
};
|
|
4524
4712
|
}
|
|
4525
|
-
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) {
|
|
4526
4745
|
const lines = patchContent.split("\n");
|
|
4527
4746
|
const sectionStarts = [];
|
|
4528
4747
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4544,7 +4763,7 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4544
4763
|
const section = sectionStarts[i];
|
|
4545
4764
|
const next = sectionStarts[i + 1];
|
|
4546
4765
|
const endIdx = next ? next.idx : lines.length;
|
|
4547
|
-
if (section.path !== null &&
|
|
4766
|
+
if (section.path !== null && shouldStrip(section.path)) {
|
|
4548
4767
|
stripped = true;
|
|
4549
4768
|
strippedSectionPaths.push(section.path);
|
|
4550
4769
|
continue;
|
|
@@ -4556,8 +4775,8 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
4556
4775
|
|
|
4557
4776
|
// src/FernignoreMigrator.ts
|
|
4558
4777
|
var import_node_crypto2 = require("crypto");
|
|
4559
|
-
var
|
|
4560
|
-
var
|
|
4778
|
+
var import_node_fs4 = require("fs");
|
|
4779
|
+
var import_node_path10 = require("path");
|
|
4561
4780
|
var import_minimatch6 = require("minimatch");
|
|
4562
4781
|
var import_yaml2 = require("yaml");
|
|
4563
4782
|
var FernignoreMigrator = class {
|
|
@@ -4570,14 +4789,14 @@ var FernignoreMigrator = class {
|
|
|
4570
4789
|
this.outputDir = outputDir;
|
|
4571
4790
|
}
|
|
4572
4791
|
fernignoreExists() {
|
|
4573
|
-
return (0,
|
|
4792
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path10.join)(this.outputDir, ".fernignore"));
|
|
4574
4793
|
}
|
|
4575
4794
|
readFernignorePatterns() {
|
|
4576
|
-
const fernignorePath = (0,
|
|
4577
|
-
if (!(0,
|
|
4795
|
+
const fernignorePath = (0, import_node_path10.join)(this.outputDir, ".fernignore");
|
|
4796
|
+
if (!(0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
4578
4797
|
return [];
|
|
4579
4798
|
}
|
|
4580
|
-
const content = (0,
|
|
4799
|
+
const content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
4581
4800
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
4582
4801
|
}
|
|
4583
4802
|
/** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
|
|
@@ -4682,16 +4901,16 @@ var FernignoreMigrator = class {
|
|
|
4682
4901
|
return results;
|
|
4683
4902
|
}
|
|
4684
4903
|
readFileContent(filePath) {
|
|
4685
|
-
const fullPath = (0,
|
|
4686
|
-
if (!(0,
|
|
4904
|
+
const fullPath = (0, import_node_path10.join)(this.outputDir, filePath);
|
|
4905
|
+
if (!(0, import_node_fs4.existsSync)(fullPath)) {
|
|
4687
4906
|
return null;
|
|
4688
4907
|
}
|
|
4689
4908
|
try {
|
|
4690
|
-
const stat = (0,
|
|
4909
|
+
const stat = (0, import_node_fs4.statSync)(fullPath);
|
|
4691
4910
|
if (stat.isDirectory()) {
|
|
4692
4911
|
return null;
|
|
4693
4912
|
}
|
|
4694
|
-
return (0,
|
|
4913
|
+
return (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
4695
4914
|
} catch {
|
|
4696
4915
|
return null;
|
|
4697
4916
|
}
|
|
@@ -4747,27 +4966,27 @@ var FernignoreMigrator = class {
|
|
|
4747
4966
|
};
|
|
4748
4967
|
}
|
|
4749
4968
|
movePatternsToReplayYml(patterns) {
|
|
4750
|
-
const replayYmlPath = (0,
|
|
4969
|
+
const replayYmlPath = (0, import_node_path10.join)(this.outputDir, ".fern", "replay.yml");
|
|
4751
4970
|
let config = {};
|
|
4752
|
-
if ((0,
|
|
4753
|
-
const content = (0,
|
|
4971
|
+
if ((0, import_node_fs4.existsSync)(replayYmlPath)) {
|
|
4972
|
+
const content = (0, import_node_fs4.readFileSync)(replayYmlPath, "utf-8");
|
|
4754
4973
|
config = (0, import_yaml2.parse)(content) ?? {};
|
|
4755
4974
|
}
|
|
4756
4975
|
const existing = config.exclude ?? [];
|
|
4757
4976
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
4758
4977
|
config.exclude = merged;
|
|
4759
|
-
const dir = (0,
|
|
4760
|
-
if (!(0,
|
|
4761
|
-
(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 });
|
|
4762
4981
|
}
|
|
4763
|
-
(0,
|
|
4982
|
+
(0, import_node_fs4.writeFileSync)(replayYmlPath, (0, import_yaml2.stringify)(config, { lineWidth: 0 }), "utf-8");
|
|
4764
4983
|
}
|
|
4765
4984
|
};
|
|
4766
4985
|
|
|
4767
4986
|
// src/commands/bootstrap.ts
|
|
4768
4987
|
var import_node_crypto3 = require("crypto");
|
|
4769
|
-
var
|
|
4770
|
-
var
|
|
4988
|
+
var import_node_fs5 = require("fs");
|
|
4989
|
+
var import_node_path11 = require("path");
|
|
4771
4990
|
init_GitClient();
|
|
4772
4991
|
async function bootstrap(outputDir, options) {
|
|
4773
4992
|
const git = new GitClient(outputDir);
|
|
@@ -4934,10 +5153,10 @@ function parseGitLog(log) {
|
|
|
4934
5153
|
}
|
|
4935
5154
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
4936
5155
|
function ensureFernignoreEntries(outputDir) {
|
|
4937
|
-
const fernignorePath = (0,
|
|
5156
|
+
const fernignorePath = (0, import_node_path11.join)(outputDir, ".fernignore");
|
|
4938
5157
|
let content = "";
|
|
4939
|
-
if ((0,
|
|
4940
|
-
content = (0,
|
|
5158
|
+
if ((0, import_node_fs5.existsSync)(fernignorePath)) {
|
|
5159
|
+
content = (0, import_node_fs5.readFileSync)(fernignorePath, "utf-8");
|
|
4941
5160
|
}
|
|
4942
5161
|
const lines = content.split("\n");
|
|
4943
5162
|
const toAdd = [];
|
|
@@ -4953,15 +5172,15 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
4953
5172
|
content += "\n";
|
|
4954
5173
|
}
|
|
4955
5174
|
content += toAdd.join("\n") + "\n";
|
|
4956
|
-
(0,
|
|
5175
|
+
(0, import_node_fs5.writeFileSync)(fernignorePath, content, "utf-8");
|
|
4957
5176
|
return true;
|
|
4958
5177
|
}
|
|
4959
5178
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
4960
5179
|
function ensureGitattributesEntries(outputDir) {
|
|
4961
|
-
const gitattributesPath = (0,
|
|
5180
|
+
const gitattributesPath = (0, import_node_path11.join)(outputDir, ".gitattributes");
|
|
4962
5181
|
let content = "";
|
|
4963
|
-
if ((0,
|
|
4964
|
-
content = (0,
|
|
5182
|
+
if ((0, import_node_fs5.existsSync)(gitattributesPath)) {
|
|
5183
|
+
content = (0, import_node_fs5.readFileSync)(gitattributesPath, "utf-8");
|
|
4965
5184
|
}
|
|
4966
5185
|
const lines = content.split("\n");
|
|
4967
5186
|
const toAdd = [];
|
|
@@ -4977,7 +5196,7 @@ function ensureGitattributesEntries(outputDir) {
|
|
|
4977
5196
|
content += "\n";
|
|
4978
5197
|
}
|
|
4979
5198
|
content += toAdd.join("\n") + "\n";
|
|
4980
|
-
(0,
|
|
5199
|
+
(0, import_node_fs5.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
4981
5200
|
}
|
|
4982
5201
|
function computeContentHash(patchContent) {
|
|
4983
5202
|
const lines = patchContent.split("\n");
|
|
@@ -4989,6 +5208,7 @@ function computeContentHash(patchContent) {
|
|
|
4989
5208
|
|
|
4990
5209
|
// src/commands/forget.ts
|
|
4991
5210
|
var import_minimatch7 = require("minimatch");
|
|
5211
|
+
init_GitClient();
|
|
4992
5212
|
function parseDiffStat(patchContent) {
|
|
4993
5213
|
let additions = 0;
|
|
4994
5214
|
let deletions = 0;
|
|
@@ -5047,7 +5267,28 @@ var EMPTY_RESULT = {
|
|
|
5047
5267
|
totalPatches: 0,
|
|
5048
5268
|
warnings: []
|
|
5049
5269
|
};
|
|
5050
|
-
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) {
|
|
5051
5292
|
const lockManager = new LockfileManager(outputDir);
|
|
5052
5293
|
if (!lockManager.exists()) {
|
|
5053
5294
|
return { ...EMPTY_RESULT };
|
|
@@ -5058,9 +5299,7 @@ function forget(outputDir, options) {
|
|
|
5058
5299
|
const removed = lock.patches.map(toMatchedPatch);
|
|
5059
5300
|
const warnings = buildWarnings(lock.patches);
|
|
5060
5301
|
if (!options.dryRun) {
|
|
5061
|
-
|
|
5062
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5063
|
-
}
|
|
5302
|
+
await dismissPatches(outputDir, lockManager, lock.patches);
|
|
5064
5303
|
lockManager.clearPatches();
|
|
5065
5304
|
lockManager.save();
|
|
5066
5305
|
}
|
|
@@ -5089,8 +5328,8 @@ function forget(outputDir, options) {
|
|
|
5089
5328
|
}
|
|
5090
5329
|
const warnings = buildWarnings(patchesToRemove);
|
|
5091
5330
|
if (!options.dryRun) {
|
|
5331
|
+
await dismissPatches(outputDir, lockManager, patchesToRemove);
|
|
5092
5332
|
for (const patch of patchesToRemove) {
|
|
5093
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
5094
5333
|
lockManager.removePatch(patch.id);
|
|
5095
5334
|
}
|
|
5096
5335
|
lockManager.save();
|
|
@@ -5131,7 +5370,7 @@ function forget(outputDir, options) {
|
|
|
5131
5370
|
}
|
|
5132
5371
|
|
|
5133
5372
|
// src/commands/reset.ts
|
|
5134
|
-
var
|
|
5373
|
+
var import_node_fs6 = require("fs");
|
|
5135
5374
|
function reset(outputDir, options) {
|
|
5136
5375
|
const lockManager = new LockfileManager(outputDir);
|
|
5137
5376
|
if (!lockManager.exists()) {
|
|
@@ -5145,7 +5384,7 @@ function reset(outputDir, options) {
|
|
|
5145
5384
|
const lock = lockManager.read();
|
|
5146
5385
|
const patchCount = lock.patches.length;
|
|
5147
5386
|
if (!options?.dryRun) {
|
|
5148
|
-
(0,
|
|
5387
|
+
(0, import_node_fs6.unlinkSync)(lockManager.lockfilePath);
|
|
5149
5388
|
}
|
|
5150
5389
|
return {
|
|
5151
5390
|
success: true,
|
|
@@ -5157,7 +5396,7 @@ function reset(outputDir, options) {
|
|
|
5157
5396
|
|
|
5158
5397
|
// src/commands/resolve.ts
|
|
5159
5398
|
var import_promises7 = require("fs/promises");
|
|
5160
|
-
var
|
|
5399
|
+
var import_node_path12 = require("path");
|
|
5161
5400
|
init_GitClient();
|
|
5162
5401
|
async function resolve(outputDir, options) {
|
|
5163
5402
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -5198,15 +5437,16 @@ async function resolve(outputDir, options) {
|
|
|
5198
5437
|
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
5199
5438
|
if (patchesToCommit.length > 0) {
|
|
5200
5439
|
const currentGen = lock.current_generation;
|
|
5201
|
-
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
5440
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
5202
5441
|
const warnings = [];
|
|
5442
|
+
const patchesDismissed = [];
|
|
5203
5443
|
let patchesResolved = 0;
|
|
5204
5444
|
const pass1 = [];
|
|
5205
5445
|
for (const patch of patchesToCommit) {
|
|
5206
5446
|
const result = await computePerPatchDiffWithFallback(
|
|
5207
5447
|
patch,
|
|
5208
5448
|
(f) => git.showFile(currentGen, f),
|
|
5209
|
-
(f) => (0, import_promises7.readFile)((0,
|
|
5449
|
+
(f) => (0, import_promises7.readFile)((0, import_node_path12.join)(outputDir, f), "utf-8").catch(() => null),
|
|
5210
5450
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
5211
5451
|
);
|
|
5212
5452
|
if (result === null) {
|
|
@@ -5240,7 +5480,27 @@ async function resolve(outputDir, options) {
|
|
|
5240
5480
|
const r = pass1[i];
|
|
5241
5481
|
if (r.kind === "skip") continue;
|
|
5242
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
|
+
});
|
|
5243
5502
|
lockManager.removePatch(patch.id);
|
|
5503
|
+
patchesDismissed.push({ id: patch.id, message: patch.original_message });
|
|
5244
5504
|
continue;
|
|
5245
5505
|
}
|
|
5246
5506
|
if (lastIndexByHash.get(r.hash) !== i) {
|
|
@@ -5262,7 +5522,7 @@ async function resolve(outputDir, options) {
|
|
|
5262
5522
|
const theirsSnapshot = {};
|
|
5263
5523
|
for (const file of filesForSnapshot) {
|
|
5264
5524
|
if (isBinaryFile(file)) continue;
|
|
5265
|
-
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);
|
|
5266
5526
|
if (content != null) {
|
|
5267
5527
|
theirsSnapshot[file] = content;
|
|
5268
5528
|
}
|
|
@@ -5289,6 +5549,7 @@ async function resolve(outputDir, options) {
|
|
|
5289
5549
|
commitSha: commitSha2,
|
|
5290
5550
|
phase: "committed",
|
|
5291
5551
|
patchesResolved,
|
|
5552
|
+
patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
|
|
5292
5553
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
5293
5554
|
};
|
|
5294
5555
|
}
|
|
@@ -5336,7 +5597,9 @@ function status(outputDir) {
|
|
|
5336
5597
|
lastGeneration: void 0,
|
|
5337
5598
|
patches: [],
|
|
5338
5599
|
unresolvedCount: 0,
|
|
5339
|
-
excludePatterns: []
|
|
5600
|
+
excludePatterns: [],
|
|
5601
|
+
dismissed: [],
|
|
5602
|
+
legacyDismissedCount: 0
|
|
5340
5603
|
};
|
|
5341
5604
|
}
|
|
5342
5605
|
const lock = lockManager.read();
|
|
@@ -5365,13 +5628,24 @@ function status(outputDir) {
|
|
|
5365
5628
|
}
|
|
5366
5629
|
const config = lockManager.getCustomizationsConfig();
|
|
5367
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;
|
|
5368
5640
|
return {
|
|
5369
5641
|
initialized: true,
|
|
5370
5642
|
generationCount: lock.generations.length,
|
|
5371
5643
|
lastGeneration,
|
|
5372
5644
|
patches,
|
|
5373
5645
|
unresolvedCount,
|
|
5374
|
-
excludePatterns
|
|
5646
|
+
excludePatterns,
|
|
5647
|
+
dismissed,
|
|
5648
|
+
legacyDismissedCount
|
|
5375
5649
|
};
|
|
5376
5650
|
}
|
|
5377
5651
|
// Annotate the CommonJS export names for ESM import in node:
|