@fern-api/replay 0.6.2 → 0.8.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 +354 -48
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +329 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -7
- package/dist/index.d.ts +50 -7
- package/dist/index.js +332 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -133,6 +133,9 @@ var init_GitClient = __esm({
|
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
async getCommitBody(commitSha) {
|
|
137
|
+
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
138
|
+
}
|
|
136
139
|
getRepoPath() {
|
|
137
140
|
return this.repoPath;
|
|
138
141
|
}
|
|
@@ -157,6 +160,9 @@ __export(index_exports, {
|
|
|
157
160
|
forget: () => forget,
|
|
158
161
|
isGenerationCommit: () => isGenerationCommit,
|
|
159
162
|
isReplayCommit: () => isReplayCommit,
|
|
163
|
+
isRevertCommit: () => isRevertCommit,
|
|
164
|
+
parseRevertedMessage: () => parseRevertedMessage,
|
|
165
|
+
parseRevertedSha: () => parseRevertedSha,
|
|
160
166
|
reset: () => reset,
|
|
161
167
|
resolve: () => resolve,
|
|
162
168
|
status: () => status,
|
|
@@ -182,6 +188,17 @@ function isGenerationCommit(commit) {
|
|
|
182
188
|
function isReplayCommit(commit) {
|
|
183
189
|
return commit.message.startsWith("[fern-replay]");
|
|
184
190
|
}
|
|
191
|
+
function isRevertCommit(message) {
|
|
192
|
+
return message.startsWith('Revert "');
|
|
193
|
+
}
|
|
194
|
+
function parseRevertedSha(fullBody) {
|
|
195
|
+
const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
|
|
196
|
+
return match?.[1];
|
|
197
|
+
}
|
|
198
|
+
function parseRevertedMessage(subject) {
|
|
199
|
+
const match = subject.match(/^Revert "(.+)"$/);
|
|
200
|
+
return match?.[1];
|
|
201
|
+
}
|
|
185
202
|
|
|
186
203
|
// src/LockfileManager.ts
|
|
187
204
|
var import_node_fs = require("fs");
|
|
@@ -273,6 +290,26 @@ var LockfileManager = class {
|
|
|
273
290
|
this.ensureLoaded();
|
|
274
291
|
this.lock.patches = [];
|
|
275
292
|
}
|
|
293
|
+
getUnresolvedPatches() {
|
|
294
|
+
this.ensureLoaded();
|
|
295
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
296
|
+
}
|
|
297
|
+
getResolvingPatches() {
|
|
298
|
+
this.ensureLoaded();
|
|
299
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
300
|
+
}
|
|
301
|
+
markPatchUnresolved(patchId) {
|
|
302
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
303
|
+
}
|
|
304
|
+
markPatchResolved(patchId, updates) {
|
|
305
|
+
this.ensureLoaded();
|
|
306
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
307
|
+
if (!patch) {
|
|
308
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
309
|
+
}
|
|
310
|
+
delete patch.status;
|
|
311
|
+
Object.assign(patch, updates);
|
|
312
|
+
}
|
|
276
313
|
getPatches() {
|
|
277
314
|
this.ensureLoaded();
|
|
278
315
|
return this.lock.patches;
|
|
@@ -324,14 +361,14 @@ var ReplayDetector = class {
|
|
|
324
361
|
const lock = this.lockManager.read();
|
|
325
362
|
const lastGen = this.getLastGeneration(lock);
|
|
326
363
|
if (!lastGen) {
|
|
327
|
-
return [];
|
|
364
|
+
return { patches: [], revertedPatchIds: [] };
|
|
328
365
|
}
|
|
329
366
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
330
367
|
if (!exists) {
|
|
331
368
|
this.warnings.push(
|
|
332
369
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
|
|
333
370
|
);
|
|
334
|
-
return [];
|
|
371
|
+
return { patches: [], revertedPatchIds: [] };
|
|
335
372
|
}
|
|
336
373
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
337
374
|
if (!isAncestor) {
|
|
@@ -345,7 +382,7 @@ var ReplayDetector = class {
|
|
|
345
382
|
this.sdkOutputDir
|
|
346
383
|
]);
|
|
347
384
|
if (!log.trim()) {
|
|
348
|
-
return [];
|
|
385
|
+
return { patches: [], revertedPatchIds: [] };
|
|
349
386
|
}
|
|
350
387
|
const commits = this.parseGitLog(log);
|
|
351
388
|
const newPatches = [];
|
|
@@ -381,7 +418,60 @@ var ReplayDetector = class {
|
|
|
381
418
|
patch_content: patchContent
|
|
382
419
|
});
|
|
383
420
|
}
|
|
384
|
-
|
|
421
|
+
newPatches.reverse();
|
|
422
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
423
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
424
|
+
for (let i = 0; i < newPatches.length; i++) {
|
|
425
|
+
const patch = newPatches[i];
|
|
426
|
+
if (!isRevertCommit(patch.original_message)) continue;
|
|
427
|
+
let body = "";
|
|
428
|
+
try {
|
|
429
|
+
body = await this.git.getCommitBody(patch.original_commit);
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
const revertedSha = parseRevertedSha(body);
|
|
433
|
+
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
434
|
+
let matchedExisting = false;
|
|
435
|
+
if (revertedSha) {
|
|
436
|
+
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
437
|
+
if (existing) {
|
|
438
|
+
revertedPatchIdSet.add(existing.id);
|
|
439
|
+
revertIndicesToRemove.add(i);
|
|
440
|
+
matchedExisting = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!matchedExisting && revertedMessage) {
|
|
444
|
+
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
445
|
+
if (existing) {
|
|
446
|
+
revertedPatchIdSet.add(existing.id);
|
|
447
|
+
revertIndicesToRemove.add(i);
|
|
448
|
+
matchedExisting = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (matchedExisting) continue;
|
|
452
|
+
let matchedNew = false;
|
|
453
|
+
if (revertedSha) {
|
|
454
|
+
const idx = newPatches.findIndex(
|
|
455
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
|
|
456
|
+
);
|
|
457
|
+
if (idx !== -1) {
|
|
458
|
+
revertIndicesToRemove.add(i);
|
|
459
|
+
revertIndicesToRemove.add(idx);
|
|
460
|
+
matchedNew = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!matchedNew && revertedMessage) {
|
|
464
|
+
const idx = newPatches.findIndex(
|
|
465
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
|
|
466
|
+
);
|
|
467
|
+
if (idx !== -1) {
|
|
468
|
+
revertIndicesToRemove.add(i);
|
|
469
|
+
revertIndicesToRemove.add(idx);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
474
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
385
475
|
}
|
|
386
476
|
/**
|
|
387
477
|
* Compute content hash for deduplication.
|
|
@@ -392,31 +482,34 @@ var ReplayDetector = class {
|
|
|
392
482
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
393
483
|
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
394
484
|
}
|
|
395
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
487
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
488
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
489
|
+
*/
|
|
396
490
|
async detectPatchesViaTreeDiff(lastGen) {
|
|
397
491
|
const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
|
|
398
492
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
399
|
-
if (files.length === 0) return [];
|
|
493
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
400
494
|
const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
|
|
401
|
-
if (!diff.trim()) return [];
|
|
495
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
402
496
|
const contentHash = this.computeContentHash(diff);
|
|
403
497
|
const lock = this.lockManager.read();
|
|
404
498
|
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
405
|
-
return [];
|
|
499
|
+
return { patches: [], revertedPatchIds: [] };
|
|
406
500
|
}
|
|
407
501
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
408
|
-
|
|
409
|
-
{
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
];
|
|
502
|
+
const compositePatch = {
|
|
503
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
504
|
+
content_hash: contentHash,
|
|
505
|
+
original_commit: headSha,
|
|
506
|
+
original_message: "Customer customizations (composite)",
|
|
507
|
+
original_author: "composite",
|
|
508
|
+
base_generation: lastGen.commit_sha,
|
|
509
|
+
files,
|
|
510
|
+
patch_content: diff
|
|
511
|
+
};
|
|
512
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
420
513
|
}
|
|
421
514
|
parseGitLog(log) {
|
|
422
515
|
return log.trim().split("\n").map((line) => {
|
|
@@ -973,12 +1066,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
973
1066
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
974
1067
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
975
1068
|
}
|
|
976
|
-
async commitReplay(_patchCount, patches) {
|
|
1069
|
+
async commitReplay(_patchCount, patches, message) {
|
|
977
1070
|
await this.stageAll();
|
|
978
1071
|
if (!await this.hasStagedChanges()) {
|
|
979
1072
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
980
1073
|
}
|
|
981
|
-
let fullMessage = `[fern-replay] Applied customizations`;
|
|
1074
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
982
1075
|
if (patches && patches.length > 0) {
|
|
983
1076
|
fullMessage += "\n\nPatches replayed:";
|
|
984
1077
|
for (const patch of patches) {
|
|
@@ -1018,6 +1111,36 @@ var import_node_fs2 = require("fs");
|
|
|
1018
1111
|
var import_node_path3 = require("path");
|
|
1019
1112
|
var import_minimatch2 = require("minimatch");
|
|
1020
1113
|
init_GitClient();
|
|
1114
|
+
|
|
1115
|
+
// src/conflict-utils.ts
|
|
1116
|
+
function stripConflictMarkers(content) {
|
|
1117
|
+
const lines = content.split("\n");
|
|
1118
|
+
const result = [];
|
|
1119
|
+
let inConflict = false;
|
|
1120
|
+
let inOurs = false;
|
|
1121
|
+
for (const line of lines) {
|
|
1122
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1123
|
+
inConflict = true;
|
|
1124
|
+
inOurs = true;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (inConflict && line === "=======") {
|
|
1128
|
+
inOurs = false;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1132
|
+
inConflict = false;
|
|
1133
|
+
inOurs = false;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (!inConflict || inOurs) {
|
|
1137
|
+
result.push(line);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return result.join("\n");
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/ReplayService.ts
|
|
1021
1144
|
var ReplayService = class {
|
|
1022
1145
|
git;
|
|
1023
1146
|
detector;
|
|
@@ -1073,13 +1196,27 @@ var ReplayService = class {
|
|
|
1073
1196
|
this.lockManager.initializeInMemory(record);
|
|
1074
1197
|
} else {
|
|
1075
1198
|
this.lockManager.read();
|
|
1199
|
+
const unresolvedPatches = [
|
|
1200
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
1201
|
+
...this.lockManager.getResolvingPatches()
|
|
1202
|
+
];
|
|
1076
1203
|
this.lockManager.addGeneration(record);
|
|
1077
1204
|
this.lockManager.clearPatches();
|
|
1205
|
+
for (const patch of unresolvedPatches) {
|
|
1206
|
+
this.lockManager.addPatch(patch);
|
|
1207
|
+
}
|
|
1078
1208
|
}
|
|
1079
1209
|
this.lockManager.save();
|
|
1080
1210
|
try {
|
|
1081
|
-
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1211
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
1082
1212
|
if (redetectedPatches.length > 0) {
|
|
1213
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1214
|
+
const currentPatches = this.lockManager.getPatches();
|
|
1215
|
+
for (const patch of currentPatches) {
|
|
1216
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
1217
|
+
this.lockManager.removePatch(patch.id);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1083
1220
|
for (const patch of redetectedPatches) {
|
|
1084
1221
|
this.lockManager.addPatch(patch);
|
|
1085
1222
|
}
|
|
@@ -1162,7 +1299,7 @@ var ReplayService = class {
|
|
|
1162
1299
|
};
|
|
1163
1300
|
}
|
|
1164
1301
|
async handleNoPatchesRegeneration(options) {
|
|
1165
|
-
const newPatches = await this.detector.detectNewPatches();
|
|
1302
|
+
const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1166
1303
|
const warnings = [...this.detector.warnings];
|
|
1167
1304
|
if (options?.dryRun) {
|
|
1168
1305
|
return {
|
|
@@ -1171,11 +1308,18 @@ var ReplayService = class {
|
|
|
1171
1308
|
patchesApplied: 0,
|
|
1172
1309
|
patchesWithConflicts: 0,
|
|
1173
1310
|
patchesSkipped: 0,
|
|
1311
|
+
patchesReverted: revertedPatchIds.length,
|
|
1174
1312
|
conflicts: [],
|
|
1175
1313
|
wouldApply: newPatches,
|
|
1176
1314
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
1177
1315
|
};
|
|
1178
1316
|
}
|
|
1317
|
+
for (const id of revertedPatchIds) {
|
|
1318
|
+
try {
|
|
1319
|
+
this.lockManager.removePatch(id);
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1179
1323
|
const commitOpts = options ? {
|
|
1180
1324
|
cliVersion: options.cliVersion ?? "unknown",
|
|
1181
1325
|
generatorVersions: options.generatorVersions ?? {},
|
|
@@ -1187,6 +1331,12 @@ var ReplayService = class {
|
|
|
1187
1331
|
let results = [];
|
|
1188
1332
|
if (newPatches.length > 0) {
|
|
1189
1333
|
results = await this.applicator.applyPatches(newPatches);
|
|
1334
|
+
this.revertConflictingFiles(results);
|
|
1335
|
+
for (const result of results) {
|
|
1336
|
+
if (result.status === "conflict") {
|
|
1337
|
+
result.patch.status = "unresolved";
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1190
1340
|
}
|
|
1191
1341
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1192
1342
|
for (const patch of newPatches) {
|
|
@@ -1197,20 +1347,18 @@ var ReplayService = class {
|
|
|
1197
1347
|
this.lockManager.save();
|
|
1198
1348
|
if (newPatches.length > 0) {
|
|
1199
1349
|
if (!options?.stageOnly) {
|
|
1200
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1201
|
-
|
|
1202
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1203
|
-
}
|
|
1350
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1351
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1204
1352
|
} else {
|
|
1205
1353
|
await this.committer.stageAll();
|
|
1206
1354
|
}
|
|
1207
1355
|
}
|
|
1208
|
-
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
|
|
1356
|
+
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
|
|
1209
1357
|
}
|
|
1210
1358
|
async handleNormalRegeneration(options) {
|
|
1211
1359
|
if (options?.dryRun) {
|
|
1212
1360
|
const existingPatches2 = this.lockManager.getPatches();
|
|
1213
|
-
const newPatches2 = await this.detector.detectNewPatches();
|
|
1361
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
1214
1362
|
const warnings2 = [...this.detector.warnings];
|
|
1215
1363
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
1216
1364
|
return {
|
|
@@ -1219,13 +1367,19 @@ var ReplayService = class {
|
|
|
1219
1367
|
patchesApplied: 0,
|
|
1220
1368
|
patchesWithConflicts: 0,
|
|
1221
1369
|
patchesSkipped: 0,
|
|
1370
|
+
patchesReverted: dryRunReverted.length,
|
|
1222
1371
|
conflicts: [],
|
|
1223
1372
|
wouldApply: allPatches2,
|
|
1224
1373
|
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
1225
1374
|
};
|
|
1226
1375
|
}
|
|
1227
1376
|
let existingPatches = this.lockManager.getPatches();
|
|
1377
|
+
const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
|
|
1228
1378
|
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
1379
|
+
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
|
|
1380
|
+
const removedByPreRebase = existingPatches.filter(
|
|
1381
|
+
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
1382
|
+
);
|
|
1229
1383
|
existingPatches = this.lockManager.getPatches();
|
|
1230
1384
|
const seenHashes = /* @__PURE__ */ new Set();
|
|
1231
1385
|
for (const p of existingPatches) {
|
|
@@ -1236,8 +1390,28 @@ var ReplayService = class {
|
|
|
1236
1390
|
}
|
|
1237
1391
|
}
|
|
1238
1392
|
existingPatches = this.lockManager.getPatches();
|
|
1239
|
-
|
|
1393
|
+
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1240
1394
|
const warnings = [...this.detector.warnings];
|
|
1395
|
+
if (removedByPreRebase.length > 0) {
|
|
1396
|
+
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
1397
|
+
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
1398
|
+
newPatches = newPatches.filter((p) => {
|
|
1399
|
+
if (removedOriginalCommits.has(p.original_commit)) return false;
|
|
1400
|
+
if (isRevertCommit(p.original_message)) {
|
|
1401
|
+
const revertedMsg = parseRevertedMessage(p.original_message);
|
|
1402
|
+
if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
|
|
1403
|
+
}
|
|
1404
|
+
return true;
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
for (const id of revertedPatchIds) {
|
|
1408
|
+
try {
|
|
1409
|
+
this.lockManager.removePatch(id);
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const revertedSet = new Set(revertedPatchIds);
|
|
1414
|
+
existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
|
|
1241
1415
|
const allPatches = [...existingPatches, ...newPatches];
|
|
1242
1416
|
const commitOpts = options ? {
|
|
1243
1417
|
cliVersion: options.cliVersion ?? "unknown",
|
|
@@ -1248,20 +1422,32 @@ var ReplayService = class {
|
|
|
1248
1422
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1249
1423
|
this.lockManager.addGeneration(genRecord);
|
|
1250
1424
|
const results = await this.applicator.applyPatches(allPatches);
|
|
1425
|
+
this.revertConflictingFiles(results);
|
|
1426
|
+
for (const result of results) {
|
|
1427
|
+
if (result.status === "conflict") {
|
|
1428
|
+
result.patch.status = "unresolved";
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1251
1431
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1252
1432
|
for (const patch of newPatches) {
|
|
1253
1433
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
1254
1434
|
this.lockManager.addPatch(patch);
|
|
1255
1435
|
}
|
|
1256
1436
|
}
|
|
1437
|
+
for (const result of results) {
|
|
1438
|
+
if (result.status === "conflict") {
|
|
1439
|
+
try {
|
|
1440
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1257
1445
|
this.lockManager.save();
|
|
1258
1446
|
if (options?.stageOnly) {
|
|
1259
1447
|
await this.committer.stageAll();
|
|
1260
1448
|
} else {
|
|
1261
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1262
|
-
|
|
1263
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1264
|
-
}
|
|
1449
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1450
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1265
1451
|
}
|
|
1266
1452
|
return this.buildReport(
|
|
1267
1453
|
"normal-regeneration",
|
|
@@ -1270,7 +1456,8 @@ var ReplayService = class {
|
|
|
1270
1456
|
options,
|
|
1271
1457
|
warnings,
|
|
1272
1458
|
rebaseCounts,
|
|
1273
|
-
preRebaseCounts
|
|
1459
|
+
preRebaseCounts,
|
|
1460
|
+
revertedPatchIds.length
|
|
1274
1461
|
);
|
|
1275
1462
|
}
|
|
1276
1463
|
/**
|
|
@@ -1409,6 +1596,10 @@ var ReplayService = class {
|
|
|
1409
1596
|
let conflictAbsorbed = 0;
|
|
1410
1597
|
let contentRefreshed = 0;
|
|
1411
1598
|
for (const patch of patches) {
|
|
1599
|
+
if (patch.status != null) {
|
|
1600
|
+
delete patch.status;
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1412
1603
|
if (patch.base_generation === currentGen) {
|
|
1413
1604
|
try {
|
|
1414
1605
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -1459,12 +1650,31 @@ var ReplayService = class {
|
|
|
1459
1650
|
}
|
|
1460
1651
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1461
1652
|
}
|
|
1653
|
+
/**
|
|
1654
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
1655
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
1656
|
+
*/
|
|
1657
|
+
revertConflictingFiles(results) {
|
|
1658
|
+
for (const result of results) {
|
|
1659
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
1660
|
+
for (const fileResult of result.fileResults) {
|
|
1661
|
+
if (fileResult.status !== "conflict") continue;
|
|
1662
|
+
const filePath = (0, import_node_path3.join)(this.outputDir, fileResult.file);
|
|
1663
|
+
try {
|
|
1664
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
1665
|
+
const stripped = stripConflictMarkers(content);
|
|
1666
|
+
(0, import_node_fs2.writeFileSync)(filePath, stripped);
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1462
1672
|
readFernignorePatterns() {
|
|
1463
1673
|
const fernignorePath = (0, import_node_path3.join)(this.outputDir, ".fernignore");
|
|
1464
1674
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
1465
1675
|
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1466
1676
|
}
|
|
1467
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
|
|
1677
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
1468
1678
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
1469
1679
|
const conflictDetails = conflictResults.map((r) => {
|
|
1470
1680
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -1489,10 +1699,17 @@ var ReplayService = class {
|
|
|
1489
1699
|
patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
|
|
1490
1700
|
patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
|
|
1491
1701
|
patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
|
|
1702
|
+
patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
|
|
1492
1703
|
patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
|
|
1493
1704
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1494
1705
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
1495
1706
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
1707
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
1708
|
+
patchId: r.patch.id,
|
|
1709
|
+
patchMessage: r.patch.original_message,
|
|
1710
|
+
files: r.patch.files,
|
|
1711
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
1712
|
+
})) : void 0,
|
|
1496
1713
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
1497
1714
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
1498
1715
|
};
|
|
@@ -1529,7 +1746,7 @@ var FernignoreMigrator = class {
|
|
|
1529
1746
|
async analyzeMigration() {
|
|
1530
1747
|
const patterns = this.readFernignorePatterns();
|
|
1531
1748
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1532
|
-
const patches = await detector.detectNewPatches();
|
|
1749
|
+
const { patches } = await detector.detectNewPatches();
|
|
1533
1750
|
const trackedByBoth = [];
|
|
1534
1751
|
const fernignoreOnly = [];
|
|
1535
1752
|
const commitsOnly = [];
|
|
@@ -1654,7 +1871,7 @@ var FernignoreMigrator = class {
|
|
|
1654
1871
|
async migrate() {
|
|
1655
1872
|
const analysis = await this.analyzeMigration();
|
|
1656
1873
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1657
|
-
const patches = await detector.detectNewPatches();
|
|
1874
|
+
const { patches } = await detector.detectNewPatches();
|
|
1658
1875
|
const warnings = [];
|
|
1659
1876
|
let patchesCreated = 0;
|
|
1660
1877
|
for (const patch of patches) {
|
|
@@ -1954,17 +2171,82 @@ async function resolve(outputDir, options) {
|
|
|
1954
2171
|
return { success: false, reason: "no-patches" };
|
|
1955
2172
|
}
|
|
1956
2173
|
const git = new GitClient(outputDir);
|
|
2174
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
2175
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
2176
|
+
if (unresolvedPatches.length > 0) {
|
|
2177
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
2178
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
2179
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
2180
|
+
if (markerFiles.length > 0) {
|
|
2181
|
+
for (const patch of unresolvedPatches) {
|
|
2182
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
2183
|
+
}
|
|
2184
|
+
lockManager.save();
|
|
2185
|
+
return {
|
|
2186
|
+
success: false,
|
|
2187
|
+
reason: "conflicts-applied",
|
|
2188
|
+
unresolvedFiles: markerFiles,
|
|
2189
|
+
phase: "applied",
|
|
2190
|
+
patchesApplied: unresolvedPatches.length
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1957
2194
|
if (options?.checkMarkers !== false) {
|
|
1958
|
-
const
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
2195
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
2196
|
+
if (currentMarkerFiles.length > 0) {
|
|
2197
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
2201
|
+
if (patchesToCommit.length > 0) {
|
|
2202
|
+
const currentGen = lock.current_generation;
|
|
2203
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
2204
|
+
let patchesResolved = 0;
|
|
2205
|
+
for (const patch of patchesToCommit) {
|
|
2206
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
2207
|
+
if (!diff || !diff.trim()) {
|
|
2208
|
+
lockManager.removePatch(patch.id);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
2212
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
2213
|
+
lockManager.markPatchResolved(patch.id, {
|
|
2214
|
+
patch_content: diff,
|
|
2215
|
+
content_hash: newContentHash,
|
|
2216
|
+
base_generation: currentGen,
|
|
2217
|
+
files: changedFiles
|
|
2218
|
+
});
|
|
2219
|
+
patchesResolved++;
|
|
1962
2220
|
}
|
|
2221
|
+
lockManager.save();
|
|
2222
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
2223
|
+
await committer2.stageAll();
|
|
2224
|
+
const commitSha2 = await committer2.commitReplay(
|
|
2225
|
+
lock.patches.length,
|
|
2226
|
+
lock.patches,
|
|
2227
|
+
"[fern-replay] Resolved conflicts"
|
|
2228
|
+
);
|
|
2229
|
+
return {
|
|
2230
|
+
success: true,
|
|
2231
|
+
commitSha: commitSha2,
|
|
2232
|
+
phase: "committed",
|
|
2233
|
+
patchesResolved
|
|
2234
|
+
};
|
|
1963
2235
|
}
|
|
1964
2236
|
const committer = new ReplayCommitter(git, outputDir);
|
|
1965
2237
|
await committer.stageAll();
|
|
1966
2238
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
1967
|
-
return { success: true, commitSha };
|
|
2239
|
+
return { success: true, commitSha, phase: "committed" };
|
|
2240
|
+
}
|
|
2241
|
+
async function findConflictMarkerFiles(git) {
|
|
2242
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
2243
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
2244
|
+
}
|
|
2245
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
2246
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
2247
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
2248
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
2249
|
+
return changed.length > 0 ? changed : files;
|
|
1968
2250
|
}
|
|
1969
2251
|
|
|
1970
2252
|
// src/commands/status.ts
|
|
@@ -2006,6 +2288,9 @@ function status(outputDir) {
|
|
|
2006
2288
|
forget,
|
|
2007
2289
|
isGenerationCommit,
|
|
2008
2290
|
isReplayCommit,
|
|
2291
|
+
isRevertCommit,
|
|
2292
|
+
parseRevertedMessage,
|
|
2293
|
+
parseRevertedSha,
|
|
2009
2294
|
reset,
|
|
2010
2295
|
resolve,
|
|
2011
2296
|
status,
|