@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/cli.cjs
CHANGED
|
@@ -13111,6 +13111,12 @@ var import_node_fs = require("fs");
|
|
|
13111
13111
|
var import_node_path2 = require("path");
|
|
13112
13112
|
var import_yaml = __toESM(require_dist3(), 1);
|
|
13113
13113
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
13114
|
+
var LockfileNotFoundError = class extends Error {
|
|
13115
|
+
constructor(path2) {
|
|
13116
|
+
super(`Lockfile not found: ${path2}`);
|
|
13117
|
+
this.name = "LockfileNotFoundError";
|
|
13118
|
+
}
|
|
13119
|
+
};
|
|
13114
13120
|
var LockfileManager = class {
|
|
13115
13121
|
outputDir;
|
|
13116
13122
|
lock = null;
|
|
@@ -13130,12 +13136,16 @@ var LockfileManager = class {
|
|
|
13130
13136
|
if (this.lock) {
|
|
13131
13137
|
return this.lock;
|
|
13132
13138
|
}
|
|
13133
|
-
|
|
13134
|
-
|
|
13139
|
+
try {
|
|
13140
|
+
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
13141
|
+
this.lock = (0, import_yaml.parse)(content);
|
|
13142
|
+
return this.lock;
|
|
13143
|
+
} catch (error) {
|
|
13144
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
13145
|
+
throw new LockfileNotFoundError(this.lockfilePath);
|
|
13146
|
+
}
|
|
13147
|
+
throw error;
|
|
13135
13148
|
}
|
|
13136
|
-
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
13137
|
-
this.lock = (0, import_yaml.parse)(content);
|
|
13138
|
-
return this.lock;
|
|
13139
13149
|
}
|
|
13140
13150
|
initialize(firstGeneration) {
|
|
13141
13151
|
this.initializeInMemory(firstGeneration);
|
|
@@ -13312,13 +13322,21 @@ var ReplayDetector = class {
|
|
|
13312
13322
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
13313
13323
|
if (!exists2) {
|
|
13314
13324
|
this.warnings.push(
|
|
13315
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history.
|
|
13325
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
13326
|
+
);
|
|
13327
|
+
return this.detectPatchesViaTreeDiff(
|
|
13328
|
+
lastGen,
|
|
13329
|
+
/* commitKnownMissing */
|
|
13330
|
+
true
|
|
13316
13331
|
);
|
|
13317
|
-
return { patches: [], revertedPatchIds: [] };
|
|
13318
13332
|
}
|
|
13319
13333
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
13320
13334
|
if (!isAncestor) {
|
|
13321
|
-
return this.detectPatchesViaTreeDiff(
|
|
13335
|
+
return this.detectPatchesViaTreeDiff(
|
|
13336
|
+
lastGen,
|
|
13337
|
+
/* commitKnownMissing */
|
|
13338
|
+
false
|
|
13339
|
+
);
|
|
13322
13340
|
}
|
|
13323
13341
|
const log = await this.git.exec([
|
|
13324
13342
|
"log",
|
|
@@ -13344,7 +13362,15 @@ var ReplayDetector = class {
|
|
|
13344
13362
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
13345
13363
|
continue;
|
|
13346
13364
|
}
|
|
13347
|
-
|
|
13365
|
+
let patchContent;
|
|
13366
|
+
try {
|
|
13367
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
13368
|
+
} catch {
|
|
13369
|
+
this.warnings.push(
|
|
13370
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
13371
|
+
);
|
|
13372
|
+
continue;
|
|
13373
|
+
}
|
|
13348
13374
|
const contentHash = this.computeContentHash(patchContent);
|
|
13349
13375
|
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
13350
13376
|
continue;
|
|
@@ -13437,11 +13463,15 @@ var ReplayDetector = class {
|
|
|
13437
13463
|
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
13438
13464
|
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
13439
13465
|
*/
|
|
13440
|
-
async detectPatchesViaTreeDiff(lastGen) {
|
|
13441
|
-
const
|
|
13466
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
13467
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
13468
|
+
if (!diffBase) {
|
|
13469
|
+
return this.detectPatchesViaCommitScan();
|
|
13470
|
+
}
|
|
13471
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
13442
13472
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
13443
13473
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
13444
|
-
const diff = await this.git.exec(["diff",
|
|
13474
|
+
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
13445
13475
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
13446
13476
|
const contentHash = this.computeContentHash(diff);
|
|
13447
13477
|
const lock = this.lockManager.read();
|
|
@@ -13455,12 +13485,113 @@ var ReplayDetector = class {
|
|
|
13455
13485
|
original_commit: headSha,
|
|
13456
13486
|
original_message: "Customer customizations (composite)",
|
|
13457
13487
|
original_author: "composite",
|
|
13458
|
-
|
|
13488
|
+
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
13489
|
+
// reference to find base file content. diffBase may be the tree_hash.
|
|
13490
|
+
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
13459
13491
|
files,
|
|
13460
13492
|
patch_content: diff
|
|
13461
13493
|
};
|
|
13462
13494
|
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
13463
13495
|
}
|
|
13496
|
+
/**
|
|
13497
|
+
* Last-resort detection when both generation commit and tree are unreachable.
|
|
13498
|
+
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
13499
|
+
* skips creation-only commits (squashed history after force push).
|
|
13500
|
+
*
|
|
13501
|
+
* Detected patches use the commit's parent as base_generation so the applicator
|
|
13502
|
+
* can find base file content from the parent's tree (which IS reachable).
|
|
13503
|
+
*/
|
|
13504
|
+
async detectPatchesViaCommitScan() {
|
|
13505
|
+
const lock = this.lockManager.read();
|
|
13506
|
+
const log = await this.git.exec([
|
|
13507
|
+
"log",
|
|
13508
|
+
"--max-count=200",
|
|
13509
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
13510
|
+
"HEAD",
|
|
13511
|
+
"--",
|
|
13512
|
+
this.sdkOutputDir
|
|
13513
|
+
]);
|
|
13514
|
+
if (!log.trim()) {
|
|
13515
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13516
|
+
}
|
|
13517
|
+
const commits = this.parseGitLog(log);
|
|
13518
|
+
const newPatches = [];
|
|
13519
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
13520
|
+
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
13521
|
+
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
13522
|
+
for (const commit of commits) {
|
|
13523
|
+
if (isGenerationCommit(commit)) {
|
|
13524
|
+
continue;
|
|
13525
|
+
}
|
|
13526
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
13527
|
+
if (parents.length > 1) {
|
|
13528
|
+
continue;
|
|
13529
|
+
}
|
|
13530
|
+
if (existingCommits.has(commit.sha)) {
|
|
13531
|
+
continue;
|
|
13532
|
+
}
|
|
13533
|
+
let patchContent;
|
|
13534
|
+
try {
|
|
13535
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
13536
|
+
} catch {
|
|
13537
|
+
continue;
|
|
13538
|
+
}
|
|
13539
|
+
const contentHash = this.computeContentHash(patchContent);
|
|
13540
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
13541
|
+
continue;
|
|
13542
|
+
}
|
|
13543
|
+
if (this.isCreationOnlyPatch(patchContent)) {
|
|
13544
|
+
continue;
|
|
13545
|
+
}
|
|
13546
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
13547
|
+
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f));
|
|
13548
|
+
if (files.length === 0) {
|
|
13549
|
+
continue;
|
|
13550
|
+
}
|
|
13551
|
+
if (parents.length === 0) {
|
|
13552
|
+
continue;
|
|
13553
|
+
}
|
|
13554
|
+
const parentSha = parents[0];
|
|
13555
|
+
newPatches.push({
|
|
13556
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
13557
|
+
content_hash: contentHash,
|
|
13558
|
+
original_commit: commit.sha,
|
|
13559
|
+
original_message: commit.message,
|
|
13560
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
13561
|
+
base_generation: parentSha,
|
|
13562
|
+
files,
|
|
13563
|
+
patch_content: patchContent
|
|
13564
|
+
});
|
|
13565
|
+
}
|
|
13566
|
+
newPatches.reverse();
|
|
13567
|
+
return { patches: newPatches, revertedPatchIds: [] };
|
|
13568
|
+
}
|
|
13569
|
+
/**
|
|
13570
|
+
* Check if a format-patch consists entirely of new-file creations.
|
|
13571
|
+
* Used to identify squashed commits after force push, which create all files
|
|
13572
|
+
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
13573
|
+
*/
|
|
13574
|
+
isCreationOnlyPatch(patchContent) {
|
|
13575
|
+
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
13576
|
+
if (diffOldHeaders.length === 0) {
|
|
13577
|
+
return false;
|
|
13578
|
+
}
|
|
13579
|
+
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
13580
|
+
}
|
|
13581
|
+
/**
|
|
13582
|
+
* Resolve the best available diff base for a generation record.
|
|
13583
|
+
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
13584
|
+
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
13585
|
+
*/
|
|
13586
|
+
async resolveDiffBase(gen, commitKnownMissing) {
|
|
13587
|
+
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
13588
|
+
return gen.commit_sha;
|
|
13589
|
+
}
|
|
13590
|
+
if (await this.git.treeExists(gen.tree_hash)) {
|
|
13591
|
+
return gen.tree_hash;
|
|
13592
|
+
}
|
|
13593
|
+
return null;
|
|
13594
|
+
}
|
|
13464
13595
|
parseGitLog(log) {
|
|
13465
13596
|
return log.trim().split("\n").map((line) => {
|
|
13466
13597
|
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
@@ -14815,29 +14946,74 @@ var import_node_os = require("os");
|
|
|
14815
14946
|
var import_node_path3 = require("path");
|
|
14816
14947
|
|
|
14817
14948
|
// src/conflict-utils.ts
|
|
14949
|
+
var CONFLICT_OPENER = "<<<<<<< Generated";
|
|
14950
|
+
var CONFLICT_SEPARATOR = "=======";
|
|
14951
|
+
var CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
14952
|
+
function trimCR(line) {
|
|
14953
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
14954
|
+
}
|
|
14955
|
+
function findConflictRanges(lines) {
|
|
14956
|
+
const ranges = [];
|
|
14957
|
+
let i = 0;
|
|
14958
|
+
while (i < lines.length) {
|
|
14959
|
+
if (trimCR(lines[i]) === CONFLICT_OPENER) {
|
|
14960
|
+
let separatorIdx = -1;
|
|
14961
|
+
let j = i + 1;
|
|
14962
|
+
let found = false;
|
|
14963
|
+
while (j < lines.length) {
|
|
14964
|
+
const trimmed2 = trimCR(lines[j]);
|
|
14965
|
+
if (trimmed2 === CONFLICT_OPENER) {
|
|
14966
|
+
break;
|
|
14967
|
+
}
|
|
14968
|
+
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
14969
|
+
separatorIdx = j;
|
|
14970
|
+
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
14971
|
+
ranges.push({ start: i, separator: separatorIdx, end: j });
|
|
14972
|
+
i = j;
|
|
14973
|
+
found = true;
|
|
14974
|
+
break;
|
|
14975
|
+
}
|
|
14976
|
+
j++;
|
|
14977
|
+
}
|
|
14978
|
+
if (!found) {
|
|
14979
|
+
i++;
|
|
14980
|
+
continue;
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14983
|
+
i++;
|
|
14984
|
+
}
|
|
14985
|
+
return ranges;
|
|
14986
|
+
}
|
|
14818
14987
|
function stripConflictMarkers(content) {
|
|
14819
|
-
const lines = content.split(
|
|
14988
|
+
const lines = content.split(/\r?\n/);
|
|
14989
|
+
const ranges = findConflictRanges(lines);
|
|
14990
|
+
if (ranges.length === 0) {
|
|
14991
|
+
return content;
|
|
14992
|
+
}
|
|
14820
14993
|
const result = [];
|
|
14821
|
-
let
|
|
14822
|
-
let
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14994
|
+
let rangeIdx = 0;
|
|
14995
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14996
|
+
if (rangeIdx < ranges.length) {
|
|
14997
|
+
const range = ranges[rangeIdx];
|
|
14998
|
+
if (i === range.start) {
|
|
14999
|
+
continue;
|
|
15000
|
+
}
|
|
15001
|
+
if (i > range.start && i < range.separator) {
|
|
15002
|
+
result.push(lines[i]);
|
|
15003
|
+
continue;
|
|
15004
|
+
}
|
|
15005
|
+
if (i === range.separator) {
|
|
15006
|
+
continue;
|
|
15007
|
+
}
|
|
15008
|
+
if (i > range.separator && i < range.end) {
|
|
15009
|
+
continue;
|
|
15010
|
+
}
|
|
15011
|
+
if (i === range.end) {
|
|
15012
|
+
rangeIdx++;
|
|
15013
|
+
continue;
|
|
15014
|
+
}
|
|
14840
15015
|
}
|
|
15016
|
+
result.push(lines[i]);
|
|
14841
15017
|
}
|
|
14842
15018
|
return result.join("\n");
|
|
14843
15019
|
}
|
|
@@ -14908,6 +15084,37 @@ function diffIndices(buffer1, buffer2) {
|
|
|
14908
15084
|
result.reverse();
|
|
14909
15085
|
return result;
|
|
14910
15086
|
}
|
|
15087
|
+
function diffPatch(buffer1, buffer2) {
|
|
15088
|
+
const lcs = LCS(buffer1, buffer2);
|
|
15089
|
+
let result = [];
|
|
15090
|
+
let tail1 = buffer1.length;
|
|
15091
|
+
let tail2 = buffer2.length;
|
|
15092
|
+
function chunkDescription(buffer, offset, length) {
|
|
15093
|
+
let chunk = [];
|
|
15094
|
+
for (let i = 0; i < length; i++) {
|
|
15095
|
+
chunk.push(buffer[offset + i]);
|
|
15096
|
+
}
|
|
15097
|
+
return {
|
|
15098
|
+
offset,
|
|
15099
|
+
length,
|
|
15100
|
+
chunk
|
|
15101
|
+
};
|
|
15102
|
+
}
|
|
15103
|
+
for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {
|
|
15104
|
+
const mismatchLength1 = tail1 - candidate.buffer1index - 1;
|
|
15105
|
+
const mismatchLength2 = tail2 - candidate.buffer2index - 1;
|
|
15106
|
+
tail1 = candidate.buffer1index;
|
|
15107
|
+
tail2 = candidate.buffer2index;
|
|
15108
|
+
if (mismatchLength1 || mismatchLength2) {
|
|
15109
|
+
result.push({
|
|
15110
|
+
buffer1: chunkDescription(buffer1, candidate.buffer1index + 1, mismatchLength1),
|
|
15111
|
+
buffer2: chunkDescription(buffer2, candidate.buffer2index + 1, mismatchLength2)
|
|
15112
|
+
});
|
|
15113
|
+
}
|
|
15114
|
+
}
|
|
15115
|
+
result.reverse();
|
|
15116
|
+
return result;
|
|
15117
|
+
}
|
|
14911
15118
|
function diff3MergeRegions(a, o, b) {
|
|
14912
15119
|
let hunks = [];
|
|
14913
15120
|
function addHunk(h, ab) {
|
|
@@ -15070,20 +15277,33 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
15070
15277
|
outputLines.push(...region.ok);
|
|
15071
15278
|
currentLine += region.ok.length;
|
|
15072
15279
|
} else if (region.conflict) {
|
|
15073
|
-
const
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15280
|
+
const resolved = tryResolveConflict(
|
|
15281
|
+
region.conflict.a,
|
|
15282
|
+
// ours (generator)
|
|
15283
|
+
region.conflict.o,
|
|
15284
|
+
// base
|
|
15285
|
+
region.conflict.b
|
|
15286
|
+
// theirs (user)
|
|
15287
|
+
);
|
|
15288
|
+
if (resolved !== null) {
|
|
15289
|
+
outputLines.push(...resolved);
|
|
15290
|
+
currentLine += resolved.length;
|
|
15291
|
+
} else {
|
|
15292
|
+
const startLine = currentLine;
|
|
15293
|
+
outputLines.push("<<<<<<< Generated");
|
|
15294
|
+
outputLines.push(...region.conflict.a);
|
|
15295
|
+
outputLines.push("=======");
|
|
15296
|
+
outputLines.push(...region.conflict.b);
|
|
15297
|
+
outputLines.push(">>>>>>> Your customization");
|
|
15298
|
+
const conflictLines = region.conflict.a.length + region.conflict.b.length + 3;
|
|
15299
|
+
conflicts2.push({
|
|
15300
|
+
startLine,
|
|
15301
|
+
endLine: startLine + conflictLines - 1,
|
|
15302
|
+
ours: region.conflict.a,
|
|
15303
|
+
theirs: region.conflict.b
|
|
15304
|
+
});
|
|
15305
|
+
currentLine += conflictLines;
|
|
15306
|
+
}
|
|
15087
15307
|
}
|
|
15088
15308
|
}
|
|
15089
15309
|
return {
|
|
@@ -15092,6 +15312,62 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
15092
15312
|
conflicts: conflicts2
|
|
15093
15313
|
};
|
|
15094
15314
|
}
|
|
15315
|
+
function tryResolveConflict(oursLines, baseLines, theirsLines) {
|
|
15316
|
+
if (baseLines.length === 0) {
|
|
15317
|
+
return null;
|
|
15318
|
+
}
|
|
15319
|
+
const oursPatches = diffPatch(baseLines, oursLines);
|
|
15320
|
+
const theirsPatches = diffPatch(baseLines, theirsLines);
|
|
15321
|
+
if (oursPatches.length === 0) return theirsLines;
|
|
15322
|
+
if (theirsPatches.length === 0) return oursLines;
|
|
15323
|
+
if (patchesOverlap(oursPatches, theirsPatches)) {
|
|
15324
|
+
return null;
|
|
15325
|
+
}
|
|
15326
|
+
return applyBothPatches(baseLines, oursPatches, theirsPatches);
|
|
15327
|
+
}
|
|
15328
|
+
function patchesOverlap(oursPatches, theirsPatches) {
|
|
15329
|
+
for (const op of oursPatches) {
|
|
15330
|
+
const oStart = op.buffer1.offset;
|
|
15331
|
+
const oEnd = oStart + op.buffer1.length;
|
|
15332
|
+
for (const tp of theirsPatches) {
|
|
15333
|
+
const tStart = tp.buffer1.offset;
|
|
15334
|
+
const tEnd = tStart + tp.buffer1.length;
|
|
15335
|
+
if (op.buffer1.length === 0 && tp.buffer1.length === 0 && oStart === tStart) {
|
|
15336
|
+
return true;
|
|
15337
|
+
}
|
|
15338
|
+
if (tp.buffer1.length === 0 && tStart === oEnd) {
|
|
15339
|
+
return true;
|
|
15340
|
+
}
|
|
15341
|
+
if (op.buffer1.length === 0 && oStart === tEnd) {
|
|
15342
|
+
return true;
|
|
15343
|
+
}
|
|
15344
|
+
if (oStart < tEnd && tStart < oEnd) {
|
|
15345
|
+
return true;
|
|
15346
|
+
}
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15349
|
+
return false;
|
|
15350
|
+
}
|
|
15351
|
+
function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
15352
|
+
const allPatches = [
|
|
15353
|
+
...oursPatches.map((p) => ({
|
|
15354
|
+
offset: p.buffer1.offset,
|
|
15355
|
+
length: p.buffer1.length,
|
|
15356
|
+
replacement: p.buffer2.chunk
|
|
15357
|
+
})),
|
|
15358
|
+
...theirsPatches.map((p) => ({
|
|
15359
|
+
offset: p.buffer1.offset,
|
|
15360
|
+
length: p.buffer1.length,
|
|
15361
|
+
replacement: p.buffer2.chunk
|
|
15362
|
+
}))
|
|
15363
|
+
];
|
|
15364
|
+
allPatches.sort((a, b) => b.offset - a.offset);
|
|
15365
|
+
const result = [...baseLines];
|
|
15366
|
+
for (const p of allPatches) {
|
|
15367
|
+
result.splice(p.offset, p.length, ...p.replacement);
|
|
15368
|
+
}
|
|
15369
|
+
return result;
|
|
15370
|
+
}
|
|
15095
15371
|
|
|
15096
15372
|
// src/ReplayApplicator.ts
|
|
15097
15373
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -15155,6 +15431,27 @@ var ReplayApplicator = class {
|
|
|
15155
15431
|
this.lockManager = lockManager;
|
|
15156
15432
|
this.outputDir = outputDir;
|
|
15157
15433
|
}
|
|
15434
|
+
/**
|
|
15435
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
15436
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
15437
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
15438
|
+
*/
|
|
15439
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
15440
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
15441
|
+
if (gen) return gen;
|
|
15442
|
+
try {
|
|
15443
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
15444
|
+
return {
|
|
15445
|
+
commit_sha: baseGeneration,
|
|
15446
|
+
tree_hash: treeHash,
|
|
15447
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15448
|
+
cli_version: "unknown",
|
|
15449
|
+
generator_versions: {}
|
|
15450
|
+
};
|
|
15451
|
+
} catch {
|
|
15452
|
+
return void 0;
|
|
15453
|
+
}
|
|
15454
|
+
}
|
|
15158
15455
|
/** Reset inter-patch accumulator for a new cycle. */
|
|
15159
15456
|
resetAccumulator() {
|
|
15160
15457
|
this.fileTheirsAccumulator.clear();
|
|
@@ -15236,7 +15533,7 @@ var ReplayApplicator = class {
|
|
|
15236
15533
|
}
|
|
15237
15534
|
}
|
|
15238
15535
|
async applyPatchWithFallback(patch) {
|
|
15239
|
-
const baseGen = this.
|
|
15536
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
15240
15537
|
const lock = this.lockManager.read();
|
|
15241
15538
|
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
15242
15539
|
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
@@ -15320,7 +15617,7 @@ var ReplayApplicator = class {
|
|
|
15320
15617
|
}
|
|
15321
15618
|
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
15322
15619
|
try {
|
|
15323
|
-
const baseGen = this.
|
|
15620
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
15324
15621
|
if (!baseGen) {
|
|
15325
15622
|
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
15326
15623
|
}
|
|
@@ -15372,6 +15669,17 @@ var ReplayApplicator = class {
|
|
|
15372
15669
|
renameSourcePath
|
|
15373
15670
|
);
|
|
15374
15671
|
}
|
|
15672
|
+
if (theirs) {
|
|
15673
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
15674
|
+
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
15675
|
+
if (theirsHasMarkers && !baseHasMarkers) {
|
|
15676
|
+
return {
|
|
15677
|
+
file: resolvedPath,
|
|
15678
|
+
status: "skipped",
|
|
15679
|
+
reason: "stale-conflict-markers"
|
|
15680
|
+
};
|
|
15681
|
+
}
|
|
15682
|
+
}
|
|
15375
15683
|
let useAccumulatorAsMergeBase = false;
|
|
15376
15684
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
15377
15685
|
if (!theirs && accumulatorEntry) {
|
|
@@ -15386,6 +15694,17 @@ var ReplayApplicator = class {
|
|
|
15386
15694
|
useAccumulatorAsMergeBase = true;
|
|
15387
15695
|
}
|
|
15388
15696
|
}
|
|
15697
|
+
if (theirs) {
|
|
15698
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
15699
|
+
const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
|
|
15700
|
+
if (theirsHasMarkers && !accBaseHasMarkers && !(base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization")))) {
|
|
15701
|
+
return {
|
|
15702
|
+
file: resolvedPath,
|
|
15703
|
+
status: "skipped",
|
|
15704
|
+
reason: "stale-conflict-markers"
|
|
15705
|
+
};
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15389
15708
|
let effective_theirs = theirs;
|
|
15390
15709
|
let baseMismatchSkipped = false;
|
|
15391
15710
|
if (theirs && base && !useAccumulatorAsMergeBase) {
|
|
@@ -15452,7 +15771,7 @@ var ReplayApplicator = class {
|
|
|
15452
15771
|
const outDir = (0, import_node_path3.dirname)(oursPath);
|
|
15453
15772
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
15454
15773
|
await (0, import_promises.writeFile)(oursPath, merged.content);
|
|
15455
|
-
if (effective_theirs) {
|
|
15774
|
+
if (effective_theirs && !merged.hasConflicts) {
|
|
15456
15775
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
15457
15776
|
content: effective_theirs,
|
|
15458
15777
|
baseGeneration: patch.base_generation
|
|
@@ -15750,6 +16069,7 @@ var ReplayService = class {
|
|
|
15750
16069
|
generator_versions: options?.generatorVersions ?? {},
|
|
15751
16070
|
base_branch_head: options?.baseBranchHead
|
|
15752
16071
|
};
|
|
16072
|
+
let resolvedPatches;
|
|
15753
16073
|
if (!this.lockManager.exists()) {
|
|
15754
16074
|
this.lockManager.initializeInMemory(record);
|
|
15755
16075
|
} else {
|
|
@@ -15758,13 +16078,13 @@ var ReplayService = class {
|
|
|
15758
16078
|
...this.lockManager.getUnresolvedPatches(),
|
|
15759
16079
|
...this.lockManager.getResolvingPatches()
|
|
15760
16080
|
];
|
|
16081
|
+
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
15761
16082
|
this.lockManager.addGeneration(record);
|
|
15762
16083
|
this.lockManager.clearPatches();
|
|
15763
16084
|
for (const patch of unresolvedPatches) {
|
|
15764
16085
|
this.lockManager.addPatch(patch);
|
|
15765
16086
|
}
|
|
15766
16087
|
}
|
|
15767
|
-
this.lockManager.save();
|
|
15768
16088
|
try {
|
|
15769
16089
|
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
15770
16090
|
if (redetectedPatches.length > 0) {
|
|
@@ -15778,20 +16098,27 @@ var ReplayService = class {
|
|
|
15778
16098
|
for (const patch of redetectedPatches) {
|
|
15779
16099
|
this.lockManager.addPatch(patch);
|
|
15780
16100
|
}
|
|
15781
|
-
this.lockManager.save();
|
|
15782
16101
|
}
|
|
15783
|
-
} catch {
|
|
16102
|
+
} catch (error) {
|
|
16103
|
+
for (const patch of resolvedPatches ?? []) {
|
|
16104
|
+
this.lockManager.addPatch(patch);
|
|
16105
|
+
}
|
|
16106
|
+
this.detector.warnings.push(
|
|
16107
|
+
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
16108
|
+
);
|
|
15784
16109
|
}
|
|
16110
|
+
this.lockManager.save();
|
|
15785
16111
|
}
|
|
15786
16112
|
determineFlow() {
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
16113
|
+
try {
|
|
16114
|
+
const lock = this.lockManager.read();
|
|
16115
|
+
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
16116
|
+
} catch (error) {
|
|
16117
|
+
if (error instanceof LockfileNotFoundError) {
|
|
16118
|
+
return "first-generation";
|
|
16119
|
+
}
|
|
16120
|
+
throw error;
|
|
15793
16121
|
}
|
|
15794
|
-
return "normal-regeneration";
|
|
15795
16122
|
}
|
|
15796
16123
|
async handleFirstGeneration(options) {
|
|
15797
16124
|
if (options?.dryRun) {
|
|
@@ -15833,12 +16160,17 @@ var ReplayService = class {
|
|
|
15833
16160
|
baseBranchHead: options.baseBranchHead
|
|
15834
16161
|
} : void 0;
|
|
15835
16162
|
await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
16163
|
+
await this.cleanupStaleConflictMarkers();
|
|
15836
16164
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15837
|
-
|
|
16165
|
+
try {
|
|
15838
16166
|
this.lockManager.read();
|
|
15839
16167
|
this.lockManager.addGeneration(genRecord);
|
|
15840
|
-
}
|
|
15841
|
-
|
|
16168
|
+
} catch (error) {
|
|
16169
|
+
if (error instanceof LockfileNotFoundError) {
|
|
16170
|
+
this.lockManager.initializeInMemory(genRecord);
|
|
16171
|
+
} else {
|
|
16172
|
+
throw error;
|
|
16173
|
+
}
|
|
15842
16174
|
}
|
|
15843
16175
|
this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
15844
16176
|
this.lockManager.save();
|
|
@@ -15884,6 +16216,7 @@ var ReplayService = class {
|
|
|
15884
16216
|
baseBranchHead: options.baseBranchHead
|
|
15885
16217
|
} : void 0;
|
|
15886
16218
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
16219
|
+
await this.cleanupStaleConflictMarkers();
|
|
15887
16220
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15888
16221
|
this.lockManager.addGeneration(genRecord);
|
|
15889
16222
|
let results = [];
|
|
@@ -15977,6 +16310,7 @@ var ReplayService = class {
|
|
|
15977
16310
|
baseBranchHead: options.baseBranchHead
|
|
15978
16311
|
} : void 0;
|
|
15979
16312
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
16313
|
+
await this.cleanupStaleConflictMarkers();
|
|
15980
16314
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15981
16315
|
this.lockManager.addGeneration(genRecord);
|
|
15982
16316
|
const results = await this.applicator.applyPatches(allPatches);
|
|
@@ -16167,6 +16501,13 @@ var ReplayService = class {
|
|
|
16167
16501
|
contentRefreshed++;
|
|
16168
16502
|
continue;
|
|
16169
16503
|
}
|
|
16504
|
+
const diffLines = diff.split("\n");
|
|
16505
|
+
const hasStaleMarkers = diffLines.some(
|
|
16506
|
+
(l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
|
|
16507
|
+
);
|
|
16508
|
+
if (hasStaleMarkers) {
|
|
16509
|
+
continue;
|
|
16510
|
+
}
|
|
16170
16511
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
16171
16512
|
if (newContentHash !== patch.content_hash) {
|
|
16172
16513
|
const filesOutput = await this.git.exec(["diff", "--name-only", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -16187,7 +16528,7 @@ var ReplayService = class {
|
|
|
16187
16528
|
continue;
|
|
16188
16529
|
}
|
|
16189
16530
|
try {
|
|
16190
|
-
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<<", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
16531
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
16191
16532
|
if (markerFiles.trim()) continue;
|
|
16192
16533
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
16193
16534
|
if (diff === null) continue;
|
|
@@ -16227,6 +16568,25 @@ var ReplayService = class {
|
|
|
16227
16568
|
}
|
|
16228
16569
|
}
|
|
16229
16570
|
}
|
|
16571
|
+
/**
|
|
16572
|
+
* Clean up stale conflict markers left by a previous crashed run.
|
|
16573
|
+
* Called after commitGeneration() when HEAD is the [fern-generated] commit.
|
|
16574
|
+
* Restores files to their clean generated state from HEAD.
|
|
16575
|
+
* Skips .fernignore-protected files to prevent overwriting user content.
|
|
16576
|
+
*/
|
|
16577
|
+
async cleanupStaleConflictMarkers() {
|
|
16578
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
|
|
16579
|
+
const files = markerFiles.trim().split("\n").filter(Boolean);
|
|
16580
|
+
if (files.length === 0) return;
|
|
16581
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
16582
|
+
for (const file of files) {
|
|
16583
|
+
if (fernignorePatterns.some((pattern) => minimatch(file, pattern))) continue;
|
|
16584
|
+
try {
|
|
16585
|
+
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
16586
|
+
} catch {
|
|
16587
|
+
}
|
|
16588
|
+
}
|
|
16589
|
+
}
|
|
16230
16590
|
readFernignorePatterns() {
|
|
16231
16591
|
const fernignorePath = (0, import_node_path4.join)(this.outputDir, ".fernignore");
|
|
16232
16592
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|