@fern-api/replay 0.9.1 → 0.10.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 +424 -64
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +395 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +395 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -409,6 +409,7 @@ __export(index_exports, {
|
|
|
409
409
|
FernignoreMigrator: () => FernignoreMigrator,
|
|
410
410
|
GitClient: () => GitClient,
|
|
411
411
|
LockfileManager: () => LockfileManager,
|
|
412
|
+
LockfileNotFoundError: () => LockfileNotFoundError,
|
|
412
413
|
ReplayApplicator: () => ReplayApplicator,
|
|
413
414
|
ReplayCommitter: () => ReplayCommitter,
|
|
414
415
|
ReplayDetector: () => ReplayDetector,
|
|
@@ -462,6 +463,12 @@ var import_node_fs = require("fs");
|
|
|
462
463
|
var import_node_path = require("path");
|
|
463
464
|
var import_yaml = require("yaml");
|
|
464
465
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
466
|
+
var LockfileNotFoundError = class extends Error {
|
|
467
|
+
constructor(path) {
|
|
468
|
+
super(`Lockfile not found: ${path}`);
|
|
469
|
+
this.name = "LockfileNotFoundError";
|
|
470
|
+
}
|
|
471
|
+
};
|
|
465
472
|
var LockfileManager = class {
|
|
466
473
|
outputDir;
|
|
467
474
|
lock = null;
|
|
@@ -481,12 +488,16 @@ var LockfileManager = class {
|
|
|
481
488
|
if (this.lock) {
|
|
482
489
|
return this.lock;
|
|
483
490
|
}
|
|
484
|
-
|
|
485
|
-
|
|
491
|
+
try {
|
|
492
|
+
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
493
|
+
this.lock = (0, import_yaml.parse)(content);
|
|
494
|
+
return this.lock;
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
497
|
+
throw new LockfileNotFoundError(this.lockfilePath);
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
486
500
|
}
|
|
487
|
-
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
488
|
-
this.lock = (0, import_yaml.parse)(content);
|
|
489
|
-
return this.lock;
|
|
490
501
|
}
|
|
491
502
|
initialize(firstGeneration) {
|
|
492
503
|
this.initializeInMemory(firstGeneration);
|
|
@@ -632,13 +643,21 @@ var ReplayDetector = class {
|
|
|
632
643
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
633
644
|
if (!exists) {
|
|
634
645
|
this.warnings.push(
|
|
635
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history.
|
|
646
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
647
|
+
);
|
|
648
|
+
return this.detectPatchesViaTreeDiff(
|
|
649
|
+
lastGen,
|
|
650
|
+
/* commitKnownMissing */
|
|
651
|
+
true
|
|
636
652
|
);
|
|
637
|
-
return { patches: [], revertedPatchIds: [] };
|
|
638
653
|
}
|
|
639
654
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
640
655
|
if (!isAncestor) {
|
|
641
|
-
return this.detectPatchesViaTreeDiff(
|
|
656
|
+
return this.detectPatchesViaTreeDiff(
|
|
657
|
+
lastGen,
|
|
658
|
+
/* commitKnownMissing */
|
|
659
|
+
false
|
|
660
|
+
);
|
|
642
661
|
}
|
|
643
662
|
const log = await this.git.exec([
|
|
644
663
|
"log",
|
|
@@ -664,7 +683,15 @@ var ReplayDetector = class {
|
|
|
664
683
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
665
684
|
continue;
|
|
666
685
|
}
|
|
667
|
-
|
|
686
|
+
let patchContent;
|
|
687
|
+
try {
|
|
688
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
689
|
+
} catch {
|
|
690
|
+
this.warnings.push(
|
|
691
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
692
|
+
);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
668
695
|
const contentHash = this.computeContentHash(patchContent);
|
|
669
696
|
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
670
697
|
continue;
|
|
@@ -757,11 +784,15 @@ var ReplayDetector = class {
|
|
|
757
784
|
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
758
785
|
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
759
786
|
*/
|
|
760
|
-
async detectPatchesViaTreeDiff(lastGen) {
|
|
761
|
-
const
|
|
787
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
788
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
789
|
+
if (!diffBase) {
|
|
790
|
+
return this.detectPatchesViaCommitScan();
|
|
791
|
+
}
|
|
792
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
762
793
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
763
794
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
764
|
-
const diff = await this.git.exec(["diff",
|
|
795
|
+
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
765
796
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
766
797
|
const contentHash = this.computeContentHash(diff);
|
|
767
798
|
const lock = this.lockManager.read();
|
|
@@ -775,12 +806,113 @@ var ReplayDetector = class {
|
|
|
775
806
|
original_commit: headSha,
|
|
776
807
|
original_message: "Customer customizations (composite)",
|
|
777
808
|
original_author: "composite",
|
|
778
|
-
|
|
809
|
+
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
810
|
+
// reference to find base file content. diffBase may be the tree_hash.
|
|
811
|
+
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
779
812
|
files,
|
|
780
813
|
patch_content: diff
|
|
781
814
|
};
|
|
782
815
|
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
783
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Last-resort detection when both generation commit and tree are unreachable.
|
|
819
|
+
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
820
|
+
* skips creation-only commits (squashed history after force push).
|
|
821
|
+
*
|
|
822
|
+
* Detected patches use the commit's parent as base_generation so the applicator
|
|
823
|
+
* can find base file content from the parent's tree (which IS reachable).
|
|
824
|
+
*/
|
|
825
|
+
async detectPatchesViaCommitScan() {
|
|
826
|
+
const lock = this.lockManager.read();
|
|
827
|
+
const log = await this.git.exec([
|
|
828
|
+
"log",
|
|
829
|
+
"--max-count=200",
|
|
830
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
831
|
+
"HEAD",
|
|
832
|
+
"--",
|
|
833
|
+
this.sdkOutputDir
|
|
834
|
+
]);
|
|
835
|
+
if (!log.trim()) {
|
|
836
|
+
return { patches: [], revertedPatchIds: [] };
|
|
837
|
+
}
|
|
838
|
+
const commits = this.parseGitLog(log);
|
|
839
|
+
const newPatches = [];
|
|
840
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
841
|
+
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
842
|
+
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
843
|
+
for (const commit of commits) {
|
|
844
|
+
if (isGenerationCommit(commit)) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
848
|
+
if (parents.length > 1) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (existingCommits.has(commit.sha)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
let patchContent;
|
|
855
|
+
try {
|
|
856
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
857
|
+
} catch {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const contentHash = this.computeContentHash(patchContent);
|
|
861
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (this.isCreationOnlyPatch(patchContent)) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
868
|
+
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f));
|
|
869
|
+
if (files.length === 0) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (parents.length === 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const parentSha = parents[0];
|
|
876
|
+
newPatches.push({
|
|
877
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
878
|
+
content_hash: contentHash,
|
|
879
|
+
original_commit: commit.sha,
|
|
880
|
+
original_message: commit.message,
|
|
881
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
882
|
+
base_generation: parentSha,
|
|
883
|
+
files,
|
|
884
|
+
patch_content: patchContent
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
newPatches.reverse();
|
|
888
|
+
return { patches: newPatches, revertedPatchIds: [] };
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Check if a format-patch consists entirely of new-file creations.
|
|
892
|
+
* Used to identify squashed commits after force push, which create all files
|
|
893
|
+
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
894
|
+
*/
|
|
895
|
+
isCreationOnlyPatch(patchContent) {
|
|
896
|
+
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
897
|
+
if (diffOldHeaders.length === 0) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Resolve the best available diff base for a generation record.
|
|
904
|
+
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
905
|
+
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
906
|
+
*/
|
|
907
|
+
async resolveDiffBase(gen, commitKnownMissing) {
|
|
908
|
+
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
909
|
+
return gen.commit_sha;
|
|
910
|
+
}
|
|
911
|
+
if (await this.git.treeExists(gen.tree_hash)) {
|
|
912
|
+
return gen.tree_hash;
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
784
916
|
parseGitLog(log) {
|
|
785
917
|
return log.trim().split("\n").map((line) => {
|
|
786
918
|
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
@@ -807,20 +939,33 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
807
939
|
outputLines.push(...region.ok);
|
|
808
940
|
currentLine += region.ok.length;
|
|
809
941
|
} else if (region.conflict) {
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
942
|
+
const resolved = tryResolveConflict(
|
|
943
|
+
region.conflict.a,
|
|
944
|
+
// ours (generator)
|
|
945
|
+
region.conflict.o,
|
|
946
|
+
// base
|
|
947
|
+
region.conflict.b
|
|
948
|
+
// theirs (user)
|
|
949
|
+
);
|
|
950
|
+
if (resolved !== null) {
|
|
951
|
+
outputLines.push(...resolved);
|
|
952
|
+
currentLine += resolved.length;
|
|
953
|
+
} else {
|
|
954
|
+
const startLine = currentLine;
|
|
955
|
+
outputLines.push("<<<<<<< Generated");
|
|
956
|
+
outputLines.push(...region.conflict.a);
|
|
957
|
+
outputLines.push("=======");
|
|
958
|
+
outputLines.push(...region.conflict.b);
|
|
959
|
+
outputLines.push(">>>>>>> Your customization");
|
|
960
|
+
const conflictLines = region.conflict.a.length + region.conflict.b.length + 3;
|
|
961
|
+
conflicts.push({
|
|
962
|
+
startLine,
|
|
963
|
+
endLine: startLine + conflictLines - 1,
|
|
964
|
+
ours: region.conflict.a,
|
|
965
|
+
theirs: region.conflict.b
|
|
966
|
+
});
|
|
967
|
+
currentLine += conflictLines;
|
|
968
|
+
}
|
|
824
969
|
}
|
|
825
970
|
}
|
|
826
971
|
return {
|
|
@@ -829,6 +974,62 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
829
974
|
conflicts
|
|
830
975
|
};
|
|
831
976
|
}
|
|
977
|
+
function tryResolveConflict(oursLines, baseLines, theirsLines) {
|
|
978
|
+
if (baseLines.length === 0) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
const oursPatches = (0, import_node_diff3.diffPatch)(baseLines, oursLines);
|
|
982
|
+
const theirsPatches = (0, import_node_diff3.diffPatch)(baseLines, theirsLines);
|
|
983
|
+
if (oursPatches.length === 0) return theirsLines;
|
|
984
|
+
if (theirsPatches.length === 0) return oursLines;
|
|
985
|
+
if (patchesOverlap(oursPatches, theirsPatches)) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
return applyBothPatches(baseLines, oursPatches, theirsPatches);
|
|
989
|
+
}
|
|
990
|
+
function patchesOverlap(oursPatches, theirsPatches) {
|
|
991
|
+
for (const op of oursPatches) {
|
|
992
|
+
const oStart = op.buffer1.offset;
|
|
993
|
+
const oEnd = oStart + op.buffer1.length;
|
|
994
|
+
for (const tp of theirsPatches) {
|
|
995
|
+
const tStart = tp.buffer1.offset;
|
|
996
|
+
const tEnd = tStart + tp.buffer1.length;
|
|
997
|
+
if (op.buffer1.length === 0 && tp.buffer1.length === 0 && oStart === tStart) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
if (tp.buffer1.length === 0 && tStart === oEnd) {
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
if (op.buffer1.length === 0 && oStart === tEnd) {
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
if (oStart < tEnd && tStart < oEnd) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
1014
|
+
const allPatches = [
|
|
1015
|
+
...oursPatches.map((p) => ({
|
|
1016
|
+
offset: p.buffer1.offset,
|
|
1017
|
+
length: p.buffer1.length,
|
|
1018
|
+
replacement: p.buffer2.chunk
|
|
1019
|
+
})),
|
|
1020
|
+
...theirsPatches.map((p) => ({
|
|
1021
|
+
offset: p.buffer1.offset,
|
|
1022
|
+
length: p.buffer1.length,
|
|
1023
|
+
replacement: p.buffer2.chunk
|
|
1024
|
+
}))
|
|
1025
|
+
];
|
|
1026
|
+
allPatches.sort((a, b) => b.offset - a.offset);
|
|
1027
|
+
const result = [...baseLines];
|
|
1028
|
+
for (const p of allPatches) {
|
|
1029
|
+
result.splice(p.offset, p.length, ...p.replacement);
|
|
1030
|
+
}
|
|
1031
|
+
return result;
|
|
1032
|
+
}
|
|
832
1033
|
|
|
833
1034
|
// src/ReplayApplicator.ts
|
|
834
1035
|
var import_promises = require("fs/promises");
|
|
@@ -837,29 +1038,74 @@ var import_node_path2 = require("path");
|
|
|
837
1038
|
var import_minimatch = require("minimatch");
|
|
838
1039
|
|
|
839
1040
|
// src/conflict-utils.ts
|
|
1041
|
+
var CONFLICT_OPENER = "<<<<<<< Generated";
|
|
1042
|
+
var CONFLICT_SEPARATOR = "=======";
|
|
1043
|
+
var CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
1044
|
+
function trimCR(line) {
|
|
1045
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1046
|
+
}
|
|
1047
|
+
function findConflictRanges(lines) {
|
|
1048
|
+
const ranges = [];
|
|
1049
|
+
let i = 0;
|
|
1050
|
+
while (i < lines.length) {
|
|
1051
|
+
if (trimCR(lines[i]) === CONFLICT_OPENER) {
|
|
1052
|
+
let separatorIdx = -1;
|
|
1053
|
+
let j = i + 1;
|
|
1054
|
+
let found = false;
|
|
1055
|
+
while (j < lines.length) {
|
|
1056
|
+
const trimmed = trimCR(lines[j]);
|
|
1057
|
+
if (trimmed === CONFLICT_OPENER) {
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
if (separatorIdx === -1 && trimmed === CONFLICT_SEPARATOR) {
|
|
1061
|
+
separatorIdx = j;
|
|
1062
|
+
} else if (separatorIdx !== -1 && trimmed === CONFLICT_CLOSER) {
|
|
1063
|
+
ranges.push({ start: i, separator: separatorIdx, end: j });
|
|
1064
|
+
i = j;
|
|
1065
|
+
found = true;
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
j++;
|
|
1069
|
+
}
|
|
1070
|
+
if (!found) {
|
|
1071
|
+
i++;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
i++;
|
|
1076
|
+
}
|
|
1077
|
+
return ranges;
|
|
1078
|
+
}
|
|
840
1079
|
function stripConflictMarkers(content) {
|
|
841
|
-
const lines = content.split(
|
|
1080
|
+
const lines = content.split(/\r?\n/);
|
|
1081
|
+
const ranges = findConflictRanges(lines);
|
|
1082
|
+
if (ranges.length === 0) {
|
|
1083
|
+
return content;
|
|
1084
|
+
}
|
|
842
1085
|
const result = [];
|
|
843
|
-
let
|
|
844
|
-
let
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1086
|
+
let rangeIdx = 0;
|
|
1087
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1088
|
+
if (rangeIdx < ranges.length) {
|
|
1089
|
+
const range = ranges[rangeIdx];
|
|
1090
|
+
if (i === range.start) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (i > range.start && i < range.separator) {
|
|
1094
|
+
result.push(lines[i]);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (i === range.separator) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (i > range.separator && i < range.end) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (i === range.end) {
|
|
1104
|
+
rangeIdx++;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
862
1107
|
}
|
|
1108
|
+
result.push(lines[i]);
|
|
863
1109
|
}
|
|
864
1110
|
return result.join("\n");
|
|
865
1111
|
}
|
|
@@ -926,6 +1172,27 @@ var ReplayApplicator = class {
|
|
|
926
1172
|
this.lockManager = lockManager;
|
|
927
1173
|
this.outputDir = outputDir;
|
|
928
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
1177
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
1178
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
1179
|
+
*/
|
|
1180
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
1181
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
1182
|
+
if (gen) return gen;
|
|
1183
|
+
try {
|
|
1184
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
1185
|
+
return {
|
|
1186
|
+
commit_sha: baseGeneration,
|
|
1187
|
+
tree_hash: treeHash,
|
|
1188
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1189
|
+
cli_version: "unknown",
|
|
1190
|
+
generator_versions: {}
|
|
1191
|
+
};
|
|
1192
|
+
} catch {
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
929
1196
|
/** Reset inter-patch accumulator for a new cycle. */
|
|
930
1197
|
resetAccumulator() {
|
|
931
1198
|
this.fileTheirsAccumulator.clear();
|
|
@@ -1007,7 +1274,7 @@ var ReplayApplicator = class {
|
|
|
1007
1274
|
}
|
|
1008
1275
|
}
|
|
1009
1276
|
async applyPatchWithFallback(patch) {
|
|
1010
|
-
const baseGen = this.
|
|
1277
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
1011
1278
|
const lock = this.lockManager.read();
|
|
1012
1279
|
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
1013
1280
|
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
@@ -1091,7 +1358,7 @@ var ReplayApplicator = class {
|
|
|
1091
1358
|
}
|
|
1092
1359
|
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
1093
1360
|
try {
|
|
1094
|
-
const baseGen = this.
|
|
1361
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
1095
1362
|
if (!baseGen) {
|
|
1096
1363
|
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
1097
1364
|
}
|
|
@@ -1143,6 +1410,17 @@ var ReplayApplicator = class {
|
|
|
1143
1410
|
renameSourcePath
|
|
1144
1411
|
);
|
|
1145
1412
|
}
|
|
1413
|
+
if (theirs) {
|
|
1414
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
1415
|
+
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
1416
|
+
if (theirsHasMarkers && !baseHasMarkers) {
|
|
1417
|
+
return {
|
|
1418
|
+
file: resolvedPath,
|
|
1419
|
+
status: "skipped",
|
|
1420
|
+
reason: "stale-conflict-markers"
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1146
1424
|
let useAccumulatorAsMergeBase = false;
|
|
1147
1425
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
1148
1426
|
if (!theirs && accumulatorEntry) {
|
|
@@ -1157,6 +1435,17 @@ var ReplayApplicator = class {
|
|
|
1157
1435
|
useAccumulatorAsMergeBase = true;
|
|
1158
1436
|
}
|
|
1159
1437
|
}
|
|
1438
|
+
if (theirs) {
|
|
1439
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
1440
|
+
const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
|
|
1441
|
+
if (theirsHasMarkers && !accBaseHasMarkers && !(base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization")))) {
|
|
1442
|
+
return {
|
|
1443
|
+
file: resolvedPath,
|
|
1444
|
+
status: "skipped",
|
|
1445
|
+
reason: "stale-conflict-markers"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1160
1449
|
let effective_theirs = theirs;
|
|
1161
1450
|
let baseMismatchSkipped = false;
|
|
1162
1451
|
if (theirs && base && !useAccumulatorAsMergeBase) {
|
|
@@ -1223,7 +1512,7 @@ var ReplayApplicator = class {
|
|
|
1223
1512
|
const outDir = (0, import_node_path2.dirname)(oursPath);
|
|
1224
1513
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
1225
1514
|
await (0, import_promises.writeFile)(oursPath, merged.content);
|
|
1226
|
-
if (effective_theirs) {
|
|
1515
|
+
if (effective_theirs && !merged.hasConflicts) {
|
|
1227
1516
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
1228
1517
|
content: effective_theirs,
|
|
1229
1518
|
baseGeneration: patch.base_generation
|
|
@@ -1525,6 +1814,7 @@ var ReplayService = class {
|
|
|
1525
1814
|
generator_versions: options?.generatorVersions ?? {},
|
|
1526
1815
|
base_branch_head: options?.baseBranchHead
|
|
1527
1816
|
};
|
|
1817
|
+
let resolvedPatches;
|
|
1528
1818
|
if (!this.lockManager.exists()) {
|
|
1529
1819
|
this.lockManager.initializeInMemory(record);
|
|
1530
1820
|
} else {
|
|
@@ -1533,13 +1823,13 @@ var ReplayService = class {
|
|
|
1533
1823
|
...this.lockManager.getUnresolvedPatches(),
|
|
1534
1824
|
...this.lockManager.getResolvingPatches()
|
|
1535
1825
|
];
|
|
1826
|
+
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
1536
1827
|
this.lockManager.addGeneration(record);
|
|
1537
1828
|
this.lockManager.clearPatches();
|
|
1538
1829
|
for (const patch of unresolvedPatches) {
|
|
1539
1830
|
this.lockManager.addPatch(patch);
|
|
1540
1831
|
}
|
|
1541
1832
|
}
|
|
1542
|
-
this.lockManager.save();
|
|
1543
1833
|
try {
|
|
1544
1834
|
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
1545
1835
|
if (redetectedPatches.length > 0) {
|
|
@@ -1553,20 +1843,27 @@ var ReplayService = class {
|
|
|
1553
1843
|
for (const patch of redetectedPatches) {
|
|
1554
1844
|
this.lockManager.addPatch(patch);
|
|
1555
1845
|
}
|
|
1556
|
-
this.lockManager.save();
|
|
1557
1846
|
}
|
|
1558
|
-
} catch {
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
for (const patch of resolvedPatches ?? []) {
|
|
1849
|
+
this.lockManager.addPatch(patch);
|
|
1850
|
+
}
|
|
1851
|
+
this.detector.warnings.push(
|
|
1852
|
+
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1853
|
+
);
|
|
1559
1854
|
}
|
|
1855
|
+
this.lockManager.save();
|
|
1560
1856
|
}
|
|
1561
1857
|
determineFlow() {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1858
|
+
try {
|
|
1859
|
+
const lock = this.lockManager.read();
|
|
1860
|
+
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
if (error instanceof LockfileNotFoundError) {
|
|
1863
|
+
return "first-generation";
|
|
1864
|
+
}
|
|
1865
|
+
throw error;
|
|
1568
1866
|
}
|
|
1569
|
-
return "normal-regeneration";
|
|
1570
1867
|
}
|
|
1571
1868
|
async handleFirstGeneration(options) {
|
|
1572
1869
|
if (options?.dryRun) {
|
|
@@ -1608,12 +1905,17 @@ var ReplayService = class {
|
|
|
1608
1905
|
baseBranchHead: options.baseBranchHead
|
|
1609
1906
|
} : void 0;
|
|
1610
1907
|
await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
1908
|
+
await this.cleanupStaleConflictMarkers();
|
|
1611
1909
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1612
|
-
|
|
1910
|
+
try {
|
|
1613
1911
|
this.lockManager.read();
|
|
1614
1912
|
this.lockManager.addGeneration(genRecord);
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
if (error instanceof LockfileNotFoundError) {
|
|
1915
|
+
this.lockManager.initializeInMemory(genRecord);
|
|
1916
|
+
} else {
|
|
1917
|
+
throw error;
|
|
1918
|
+
}
|
|
1617
1919
|
}
|
|
1618
1920
|
this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
1619
1921
|
this.lockManager.save();
|
|
@@ -1659,6 +1961,7 @@ var ReplayService = class {
|
|
|
1659
1961
|
baseBranchHead: options.baseBranchHead
|
|
1660
1962
|
} : void 0;
|
|
1661
1963
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
1964
|
+
await this.cleanupStaleConflictMarkers();
|
|
1662
1965
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1663
1966
|
this.lockManager.addGeneration(genRecord);
|
|
1664
1967
|
let results = [];
|
|
@@ -1752,6 +2055,7 @@ var ReplayService = class {
|
|
|
1752
2055
|
baseBranchHead: options.baseBranchHead
|
|
1753
2056
|
} : void 0;
|
|
1754
2057
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
2058
|
+
await this.cleanupStaleConflictMarkers();
|
|
1755
2059
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1756
2060
|
this.lockManager.addGeneration(genRecord);
|
|
1757
2061
|
const results = await this.applicator.applyPatches(allPatches);
|
|
@@ -1942,6 +2246,13 @@ var ReplayService = class {
|
|
|
1942
2246
|
contentRefreshed++;
|
|
1943
2247
|
continue;
|
|
1944
2248
|
}
|
|
2249
|
+
const diffLines = diff.split("\n");
|
|
2250
|
+
const hasStaleMarkers = diffLines.some(
|
|
2251
|
+
(l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
|
|
2252
|
+
);
|
|
2253
|
+
if (hasStaleMarkers) {
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
1945
2256
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
1946
2257
|
if (newContentHash !== patch.content_hash) {
|
|
1947
2258
|
const filesOutput = await this.git.exec(["diff", "--name-only", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -1962,7 +2273,7 @@ var ReplayService = class {
|
|
|
1962
2273
|
continue;
|
|
1963
2274
|
}
|
|
1964
2275
|
try {
|
|
1965
|
-
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<<", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
2276
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
1966
2277
|
if (markerFiles.trim()) continue;
|
|
1967
2278
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
1968
2279
|
if (diff === null) continue;
|
|
@@ -2002,6 +2313,25 @@ var ReplayService = class {
|
|
|
2002
2313
|
}
|
|
2003
2314
|
}
|
|
2004
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Clean up stale conflict markers left by a previous crashed run.
|
|
2318
|
+
* Called after commitGeneration() when HEAD is the [fern-generated] commit.
|
|
2319
|
+
* Restores files to their clean generated state from HEAD.
|
|
2320
|
+
* Skips .fernignore-protected files to prevent overwriting user content.
|
|
2321
|
+
*/
|
|
2322
|
+
async cleanupStaleConflictMarkers() {
|
|
2323
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
|
|
2324
|
+
const files = markerFiles.trim().split("\n").filter(Boolean);
|
|
2325
|
+
if (files.length === 0) return;
|
|
2326
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
2327
|
+
for (const file of files) {
|
|
2328
|
+
if (fernignorePatterns.some((pattern) => (0, import_minimatch2.minimatch)(file, pattern))) continue;
|
|
2329
|
+
try {
|
|
2330
|
+
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2005
2335
|
readFernignorePatterns() {
|
|
2006
2336
|
const fernignorePath = (0, import_node_path3.join)(this.outputDir, ".fernignore");
|
|
2007
2337
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -2777,6 +3107,7 @@ function status(outputDir) {
|
|
|
2777
3107
|
FernignoreMigrator,
|
|
2778
3108
|
GitClient,
|
|
2779
3109
|
LockfileManager,
|
|
3110
|
+
LockfileNotFoundError,
|
|
2780
3111
|
ReplayApplicator,
|
|
2781
3112
|
ReplayCommitter,
|
|
2782
3113
|
ReplayDetector,
|