@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.js
CHANGED
|
@@ -111,6 +111,9 @@ var init_GitClient = __esm({
|
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
async getCommitBody(commitSha) {
|
|
115
|
+
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
116
|
+
}
|
|
114
117
|
getRepoPath() {
|
|
115
118
|
return this.repoPath;
|
|
116
119
|
}
|
|
@@ -138,6 +141,17 @@ function isGenerationCommit(commit) {
|
|
|
138
141
|
function isReplayCommit(commit) {
|
|
139
142
|
return commit.message.startsWith("[fern-replay]");
|
|
140
143
|
}
|
|
144
|
+
function isRevertCommit(message) {
|
|
145
|
+
return message.startsWith('Revert "');
|
|
146
|
+
}
|
|
147
|
+
function parseRevertedSha(fullBody) {
|
|
148
|
+
const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
|
|
149
|
+
return match?.[1];
|
|
150
|
+
}
|
|
151
|
+
function parseRevertedMessage(subject) {
|
|
152
|
+
const match = subject.match(/^Revert "(.+)"$/);
|
|
153
|
+
return match?.[1];
|
|
154
|
+
}
|
|
141
155
|
|
|
142
156
|
// src/LockfileManager.ts
|
|
143
157
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
@@ -229,6 +243,26 @@ var LockfileManager = class {
|
|
|
229
243
|
this.ensureLoaded();
|
|
230
244
|
this.lock.patches = [];
|
|
231
245
|
}
|
|
246
|
+
getUnresolvedPatches() {
|
|
247
|
+
this.ensureLoaded();
|
|
248
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
249
|
+
}
|
|
250
|
+
getResolvingPatches() {
|
|
251
|
+
this.ensureLoaded();
|
|
252
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
253
|
+
}
|
|
254
|
+
markPatchUnresolved(patchId) {
|
|
255
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
256
|
+
}
|
|
257
|
+
markPatchResolved(patchId, updates) {
|
|
258
|
+
this.ensureLoaded();
|
|
259
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
260
|
+
if (!patch) {
|
|
261
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
262
|
+
}
|
|
263
|
+
delete patch.status;
|
|
264
|
+
Object.assign(patch, updates);
|
|
265
|
+
}
|
|
232
266
|
getPatches() {
|
|
233
267
|
this.ensureLoaded();
|
|
234
268
|
return this.lock.patches;
|
|
@@ -280,14 +314,14 @@ var ReplayDetector = class {
|
|
|
280
314
|
const lock = this.lockManager.read();
|
|
281
315
|
const lastGen = this.getLastGeneration(lock);
|
|
282
316
|
if (!lastGen) {
|
|
283
|
-
return [];
|
|
317
|
+
return { patches: [], revertedPatchIds: [] };
|
|
284
318
|
}
|
|
285
319
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
286
320
|
if (!exists) {
|
|
287
321
|
this.warnings.push(
|
|
288
322
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
|
|
289
323
|
);
|
|
290
|
-
return [];
|
|
324
|
+
return { patches: [], revertedPatchIds: [] };
|
|
291
325
|
}
|
|
292
326
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
293
327
|
if (!isAncestor) {
|
|
@@ -301,7 +335,7 @@ var ReplayDetector = class {
|
|
|
301
335
|
this.sdkOutputDir
|
|
302
336
|
]);
|
|
303
337
|
if (!log.trim()) {
|
|
304
|
-
return [];
|
|
338
|
+
return { patches: [], revertedPatchIds: [] };
|
|
305
339
|
}
|
|
306
340
|
const commits = this.parseGitLog(log);
|
|
307
341
|
const newPatches = [];
|
|
@@ -337,7 +371,60 @@ var ReplayDetector = class {
|
|
|
337
371
|
patch_content: patchContent
|
|
338
372
|
});
|
|
339
373
|
}
|
|
340
|
-
|
|
374
|
+
newPatches.reverse();
|
|
375
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
376
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
377
|
+
for (let i = 0; i < newPatches.length; i++) {
|
|
378
|
+
const patch = newPatches[i];
|
|
379
|
+
if (!isRevertCommit(patch.original_message)) continue;
|
|
380
|
+
let body = "";
|
|
381
|
+
try {
|
|
382
|
+
body = await this.git.getCommitBody(patch.original_commit);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
const revertedSha = parseRevertedSha(body);
|
|
386
|
+
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
387
|
+
let matchedExisting = false;
|
|
388
|
+
if (revertedSha) {
|
|
389
|
+
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
390
|
+
if (existing) {
|
|
391
|
+
revertedPatchIdSet.add(existing.id);
|
|
392
|
+
revertIndicesToRemove.add(i);
|
|
393
|
+
matchedExisting = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!matchedExisting && revertedMessage) {
|
|
397
|
+
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
398
|
+
if (existing) {
|
|
399
|
+
revertedPatchIdSet.add(existing.id);
|
|
400
|
+
revertIndicesToRemove.add(i);
|
|
401
|
+
matchedExisting = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (matchedExisting) continue;
|
|
405
|
+
let matchedNew = false;
|
|
406
|
+
if (revertedSha) {
|
|
407
|
+
const idx = newPatches.findIndex(
|
|
408
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
|
|
409
|
+
);
|
|
410
|
+
if (idx !== -1) {
|
|
411
|
+
revertIndicesToRemove.add(i);
|
|
412
|
+
revertIndicesToRemove.add(idx);
|
|
413
|
+
matchedNew = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!matchedNew && revertedMessage) {
|
|
417
|
+
const idx = newPatches.findIndex(
|
|
418
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
|
|
419
|
+
);
|
|
420
|
+
if (idx !== -1) {
|
|
421
|
+
revertIndicesToRemove.add(i);
|
|
422
|
+
revertIndicesToRemove.add(idx);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
427
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
341
428
|
}
|
|
342
429
|
/**
|
|
343
430
|
* Compute content hash for deduplication.
|
|
@@ -348,31 +435,34 @@ var ReplayDetector = class {
|
|
|
348
435
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
349
436
|
return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
|
|
350
437
|
}
|
|
351
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
440
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
441
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
442
|
+
*/
|
|
352
443
|
async detectPatchesViaTreeDiff(lastGen) {
|
|
353
444
|
const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
|
|
354
445
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
355
|
-
if (files.length === 0) return [];
|
|
446
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
356
447
|
const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
|
|
357
|
-
if (!diff.trim()) return [];
|
|
448
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
358
449
|
const contentHash = this.computeContentHash(diff);
|
|
359
450
|
const lock = this.lockManager.read();
|
|
360
451
|
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
361
|
-
return [];
|
|
452
|
+
return { patches: [], revertedPatchIds: [] };
|
|
362
453
|
}
|
|
363
454
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
];
|
|
455
|
+
const compositePatch = {
|
|
456
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
457
|
+
content_hash: contentHash,
|
|
458
|
+
original_commit: headSha,
|
|
459
|
+
original_message: "Customer customizations (composite)",
|
|
460
|
+
original_author: "composite",
|
|
461
|
+
base_generation: lastGen.commit_sha,
|
|
462
|
+
files,
|
|
463
|
+
patch_content: diff
|
|
464
|
+
};
|
|
465
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
376
466
|
}
|
|
377
467
|
parseGitLog(log) {
|
|
378
468
|
return log.trim().split("\n").map((line) => {
|
|
@@ -929,12 +1019,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
929
1019
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
930
1020
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
931
1021
|
}
|
|
932
|
-
async commitReplay(_patchCount, patches) {
|
|
1022
|
+
async commitReplay(_patchCount, patches, message) {
|
|
933
1023
|
await this.stageAll();
|
|
934
1024
|
if (!await this.hasStagedChanges()) {
|
|
935
1025
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
936
1026
|
}
|
|
937
|
-
let fullMessage = `[fern-replay] Applied customizations`;
|
|
1027
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
938
1028
|
if (patches && patches.length > 0) {
|
|
939
1029
|
fullMessage += "\n\nPatches replayed:";
|
|
940
1030
|
for (const patch of patches) {
|
|
@@ -970,10 +1060,40 @@ CLI Version: ${options.cliVersion}`;
|
|
|
970
1060
|
};
|
|
971
1061
|
|
|
972
1062
|
// src/ReplayService.ts
|
|
973
|
-
|
|
974
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1063
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
975
1064
|
import { join as join3 } from "path";
|
|
976
1065
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1066
|
+
init_GitClient();
|
|
1067
|
+
|
|
1068
|
+
// src/conflict-utils.ts
|
|
1069
|
+
function stripConflictMarkers(content) {
|
|
1070
|
+
const lines = content.split("\n");
|
|
1071
|
+
const result = [];
|
|
1072
|
+
let inConflict = false;
|
|
1073
|
+
let inOurs = false;
|
|
1074
|
+
for (const line of lines) {
|
|
1075
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1076
|
+
inConflict = true;
|
|
1077
|
+
inOurs = true;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (inConflict && line === "=======") {
|
|
1081
|
+
inOurs = false;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1085
|
+
inConflict = false;
|
|
1086
|
+
inOurs = false;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!inConflict || inOurs) {
|
|
1090
|
+
result.push(line);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return result.join("\n");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/ReplayService.ts
|
|
977
1097
|
var ReplayService = class {
|
|
978
1098
|
git;
|
|
979
1099
|
detector;
|
|
@@ -1029,13 +1149,27 @@ var ReplayService = class {
|
|
|
1029
1149
|
this.lockManager.initializeInMemory(record);
|
|
1030
1150
|
} else {
|
|
1031
1151
|
this.lockManager.read();
|
|
1152
|
+
const unresolvedPatches = [
|
|
1153
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
1154
|
+
...this.lockManager.getResolvingPatches()
|
|
1155
|
+
];
|
|
1032
1156
|
this.lockManager.addGeneration(record);
|
|
1033
1157
|
this.lockManager.clearPatches();
|
|
1158
|
+
for (const patch of unresolvedPatches) {
|
|
1159
|
+
this.lockManager.addPatch(patch);
|
|
1160
|
+
}
|
|
1034
1161
|
}
|
|
1035
1162
|
this.lockManager.save();
|
|
1036
1163
|
try {
|
|
1037
|
-
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1164
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
1038
1165
|
if (redetectedPatches.length > 0) {
|
|
1166
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1167
|
+
const currentPatches = this.lockManager.getPatches();
|
|
1168
|
+
for (const patch of currentPatches) {
|
|
1169
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
1170
|
+
this.lockManager.removePatch(patch.id);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1039
1173
|
for (const patch of redetectedPatches) {
|
|
1040
1174
|
this.lockManager.addPatch(patch);
|
|
1041
1175
|
}
|
|
@@ -1118,7 +1252,7 @@ var ReplayService = class {
|
|
|
1118
1252
|
};
|
|
1119
1253
|
}
|
|
1120
1254
|
async handleNoPatchesRegeneration(options) {
|
|
1121
|
-
const newPatches = await this.detector.detectNewPatches();
|
|
1255
|
+
const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1122
1256
|
const warnings = [...this.detector.warnings];
|
|
1123
1257
|
if (options?.dryRun) {
|
|
1124
1258
|
return {
|
|
@@ -1127,11 +1261,18 @@ var ReplayService = class {
|
|
|
1127
1261
|
patchesApplied: 0,
|
|
1128
1262
|
patchesWithConflicts: 0,
|
|
1129
1263
|
patchesSkipped: 0,
|
|
1264
|
+
patchesReverted: revertedPatchIds.length,
|
|
1130
1265
|
conflicts: [],
|
|
1131
1266
|
wouldApply: newPatches,
|
|
1132
1267
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
1133
1268
|
};
|
|
1134
1269
|
}
|
|
1270
|
+
for (const id of revertedPatchIds) {
|
|
1271
|
+
try {
|
|
1272
|
+
this.lockManager.removePatch(id);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1135
1276
|
const commitOpts = options ? {
|
|
1136
1277
|
cliVersion: options.cliVersion ?? "unknown",
|
|
1137
1278
|
generatorVersions: options.generatorVersions ?? {},
|
|
@@ -1143,6 +1284,12 @@ var ReplayService = class {
|
|
|
1143
1284
|
let results = [];
|
|
1144
1285
|
if (newPatches.length > 0) {
|
|
1145
1286
|
results = await this.applicator.applyPatches(newPatches);
|
|
1287
|
+
this.revertConflictingFiles(results);
|
|
1288
|
+
for (const result of results) {
|
|
1289
|
+
if (result.status === "conflict") {
|
|
1290
|
+
result.patch.status = "unresolved";
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1146
1293
|
}
|
|
1147
1294
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1148
1295
|
for (const patch of newPatches) {
|
|
@@ -1153,20 +1300,18 @@ var ReplayService = class {
|
|
|
1153
1300
|
this.lockManager.save();
|
|
1154
1301
|
if (newPatches.length > 0) {
|
|
1155
1302
|
if (!options?.stageOnly) {
|
|
1156
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1157
|
-
|
|
1158
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1159
|
-
}
|
|
1303
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1304
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1160
1305
|
} else {
|
|
1161
1306
|
await this.committer.stageAll();
|
|
1162
1307
|
}
|
|
1163
1308
|
}
|
|
1164
|
-
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
|
|
1309
|
+
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
|
|
1165
1310
|
}
|
|
1166
1311
|
async handleNormalRegeneration(options) {
|
|
1167
1312
|
if (options?.dryRun) {
|
|
1168
1313
|
const existingPatches2 = this.lockManager.getPatches();
|
|
1169
|
-
const newPatches2 = await this.detector.detectNewPatches();
|
|
1314
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
1170
1315
|
const warnings2 = [...this.detector.warnings];
|
|
1171
1316
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
1172
1317
|
return {
|
|
@@ -1175,13 +1320,19 @@ var ReplayService = class {
|
|
|
1175
1320
|
patchesApplied: 0,
|
|
1176
1321
|
patchesWithConflicts: 0,
|
|
1177
1322
|
patchesSkipped: 0,
|
|
1323
|
+
patchesReverted: dryRunReverted.length,
|
|
1178
1324
|
conflicts: [],
|
|
1179
1325
|
wouldApply: allPatches2,
|
|
1180
1326
|
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
1181
1327
|
};
|
|
1182
1328
|
}
|
|
1183
1329
|
let existingPatches = this.lockManager.getPatches();
|
|
1330
|
+
const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
|
|
1184
1331
|
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
1332
|
+
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
|
|
1333
|
+
const removedByPreRebase = existingPatches.filter(
|
|
1334
|
+
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
1335
|
+
);
|
|
1185
1336
|
existingPatches = this.lockManager.getPatches();
|
|
1186
1337
|
const seenHashes = /* @__PURE__ */ new Set();
|
|
1187
1338
|
for (const p of existingPatches) {
|
|
@@ -1192,8 +1343,28 @@ var ReplayService = class {
|
|
|
1192
1343
|
}
|
|
1193
1344
|
}
|
|
1194
1345
|
existingPatches = this.lockManager.getPatches();
|
|
1195
|
-
|
|
1346
|
+
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
1196
1347
|
const warnings = [...this.detector.warnings];
|
|
1348
|
+
if (removedByPreRebase.length > 0) {
|
|
1349
|
+
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
1350
|
+
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
1351
|
+
newPatches = newPatches.filter((p) => {
|
|
1352
|
+
if (removedOriginalCommits.has(p.original_commit)) return false;
|
|
1353
|
+
if (isRevertCommit(p.original_message)) {
|
|
1354
|
+
const revertedMsg = parseRevertedMessage(p.original_message);
|
|
1355
|
+
if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
|
|
1356
|
+
}
|
|
1357
|
+
return true;
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
for (const id of revertedPatchIds) {
|
|
1361
|
+
try {
|
|
1362
|
+
this.lockManager.removePatch(id);
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const revertedSet = new Set(revertedPatchIds);
|
|
1367
|
+
existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
|
|
1197
1368
|
const allPatches = [...existingPatches, ...newPatches];
|
|
1198
1369
|
const commitOpts = options ? {
|
|
1199
1370
|
cliVersion: options.cliVersion ?? "unknown",
|
|
@@ -1204,20 +1375,32 @@ var ReplayService = class {
|
|
|
1204
1375
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1205
1376
|
this.lockManager.addGeneration(genRecord);
|
|
1206
1377
|
const results = await this.applicator.applyPatches(allPatches);
|
|
1378
|
+
this.revertConflictingFiles(results);
|
|
1379
|
+
for (const result of results) {
|
|
1380
|
+
if (result.status === "conflict") {
|
|
1381
|
+
result.patch.status = "unresolved";
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1207
1384
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1208
1385
|
for (const patch of newPatches) {
|
|
1209
1386
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
1210
1387
|
this.lockManager.addPatch(patch);
|
|
1211
1388
|
}
|
|
1212
1389
|
}
|
|
1390
|
+
for (const result of results) {
|
|
1391
|
+
if (result.status === "conflict") {
|
|
1392
|
+
try {
|
|
1393
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1213
1398
|
this.lockManager.save();
|
|
1214
1399
|
if (options?.stageOnly) {
|
|
1215
1400
|
await this.committer.stageAll();
|
|
1216
1401
|
} else {
|
|
1217
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1218
|
-
|
|
1219
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1220
|
-
}
|
|
1402
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1403
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1221
1404
|
}
|
|
1222
1405
|
return this.buildReport(
|
|
1223
1406
|
"normal-regeneration",
|
|
@@ -1226,7 +1409,8 @@ var ReplayService = class {
|
|
|
1226
1409
|
options,
|
|
1227
1410
|
warnings,
|
|
1228
1411
|
rebaseCounts,
|
|
1229
|
-
preRebaseCounts
|
|
1412
|
+
preRebaseCounts,
|
|
1413
|
+
revertedPatchIds.length
|
|
1230
1414
|
);
|
|
1231
1415
|
}
|
|
1232
1416
|
/**
|
|
@@ -1365,6 +1549,10 @@ var ReplayService = class {
|
|
|
1365
1549
|
let conflictAbsorbed = 0;
|
|
1366
1550
|
let contentRefreshed = 0;
|
|
1367
1551
|
for (const patch of patches) {
|
|
1552
|
+
if (patch.status != null) {
|
|
1553
|
+
delete patch.status;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1368
1556
|
if (patch.base_generation === currentGen) {
|
|
1369
1557
|
try {
|
|
1370
1558
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -1415,12 +1603,31 @@ var ReplayService = class {
|
|
|
1415
1603
|
}
|
|
1416
1604
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1417
1605
|
}
|
|
1606
|
+
/**
|
|
1607
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
1608
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
1609
|
+
*/
|
|
1610
|
+
revertConflictingFiles(results) {
|
|
1611
|
+
for (const result of results) {
|
|
1612
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
1613
|
+
for (const fileResult of result.fileResults) {
|
|
1614
|
+
if (fileResult.status !== "conflict") continue;
|
|
1615
|
+
const filePath = join3(this.outputDir, fileResult.file);
|
|
1616
|
+
try {
|
|
1617
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1618
|
+
const stripped = stripConflictMarkers(content);
|
|
1619
|
+
writeFileSync2(filePath, stripped);
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1418
1625
|
readFernignorePatterns() {
|
|
1419
1626
|
const fernignorePath = join3(this.outputDir, ".fernignore");
|
|
1420
1627
|
if (!existsSync2(fernignorePath)) return [];
|
|
1421
1628
|
return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1422
1629
|
}
|
|
1423
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
|
|
1630
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
1424
1631
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
1425
1632
|
const conflictDetails = conflictResults.map((r) => {
|
|
1426
1633
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -1445,10 +1652,17 @@ var ReplayService = class {
|
|
|
1445
1652
|
patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
|
|
1446
1653
|
patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
|
|
1447
1654
|
patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
|
|
1655
|
+
patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
|
|
1448
1656
|
patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
|
|
1449
1657
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1450
1658
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
1451
1659
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
1660
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
1661
|
+
patchId: r.patch.id,
|
|
1662
|
+
patchMessage: r.patch.original_message,
|
|
1663
|
+
files: r.patch.files,
|
|
1664
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
1665
|
+
})) : void 0,
|
|
1452
1666
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
1453
1667
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
1454
1668
|
};
|
|
@@ -1457,7 +1671,7 @@ var ReplayService = class {
|
|
|
1457
1671
|
|
|
1458
1672
|
// src/FernignoreMigrator.ts
|
|
1459
1673
|
import { createHash as createHash2 } from "crypto";
|
|
1460
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as
|
|
1674
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1461
1675
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1462
1676
|
import { minimatch as minimatch3 } from "minimatch";
|
|
1463
1677
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
@@ -1485,7 +1699,7 @@ var FernignoreMigrator = class {
|
|
|
1485
1699
|
async analyzeMigration() {
|
|
1486
1700
|
const patterns = this.readFernignorePatterns();
|
|
1487
1701
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1488
|
-
const patches = await detector.detectNewPatches();
|
|
1702
|
+
const { patches } = await detector.detectNewPatches();
|
|
1489
1703
|
const trackedByBoth = [];
|
|
1490
1704
|
const fernignoreOnly = [];
|
|
1491
1705
|
const commitsOnly = [];
|
|
@@ -1610,7 +1824,7 @@ var FernignoreMigrator = class {
|
|
|
1610
1824
|
async migrate() {
|
|
1611
1825
|
const analysis = await this.analyzeMigration();
|
|
1612
1826
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
1613
|
-
const patches = await detector.detectNewPatches();
|
|
1827
|
+
const { patches } = await detector.detectNewPatches();
|
|
1614
1828
|
const warnings = [];
|
|
1615
1829
|
let patchesCreated = 0;
|
|
1616
1830
|
for (const patch of patches) {
|
|
@@ -1645,13 +1859,13 @@ var FernignoreMigrator = class {
|
|
|
1645
1859
|
if (!existsSync3(dir)) {
|
|
1646
1860
|
mkdirSync2(dir, { recursive: true });
|
|
1647
1861
|
}
|
|
1648
|
-
|
|
1862
|
+
writeFileSync3(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
|
|
1649
1863
|
}
|
|
1650
1864
|
};
|
|
1651
1865
|
|
|
1652
1866
|
// src/commands/bootstrap.ts
|
|
1653
1867
|
import { createHash as createHash3 } from "crypto";
|
|
1654
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
1868
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1655
1869
|
import { join as join5 } from "path";
|
|
1656
1870
|
init_GitClient();
|
|
1657
1871
|
async function bootstrap(outputDir, options) {
|
|
@@ -1837,7 +2051,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
1837
2051
|
content += "\n";
|
|
1838
2052
|
}
|
|
1839
2053
|
content += toAdd.join("\n") + "\n";
|
|
1840
|
-
|
|
2054
|
+
writeFileSync4(fernignorePath, content, "utf-8");
|
|
1841
2055
|
return true;
|
|
1842
2056
|
}
|
|
1843
2057
|
function computeContentHash(patchContent) {
|
|
@@ -1910,17 +2124,82 @@ async function resolve(outputDir, options) {
|
|
|
1910
2124
|
return { success: false, reason: "no-patches" };
|
|
1911
2125
|
}
|
|
1912
2126
|
const git = new GitClient(outputDir);
|
|
2127
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
2128
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
2129
|
+
if (unresolvedPatches.length > 0) {
|
|
2130
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
2131
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
2132
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
2133
|
+
if (markerFiles.length > 0) {
|
|
2134
|
+
for (const patch of unresolvedPatches) {
|
|
2135
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
2136
|
+
}
|
|
2137
|
+
lockManager.save();
|
|
2138
|
+
return {
|
|
2139
|
+
success: false,
|
|
2140
|
+
reason: "conflicts-applied",
|
|
2141
|
+
unresolvedFiles: markerFiles,
|
|
2142
|
+
phase: "applied",
|
|
2143
|
+
patchesApplied: unresolvedPatches.length
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1913
2147
|
if (options?.checkMarkers !== false) {
|
|
1914
|
-
const
|
|
1915
|
-
if (
|
|
1916
|
-
|
|
1917
|
-
|
|
2148
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
2149
|
+
if (currentMarkerFiles.length > 0) {
|
|
2150
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
2154
|
+
if (patchesToCommit.length > 0) {
|
|
2155
|
+
const currentGen = lock.current_generation;
|
|
2156
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
2157
|
+
let patchesResolved = 0;
|
|
2158
|
+
for (const patch of patchesToCommit) {
|
|
2159
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
2160
|
+
if (!diff || !diff.trim()) {
|
|
2161
|
+
lockManager.removePatch(patch.id);
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
2165
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
2166
|
+
lockManager.markPatchResolved(patch.id, {
|
|
2167
|
+
patch_content: diff,
|
|
2168
|
+
content_hash: newContentHash,
|
|
2169
|
+
base_generation: currentGen,
|
|
2170
|
+
files: changedFiles
|
|
2171
|
+
});
|
|
2172
|
+
patchesResolved++;
|
|
1918
2173
|
}
|
|
2174
|
+
lockManager.save();
|
|
2175
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
2176
|
+
await committer2.stageAll();
|
|
2177
|
+
const commitSha2 = await committer2.commitReplay(
|
|
2178
|
+
lock.patches.length,
|
|
2179
|
+
lock.patches,
|
|
2180
|
+
"[fern-replay] Resolved conflicts"
|
|
2181
|
+
);
|
|
2182
|
+
return {
|
|
2183
|
+
success: true,
|
|
2184
|
+
commitSha: commitSha2,
|
|
2185
|
+
phase: "committed",
|
|
2186
|
+
patchesResolved
|
|
2187
|
+
};
|
|
1919
2188
|
}
|
|
1920
2189
|
const committer = new ReplayCommitter(git, outputDir);
|
|
1921
2190
|
await committer.stageAll();
|
|
1922
2191
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
1923
|
-
return { success: true, commitSha };
|
|
2192
|
+
return { success: true, commitSha, phase: "committed" };
|
|
2193
|
+
}
|
|
2194
|
+
async function findConflictMarkerFiles(git) {
|
|
2195
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
2196
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
2197
|
+
}
|
|
2198
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
2199
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
2200
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
2201
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
2202
|
+
return changed.length > 0 ? changed : files;
|
|
1924
2203
|
}
|
|
1925
2204
|
|
|
1926
2205
|
// src/commands/status.ts
|
|
@@ -1961,6 +2240,9 @@ export {
|
|
|
1961
2240
|
forget,
|
|
1962
2241
|
isGenerationCommit,
|
|
1963
2242
|
isReplayCommit,
|
|
2243
|
+
isRevertCommit,
|
|
2244
|
+
parseRevertedMessage,
|
|
2245
|
+
parseRevertedSha,
|
|
1964
2246
|
reset,
|
|
1965
2247
|
resolve,
|
|
1966
2248
|
status,
|