@fern-api/replay 0.6.1 → 0.7.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 +215 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +189 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -5
- package/dist/index.d.ts +37 -5
- package/dist/index.js +194 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -273,6 +273,26 @@ var LockfileManager = class {
|
|
|
273
273
|
this.ensureLoaded();
|
|
274
274
|
this.lock.patches = [];
|
|
275
275
|
}
|
|
276
|
+
getUnresolvedPatches() {
|
|
277
|
+
this.ensureLoaded();
|
|
278
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
279
|
+
}
|
|
280
|
+
getResolvingPatches() {
|
|
281
|
+
this.ensureLoaded();
|
|
282
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
283
|
+
}
|
|
284
|
+
markPatchUnresolved(patchId) {
|
|
285
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
286
|
+
}
|
|
287
|
+
markPatchResolved(patchId, updates) {
|
|
288
|
+
this.ensureLoaded();
|
|
289
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
290
|
+
if (!patch) {
|
|
291
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
292
|
+
}
|
|
293
|
+
delete patch.status;
|
|
294
|
+
Object.assign(patch, updates);
|
|
295
|
+
}
|
|
276
296
|
getPatches() {
|
|
277
297
|
this.ensureLoaded();
|
|
278
298
|
return this.lock.patches;
|
|
@@ -973,12 +993,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
973
993
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
974
994
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
975
995
|
}
|
|
976
|
-
async commitReplay(
|
|
996
|
+
async commitReplay(_patchCount, patches, message) {
|
|
977
997
|
await this.stageAll();
|
|
978
998
|
if (!await this.hasStagedChanges()) {
|
|
979
999
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
980
1000
|
}
|
|
981
|
-
let fullMessage = `[fern-replay] Applied
|
|
1001
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
982
1002
|
if (patches && patches.length > 0) {
|
|
983
1003
|
fullMessage += "\n\nPatches replayed:";
|
|
984
1004
|
for (const patch of patches) {
|
|
@@ -1073,13 +1093,27 @@ var ReplayService = class {
|
|
|
1073
1093
|
this.lockManager.initializeInMemory(record);
|
|
1074
1094
|
} else {
|
|
1075
1095
|
this.lockManager.read();
|
|
1096
|
+
const unresolvedPatches = [
|
|
1097
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
1098
|
+
...this.lockManager.getResolvingPatches()
|
|
1099
|
+
];
|
|
1076
1100
|
this.lockManager.addGeneration(record);
|
|
1077
1101
|
this.lockManager.clearPatches();
|
|
1102
|
+
for (const patch of unresolvedPatches) {
|
|
1103
|
+
this.lockManager.addPatch(patch);
|
|
1104
|
+
}
|
|
1078
1105
|
}
|
|
1079
1106
|
this.lockManager.save();
|
|
1080
1107
|
try {
|
|
1081
1108
|
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1082
1109
|
if (redetectedPatches.length > 0) {
|
|
1110
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1111
|
+
const currentPatches = this.lockManager.getPatches();
|
|
1112
|
+
for (const patch of currentPatches) {
|
|
1113
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
1114
|
+
this.lockManager.removePatch(patch.id);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1083
1117
|
for (const patch of redetectedPatches) {
|
|
1084
1118
|
this.lockManager.addPatch(patch);
|
|
1085
1119
|
}
|
|
@@ -1187,6 +1221,12 @@ var ReplayService = class {
|
|
|
1187
1221
|
let results = [];
|
|
1188
1222
|
if (newPatches.length > 0) {
|
|
1189
1223
|
results = await this.applicator.applyPatches(newPatches);
|
|
1224
|
+
this.revertConflictingFiles(results);
|
|
1225
|
+
for (const result of results) {
|
|
1226
|
+
if (result.status === "conflict") {
|
|
1227
|
+
result.patch.status = "unresolved";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1190
1230
|
}
|
|
1191
1231
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1192
1232
|
for (const patch of newPatches) {
|
|
@@ -1197,10 +1237,8 @@ var ReplayService = class {
|
|
|
1197
1237
|
this.lockManager.save();
|
|
1198
1238
|
if (newPatches.length > 0) {
|
|
1199
1239
|
if (!options?.stageOnly) {
|
|
1200
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1201
|
-
|
|
1202
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1203
|
-
}
|
|
1240
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1241
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1204
1242
|
} else {
|
|
1205
1243
|
await this.committer.stageAll();
|
|
1206
1244
|
}
|
|
@@ -1248,20 +1286,32 @@ var ReplayService = class {
|
|
|
1248
1286
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1249
1287
|
this.lockManager.addGeneration(genRecord);
|
|
1250
1288
|
const results = await this.applicator.applyPatches(allPatches);
|
|
1289
|
+
this.revertConflictingFiles(results);
|
|
1290
|
+
for (const result of results) {
|
|
1291
|
+
if (result.status === "conflict") {
|
|
1292
|
+
result.patch.status = "unresolved";
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1251
1295
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1252
1296
|
for (const patch of newPatches) {
|
|
1253
1297
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
1254
1298
|
this.lockManager.addPatch(patch);
|
|
1255
1299
|
}
|
|
1256
1300
|
}
|
|
1301
|
+
for (const result of results) {
|
|
1302
|
+
if (result.status === "conflict") {
|
|
1303
|
+
try {
|
|
1304
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1257
1309
|
this.lockManager.save();
|
|
1258
1310
|
if (options?.stageOnly) {
|
|
1259
1311
|
await this.committer.stageAll();
|
|
1260
1312
|
} else {
|
|
1261
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1262
|
-
|
|
1263
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1264
|
-
}
|
|
1313
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1314
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1265
1315
|
}
|
|
1266
1316
|
return this.buildReport(
|
|
1267
1317
|
"normal-regeneration",
|
|
@@ -1409,6 +1459,10 @@ var ReplayService = class {
|
|
|
1409
1459
|
let conflictAbsorbed = 0;
|
|
1410
1460
|
let contentRefreshed = 0;
|
|
1411
1461
|
for (const patch of patches) {
|
|
1462
|
+
if (patch.status != null) {
|
|
1463
|
+
delete patch.status;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1412
1466
|
if (patch.base_generation === currentGen) {
|
|
1413
1467
|
try {
|
|
1414
1468
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -1459,6 +1513,55 @@ var ReplayService = class {
|
|
|
1459
1513
|
}
|
|
1460
1514
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1461
1515
|
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
1518
|
+
* Preserves clean patches' non-conflicting changes on shared files.
|
|
1519
|
+
*/
|
|
1520
|
+
stripConflictMarkers(content) {
|
|
1521
|
+
const lines = content.split("\n");
|
|
1522
|
+
const result = [];
|
|
1523
|
+
let inConflict = false;
|
|
1524
|
+
let inOurs = false;
|
|
1525
|
+
for (const line of lines) {
|
|
1526
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1527
|
+
inConflict = true;
|
|
1528
|
+
inOurs = true;
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (inConflict && line === "=======") {
|
|
1532
|
+
inOurs = false;
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1536
|
+
inConflict = false;
|
|
1537
|
+
inOurs = false;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (!inConflict || inOurs) {
|
|
1541
|
+
result.push(line);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return result.join("\n");
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
1548
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
1549
|
+
*/
|
|
1550
|
+
revertConflictingFiles(results) {
|
|
1551
|
+
for (const result of results) {
|
|
1552
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
1553
|
+
for (const fileResult of result.fileResults) {
|
|
1554
|
+
if (fileResult.status !== "conflict") continue;
|
|
1555
|
+
const filePath = (0, import_node_path3.join)(this.outputDir, fileResult.file);
|
|
1556
|
+
try {
|
|
1557
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
1558
|
+
const stripped = this.stripConflictMarkers(content);
|
|
1559
|
+
(0, import_node_fs2.writeFileSync)(filePath, stripped);
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1462
1565
|
readFernignorePatterns() {
|
|
1463
1566
|
const fernignorePath = (0, import_node_path3.join)(this.outputDir, ".fernignore");
|
|
1464
1567
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -1493,6 +1596,12 @@ var ReplayService = class {
|
|
|
1493
1596
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1494
1597
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
1495
1598
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
1599
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
1600
|
+
patchId: r.patch.id,
|
|
1601
|
+
patchMessage: r.patch.original_message,
|
|
1602
|
+
files: r.patch.files,
|
|
1603
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
1604
|
+
})) : void 0,
|
|
1496
1605
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
1497
1606
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
1498
1607
|
};
|
|
@@ -1954,17 +2063,82 @@ async function resolve(outputDir, options) {
|
|
|
1954
2063
|
return { success: false, reason: "no-patches" };
|
|
1955
2064
|
}
|
|
1956
2065
|
const git = new GitClient(outputDir);
|
|
2066
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
2067
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
2068
|
+
if (unresolvedPatches.length > 0) {
|
|
2069
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
2070
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
2071
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
2072
|
+
if (markerFiles.length > 0) {
|
|
2073
|
+
for (const patch of unresolvedPatches) {
|
|
2074
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
2075
|
+
}
|
|
2076
|
+
lockManager.save();
|
|
2077
|
+
return {
|
|
2078
|
+
success: false,
|
|
2079
|
+
reason: "conflicts-applied",
|
|
2080
|
+
unresolvedFiles: markerFiles,
|
|
2081
|
+
phase: "applied",
|
|
2082
|
+
patchesApplied: unresolvedPatches.length
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1957
2086
|
if (options?.checkMarkers !== false) {
|
|
1958
|
-
const
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
2087
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
2088
|
+
if (currentMarkerFiles.length > 0) {
|
|
2089
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
2093
|
+
if (patchesToCommit.length > 0) {
|
|
2094
|
+
const currentGen = lock.current_generation;
|
|
2095
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
2096
|
+
let patchesResolved = 0;
|
|
2097
|
+
for (const patch of patchesToCommit) {
|
|
2098
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
2099
|
+
if (!diff || !diff.trim()) {
|
|
2100
|
+
lockManager.removePatch(patch.id);
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
2104
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
2105
|
+
lockManager.markPatchResolved(patch.id, {
|
|
2106
|
+
patch_content: diff,
|
|
2107
|
+
content_hash: newContentHash,
|
|
2108
|
+
base_generation: currentGen,
|
|
2109
|
+
files: changedFiles
|
|
2110
|
+
});
|
|
2111
|
+
patchesResolved++;
|
|
1962
2112
|
}
|
|
2113
|
+
lockManager.save();
|
|
2114
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
2115
|
+
await committer2.stageAll();
|
|
2116
|
+
const commitSha2 = await committer2.commitReplay(
|
|
2117
|
+
lock.patches.length,
|
|
2118
|
+
lock.patches,
|
|
2119
|
+
"[fern-replay] Resolved conflicts"
|
|
2120
|
+
);
|
|
2121
|
+
return {
|
|
2122
|
+
success: true,
|
|
2123
|
+
commitSha: commitSha2,
|
|
2124
|
+
phase: "committed",
|
|
2125
|
+
patchesResolved
|
|
2126
|
+
};
|
|
1963
2127
|
}
|
|
1964
2128
|
const committer = new ReplayCommitter(git, outputDir);
|
|
1965
2129
|
await committer.stageAll();
|
|
1966
2130
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
1967
|
-
return { success: true, commitSha };
|
|
2131
|
+
return { success: true, commitSha, phase: "committed" };
|
|
2132
|
+
}
|
|
2133
|
+
async function findConflictMarkerFiles(git) {
|
|
2134
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
2135
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
2136
|
+
}
|
|
2137
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
2138
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
2139
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
2140
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
2141
|
+
return changed.length > 0 ? changed : files;
|
|
1968
2142
|
}
|
|
1969
2143
|
|
|
1970
2144
|
// src/commands/status.ts
|