@fern-api/generator-cli 0.9.20 → 0.9.22
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.js +2142 -1212
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -7031,18 +7031,18 @@ var require_build3 = __commonJS({
|
|
|
7031
7031
|
var require_sync = __commonJS({
|
|
7032
7032
|
"../../node_modules/.pnpm/escalade@3.2.0/node_modules/escalade/sync/index.js"(exports2, module2) {
|
|
7033
7033
|
"use strict";
|
|
7034
|
-
var { dirname:
|
|
7034
|
+
var { dirname: dirname5, resolve: resolve8 } = require("path");
|
|
7035
7035
|
var { readdirSync: readdirSync2, statSync: statSync4 } = require("fs");
|
|
7036
7036
|
module2.exports = function(start, callback) {
|
|
7037
7037
|
let dir = resolve8(".", start);
|
|
7038
7038
|
let tmp3, stats = statSync4(dir);
|
|
7039
7039
|
if (!stats.isDirectory()) {
|
|
7040
|
-
dir =
|
|
7040
|
+
dir = dirname5(dir);
|
|
7041
7041
|
}
|
|
7042
7042
|
while (true) {
|
|
7043
7043
|
tmp3 = callback(dir, readdirSync2(dir));
|
|
7044
7044
|
if (tmp3) return resolve8(dir, tmp3);
|
|
7045
|
-
dir =
|
|
7045
|
+
dir = dirname5(tmp3 = dir);
|
|
7046
7046
|
if (tmp3 === dir) break;
|
|
7047
7047
|
}
|
|
7048
7048
|
};
|
|
@@ -7078,9 +7078,9 @@ var require_require_directory = __commonJS({
|
|
|
7078
7078
|
"../../node_modules/.pnpm/require-directory@2.1.1/node_modules/require-directory/index.js"(exports2, module2) {
|
|
7079
7079
|
"use strict";
|
|
7080
7080
|
var fs3 = require("fs");
|
|
7081
|
-
var
|
|
7081
|
+
var join15 = require("path").join;
|
|
7082
7082
|
var resolve8 = require("path").resolve;
|
|
7083
|
-
var
|
|
7083
|
+
var dirname5 = require("path").dirname;
|
|
7084
7084
|
var defaultOptions2 = {
|
|
7085
7085
|
extensions: ["js", "json", "coffee"],
|
|
7086
7086
|
recurse: true,
|
|
@@ -7113,9 +7113,9 @@ var require_require_directory = __commonJS({
|
|
|
7113
7113
|
options[prop] = defaultOptions2[prop];
|
|
7114
7114
|
}
|
|
7115
7115
|
}
|
|
7116
|
-
path5 = !path5 ?
|
|
7116
|
+
path5 = !path5 ? dirname5(m.filename) : resolve8(dirname5(m.filename), path5);
|
|
7117
7117
|
fs3.readdirSync(path5).forEach(function(filename) {
|
|
7118
|
-
var joined =
|
|
7118
|
+
var joined = join15(path5, filename), files, key, obj;
|
|
7119
7119
|
if (fs3.statSync(joined).isDirectory() && options.recurse) {
|
|
7120
7120
|
files = requireDirectory(m, joined, options);
|
|
7121
7121
|
if (Object.keys(files).length) {
|
|
@@ -18885,7 +18885,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
18885
18885
|
if (!header)
|
|
18886
18886
|
return { value: "", type: null, comment: "", range: [start, start, start] };
|
|
18887
18887
|
const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
|
|
18888
|
-
const lines = scalar.source ?
|
|
18888
|
+
const lines = scalar.source ? splitLines2(scalar.source) : [];
|
|
18889
18889
|
let chompStart = lines.length;
|
|
18890
18890
|
for (let i = lines.length - 1; i >= 0; --i) {
|
|
18891
18891
|
const content = lines[i][1];
|
|
@@ -19043,7 +19043,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
19043
19043
|
}
|
|
19044
19044
|
return { mode, indent, chomp, comment, length };
|
|
19045
19045
|
}
|
|
19046
|
-
function
|
|
19046
|
+
function splitLines2(source) {
|
|
19047
19047
|
const split = source.split(/\n( *)/);
|
|
19048
19048
|
const first2 = split[0];
|
|
19049
19049
|
const m = first2.match(/^( *)/);
|
|
@@ -22028,7 +22028,7 @@ var init_diff3 = __esm({
|
|
|
22028
22028
|
}
|
|
22029
22029
|
});
|
|
22030
22030
|
|
|
22031
|
-
// ../../node_modules/.pnpm/@fern-api+replay@0.
|
|
22031
|
+
// ../../node_modules/.pnpm/@fern-api+replay@0.14.1/node_modules/@fern-api/replay/dist/index.js
|
|
22032
22032
|
var dist_exports = {};
|
|
22033
22033
|
__export(dist_exports, {
|
|
22034
22034
|
CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
|
|
@@ -22293,6 +22293,48 @@ function reconstructFromGhostPatch(fileDiff, ours) {
|
|
|
22293
22293
|
}
|
|
22294
22294
|
return assembleHybrid(located, oursLines);
|
|
22295
22295
|
}
|
|
22296
|
+
async function applyPatchToContent(base, patchContent, filePath) {
|
|
22297
|
+
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
22298
|
+
if (!fileDiff) return null;
|
|
22299
|
+
const tempDir = await (0, import_promises3.mkdtemp)((0, import_path7.join)((0, import_os.tmpdir)(), "replay-migrate-"));
|
|
22300
|
+
const tempGit = new GitClient(tempDir);
|
|
22301
|
+
try {
|
|
22302
|
+
await tempGit.exec(["init"]);
|
|
22303
|
+
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
22304
|
+
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
22305
|
+
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
22306
|
+
const tempFilePath = (0, import_path7.join)(tempDir, filePath);
|
|
22307
|
+
await (0, import_promises3.mkdir)((0, import_path7.dirname)(tempFilePath), { recursive: true });
|
|
22308
|
+
await (0, import_promises3.writeFile)(tempFilePath, base);
|
|
22309
|
+
await tempGit.exec(["add", filePath]);
|
|
22310
|
+
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
22311
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
22312
|
+
return await (0, import_promises3.readFile)(tempFilePath, "utf-8");
|
|
22313
|
+
} catch {
|
|
22314
|
+
return null;
|
|
22315
|
+
} finally {
|
|
22316
|
+
await (0, import_promises3.rm)(tempDir, { recursive: true, force: true }).catch(() => {
|
|
22317
|
+
});
|
|
22318
|
+
}
|
|
22319
|
+
}
|
|
22320
|
+
function extractFileDiff(patchContent, filePath) {
|
|
22321
|
+
const lines = patchContent.split("\n");
|
|
22322
|
+
const out = [];
|
|
22323
|
+
let inTarget = false;
|
|
22324
|
+
for (const line of lines) {
|
|
22325
|
+
if (line.startsWith("diff --git")) {
|
|
22326
|
+
if (inTarget) break;
|
|
22327
|
+
const match2 = line.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
22328
|
+
if (match2 && match2[1] === filePath) {
|
|
22329
|
+
inTarget = true;
|
|
22330
|
+
out.push(line);
|
|
22331
|
+
}
|
|
22332
|
+
continue;
|
|
22333
|
+
}
|
|
22334
|
+
if (inTarget) out.push(line);
|
|
22335
|
+
}
|
|
22336
|
+
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
22337
|
+
}
|
|
22296
22338
|
function isSensitiveFile(filePath) {
|
|
22297
22339
|
return SENSITIVE_FILE_PATTERNS.some((re) => re.test(filePath));
|
|
22298
22340
|
}
|
|
@@ -22328,36 +22370,73 @@ function parseRevertedMessage(subject) {
|
|
|
22328
22370
|
const match2 = subject.match(/^Revert "(.+)"$/);
|
|
22329
22371
|
return match2?.[1];
|
|
22330
22372
|
}
|
|
22331
|
-
function
|
|
22332
|
-
|
|
22333
|
-
|
|
22334
|
-
|
|
22335
|
-
|
|
22336
|
-
|
|
22337
|
-
|
|
22338
|
-
|
|
22373
|
+
function trimCR(line) {
|
|
22374
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
22375
|
+
}
|
|
22376
|
+
function findConflictRanges(lines) {
|
|
22377
|
+
const ranges = [];
|
|
22378
|
+
let i = 0;
|
|
22379
|
+
while (i < lines.length) {
|
|
22380
|
+
if (trimCR(lines[i]) === CONFLICT_OPENER) {
|
|
22381
|
+
let separatorIdx = -1;
|
|
22382
|
+
let j2 = i + 1;
|
|
22383
|
+
let found = false;
|
|
22384
|
+
while (j2 < lines.length) {
|
|
22385
|
+
const trimmed2 = trimCR(lines[j2]);
|
|
22386
|
+
if (trimmed2 === CONFLICT_OPENER) {
|
|
22387
|
+
break;
|
|
22388
|
+
}
|
|
22389
|
+
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
22390
|
+
separatorIdx = j2;
|
|
22391
|
+
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
22392
|
+
ranges.push({ start: i, separator: separatorIdx, end: j2 });
|
|
22393
|
+
i = j2;
|
|
22394
|
+
found = true;
|
|
22395
|
+
break;
|
|
22396
|
+
}
|
|
22397
|
+
j2++;
|
|
22398
|
+
}
|
|
22399
|
+
if (!found) {
|
|
22400
|
+
i++;
|
|
22401
|
+
continue;
|
|
22402
|
+
}
|
|
22339
22403
|
}
|
|
22340
|
-
|
|
22341
|
-
|
|
22342
|
-
|
|
22343
|
-
|
|
22344
|
-
|
|
22345
|
-
|
|
22346
|
-
|
|
22347
|
-
|
|
22348
|
-
|
|
22349
|
-
|
|
22404
|
+
i++;
|
|
22405
|
+
}
|
|
22406
|
+
return ranges;
|
|
22407
|
+
}
|
|
22408
|
+
function stripConflictMarkers(content) {
|
|
22409
|
+
const lines = content.split(/\r?\n/);
|
|
22410
|
+
const ranges = findConflictRanges(lines);
|
|
22411
|
+
if (ranges.length === 0) {
|
|
22412
|
+
return content;
|
|
22413
|
+
}
|
|
22414
|
+
const result = [];
|
|
22415
|
+
let rangeIdx = 0;
|
|
22416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22417
|
+
if (rangeIdx < ranges.length) {
|
|
22418
|
+
const range2 = ranges[rangeIdx];
|
|
22419
|
+
if (i === range2.start) {
|
|
22420
|
+
continue;
|
|
22350
22421
|
}
|
|
22351
|
-
|
|
22352
|
-
|
|
22353
|
-
|
|
22354
|
-
}
|
|
22355
|
-
|
|
22422
|
+
if (i > range2.start && i < range2.separator) {
|
|
22423
|
+
result.push(lines[i]);
|
|
22424
|
+
continue;
|
|
22425
|
+
}
|
|
22426
|
+
if (i === range2.separator) {
|
|
22427
|
+
continue;
|
|
22428
|
+
}
|
|
22429
|
+
if (i > range2.separator && i < range2.end) {
|
|
22430
|
+
continue;
|
|
22431
|
+
}
|
|
22432
|
+
if (i === range2.end) {
|
|
22433
|
+
rangeIdx++;
|
|
22434
|
+
continue;
|
|
22356
22435
|
}
|
|
22357
22436
|
}
|
|
22437
|
+
result.push(lines[i]);
|
|
22358
22438
|
}
|
|
22359
|
-
|
|
22360
|
-
return results;
|
|
22439
|
+
return result.join("\n");
|
|
22361
22440
|
}
|
|
22362
22441
|
function threeWayMerge(base, ours, theirs) {
|
|
22363
22442
|
const baseLines = base.split("\n");
|
|
@@ -22401,11 +22480,43 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
22401
22480
|
}
|
|
22402
22481
|
}
|
|
22403
22482
|
}
|
|
22404
|
-
|
|
22483
|
+
const result = {
|
|
22405
22484
|
content: outputLines.join("\n"),
|
|
22406
22485
|
hasConflicts: conflicts2.length > 0,
|
|
22407
22486
|
conflicts: conflicts2
|
|
22408
22487
|
};
|
|
22488
|
+
if (conflicts2.length === 0) {
|
|
22489
|
+
const dropped = computeDroppedContextLines(baseLines, theirsLines, outputLines);
|
|
22490
|
+
if (dropped.length > 0) {
|
|
22491
|
+
result.droppedContextLines = dropped;
|
|
22492
|
+
}
|
|
22493
|
+
}
|
|
22494
|
+
return result;
|
|
22495
|
+
}
|
|
22496
|
+
function computeDroppedContextLines(baseLines, theirsLines, mergedLines) {
|
|
22497
|
+
const baseCounts = countLines(baseLines);
|
|
22498
|
+
const theirsCounts = countLines(theirsLines);
|
|
22499
|
+
const mergedCounts = countLines(mergedLines);
|
|
22500
|
+
const dropped = [];
|
|
22501
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22502
|
+
for (const line of baseLines) {
|
|
22503
|
+
if (seen.has(line)) continue;
|
|
22504
|
+
const inBase = baseCounts.get(line) ?? 0;
|
|
22505
|
+
const inTheirs = theirsCounts.get(line) ?? 0;
|
|
22506
|
+
const inMerged = mergedCounts.get(line) ?? 0;
|
|
22507
|
+
if (inBase > 0 && inTheirs > 0 && inMerged < Math.min(inBase, inTheirs)) {
|
|
22508
|
+
dropped.push(line);
|
|
22509
|
+
seen.add(line);
|
|
22510
|
+
}
|
|
22511
|
+
}
|
|
22512
|
+
return dropped;
|
|
22513
|
+
}
|
|
22514
|
+
function countLines(lines) {
|
|
22515
|
+
const counts = /* @__PURE__ */ new Map();
|
|
22516
|
+
for (const line of lines) {
|
|
22517
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
22518
|
+
}
|
|
22519
|
+
return counts;
|
|
22409
22520
|
}
|
|
22410
22521
|
function tryResolveConflict(oursLines, baseLines, theirsLines) {
|
|
22411
22522
|
if (baseLines.length === 0) {
|
|
@@ -22463,81 +22574,380 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
22463
22574
|
}
|
|
22464
22575
|
return result;
|
|
22465
22576
|
}
|
|
22466
|
-
function
|
|
22467
|
-
|
|
22577
|
+
function isBinaryFile(filePath) {
|
|
22578
|
+
const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
|
|
22579
|
+
return BINARY_EXTENSIONS.has(ext2);
|
|
22468
22580
|
}
|
|
22469
|
-
function
|
|
22470
|
-
const
|
|
22471
|
-
|
|
22472
|
-
|
|
22473
|
-
|
|
22474
|
-
|
|
22475
|
-
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22581
|
+
function computeDroppedContextLinesForFile(base, theirs, finalContent) {
|
|
22582
|
+
const baseLines = base.split("\n");
|
|
22583
|
+
const theirsLines = theirs.split("\n");
|
|
22584
|
+
const finalLines = finalContent.split("\n");
|
|
22585
|
+
const baseCounts = /* @__PURE__ */ new Map();
|
|
22586
|
+
const theirsCounts = /* @__PURE__ */ new Map();
|
|
22587
|
+
const finalCounts = /* @__PURE__ */ new Map();
|
|
22588
|
+
for (const l of baseLines) baseCounts.set(l, (baseCounts.get(l) ?? 0) + 1);
|
|
22589
|
+
for (const l of theirsLines) theirsCounts.set(l, (theirsCounts.get(l) ?? 0) + 1);
|
|
22590
|
+
for (const l of finalLines) finalCounts.set(l, (finalCounts.get(l) ?? 0) + 1);
|
|
22591
|
+
const dropped = [];
|
|
22592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22593
|
+
for (const line of baseLines) {
|
|
22594
|
+
if (seen.has(line)) continue;
|
|
22595
|
+
const inBase = baseCounts.get(line) ?? 0;
|
|
22596
|
+
const inTheirs = theirsCounts.get(line) ?? 0;
|
|
22597
|
+
const inFinal = finalCounts.get(line) ?? 0;
|
|
22598
|
+
if (inBase > 0 && inTheirs > 0 && inFinal < Math.min(inBase, inTheirs)) {
|
|
22599
|
+
dropped.push(line);
|
|
22600
|
+
seen.add(line);
|
|
22601
|
+
}
|
|
22602
|
+
}
|
|
22603
|
+
return dropped;
|
|
22604
|
+
}
|
|
22605
|
+
function isDiffLineForFile(diffLine, filePath) {
|
|
22606
|
+
const match2 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
22607
|
+
return match2 !== null && match2[1] === filePath;
|
|
22608
|
+
}
|
|
22609
|
+
function parsePatchFileHeaders(patchContent) {
|
|
22610
|
+
const results = [];
|
|
22611
|
+
let currentPath = null;
|
|
22612
|
+
let currentIsCreate = false;
|
|
22613
|
+
let currentIsDelete = false;
|
|
22614
|
+
const flush2 = () => {
|
|
22615
|
+
if (currentPath !== null) {
|
|
22616
|
+
results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
|
|
22617
|
+
}
|
|
22618
|
+
};
|
|
22619
|
+
for (const line of patchContent.split("\n")) {
|
|
22620
|
+
if (line.startsWith("diff --git ")) {
|
|
22621
|
+
flush2();
|
|
22622
|
+
currentPath = null;
|
|
22623
|
+
currentIsCreate = false;
|
|
22624
|
+
currentIsDelete = false;
|
|
22625
|
+
const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
22626
|
+
if (match2) {
|
|
22627
|
+
currentPath = match2[2] ?? null;
|
|
22491
22628
|
}
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
|
|
22629
|
+
} else if (currentPath !== null) {
|
|
22630
|
+
if (line.startsWith("new file mode ")) {
|
|
22631
|
+
currentIsCreate = true;
|
|
22632
|
+
} else if (line.startsWith("deleted file mode ")) {
|
|
22633
|
+
currentIsDelete = true;
|
|
22495
22634
|
}
|
|
22496
22635
|
}
|
|
22497
|
-
i++;
|
|
22498
22636
|
}
|
|
22499
|
-
|
|
22637
|
+
flush2();
|
|
22638
|
+
return results;
|
|
22500
22639
|
}
|
|
22501
|
-
function
|
|
22502
|
-
|
|
22503
|
-
|
|
22504
|
-
|
|
22505
|
-
|
|
22640
|
+
function normalizeHunkBody(hunk) {
|
|
22641
|
+
let contextC = 0;
|
|
22642
|
+
let removeC = 0;
|
|
22643
|
+
let addC = 0;
|
|
22644
|
+
const trimmed2 = [];
|
|
22645
|
+
for (const line of hunk.lines) {
|
|
22646
|
+
const oldUsed = contextC + removeC;
|
|
22647
|
+
const newUsed = contextC + addC;
|
|
22648
|
+
if (oldUsed >= hunk.oldCount && newUsed >= hunk.newCount) break;
|
|
22649
|
+
trimmed2.push(line);
|
|
22650
|
+
if (line.type === "context") contextC++;
|
|
22651
|
+
else if (line.type === "remove") removeC++;
|
|
22652
|
+
else if (line.type === "add") addC++;
|
|
22506
22653
|
}
|
|
22507
|
-
|
|
22508
|
-
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
|
|
22512
|
-
|
|
22513
|
-
|
|
22654
|
+
return { ...hunk, lines: trimmed2 };
|
|
22655
|
+
}
|
|
22656
|
+
function extractFileDiffForFile(patchContent, filePath) {
|
|
22657
|
+
const lines = patchContent.split("\n");
|
|
22658
|
+
const out = [];
|
|
22659
|
+
let inTarget = false;
|
|
22660
|
+
for (const line of lines) {
|
|
22661
|
+
if (line.startsWith("diff --git")) {
|
|
22662
|
+
if (inTarget) break;
|
|
22663
|
+
if (isDiffLineForFile2(line, filePath)) {
|
|
22664
|
+
inTarget = true;
|
|
22665
|
+
out.push(line);
|
|
22514
22666
|
}
|
|
22515
|
-
|
|
22516
|
-
|
|
22667
|
+
continue;
|
|
22668
|
+
}
|
|
22669
|
+
if (inTarget) out.push(line);
|
|
22670
|
+
}
|
|
22671
|
+
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
22672
|
+
}
|
|
22673
|
+
function isDiffLineForFile2(line, filePath) {
|
|
22674
|
+
const match2 = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
22675
|
+
return match2 !== null && (match2[2] === filePath || match2[1] === filePath);
|
|
22676
|
+
}
|
|
22677
|
+
async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeContent) {
|
|
22678
|
+
const fragments = [];
|
|
22679
|
+
const unlocatableFiles = [];
|
|
22680
|
+
for (const file of patch.files) {
|
|
22681
|
+
const fileDiff = extractFileDiffForFile(patch.patch_content, file);
|
|
22682
|
+
if (fileDiff == null) {
|
|
22683
|
+
unlocatableFiles.push(file);
|
|
22684
|
+
continue;
|
|
22685
|
+
}
|
|
22686
|
+
const hunksRaw = parseHunks(fileDiff).map(normalizeHunkBody);
|
|
22687
|
+
const hunks = hunksRaw.filter((h) => {
|
|
22688
|
+
if (h.lines.length === 0) return false;
|
|
22689
|
+
let ctx = 0, rm4 = 0, add = 0;
|
|
22690
|
+
for (const ln of h.lines) {
|
|
22691
|
+
if (ln.type === "context") ctx++;
|
|
22692
|
+
else if (ln.type === "remove") rm4++;
|
|
22693
|
+
else if (ln.type === "add") add++;
|
|
22694
|
+
}
|
|
22695
|
+
return ctx + rm4 === h.oldCount && ctx + add === h.newCount;
|
|
22696
|
+
});
|
|
22697
|
+
if (hunks.length === 0) {
|
|
22698
|
+
if (hunksRaw.length > 0) unlocatableFiles.push(file);
|
|
22699
|
+
continue;
|
|
22700
|
+
}
|
|
22701
|
+
const currentGen = await getCurrentGenContent(file);
|
|
22702
|
+
const workingTree = await getWorkingTreeContent(file);
|
|
22703
|
+
const isPureNewFile = hunks.every((h) => h.oldCount === 0);
|
|
22704
|
+
const isPureDelete = hunks.every((h) => h.newCount === 0);
|
|
22705
|
+
if (currentGen == null) {
|
|
22706
|
+
if (!isPureNewFile) {
|
|
22707
|
+
unlocatableFiles.push(file);
|
|
22517
22708
|
continue;
|
|
22518
22709
|
}
|
|
22519
|
-
if (
|
|
22710
|
+
if (workingTree == null) continue;
|
|
22711
|
+
fragments.push(synthesizeNewFileDiff(file, workingTree));
|
|
22712
|
+
continue;
|
|
22713
|
+
}
|
|
22714
|
+
if (isPureNewFile) {
|
|
22715
|
+
if (workingTree == null) {
|
|
22716
|
+
fragments.push(synthesizeDeleteFileDiff(file, currentGen));
|
|
22520
22717
|
continue;
|
|
22521
22718
|
}
|
|
22522
|
-
if (
|
|
22719
|
+
if (workingTree === currentGen) {
|
|
22523
22720
|
continue;
|
|
22524
22721
|
}
|
|
22525
|
-
|
|
22526
|
-
|
|
22722
|
+
const fullDiff = await unifiedDiff(currentGen, workingTree, file);
|
|
22723
|
+
if (fullDiff && fullDiff.trim()) fragments.push(fullDiff);
|
|
22724
|
+
continue;
|
|
22725
|
+
}
|
|
22726
|
+
if (workingTree == null) {
|
|
22727
|
+
if (!isPureDelete) {
|
|
22728
|
+
unlocatableFiles.push(file);
|
|
22527
22729
|
continue;
|
|
22528
22730
|
}
|
|
22731
|
+
fragments.push(synthesizeDeleteFileDiff(file, currentGen));
|
|
22732
|
+
continue;
|
|
22733
|
+
}
|
|
22734
|
+
const currentGenLines = splitLines(currentGen);
|
|
22735
|
+
const workingTreeLines = splitLines(workingTree);
|
|
22736
|
+
const locInCurrentGenRaw = locateHunksInOurs(hunks, currentGenLines);
|
|
22737
|
+
const locInWorkingTreeRaw = locateHunksInOurs(hunks, workingTreeLines);
|
|
22738
|
+
if (locInCurrentGenRaw == null || locInWorkingTreeRaw == null) {
|
|
22739
|
+
unlocatableFiles.push(file);
|
|
22740
|
+
continue;
|
|
22741
|
+
}
|
|
22742
|
+
const locInCurrentGen = locInCurrentGenRaw.map(
|
|
22743
|
+
(l) => resolveSpan(l, currentGenLines, "old")
|
|
22744
|
+
);
|
|
22745
|
+
const locInWorkingTree = locInWorkingTreeRaw.map(
|
|
22746
|
+
(l) => resolveSpan(l, workingTreeLines, "new")
|
|
22747
|
+
);
|
|
22748
|
+
const overlay = overlayRegions(
|
|
22749
|
+
currentGenLines,
|
|
22750
|
+
locInCurrentGen,
|
|
22751
|
+
workingTreeLines,
|
|
22752
|
+
locInWorkingTree
|
|
22753
|
+
);
|
|
22754
|
+
if (overlay === null) {
|
|
22755
|
+
unlocatableFiles.push(file);
|
|
22756
|
+
continue;
|
|
22757
|
+
}
|
|
22758
|
+
if (overlay === currentGen) {
|
|
22759
|
+
continue;
|
|
22760
|
+
}
|
|
22761
|
+
const fileUnifiedDiff = await unifiedDiff(currentGen, overlay, file);
|
|
22762
|
+
if (fileUnifiedDiff && fileUnifiedDiff.trim()) {
|
|
22763
|
+
fragments.push(fileUnifiedDiff);
|
|
22529
22764
|
}
|
|
22530
|
-
result.push(lines[i]);
|
|
22531
22765
|
}
|
|
22532
|
-
return
|
|
22766
|
+
return {
|
|
22767
|
+
diff: fragments.join(""),
|
|
22768
|
+
unlocatableFiles
|
|
22769
|
+
};
|
|
22533
22770
|
}
|
|
22534
|
-
function
|
|
22535
|
-
const
|
|
22536
|
-
|
|
22771
|
+
async function computePerPatchDiffWithFallback(patch, getCurrentGenContent, getWorkingTreeContent, runCumulativeDiff) {
|
|
22772
|
+
const ppDiff = await computePerPatchDiff(
|
|
22773
|
+
patch,
|
|
22774
|
+
getCurrentGenContent,
|
|
22775
|
+
getWorkingTreeContent
|
|
22776
|
+
);
|
|
22777
|
+
if (ppDiff.unlocatableFiles.length === 0) {
|
|
22778
|
+
return { diff: ppDiff.diff, hadFallback: false, fallbackFiles: [] };
|
|
22779
|
+
}
|
|
22780
|
+
const cumulative = await runCumulativeDiff(ppDiff.unlocatableFiles);
|
|
22781
|
+
if (cumulative === null) {
|
|
22782
|
+
return null;
|
|
22783
|
+
}
|
|
22784
|
+
const merged = ppDiff.diff + (cumulative.trim() ? cumulative : "");
|
|
22785
|
+
return { diff: merged, hadFallback: true, fallbackFiles: ppDiff.unlocatableFiles };
|
|
22537
22786
|
}
|
|
22538
|
-
function
|
|
22539
|
-
|
|
22540
|
-
|
|
22787
|
+
function splitLines(content) {
|
|
22788
|
+
return content.split("\n");
|
|
22789
|
+
}
|
|
22790
|
+
function resolveSpan(loc, oursLines, prefer) {
|
|
22791
|
+
const { hunk, oursOffset } = loc;
|
|
22792
|
+
const oldSide = [];
|
|
22793
|
+
const newSide = [];
|
|
22794
|
+
for (const line of hunk.lines) {
|
|
22795
|
+
if (line.type === "context") {
|
|
22796
|
+
oldSide.push(line.content);
|
|
22797
|
+
newSide.push(line.content);
|
|
22798
|
+
} else if (line.type === "remove") {
|
|
22799
|
+
oldSide.push(line.content);
|
|
22800
|
+
} else if (line.type === "add") {
|
|
22801
|
+
newSide.push(line.content);
|
|
22802
|
+
}
|
|
22803
|
+
}
|
|
22804
|
+
const oldFits = sequenceMatches(oldSide, oursLines, oursOffset);
|
|
22805
|
+
const newFits = sequenceMatches(newSide, oursLines, oursOffset);
|
|
22806
|
+
if (prefer === "old") {
|
|
22807
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
22808
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
22809
|
+
} else {
|
|
22810
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
22811
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
22812
|
+
}
|
|
22813
|
+
return loc;
|
|
22814
|
+
}
|
|
22815
|
+
function sequenceMatches(needle, haystack, offset) {
|
|
22816
|
+
if (offset + needle.length > haystack.length) return false;
|
|
22817
|
+
for (let i = 0; i < needle.length; i++) {
|
|
22818
|
+
if (haystack[offset + i] !== needle[i]) return false;
|
|
22819
|
+
}
|
|
22820
|
+
return true;
|
|
22821
|
+
}
|
|
22822
|
+
function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locInWorkingTree) {
|
|
22823
|
+
if (locInCurrentGen.length !== locInWorkingTree.length) {
|
|
22824
|
+
return null;
|
|
22825
|
+
}
|
|
22826
|
+
const out = [];
|
|
22827
|
+
let cursor = 0;
|
|
22828
|
+
for (let i = 0; i < locInCurrentGen.length; i++) {
|
|
22829
|
+
const cg = locInCurrentGen[i];
|
|
22830
|
+
const wt = locInWorkingTree[i];
|
|
22831
|
+
if (cg.oursOffset > cursor) {
|
|
22832
|
+
out.push(...currentGenLines.slice(cursor, cg.oursOffset));
|
|
22833
|
+
}
|
|
22834
|
+
out.push(...workingTreeLines.slice(wt.oursOffset, wt.oursOffset + wt.oursSpan));
|
|
22835
|
+
cursor = cg.oursOffset + cg.oursSpan;
|
|
22836
|
+
}
|
|
22837
|
+
if (cursor < currentGenLines.length) {
|
|
22838
|
+
out.push(...currentGenLines.slice(cursor));
|
|
22839
|
+
}
|
|
22840
|
+
return out.join("\n");
|
|
22841
|
+
}
|
|
22842
|
+
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
22843
|
+
if (beforeContent === afterContent) return "";
|
|
22844
|
+
const tmp3 = await (0, import_promises6.mkdtemp)((0, import_path11.join)((0, import_os3.tmpdir)(), "fern-replay-pp-"));
|
|
22845
|
+
try {
|
|
22846
|
+
const beforePath = (0, import_path11.join)(tmp3, "before");
|
|
22847
|
+
const afterPath = (0, import_path11.join)(tmp3, "after");
|
|
22848
|
+
await (0, import_promises6.writeFile)(beforePath, beforeContent, "utf-8");
|
|
22849
|
+
await (0, import_promises6.writeFile)(afterPath, afterContent, "utf-8");
|
|
22850
|
+
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
22851
|
+
if (!raw.trim()) return "";
|
|
22852
|
+
return rewriteDiffHeaders(raw, filePath);
|
|
22853
|
+
} finally {
|
|
22854
|
+
await (0, import_promises6.rm)(tmp3, { recursive: true, force: true });
|
|
22855
|
+
}
|
|
22856
|
+
}
|
|
22857
|
+
function runGitDiffNoIndex(beforePath, afterPath) {
|
|
22858
|
+
return new Promise((resolve22, reject) => {
|
|
22859
|
+
const proc = (0, import_child_process3.spawn)("git", ["diff", "--no-index", "--", beforePath, afterPath]);
|
|
22860
|
+
let stdout = "";
|
|
22861
|
+
let stderr = "";
|
|
22862
|
+
proc.stdout.on("data", (d) => stdout += d.toString());
|
|
22863
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
22864
|
+
proc.on("close", (code) => {
|
|
22865
|
+
if (code === 0 || code === 1) resolve22(stdout);
|
|
22866
|
+
else reject(new Error(`git diff --no-index failed (code ${code}): ${stderr}`));
|
|
22867
|
+
});
|
|
22868
|
+
});
|
|
22869
|
+
}
|
|
22870
|
+
function rewriteDiffHeaders(raw, filePath) {
|
|
22871
|
+
const lines = raw.split("\n");
|
|
22872
|
+
const out = [];
|
|
22873
|
+
let seenFirstHeader = false;
|
|
22874
|
+
for (const line of lines) {
|
|
22875
|
+
if (line.startsWith("diff --git ")) {
|
|
22876
|
+
out.push(`diff --git a/${filePath} b/${filePath}`);
|
|
22877
|
+
seenFirstHeader = true;
|
|
22878
|
+
continue;
|
|
22879
|
+
}
|
|
22880
|
+
if (!seenFirstHeader) {
|
|
22881
|
+
continue;
|
|
22882
|
+
}
|
|
22883
|
+
if (line.startsWith("--- ")) {
|
|
22884
|
+
out.push(`--- a/${filePath}`);
|
|
22885
|
+
continue;
|
|
22886
|
+
}
|
|
22887
|
+
if (line.startsWith("+++ ")) {
|
|
22888
|
+
out.push(`+++ b/${filePath}`);
|
|
22889
|
+
continue;
|
|
22890
|
+
}
|
|
22891
|
+
out.push(line);
|
|
22892
|
+
}
|
|
22893
|
+
return out.join("\n");
|
|
22894
|
+
}
|
|
22895
|
+
function synthesizeNewFileDiff(file, content) {
|
|
22896
|
+
const lines = content.split("\n");
|
|
22897
|
+
const hasTrailingNewline = content.endsWith("\n");
|
|
22898
|
+
const bodyLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
22899
|
+
const header = [
|
|
22900
|
+
`diff --git a/${file} b/${file}`,
|
|
22901
|
+
"new file mode 100644",
|
|
22902
|
+
"--- /dev/null",
|
|
22903
|
+
`+++ b/${file}`,
|
|
22904
|
+
`@@ -0,0 +1,${bodyLines.length} @@`
|
|
22905
|
+
].join("\n");
|
|
22906
|
+
const body = bodyLines.map((l) => `+${l}`).join("\n");
|
|
22907
|
+
const trailer = hasTrailingNewline ? "" : "\n\";
|
|
22908
|
+
return header + "\n" + body + trailer + "\n";
|
|
22909
|
+
}
|
|
22910
|
+
function synthesizeDeleteFileDiff(file, content) {
|
|
22911
|
+
const lines = content.split("\n");
|
|
22912
|
+
const hasTrailingNewline = content.endsWith("\n");
|
|
22913
|
+
const bodyLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
22914
|
+
const header = [
|
|
22915
|
+
`diff --git a/${file} b/${file}`,
|
|
22916
|
+
"deleted file mode 100644",
|
|
22917
|
+
`--- a/${file}`,
|
|
22918
|
+
"+++ /dev/null",
|
|
22919
|
+
`@@ -1,${bodyLines.length} +0,0 @@`
|
|
22920
|
+
].join("\n");
|
|
22921
|
+
const body = bodyLines.map((l) => `-${l}`).join("\n");
|
|
22922
|
+
const trailer = hasTrailingNewline ? "" : "\n\";
|
|
22923
|
+
return header + "\n" + body + trailer + "\n";
|
|
22924
|
+
}
|
|
22925
|
+
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
22926
|
+
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
22927
|
+
for (const r2 of results) resultByPatchId.set(r2.patch.id, r2);
|
|
22928
|
+
const applied = [];
|
|
22929
|
+
const unresolved = [];
|
|
22930
|
+
const absorbed = [];
|
|
22931
|
+
for (const patch of patchesPassedToCommit) {
|
|
22932
|
+
if (absorbedPatchIds.has(patch.id)) {
|
|
22933
|
+
absorbed.push(patch);
|
|
22934
|
+
continue;
|
|
22935
|
+
}
|
|
22936
|
+
const r2 = resultByPatchId.get(patch.id);
|
|
22937
|
+
if (!r2) {
|
|
22938
|
+
applied.push(patch);
|
|
22939
|
+
continue;
|
|
22940
|
+
}
|
|
22941
|
+
if (r2.status === "conflict" || patch.status === "unresolved") {
|
|
22942
|
+
unresolved.push(patch);
|
|
22943
|
+
continue;
|
|
22944
|
+
}
|
|
22945
|
+
if (r2.status === "applied") {
|
|
22946
|
+
applied.push(patch);
|
|
22947
|
+
continue;
|
|
22948
|
+
}
|
|
22949
|
+
}
|
|
22950
|
+
return { applied, unresolved, absorbed };
|
|
22541
22951
|
}
|
|
22542
22952
|
async function bootstrap(outputDir, options) {
|
|
22543
22953
|
const git = new GitClient(outputDir);
|
|
@@ -22703,7 +23113,7 @@ function parseGitLog(log) {
|
|
|
22703
23113
|
});
|
|
22704
23114
|
}
|
|
22705
23115
|
function ensureFernignoreEntries(outputDir) {
|
|
22706
|
-
const fernignorePath = (0,
|
|
23116
|
+
const fernignorePath = (0, import_path13.join)(outputDir, ".fernignore");
|
|
22707
23117
|
let content = "";
|
|
22708
23118
|
if ((0, import_fs9.existsSync)(fernignorePath)) {
|
|
22709
23119
|
content = (0, import_fs9.readFileSync)(fernignorePath, "utf-8");
|
|
@@ -22726,7 +23136,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
22726
23136
|
return true;
|
|
22727
23137
|
}
|
|
22728
23138
|
function ensureGitattributesEntries(outputDir) {
|
|
22729
|
-
const gitattributesPath = (0,
|
|
23139
|
+
const gitattributesPath = (0, import_path13.join)(outputDir, ".gitattributes");
|
|
22730
23140
|
let content = "";
|
|
22731
23141
|
if ((0, import_fs9.existsSync)(gitattributesPath)) {
|
|
22732
23142
|
content = (0, import_fs9.readFileSync)(gitattributesPath, "utf-8");
|
|
@@ -22921,7 +23331,7 @@ async function resolve7(outputDir, options) {
|
|
|
22921
23331
|
const resolvingPatches = lockManager.getResolvingPatches();
|
|
22922
23332
|
if (unresolvedPatches.length > 0) {
|
|
22923
23333
|
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
22924
|
-
await applicator.applyPatches(unresolvedPatches);
|
|
23334
|
+
await applicator.applyPatches(unresolvedPatches, { applyMode: "resolve" });
|
|
22925
23335
|
const markerFiles = await findConflictMarkerFiles(git);
|
|
22926
23336
|
if (markerFiles.length > 0) {
|
|
22927
23337
|
for (const patch of unresolvedPatches) {
|
|
@@ -22947,20 +23357,80 @@ async function resolve7(outputDir, options) {
|
|
|
22947
23357
|
if (patchesToCommit.length > 0) {
|
|
22948
23358
|
const currentGen = lock.current_generation;
|
|
22949
23359
|
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
23360
|
+
const warnings = [];
|
|
22950
23361
|
let patchesResolved = 0;
|
|
23362
|
+
const pass1 = [];
|
|
22951
23363
|
for (const patch of patchesToCommit) {
|
|
22952
|
-
const
|
|
22953
|
-
|
|
23364
|
+
const result = await computePerPatchDiffWithFallback(
|
|
23365
|
+
patch,
|
|
23366
|
+
(f2) => git.showFile(currentGen, f2),
|
|
23367
|
+
(f2) => (0, import_promises7.readFile)((0, import_path14.join)(outputDir, f2), "utf-8").catch(() => null),
|
|
23368
|
+
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
23369
|
+
);
|
|
23370
|
+
if (result === null) {
|
|
23371
|
+
warnings.push(
|
|
23372
|
+
`Patch ${patch.id}: cumulative-diff fallback failed for unlocatable files; preserving patch unchanged`
|
|
23373
|
+
);
|
|
23374
|
+
pass1.push({ kind: "skip" });
|
|
23375
|
+
continue;
|
|
23376
|
+
}
|
|
23377
|
+
const diff = result.diff;
|
|
23378
|
+
if (!diff.trim()) {
|
|
23379
|
+
pass1.push({ kind: "revert" });
|
|
23380
|
+
continue;
|
|
23381
|
+
}
|
|
23382
|
+
const hash = detector.computeContentHash(diff);
|
|
23383
|
+
pass1.push({
|
|
23384
|
+
kind: "apply",
|
|
23385
|
+
diff,
|
|
23386
|
+
hash,
|
|
23387
|
+
hadFallback: result.hadFallback,
|
|
23388
|
+
fallbackFiles: result.fallbackFiles
|
|
23389
|
+
});
|
|
23390
|
+
}
|
|
23391
|
+
const lastIndexByHash = /* @__PURE__ */ new Map();
|
|
23392
|
+
for (let i = 0; i < pass1.length; i++) {
|
|
23393
|
+
const r2 = pass1[i];
|
|
23394
|
+
if (r2.kind === "apply") lastIndexByHash.set(r2.hash, i);
|
|
23395
|
+
}
|
|
23396
|
+
for (let i = 0; i < patchesToCommit.length; i++) {
|
|
23397
|
+
const patch = patchesToCommit[i];
|
|
23398
|
+
const r2 = pass1[i];
|
|
23399
|
+
if (r2.kind === "skip") continue;
|
|
23400
|
+
if (r2.kind === "revert") {
|
|
23401
|
+
lockManager.removePatch(patch.id);
|
|
23402
|
+
continue;
|
|
23403
|
+
}
|
|
23404
|
+
if (lastIndexByHash.get(r2.hash) !== i) {
|
|
23405
|
+
const canonicalIdx = lastIndexByHash.get(r2.hash);
|
|
23406
|
+
const canonical = patchesToCommit[canonicalIdx];
|
|
22954
23407
|
lockManager.removePatch(patch.id);
|
|
23408
|
+
warnings.push(
|
|
23409
|
+
`Consolidated patch ${patch.id} (commit ${(patch.original_commit || "<missing>").slice(0, 7)}) into patch ${canonical.id} (commit ${(canonical.original_commit || "<missing>").slice(0, 7)}): byte-identical patch_content after resolution. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
23410
|
+
);
|
|
22955
23411
|
continue;
|
|
22956
23412
|
}
|
|
22957
|
-
|
|
22958
|
-
|
|
23413
|
+
if (r2.hadFallback) {
|
|
23414
|
+
warnings.push(
|
|
23415
|
+
`Patch ${patch.id}: ${r2.fallbackFiles.join(", ")} could not be anchored; cumulative-diff fallback applied for those files`
|
|
23416
|
+
);
|
|
23417
|
+
}
|
|
23418
|
+
const changedFiles = r2.hadFallback ? await getChangedFiles(git, currentGen, patch.files) : extractFilesFromDiff(r2.diff);
|
|
23419
|
+
const filesForSnapshot = changedFiles.length > 0 ? changedFiles : patch.files;
|
|
23420
|
+
const theirsSnapshot = {};
|
|
23421
|
+
for (const file of filesForSnapshot) {
|
|
23422
|
+
if (isBinaryFile(file)) continue;
|
|
23423
|
+
const content = await (0, import_promises7.readFile)((0, import_path14.join)(outputDir, file), "utf-8").catch(() => null);
|
|
23424
|
+
if (content != null) {
|
|
23425
|
+
theirsSnapshot[file] = content;
|
|
23426
|
+
}
|
|
23427
|
+
}
|
|
22959
23428
|
lockManager.markPatchResolved(patch.id, {
|
|
22960
|
-
patch_content: diff,
|
|
22961
|
-
content_hash:
|
|
23429
|
+
patch_content: r2.diff,
|
|
23430
|
+
content_hash: r2.hash,
|
|
22962
23431
|
base_generation: currentGen,
|
|
22963
|
-
files:
|
|
23432
|
+
files: filesForSnapshot,
|
|
23433
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
22964
23434
|
});
|
|
22965
23435
|
patchesResolved++;
|
|
22966
23436
|
}
|
|
@@ -22976,7 +23446,8 @@ async function resolve7(outputDir, options) {
|
|
|
22976
23446
|
success: true,
|
|
22977
23447
|
commitSha: commitSha2,
|
|
22978
23448
|
phase: "committed",
|
|
22979
|
-
patchesResolved
|
|
23449
|
+
patchesResolved,
|
|
23450
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
22980
23451
|
};
|
|
22981
23452
|
}
|
|
22982
23453
|
const committer = new ReplayCommitter(git, outputDir);
|
|
@@ -22994,6 +23465,24 @@ async function getChangedFiles(git, currentGen, files) {
|
|
|
22994
23465
|
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !f2.startsWith(".fern/"));
|
|
22995
23466
|
return changed.length > 0 ? changed : files;
|
|
22996
23467
|
}
|
|
23468
|
+
function extractFilesFromDiff(unifiedDiff2) {
|
|
23469
|
+
const out = /* @__PURE__ */ new Set();
|
|
23470
|
+
const renameSources = /* @__PURE__ */ new Set();
|
|
23471
|
+
let currentTarget = null;
|
|
23472
|
+
for (const line of unifiedDiff2.split("\n")) {
|
|
23473
|
+
const headerMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
23474
|
+
if (headerMatch) {
|
|
23475
|
+
currentTarget = headerMatch[2];
|
|
23476
|
+
if (!currentTarget.startsWith(".fern/")) out.add(currentTarget);
|
|
23477
|
+
continue;
|
|
23478
|
+
}
|
|
23479
|
+
if (currentTarget && line.startsWith("rename from ")) {
|
|
23480
|
+
renameSources.add(line.slice("rename from ".length));
|
|
23481
|
+
}
|
|
23482
|
+
}
|
|
23483
|
+
for (const src of renameSources) out.delete(src);
|
|
23484
|
+
return Array.from(out);
|
|
23485
|
+
}
|
|
22997
23486
|
function status(outputDir) {
|
|
22998
23487
|
const lockManager = new LockfileManager(outputDir);
|
|
22999
23488
|
if (!lockManager.exists()) {
|
|
@@ -23041,33 +23530,43 @@ function status(outputDir) {
|
|
|
23041
23530
|
excludePatterns
|
|
23042
23531
|
};
|
|
23043
23532
|
}
|
|
23044
|
-
var
|
|
23533
|
+
var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_promises4, import_os2, import_path9, import_fs7, import_promises5, import_path10, import_promises6, import_os3, import_path11, import_child_process3, import_crypto2, import_fs8, import_path12, import_yaml2, import_crypto3, import_fs9, import_path13, import_fs10, import_promises7, import_path14, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, HybridReconstruction_exports, init_HybridReconstruction, PatchApplyTheirs_exports, init_PatchApplyTheirs, CREDENTIAL_PATTERNS, SENSITIVE_FILE_PATTERNS, FERN_BOT_NAME, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileNotFoundError, LockfileManager, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, BINARY_EXTENSIONS, ReplayApplicator, INFRASTRUCTURE_FILES, ReplayDetector, ReplayCommitter, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
|
|
23045
23534
|
var init_dist3 = __esm({
|
|
23046
|
-
"../../node_modules/.pnpm/@fern-api+replay@0.
|
|
23535
|
+
"../../node_modules/.pnpm/@fern-api+replay@0.14.1/node_modules/@fern-api/replay/dist/index.js"() {
|
|
23047
23536
|
"use strict";
|
|
23048
23537
|
init_esm4();
|
|
23049
|
-
import_fs6 = require("fs");
|
|
23050
|
-
import_path7 = require("path");
|
|
23051
|
-
import_yaml = __toESM(require_dist3(), 1);
|
|
23052
|
-
import_crypto = require("crypto");
|
|
23053
|
-
init_diff3();
|
|
23054
23538
|
import_promises3 = require("fs/promises");
|
|
23055
23539
|
import_os = require("os");
|
|
23540
|
+
import_path7 = require("path");
|
|
23541
|
+
import_fs6 = require("fs");
|
|
23056
23542
|
import_path8 = require("path");
|
|
23543
|
+
import_yaml = __toESM(require_dist3(), 1);
|
|
23544
|
+
import_crypto = require("crypto");
|
|
23545
|
+
import_promises4 = require("fs/promises");
|
|
23546
|
+
import_os2 = require("os");
|
|
23547
|
+
import_path9 = require("path");
|
|
23057
23548
|
init_esm3();
|
|
23549
|
+
init_diff3();
|
|
23058
23550
|
import_fs7 = require("fs");
|
|
23059
|
-
|
|
23551
|
+
import_promises5 = require("fs/promises");
|
|
23552
|
+
import_path10 = require("path");
|
|
23060
23553
|
init_esm3();
|
|
23554
|
+
import_promises6 = require("fs/promises");
|
|
23555
|
+
import_os3 = require("os");
|
|
23556
|
+
import_path11 = require("path");
|
|
23557
|
+
import_child_process3 = require("child_process");
|
|
23061
23558
|
import_crypto2 = require("crypto");
|
|
23062
23559
|
import_fs8 = require("fs");
|
|
23063
|
-
|
|
23560
|
+
import_path12 = require("path");
|
|
23064
23561
|
init_esm3();
|
|
23065
23562
|
import_yaml2 = __toESM(require_dist3(), 1);
|
|
23066
23563
|
import_crypto3 = require("crypto");
|
|
23067
23564
|
import_fs9 = require("fs");
|
|
23068
|
-
|
|
23565
|
+
import_path13 = require("path");
|
|
23069
23566
|
init_esm3();
|
|
23070
23567
|
import_fs10 = require("fs");
|
|
23568
|
+
import_promises7 = require("fs/promises");
|
|
23569
|
+
import_path14 = require("path");
|
|
23071
23570
|
__defProp3 = Object.defineProperty;
|
|
23072
23571
|
__getOwnPropNames3 = Object.getOwnPropertyNames;
|
|
23073
23572
|
__esm3 = (fn, res) => function __init() {
|
|
@@ -23095,9 +23594,9 @@ var init_dist3 = __esm({
|
|
|
23095
23594
|
return this.git.raw(args);
|
|
23096
23595
|
}
|
|
23097
23596
|
async execWithInput(args, input) {
|
|
23098
|
-
const { spawn:
|
|
23597
|
+
const { spawn: spawn22 } = await import("child_process");
|
|
23099
23598
|
return new Promise((resolve22, reject) => {
|
|
23100
|
-
const proc =
|
|
23599
|
+
const proc = spawn22("git", args, { cwd: this.repoPath });
|
|
23101
23600
|
let stdout = "";
|
|
23102
23601
|
let stderr = "";
|
|
23103
23602
|
proc.stdout.on("data", (data) => {
|
|
@@ -23223,6 +23722,16 @@ var init_dist3 = __esm({
|
|
|
23223
23722
|
"use strict";
|
|
23224
23723
|
}
|
|
23225
23724
|
});
|
|
23725
|
+
PatchApplyTheirs_exports = {};
|
|
23726
|
+
__export3(PatchApplyTheirs_exports, {
|
|
23727
|
+
applyPatchToContent: () => applyPatchToContent
|
|
23728
|
+
});
|
|
23729
|
+
init_PatchApplyTheirs = __esm3({
|
|
23730
|
+
"src/PatchApplyTheirs.ts"() {
|
|
23731
|
+
"use strict";
|
|
23732
|
+
init_GitClient();
|
|
23733
|
+
}
|
|
23734
|
+
});
|
|
23226
23735
|
CREDENTIAL_PATTERNS = [
|
|
23227
23736
|
{ name: "PRIVATE KEY block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
23228
23737
|
{ name: "OPENSSH key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
|
|
@@ -23262,10 +23771,10 @@ var init_dist3 = __esm({
|
|
|
23262
23771
|
this.outputDir = outputDir;
|
|
23263
23772
|
}
|
|
23264
23773
|
get lockfilePath() {
|
|
23265
|
-
return (0,
|
|
23774
|
+
return (0, import_path8.join)(this.outputDir, ".fern", "replay.lock");
|
|
23266
23775
|
}
|
|
23267
23776
|
get customizationsPath() {
|
|
23268
|
-
return (0,
|
|
23777
|
+
return (0, import_path8.join)(this.outputDir, ".fern", "replay.yml");
|
|
23269
23778
|
}
|
|
23270
23779
|
exists() {
|
|
23271
23780
|
return (0, import_fs6.existsSync)(this.lockfilePath);
|
|
@@ -23309,7 +23818,17 @@ var init_dist3 = __esm({
|
|
|
23309
23818
|
this.warnings.length = 0;
|
|
23310
23819
|
const cleanPatches = [];
|
|
23311
23820
|
for (const patch of this.lock.patches) {
|
|
23312
|
-
const
|
|
23821
|
+
const diffMatches = scanForCredentials(patch.patch_content);
|
|
23822
|
+
const snapshotMatches = [];
|
|
23823
|
+
if (patch.theirs_snapshot) {
|
|
23824
|
+
for (const content2 of Object.values(patch.theirs_snapshot)) {
|
|
23825
|
+
const m = scanForCredentials(content2);
|
|
23826
|
+
for (const x2 of m) {
|
|
23827
|
+
if (!snapshotMatches.includes(x2)) snapshotMatches.push(x2);
|
|
23828
|
+
}
|
|
23829
|
+
}
|
|
23830
|
+
}
|
|
23831
|
+
const credentialMatches = [...diffMatches, ...snapshotMatches.filter((m) => !diffMatches.includes(m))];
|
|
23313
23832
|
const sensitiveFiles = patch.files.filter((f2) => isSensitiveFile(f2));
|
|
23314
23833
|
if (credentialMatches.length > 0 || sensitiveFiles.length > 0) {
|
|
23315
23834
|
const reasons = [];
|
|
@@ -23327,7 +23846,7 @@ var init_dist3 = __esm({
|
|
|
23327
23846
|
}
|
|
23328
23847
|
}
|
|
23329
23848
|
this.lock.patches = cleanPatches;
|
|
23330
|
-
const dir = (0,
|
|
23849
|
+
const dir = (0, import_path8.dirname)(this.lockfilePath);
|
|
23331
23850
|
if (!(0, import_fs6.existsSync)(dir)) {
|
|
23332
23851
|
(0, import_fs6.mkdirSync)(dir, { recursive: true });
|
|
23333
23852
|
}
|
|
@@ -23427,879 +23946,440 @@ var init_dist3 = __esm({
|
|
|
23427
23946
|
}
|
|
23428
23947
|
}
|
|
23429
23948
|
};
|
|
23430
|
-
|
|
23431
|
-
|
|
23949
|
+
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
23950
|
+
CONFLICT_SEPARATOR = "=======";
|
|
23951
|
+
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
23952
|
+
BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
23953
|
+
".png",
|
|
23954
|
+
".jpg",
|
|
23955
|
+
".jpeg",
|
|
23956
|
+
".gif",
|
|
23957
|
+
".bmp",
|
|
23958
|
+
".ico",
|
|
23959
|
+
".webp",
|
|
23960
|
+
".svg",
|
|
23961
|
+
".pdf",
|
|
23962
|
+
".doc",
|
|
23963
|
+
".docx",
|
|
23964
|
+
".xls",
|
|
23965
|
+
".xlsx",
|
|
23966
|
+
".ppt",
|
|
23967
|
+
".pptx",
|
|
23968
|
+
".zip",
|
|
23969
|
+
".gz",
|
|
23970
|
+
".tar",
|
|
23971
|
+
".bz2",
|
|
23972
|
+
".7z",
|
|
23973
|
+
".rar",
|
|
23974
|
+
".jar",
|
|
23975
|
+
".war",
|
|
23976
|
+
".ear",
|
|
23977
|
+
".class",
|
|
23978
|
+
".exe",
|
|
23979
|
+
".dll",
|
|
23980
|
+
".so",
|
|
23981
|
+
".dylib",
|
|
23982
|
+
".o",
|
|
23983
|
+
".a",
|
|
23984
|
+
".woff",
|
|
23985
|
+
".woff2",
|
|
23986
|
+
".ttf",
|
|
23987
|
+
".eot",
|
|
23988
|
+
".otf",
|
|
23989
|
+
".mp3",
|
|
23990
|
+
".mp4",
|
|
23991
|
+
".avi",
|
|
23992
|
+
".mov",
|
|
23993
|
+
".wav",
|
|
23994
|
+
".flac",
|
|
23995
|
+
".sqlite",
|
|
23996
|
+
".db",
|
|
23997
|
+
".pyc",
|
|
23998
|
+
".pyo",
|
|
23999
|
+
".DS_Store"
|
|
24000
|
+
]);
|
|
24001
|
+
ReplayApplicator = class {
|
|
23432
24002
|
git;
|
|
23433
24003
|
lockManager;
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
24004
|
+
outputDir;
|
|
24005
|
+
renameCache = /* @__PURE__ */ new Map();
|
|
24006
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
24007
|
+
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
24008
|
+
/**
|
|
24009
|
+
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
24010
|
+
* of `applyPatches`, read by `mergeFile` to decide:
|
|
24011
|
+
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
24012
|
+
* - whether to populate the accumulator after a conflicted merge
|
|
24013
|
+
* (resolve mode populates so subsequent patches on the same file
|
|
24014
|
+
* can use patch A's THEIRS as a structurally-correct merge base
|
|
24015
|
+
* when their diff was authored against post-A structure)
|
|
24016
|
+
* - whether to retry THEIRS reconstruction against the accumulator
|
|
24017
|
+
* when the BASE-relative reconstruction produced markers from a
|
|
24018
|
+
* cross-patch context mismatch
|
|
24019
|
+
*/
|
|
24020
|
+
currentApplyMode = "replay";
|
|
24021
|
+
/**
|
|
24022
|
+
* Set of files that appear in 2+ patches in the current `applyPatches`
|
|
24023
|
+
* invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
|
|
24024
|
+
* and `populateAccumulatorForPatch` to decide whether to use the patch's
|
|
24025
|
+
* `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
|
|
24026
|
+
* back to line-anchored reconstruction (FER-9525 cross-patch isolation).
|
|
24027
|
+
*
|
|
24028
|
+
* Snapshot-as-primary is correct when the patch owns its file (no other
|
|
24029
|
+
* patch in this run touches it): the snapshot is what the customer
|
|
24030
|
+
* intends for that file post-regen, regardless of generator structural
|
|
24031
|
+
* changes that would invalidate the diff's line anchors.
|
|
24032
|
+
*
|
|
24033
|
+
* When a file IS shared, snapshots are CUMULATIVE across the patches
|
|
24034
|
+
* touching it (each one's snapshot includes prior patches' contributions).
|
|
24035
|
+
* Using a later patch's cumulative snapshot as its individual THEIRS
|
|
24036
|
+
* would propagate prior patches' edits into the merge — surfacing nested
|
|
24037
|
+
* conflicts at regions the patch alone never touched. Reconstruction
|
|
24038
|
+
* via `applyPatchToContent` is required there.
|
|
24039
|
+
*/
|
|
24040
|
+
sharedFiles = /* @__PURE__ */ new Set();
|
|
24041
|
+
constructor(git, lockManager, outputDir) {
|
|
23437
24042
|
this.git = git;
|
|
23438
24043
|
this.lockManager = lockManager;
|
|
23439
|
-
this.
|
|
23440
|
-
}
|
|
23441
|
-
async detectNewPatches() {
|
|
23442
|
-
const lock = this.lockManager.read();
|
|
23443
|
-
const lastGen = this.getLastGeneration(lock);
|
|
23444
|
-
if (!lastGen) {
|
|
23445
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23446
|
-
}
|
|
23447
|
-
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
23448
|
-
if (!exists2) {
|
|
23449
|
-
this.warnings.push(
|
|
23450
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
23451
|
-
);
|
|
23452
|
-
return this.detectPatchesViaTreeDiff(
|
|
23453
|
-
lastGen,
|
|
23454
|
-
/* commitKnownMissing */
|
|
23455
|
-
true
|
|
23456
|
-
);
|
|
23457
|
-
}
|
|
23458
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
23459
|
-
if (!isAncestor) {
|
|
23460
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
23461
|
-
}
|
|
23462
|
-
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
24044
|
+
this.outputDir = outputDir;
|
|
23463
24045
|
}
|
|
23464
24046
|
/**
|
|
23465
|
-
*
|
|
23466
|
-
*
|
|
23467
|
-
*
|
|
23468
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
23469
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
23470
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
23471
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
23472
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
23473
|
-
* generation) get bundled as customer customizations.
|
|
23474
|
-
*
|
|
23475
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
23476
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
24047
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
24048
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
24049
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
23477
24050
|
*/
|
|
23478
|
-
async
|
|
23479
|
-
|
|
24051
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
24052
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24053
|
+
if (gen) return gen;
|
|
23480
24054
|
try {
|
|
23481
|
-
|
|
24055
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
24056
|
+
return {
|
|
24057
|
+
commit_sha: baseGeneration,
|
|
24058
|
+
tree_hash: treeHash,
|
|
24059
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24060
|
+
cli_version: "unknown",
|
|
24061
|
+
generator_versions: {}
|
|
24062
|
+
};
|
|
23482
24063
|
} catch {
|
|
24064
|
+
return void 0;
|
|
23483
24065
|
}
|
|
23484
|
-
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
24066
|
+
}
|
|
24067
|
+
/** Reset inter-patch accumulator for a new cycle. */
|
|
24068
|
+
resetAccumulator() {
|
|
24069
|
+
this.fileTheirsAccumulator.clear();
|
|
24070
|
+
}
|
|
24071
|
+
/**
|
|
24072
|
+
* @internal Test-only.
|
|
24073
|
+
* Returns a defensive copy of the inter-patch accumulator. Used by the
|
|
24074
|
+
* adversarial test suite (FER-9791) to assert invariant I2: after every
|
|
24075
|
+
* applied patch, the accumulator has an entry for each file the patch
|
|
24076
|
+
* touched, and the entry's content matches on-disk.
|
|
24077
|
+
*/
|
|
24078
|
+
getAccumulatorSnapshot() {
|
|
24079
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
24080
|
+
for (const [path5, entry] of this.fileTheirsAccumulator) {
|
|
24081
|
+
snapshot.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
23493
24082
|
}
|
|
23494
|
-
return
|
|
24083
|
+
return snapshot;
|
|
23495
24084
|
}
|
|
23496
24085
|
/**
|
|
23497
|
-
*
|
|
23498
|
-
*
|
|
23499
|
-
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
24086
|
+
* Apply all patches, returning results for each.
|
|
24087
|
+
* Skips patches that match exclude patterns in replay.yml
|
|
23500
24088
|
*
|
|
23501
|
-
*
|
|
23502
|
-
*
|
|
23503
|
-
*
|
|
24089
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
24090
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
24091
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
24092
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
24093
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
24094
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
24095
|
+
* any markers that survive.
|
|
24096
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
24097
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
24098
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
24099
|
+
* when they do overlap, the customer gets nested markers and
|
|
24100
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
23504
24101
|
*/
|
|
23505
|
-
async
|
|
23506
|
-
const
|
|
23507
|
-
|
|
23508
|
-
|
|
23509
|
-
|
|
23510
|
-
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
]);
|
|
23514
|
-
if (!log.trim()) {
|
|
23515
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23516
|
-
}
|
|
23517
|
-
const commits = this.parseGitLog(log);
|
|
23518
|
-
const newPatches = [];
|
|
23519
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
23520
|
-
for (const commit of commits) {
|
|
23521
|
-
if (isGenerationCommit(commit)) {
|
|
23522
|
-
continue;
|
|
23523
|
-
}
|
|
23524
|
-
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
23525
|
-
continue;
|
|
24102
|
+
async applyPatches(patches, opts) {
|
|
24103
|
+
const applyMode = opts?.applyMode ?? "replay";
|
|
24104
|
+
this.currentApplyMode = applyMode;
|
|
24105
|
+
this.resetAccumulator();
|
|
24106
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
24107
|
+
for (const p of patches) {
|
|
24108
|
+
for (const f2 of p.files) {
|
|
24109
|
+
fileFreq.set(f2, (fileFreq.get(f2) ?? 0) + 1);
|
|
23526
24110
|
}
|
|
23527
|
-
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
24111
|
+
}
|
|
24112
|
+
this.sharedFiles = new Set(
|
|
24113
|
+
Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f2]) => f2)
|
|
24114
|
+
);
|
|
24115
|
+
const results = [];
|
|
24116
|
+
for (let i = 0; i < patches.length; i++) {
|
|
24117
|
+
const patch = patches[i];
|
|
24118
|
+
if (this.isExcluded(patch)) {
|
|
24119
|
+
results.push({
|
|
24120
|
+
patch,
|
|
24121
|
+
status: "skipped",
|
|
24122
|
+
method: "git-am"
|
|
23534
24123
|
});
|
|
23535
|
-
if (compositePatch) {
|
|
23536
|
-
newPatches.push(compositePatch);
|
|
23537
|
-
}
|
|
23538
|
-
continue;
|
|
23539
|
-
}
|
|
23540
|
-
let patchContent;
|
|
23541
|
-
try {
|
|
23542
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
23543
|
-
} catch {
|
|
23544
|
-
this.warnings.push(
|
|
23545
|
-
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
23546
|
-
);
|
|
23547
|
-
continue;
|
|
23548
|
-
}
|
|
23549
|
-
let contentHash = this.computeContentHash(patchContent);
|
|
23550
|
-
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
23551
24124
|
continue;
|
|
23552
24125
|
}
|
|
23553
|
-
const
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
24126
|
+
const result = await this.applyPatchWithFallback(patch);
|
|
24127
|
+
results.push(result);
|
|
24128
|
+
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
24129
|
+
const laterFiles = /* @__PURE__ */ new Set();
|
|
24130
|
+
for (let j2 = i + 1; j2 < patches.length; j2++) {
|
|
24131
|
+
for (const f2 of patches[j2].files) {
|
|
24132
|
+
laterFiles.add(f2);
|
|
24133
|
+
}
|
|
24134
|
+
}
|
|
24135
|
+
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
24136
|
+
if (result.resolvedFiles) {
|
|
24137
|
+
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
24138
|
+
resolvedToOriginal.set(resolved, orig);
|
|
24139
|
+
}
|
|
24140
|
+
}
|
|
24141
|
+
for (const fileResult of result.fileResults) {
|
|
24142
|
+
if (fileResult.status !== "conflict") continue;
|
|
24143
|
+
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
24144
|
+
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
24145
|
+
const filePath = (0, import_path9.join)(this.outputDir, fileResult.file);
|
|
23564
24146
|
try {
|
|
23565
|
-
|
|
24147
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf-8");
|
|
24148
|
+
const stripped = stripConflictMarkers(content);
|
|
24149
|
+
await (0, import_promises4.writeFile)(filePath, stripped);
|
|
23566
24150
|
} catch {
|
|
23567
|
-
continue;
|
|
23568
24151
|
}
|
|
23569
|
-
if (!patchContent.trim()) continue;
|
|
23570
|
-
contentHash = this.computeContentHash(patchContent);
|
|
23571
24152
|
}
|
|
23572
24153
|
}
|
|
23573
24154
|
}
|
|
23574
|
-
|
|
23575
|
-
|
|
24155
|
+
}
|
|
24156
|
+
return results;
|
|
24157
|
+
}
|
|
24158
|
+
/**
|
|
24159
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
24160
|
+
* results including any "dropped context lines" — lines that were
|
|
24161
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
24162
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
24163
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
24164
|
+
* 3-way slow path; both must surface the same warning.
|
|
24165
|
+
*/
|
|
24166
|
+
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
24167
|
+
const fileResults = [];
|
|
24168
|
+
if (!baseGen) return fileResults;
|
|
24169
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-acc-"));
|
|
24170
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24171
|
+
const tempGit = new GitClient2(tempDir);
|
|
24172
|
+
await tempGit.exec(["init"]);
|
|
24173
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24174
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24175
|
+
try {
|
|
24176
|
+
for (const filePath of patch.files) {
|
|
24177
|
+
if (isBinaryFile(filePath)) continue;
|
|
24178
|
+
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24179
|
+
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24180
|
+
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
24181
|
+
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
24182
|
+
const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
24183
|
+
const finalOnDisk = await (0, import_promises4.readFile)((0, import_path9.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
24184
|
+
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
24185
|
+
if (effectiveTheirs != null) {
|
|
24186
|
+
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24187
|
+
content: effectiveTheirs,
|
|
24188
|
+
baseGeneration: patch.base_generation
|
|
24189
|
+
});
|
|
24190
|
+
}
|
|
24191
|
+
if (base != null && effectiveTheirs != null && finalOnDisk != null) {
|
|
24192
|
+
const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
|
|
24193
|
+
if (dropped.length > 0) {
|
|
24194
|
+
fileResults.push({
|
|
24195
|
+
file: resolvedPath,
|
|
24196
|
+
status: "merged",
|
|
24197
|
+
droppedContextLines: dropped
|
|
24198
|
+
});
|
|
24199
|
+
}
|
|
24200
|
+
}
|
|
23576
24201
|
}
|
|
23577
|
-
|
|
23578
|
-
|
|
23579
|
-
content_hash: contentHash,
|
|
23580
|
-
original_commit: commit.sha,
|
|
23581
|
-
original_message: commit.message,
|
|
23582
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
23583
|
-
base_generation: lastGen.commit_sha,
|
|
23584
|
-
files,
|
|
23585
|
-
patch_content: patchContent,
|
|
23586
|
-
...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
|
|
24202
|
+
} finally {
|
|
24203
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
23587
24204
|
});
|
|
23588
24205
|
}
|
|
23589
|
-
|
|
23590
|
-
|
|
23591
|
-
|
|
23592
|
-
const
|
|
23593
|
-
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
|
|
24206
|
+
return fileResults;
|
|
24207
|
+
}
|
|
24208
|
+
async applyPatchWithFallback(patch) {
|
|
24209
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24210
|
+
const lock = this.lockManager.read();
|
|
24211
|
+
const currentGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24212
|
+
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
24213
|
+
const needsAccumulation = await Promise.all(
|
|
24214
|
+
patch.files.map(async (f2) => {
|
|
24215
|
+
if (!baseGen) return false;
|
|
24216
|
+
const resolved = await this.resolveFilePath(f2, baseGen.tree_hash, currentTreeHash);
|
|
24217
|
+
return this.fileTheirsAccumulator.has(resolved);
|
|
24218
|
+
})
|
|
24219
|
+
).then((results) => results.some(Boolean));
|
|
24220
|
+
const resolvedFiles = {};
|
|
24221
|
+
for (const filePath of patch.files) {
|
|
24222
|
+
const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
|
|
24223
|
+
if (resolvedPath !== filePath) {
|
|
24224
|
+
resolvedFiles[filePath] = resolvedPath;
|
|
24225
|
+
}
|
|
24226
|
+
}
|
|
24227
|
+
const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
|
|
24228
|
+
const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
|
|
24229
|
+
if (!needsAccumulation && !hasGeneratorRename) {
|
|
24230
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
24231
|
+
for (const filePath of patch.files) {
|
|
24232
|
+
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24233
|
+
snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
|
|
24234
|
+
}
|
|
23597
24235
|
try {
|
|
23598
|
-
|
|
24236
|
+
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
24237
|
+
const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
24238
|
+
return {
|
|
24239
|
+
patch,
|
|
24240
|
+
status: "applied",
|
|
24241
|
+
method: "git-am",
|
|
24242
|
+
...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
|
|
24243
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24244
|
+
};
|
|
23599
24245
|
} catch {
|
|
24246
|
+
for (const [filePath, content] of snapshots) {
|
|
24247
|
+
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24248
|
+
if (content != null) {
|
|
24249
|
+
await (0, import_promises4.writeFile)(fullPath, content);
|
|
24250
|
+
} else {
|
|
24251
|
+
await (0, import_promises4.unlink)(fullPath).catch(() => {
|
|
24252
|
+
});
|
|
24253
|
+
}
|
|
24254
|
+
}
|
|
23600
24255
|
}
|
|
23601
|
-
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
23606
|
-
|
|
23607
|
-
|
|
23608
|
-
|
|
23609
|
-
|
|
24256
|
+
}
|
|
24257
|
+
return this.applyWithThreeWayMerge(patch);
|
|
24258
|
+
}
|
|
24259
|
+
async applyWithThreeWayMerge(patch) {
|
|
24260
|
+
const fileResults = [];
|
|
24261
|
+
const resolvedFiles = {};
|
|
24262
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-"));
|
|
24263
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24264
|
+
const tempGit = new GitClient2(tempDir);
|
|
24265
|
+
await tempGit.exec(["init"]);
|
|
24266
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24267
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24268
|
+
try {
|
|
24269
|
+
for (const filePath of patch.files) {
|
|
24270
|
+
if (isBinaryFile(filePath)) {
|
|
24271
|
+
fileResults.push({
|
|
24272
|
+
file: filePath,
|
|
24273
|
+
status: "skipped",
|
|
24274
|
+
reason: "binary-file"
|
|
24275
|
+
});
|
|
24276
|
+
continue;
|
|
24277
|
+
}
|
|
24278
|
+
const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
|
|
24279
|
+
if (result.file !== filePath) {
|
|
24280
|
+
resolvedFiles[filePath] = result.file;
|
|
23610
24281
|
}
|
|
24282
|
+
fileResults.push(result);
|
|
23611
24283
|
}
|
|
23612
|
-
|
|
23613
|
-
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
|
|
24284
|
+
} finally {
|
|
24285
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24286
|
+
});
|
|
24287
|
+
}
|
|
24288
|
+
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
24289
|
+
const hasConflicts = conflictFiles.length > 0;
|
|
24290
|
+
const conflictReason = hasConflicts ? conflictFiles.some((f2) => f2.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
|
|
24291
|
+
return {
|
|
24292
|
+
patch,
|
|
24293
|
+
status: hasConflicts ? "conflict" : "applied",
|
|
24294
|
+
method: "3way-merge",
|
|
24295
|
+
fileResults,
|
|
24296
|
+
conflictReason,
|
|
24297
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24298
|
+
};
|
|
24299
|
+
}
|
|
24300
|
+
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
24301
|
+
try {
|
|
24302
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24303
|
+
if (!baseGen) {
|
|
24304
|
+
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
24305
|
+
}
|
|
24306
|
+
const lock = this.lockManager.read();
|
|
24307
|
+
const currentGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24308
|
+
const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
|
|
24309
|
+
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24310
|
+
const metadata = {
|
|
24311
|
+
patchId: patch.id,
|
|
24312
|
+
patchMessage: patch.original_message,
|
|
24313
|
+
baseGeneration: patch.base_generation,
|
|
24314
|
+
currentGeneration: lock.current_generation
|
|
24315
|
+
};
|
|
24316
|
+
let base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24317
|
+
let renameSourcePath;
|
|
24318
|
+
if (!base) {
|
|
24319
|
+
const renameSource = this.extractRenameSource(patch.patch_content, filePath);
|
|
24320
|
+
if (renameSource) {
|
|
24321
|
+
base = await this.git.showFile(baseGen.tree_hash, renameSource);
|
|
24322
|
+
renameSourcePath = renameSource;
|
|
23618
24323
|
}
|
|
23619
24324
|
}
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
);
|
|
23626
|
-
if (
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
24325
|
+
const oursPath = (0, import_path9.join)(this.outputDir, resolvedPath);
|
|
24326
|
+
const ours = await (0, import_promises4.readFile)(oursPath, "utf-8").catch(() => null);
|
|
24327
|
+
let ghostReconstructed = false;
|
|
24328
|
+
let theirs = null;
|
|
24329
|
+
if (!base && ours && !renameSourcePath) {
|
|
24330
|
+
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
24331
|
+
if (!treeReachable) {
|
|
24332
|
+
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
24333
|
+
if (fileDiff) {
|
|
24334
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
24335
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
24336
|
+
if (result) {
|
|
24337
|
+
base = result.base;
|
|
24338
|
+
theirs = result.theirs;
|
|
24339
|
+
ghostReconstructed = true;
|
|
24340
|
+
}
|
|
24341
|
+
}
|
|
23630
24342
|
}
|
|
23631
24343
|
}
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
if (
|
|
23637
|
-
|
|
23638
|
-
|
|
24344
|
+
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
24345
|
+
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
24346
|
+
const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
|
|
24347
|
+
if (!ghostReconstructed) {
|
|
24348
|
+
if (useSnapshotAsPrimary) {
|
|
24349
|
+
theirs = snapshotForFile;
|
|
24350
|
+
} else {
|
|
24351
|
+
theirs = await this.applyPatchToContent(
|
|
24352
|
+
base,
|
|
24353
|
+
patch.patch_content,
|
|
24354
|
+
filePath,
|
|
24355
|
+
tempGit,
|
|
24356
|
+
tempDir,
|
|
24357
|
+
renameSourcePath
|
|
24358
|
+
);
|
|
24359
|
+
const reconstructionHasMarkers = theirs != null && (theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization"));
|
|
24360
|
+
const baseHadMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
24361
|
+
if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
|
|
24362
|
+
theirs = snapshotForFile;
|
|
24363
|
+
}
|
|
23639
24364
|
}
|
|
23640
24365
|
}
|
|
23641
|
-
|
|
23642
|
-
|
|
23643
|
-
|
|
23644
|
-
|
|
23645
|
-
|
|
23646
|
-
|
|
23647
|
-
|
|
23648
|
-
|
|
23649
|
-
|
|
23650
|
-
|
|
23651
|
-
|
|
23652
|
-
|
|
23653
|
-
|
|
23654
|
-
|
|
23655
|
-
|
|
23656
|
-
* 1. Strip email wrapper: everything before the first `diff --git` line
|
|
23657
|
-
* (From:, Subject:, Date:, diffstat, blank separators).
|
|
23658
|
-
* 2. Strip `index` lines (blob SHA pairs change across rebases).
|
|
23659
|
-
* 3. Strip trailing git version marker (`-- \n<version>`).
|
|
23660
|
-
*
|
|
23661
|
-
* This ensures content hashes match across the no-patches and normal-
|
|
23662
|
-
* regeneration flows, which store formatPatch and git-diff formats
|
|
23663
|
-
* respectively (FER-9850, D5-D9).
|
|
23664
|
-
*/
|
|
23665
|
-
computeContentHash(patchContent) {
|
|
23666
|
-
const lines = patchContent.split("\n");
|
|
23667
|
-
const diffStart = lines.findIndex((l) => l.startsWith("diff --git "));
|
|
23668
|
-
const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
|
|
23669
|
-
const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
|
|
23670
|
-
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
23671
|
-
}
|
|
23672
|
-
/**
|
|
23673
|
-
* FER-9805 — Drop patches whose files were transiently created and then
|
|
23674
|
-
* deleted within the current linear detection window, and are absent from
|
|
23675
|
-
* HEAD at the end of the window.
|
|
23676
|
-
*
|
|
23677
|
-
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
23678
|
-
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
23679
|
-
* linear history each commit becomes its own patch, so a file that was
|
|
23680
|
-
* briefly added and later removed survives as a create+delete pair whose
|
|
23681
|
-
* cumulative diff is empty. We drop such patches before they reach the
|
|
23682
|
-
* lockfile.
|
|
23683
|
-
*
|
|
23684
|
-
* A file is "transient" when:
|
|
23685
|
-
* 1. some patch in the window sets `new file mode` for it (a creation), AND
|
|
23686
|
-
* 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
|
|
23687
|
-
* 3. it is absent from HEAD's tree.
|
|
23688
|
-
*
|
|
23689
|
-
* The HEAD guard (#3) prevents false positives when a file is deleted
|
|
23690
|
-
* and then recreated with different content — the cumulative diff of
|
|
23691
|
-
* such a pair is non-empty and must be preserved.
|
|
23692
|
-
*
|
|
23693
|
-
* This only drops patches whose `files` are a subset of the transient
|
|
23694
|
-
* set. A partial-transient patch (one that touches a transient file
|
|
23695
|
-
* alongside a persistent one) is left unmodified; surgical stripping of
|
|
23696
|
-
* single files from a multi-file patch would require a follow-up that
|
|
23697
|
-
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
23698
|
-
* current failing test exercises this, and leaving the patch intact
|
|
23699
|
-
* preserves today's behaviour. TODO(FER-9805): revisit if a future
|
|
23700
|
-
* adversarial test demands per-file stripping.
|
|
23701
|
-
*/
|
|
23702
|
-
async collapseNetZeroFiles(patches) {
|
|
23703
|
-
if (patches.length === 0) return patches;
|
|
23704
|
-
const stateByFile = /* @__PURE__ */ new Map();
|
|
23705
|
-
for (const patch of patches) {
|
|
23706
|
-
for (const header of parsePatchFileHeaders(patch.patch_content)) {
|
|
23707
|
-
if (!header.isCreate && !header.isDelete) continue;
|
|
23708
|
-
const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
|
|
23709
|
-
if (header.isCreate) prior.created = true;
|
|
23710
|
-
if (header.isDelete) prior.deleted = true;
|
|
23711
|
-
stateByFile.set(header.path, prior);
|
|
23712
|
-
}
|
|
23713
|
-
}
|
|
23714
|
-
let anyCandidate = false;
|
|
23715
|
-
for (const state of stateByFile.values()) {
|
|
23716
|
-
if (state.created && state.deleted) {
|
|
23717
|
-
anyCandidate = true;
|
|
23718
|
-
break;
|
|
23719
|
-
}
|
|
23720
|
-
}
|
|
23721
|
-
if (!anyCandidate) return patches;
|
|
23722
|
-
const headFiles = await this.listHeadFiles();
|
|
23723
|
-
const transient = /* @__PURE__ */ new Set();
|
|
23724
|
-
for (const [file, state] of stateByFile) {
|
|
23725
|
-
if (state.created && state.deleted && !headFiles.has(file)) {
|
|
23726
|
-
transient.add(file);
|
|
23727
|
-
}
|
|
23728
|
-
}
|
|
23729
|
-
if (transient.size === 0) return patches;
|
|
23730
|
-
return patches.filter((patch) => !patch.files.every((f2) => transient.has(f2)));
|
|
23731
|
-
}
|
|
23732
|
-
/**
|
|
23733
|
-
* List every tracked path at HEAD (one `git ls-tree -r` invocation).
|
|
23734
|
-
* Returns an empty Set if HEAD is unborn or the invocation fails — the
|
|
23735
|
-
* caller then treats every candidate as "not in HEAD" and may collapse
|
|
23736
|
-
* more aggressively. On typical SDK repos this is a single sub-10ms call.
|
|
23737
|
-
*/
|
|
23738
|
-
async listHeadFiles() {
|
|
23739
|
-
try {
|
|
23740
|
-
const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
23741
|
-
return new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
23742
|
-
} catch {
|
|
23743
|
-
return /* @__PURE__ */ new Set();
|
|
23744
|
-
}
|
|
23745
|
-
}
|
|
23746
|
-
/**
|
|
23747
|
-
* Create a single composite patch for a merge commit by diffing the merge
|
|
23748
|
-
* commit against its first parent. This captures the net change of the
|
|
23749
|
-
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
23750
|
-
* zero-net-change ghost conflicts.
|
|
23751
|
-
*/
|
|
23752
|
-
async createMergeCompositePatch(input) {
|
|
23753
|
-
const { mergeCommit, firstParent, lastGen, lock } = input;
|
|
23754
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
23755
|
-
const filesOutput = await this.git.exec([
|
|
23756
|
-
"diff",
|
|
23757
|
-
"--name-only",
|
|
23758
|
-
firstParent,
|
|
23759
|
-
mergeCommit.sha
|
|
23760
|
-
]);
|
|
23761
|
-
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
23762
|
-
const sensitiveMergeFiles = allMergeFiles.filter((f2) => isSensitiveFile(f2));
|
|
23763
|
-
const files = allMergeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
23764
|
-
if (sensitiveMergeFiles.length > 0) {
|
|
23765
|
-
this.warnings.push(
|
|
23766
|
-
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
23767
|
-
);
|
|
23768
|
-
}
|
|
23769
|
-
if (files.length === 0) return null;
|
|
23770
|
-
const diff = await this.git.exec([
|
|
23771
|
-
"diff",
|
|
23772
|
-
firstParent,
|
|
23773
|
-
mergeCommit.sha,
|
|
23774
|
-
"--",
|
|
23775
|
-
...files
|
|
23776
|
-
]);
|
|
23777
|
-
if (!diff.trim()) return null;
|
|
23778
|
-
const contentHash = this.computeContentHash(diff);
|
|
23779
|
-
if (lock.patches.some((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
23780
|
-
return null;
|
|
23781
|
-
}
|
|
23782
|
-
return {
|
|
23783
|
-
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
23784
|
-
content_hash: contentHash,
|
|
23785
|
-
original_commit: mergeCommit.sha,
|
|
23786
|
-
original_message: mergeCommit.message,
|
|
23787
|
-
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
23788
|
-
base_generation: lastGen.commit_sha,
|
|
23789
|
-
files,
|
|
23790
|
-
patch_content: diff,
|
|
23791
|
-
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
23792
|
-
};
|
|
23793
|
-
}
|
|
23794
|
-
/**
|
|
23795
|
-
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
23796
|
-
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
23797
|
-
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
23798
|
-
*/
|
|
23799
|
-
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
23800
|
-
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
23801
|
-
if (!diffBase) {
|
|
23802
|
-
return this.detectPatchesViaCommitScan();
|
|
23803
|
-
}
|
|
23804
|
-
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
23805
|
-
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
23806
|
-
const sensitiveTreeFiles = allTreeFiles.filter((f2) => isSensitiveFile(f2));
|
|
23807
|
-
const files = allTreeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
23808
|
-
if (sensitiveTreeFiles.length > 0) {
|
|
23809
|
-
this.warnings.push(
|
|
23810
|
-
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
23811
|
-
);
|
|
23812
|
-
}
|
|
23813
|
-
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
23814
|
-
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
23815
|
-
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
23816
|
-
const contentHash = this.computeContentHash(diff);
|
|
23817
|
-
const lock = this.lockManager.read();
|
|
23818
|
-
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
23819
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23820
|
-
}
|
|
23821
|
-
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
23822
|
-
const compositePatch = {
|
|
23823
|
-
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
23824
|
-
content_hash: contentHash,
|
|
23825
|
-
original_commit: headSha,
|
|
23826
|
-
original_message: "Customer customizations (composite)",
|
|
23827
|
-
original_author: "composite",
|
|
23828
|
-
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
23829
|
-
// reference to find base file content. diffBase may be the tree_hash.
|
|
23830
|
-
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
23831
|
-
files,
|
|
23832
|
-
patch_content: diff,
|
|
23833
|
-
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
23834
|
-
};
|
|
23835
|
-
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
23836
|
-
}
|
|
23837
|
-
/**
|
|
23838
|
-
* Last-resort detection when both generation commit and tree are unreachable.
|
|
23839
|
-
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
23840
|
-
* skips creation-only commits (squashed history after force push).
|
|
23841
|
-
*
|
|
23842
|
-
* Detected patches use the commit's parent as base_generation so the applicator
|
|
23843
|
-
* can find base file content from the parent's tree (which IS reachable).
|
|
23844
|
-
*/
|
|
23845
|
-
async detectPatchesViaCommitScan() {
|
|
23846
|
-
const lock = this.lockManager.read();
|
|
23847
|
-
const log = await this.git.exec([
|
|
23848
|
-
"log",
|
|
23849
|
-
"--max-count=200",
|
|
23850
|
-
"--format=%H%x00%an%x00%ae%x00%s",
|
|
23851
|
-
"HEAD",
|
|
23852
|
-
"--",
|
|
23853
|
-
this.sdkOutputDir
|
|
23854
|
-
]);
|
|
23855
|
-
if (!log.trim()) {
|
|
23856
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23857
|
-
}
|
|
23858
|
-
const commits = this.parseGitLog(log);
|
|
23859
|
-
const newPatches = [];
|
|
23860
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
23861
|
-
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
23862
|
-
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
23863
|
-
for (const commit of commits) {
|
|
23864
|
-
if (isGenerationCommit(commit)) {
|
|
23865
|
-
continue;
|
|
23866
|
-
}
|
|
23867
|
-
const parents = await this.git.getCommitParents(commit.sha);
|
|
23868
|
-
if (parents.length > 1) {
|
|
23869
|
-
continue;
|
|
23870
|
-
}
|
|
23871
|
-
if (existingCommits.has(commit.sha)) {
|
|
23872
|
-
continue;
|
|
23873
|
-
}
|
|
23874
|
-
let patchContent;
|
|
23875
|
-
try {
|
|
23876
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
23877
|
-
} catch {
|
|
23878
|
-
continue;
|
|
23879
|
-
}
|
|
23880
|
-
let contentHash = this.computeContentHash(patchContent);
|
|
23881
|
-
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
23882
|
-
continue;
|
|
23883
|
-
}
|
|
23884
|
-
if (this.isCreationOnlyPatch(patchContent)) {
|
|
23885
|
-
continue;
|
|
23886
|
-
}
|
|
23887
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
23888
|
-
const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2));
|
|
23889
|
-
const sensitiveScanFiles = allScanFiles.filter((f2) => isSensitiveFile(f2));
|
|
23890
|
-
const files = allScanFiles.filter((f2) => !isSensitiveFile(f2));
|
|
23891
|
-
if (sensitiveScanFiles.length > 0) {
|
|
23892
|
-
this.warnings.push(
|
|
23893
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
|
|
23894
|
-
);
|
|
23895
|
-
if (files.length > 0) {
|
|
23896
|
-
if (parents.length === 0) {
|
|
23897
|
-
continue;
|
|
23898
|
-
}
|
|
23899
|
-
try {
|
|
23900
|
-
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
23901
|
-
} catch {
|
|
23902
|
-
continue;
|
|
23903
|
-
}
|
|
23904
|
-
if (!patchContent.trim()) continue;
|
|
23905
|
-
contentHash = this.computeContentHash(patchContent);
|
|
23906
|
-
}
|
|
23907
|
-
}
|
|
23908
|
-
if (files.length === 0) {
|
|
23909
|
-
continue;
|
|
23910
|
-
}
|
|
23911
|
-
if (parents.length === 0) {
|
|
23912
|
-
continue;
|
|
23913
|
-
}
|
|
23914
|
-
const parentSha = parents[0];
|
|
23915
|
-
newPatches.push({
|
|
23916
|
-
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
23917
|
-
content_hash: contentHash,
|
|
23918
|
-
original_commit: commit.sha,
|
|
23919
|
-
original_message: commit.message,
|
|
23920
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
23921
|
-
base_generation: parentSha,
|
|
23922
|
-
files,
|
|
23923
|
-
patch_content: patchContent
|
|
23924
|
-
});
|
|
23925
|
-
}
|
|
23926
|
-
newPatches.reverse();
|
|
23927
|
-
return { patches: newPatches, revertedPatchIds: [] };
|
|
23928
|
-
}
|
|
23929
|
-
/**
|
|
23930
|
-
* Check if a format-patch consists entirely of new-file creations.
|
|
23931
|
-
* Used to identify squashed commits after force push, which create all files
|
|
23932
|
-
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
23933
|
-
*/
|
|
23934
|
-
isCreationOnlyPatch(patchContent) {
|
|
23935
|
-
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
23936
|
-
if (diffOldHeaders.length === 0) {
|
|
23937
|
-
return false;
|
|
23938
|
-
}
|
|
23939
|
-
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
23940
|
-
}
|
|
23941
|
-
/**
|
|
23942
|
-
* Resolve the best available diff base for a generation record.
|
|
23943
|
-
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
23944
|
-
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
23945
|
-
*/
|
|
23946
|
-
async resolveDiffBase(gen, commitKnownMissing) {
|
|
23947
|
-
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
23948
|
-
return gen.commit_sha;
|
|
23949
|
-
}
|
|
23950
|
-
if (await this.git.treeExists(gen.tree_hash)) {
|
|
23951
|
-
return gen.tree_hash;
|
|
23952
|
-
}
|
|
23953
|
-
return null;
|
|
23954
|
-
}
|
|
23955
|
-
parseGitLog(log) {
|
|
23956
|
-
return log.trim().split("\n").map((line) => {
|
|
23957
|
-
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
23958
|
-
return { sha, authorName, authorEmail, message };
|
|
23959
|
-
});
|
|
23960
|
-
}
|
|
23961
|
-
getLastGeneration(lock) {
|
|
23962
|
-
return lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
23963
|
-
}
|
|
23964
|
-
};
|
|
23965
|
-
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
23966
|
-
CONFLICT_SEPARATOR = "=======";
|
|
23967
|
-
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
23968
|
-
BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
23969
|
-
".png",
|
|
23970
|
-
".jpg",
|
|
23971
|
-
".jpeg",
|
|
23972
|
-
".gif",
|
|
23973
|
-
".bmp",
|
|
23974
|
-
".ico",
|
|
23975
|
-
".webp",
|
|
23976
|
-
".svg",
|
|
23977
|
-
".pdf",
|
|
23978
|
-
".doc",
|
|
23979
|
-
".docx",
|
|
23980
|
-
".xls",
|
|
23981
|
-
".xlsx",
|
|
23982
|
-
".ppt",
|
|
23983
|
-
".pptx",
|
|
23984
|
-
".zip",
|
|
23985
|
-
".gz",
|
|
23986
|
-
".tar",
|
|
23987
|
-
".bz2",
|
|
23988
|
-
".7z",
|
|
23989
|
-
".rar",
|
|
23990
|
-
".jar",
|
|
23991
|
-
".war",
|
|
23992
|
-
".ear",
|
|
23993
|
-
".class",
|
|
23994
|
-
".exe",
|
|
23995
|
-
".dll",
|
|
23996
|
-
".so",
|
|
23997
|
-
".dylib",
|
|
23998
|
-
".o",
|
|
23999
|
-
".a",
|
|
24000
|
-
".woff",
|
|
24001
|
-
".woff2",
|
|
24002
|
-
".ttf",
|
|
24003
|
-
".eot",
|
|
24004
|
-
".otf",
|
|
24005
|
-
".mp3",
|
|
24006
|
-
".mp4",
|
|
24007
|
-
".avi",
|
|
24008
|
-
".mov",
|
|
24009
|
-
".wav",
|
|
24010
|
-
".flac",
|
|
24011
|
-
".sqlite",
|
|
24012
|
-
".db",
|
|
24013
|
-
".pyc",
|
|
24014
|
-
".pyo",
|
|
24015
|
-
".DS_Store"
|
|
24016
|
-
]);
|
|
24017
|
-
ReplayApplicator = class {
|
|
24018
|
-
git;
|
|
24019
|
-
lockManager;
|
|
24020
|
-
outputDir;
|
|
24021
|
-
renameCache = /* @__PURE__ */ new Map();
|
|
24022
|
-
treeExistsCache = /* @__PURE__ */ new Map();
|
|
24023
|
-
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
24024
|
-
constructor(git, lockManager, outputDir) {
|
|
24025
|
-
this.git = git;
|
|
24026
|
-
this.lockManager = lockManager;
|
|
24027
|
-
this.outputDir = outputDir;
|
|
24028
|
-
}
|
|
24029
|
-
/**
|
|
24030
|
-
* Resolve the GenerationRecord for a patch's base_generation.
|
|
24031
|
-
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
24032
|
-
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
24033
|
-
*/
|
|
24034
|
-
async resolveBaseGeneration(baseGeneration) {
|
|
24035
|
-
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24036
|
-
if (gen) return gen;
|
|
24037
|
-
try {
|
|
24038
|
-
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
24039
|
-
return {
|
|
24040
|
-
commit_sha: baseGeneration,
|
|
24041
|
-
tree_hash: treeHash,
|
|
24042
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24043
|
-
cli_version: "unknown",
|
|
24044
|
-
generator_versions: {}
|
|
24045
|
-
};
|
|
24046
|
-
} catch {
|
|
24047
|
-
return void 0;
|
|
24048
|
-
}
|
|
24049
|
-
}
|
|
24050
|
-
/** Reset inter-patch accumulator for a new cycle. */
|
|
24051
|
-
resetAccumulator() {
|
|
24052
|
-
this.fileTheirsAccumulator.clear();
|
|
24053
|
-
}
|
|
24054
|
-
/**
|
|
24055
|
-
* @internal Test-only.
|
|
24056
|
-
* Returns a defensive copy of the inter-patch accumulator. Used by the
|
|
24057
|
-
* adversarial test suite (FER-9791) to assert invariant I2: after every
|
|
24058
|
-
* applied patch, the accumulator has an entry for each file the patch
|
|
24059
|
-
* touched, and the entry's content matches on-disk.
|
|
24060
|
-
*/
|
|
24061
|
-
getAccumulatorSnapshot() {
|
|
24062
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
24063
|
-
for (const [path5, entry] of this.fileTheirsAccumulator) {
|
|
24064
|
-
snapshot.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
24065
|
-
}
|
|
24066
|
-
return snapshot;
|
|
24067
|
-
}
|
|
24068
|
-
/**
|
|
24069
|
-
* Apply all patches, returning results for each.
|
|
24070
|
-
* Skips patches that match exclude patterns in replay.yml
|
|
24071
|
-
*/
|
|
24072
|
-
async applyPatches(patches) {
|
|
24073
|
-
this.resetAccumulator();
|
|
24074
|
-
const results = [];
|
|
24075
|
-
for (let i = 0; i < patches.length; i++) {
|
|
24076
|
-
const patch = patches[i];
|
|
24077
|
-
if (this.isExcluded(patch)) {
|
|
24078
|
-
results.push({
|
|
24079
|
-
patch,
|
|
24080
|
-
status: "skipped",
|
|
24081
|
-
method: "git-am"
|
|
24082
|
-
});
|
|
24083
|
-
continue;
|
|
24084
|
-
}
|
|
24085
|
-
const result = await this.applyPatchWithFallback(patch);
|
|
24086
|
-
results.push(result);
|
|
24087
|
-
if (result.status === "conflict" && result.fileResults) {
|
|
24088
|
-
const laterFiles = /* @__PURE__ */ new Set();
|
|
24089
|
-
for (let j2 = i + 1; j2 < patches.length; j2++) {
|
|
24090
|
-
for (const f2 of patches[j2].files) {
|
|
24091
|
-
laterFiles.add(f2);
|
|
24092
|
-
}
|
|
24093
|
-
}
|
|
24094
|
-
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
24095
|
-
if (result.resolvedFiles) {
|
|
24096
|
-
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
24097
|
-
resolvedToOriginal.set(resolved, orig);
|
|
24098
|
-
}
|
|
24099
|
-
}
|
|
24100
|
-
for (const fileResult of result.fileResults) {
|
|
24101
|
-
if (fileResult.status !== "conflict") continue;
|
|
24102
|
-
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
24103
|
-
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
24104
|
-
const filePath = (0, import_path8.join)(this.outputDir, fileResult.file);
|
|
24105
|
-
try {
|
|
24106
|
-
const content = await (0, import_promises3.readFile)(filePath, "utf-8");
|
|
24107
|
-
const stripped = stripConflictMarkers(content);
|
|
24108
|
-
await (0, import_promises3.writeFile)(filePath, stripped);
|
|
24109
|
-
} catch {
|
|
24110
|
-
}
|
|
24111
|
-
}
|
|
24112
|
-
}
|
|
24113
|
-
}
|
|
24114
|
-
}
|
|
24115
|
-
return results;
|
|
24116
|
-
}
|
|
24117
|
-
/** Populate accumulator after git apply succeeds. */
|
|
24118
|
-
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
24119
|
-
if (!baseGen) return;
|
|
24120
|
-
const tempDir = await (0, import_promises3.mkdtemp)((0, import_path8.join)((0, import_os.tmpdir)(), "replay-acc-"));
|
|
24121
|
-
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24122
|
-
const tempGit = new GitClient2(tempDir);
|
|
24123
|
-
await tempGit.exec(["init"]);
|
|
24124
|
-
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24125
|
-
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24126
|
-
try {
|
|
24127
|
-
for (const filePath of patch.files) {
|
|
24128
|
-
if (isBinaryFile(filePath)) continue;
|
|
24129
|
-
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24130
|
-
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24131
|
-
const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
24132
|
-
const effectiveTheirs = theirs ?? await (0, import_promises3.readFile)((0, import_path8.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
24133
|
-
if (effectiveTheirs != null) {
|
|
24134
|
-
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24135
|
-
content: effectiveTheirs,
|
|
24136
|
-
baseGeneration: patch.base_generation
|
|
24137
|
-
});
|
|
24138
|
-
}
|
|
24139
|
-
}
|
|
24140
|
-
} finally {
|
|
24141
|
-
await (0, import_promises3.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24142
|
-
});
|
|
24143
|
-
}
|
|
24144
|
-
}
|
|
24145
|
-
async applyPatchWithFallback(patch) {
|
|
24146
|
-
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24147
|
-
const lock = this.lockManager.read();
|
|
24148
|
-
const currentGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24149
|
-
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
24150
|
-
const needsAccumulation = await Promise.all(
|
|
24151
|
-
patch.files.map(async (f2) => {
|
|
24152
|
-
if (!baseGen) return false;
|
|
24153
|
-
const resolved = await this.resolveFilePath(f2, baseGen.tree_hash, currentTreeHash);
|
|
24154
|
-
return this.fileTheirsAccumulator.has(resolved);
|
|
24155
|
-
})
|
|
24156
|
-
).then((results) => results.some(Boolean));
|
|
24157
|
-
const resolvedFiles = {};
|
|
24158
|
-
for (const filePath of patch.files) {
|
|
24159
|
-
const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
|
|
24160
|
-
if (resolvedPath !== filePath) {
|
|
24161
|
-
resolvedFiles[filePath] = resolvedPath;
|
|
24162
|
-
}
|
|
24163
|
-
}
|
|
24164
|
-
const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
|
|
24165
|
-
const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
|
|
24166
|
-
if (!needsAccumulation && !hasGeneratorRename) {
|
|
24167
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
24168
|
-
for (const filePath of patch.files) {
|
|
24169
|
-
const fullPath = (0, import_path8.join)(this.outputDir, filePath);
|
|
24170
|
-
snapshots.set(filePath, await (0, import_promises3.readFile)(fullPath, "utf-8").catch(() => null));
|
|
24171
|
-
}
|
|
24172
|
-
try {
|
|
24173
|
-
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
24174
|
-
await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
24175
|
-
return {
|
|
24176
|
-
patch,
|
|
24177
|
-
status: "applied",
|
|
24178
|
-
method: "git-am",
|
|
24179
|
-
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24180
|
-
};
|
|
24181
|
-
} catch {
|
|
24182
|
-
for (const [filePath, content] of snapshots) {
|
|
24183
|
-
const fullPath = (0, import_path8.join)(this.outputDir, filePath);
|
|
24184
|
-
if (content != null) {
|
|
24185
|
-
await (0, import_promises3.writeFile)(fullPath, content);
|
|
24186
|
-
} else {
|
|
24187
|
-
await (0, import_promises3.unlink)(fullPath).catch(() => {
|
|
24188
|
-
});
|
|
24189
|
-
}
|
|
24190
|
-
}
|
|
24191
|
-
}
|
|
24192
|
-
}
|
|
24193
|
-
return this.applyWithThreeWayMerge(patch);
|
|
24194
|
-
}
|
|
24195
|
-
async applyWithThreeWayMerge(patch) {
|
|
24196
|
-
const fileResults = [];
|
|
24197
|
-
const resolvedFiles = {};
|
|
24198
|
-
const tempDir = await (0, import_promises3.mkdtemp)((0, import_path8.join)((0, import_os.tmpdir)(), "replay-"));
|
|
24199
|
-
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24200
|
-
const tempGit = new GitClient2(tempDir);
|
|
24201
|
-
await tempGit.exec(["init"]);
|
|
24202
|
-
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24203
|
-
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24204
|
-
try {
|
|
24205
|
-
for (const filePath of patch.files) {
|
|
24206
|
-
if (isBinaryFile(filePath)) {
|
|
24207
|
-
fileResults.push({
|
|
24208
|
-
file: filePath,
|
|
24209
|
-
status: "skipped",
|
|
24210
|
-
reason: "binary-file"
|
|
24211
|
-
});
|
|
24212
|
-
continue;
|
|
24213
|
-
}
|
|
24214
|
-
const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
|
|
24215
|
-
if (result.file !== filePath) {
|
|
24216
|
-
resolvedFiles[filePath] = result.file;
|
|
24217
|
-
}
|
|
24218
|
-
fileResults.push(result);
|
|
24219
|
-
}
|
|
24220
|
-
} finally {
|
|
24221
|
-
await (0, import_promises3.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24222
|
-
});
|
|
24223
|
-
}
|
|
24224
|
-
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
24225
|
-
const hasConflicts = conflictFiles.length > 0;
|
|
24226
|
-
const conflictReason = hasConflicts ? conflictFiles.some((f2) => f2.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
|
|
24227
|
-
return {
|
|
24228
|
-
patch,
|
|
24229
|
-
status: hasConflicts ? "conflict" : "applied",
|
|
24230
|
-
method: "3way-merge",
|
|
24231
|
-
fileResults,
|
|
24232
|
-
conflictReason,
|
|
24233
|
-
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24234
|
-
};
|
|
24235
|
-
}
|
|
24236
|
-
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
24237
|
-
try {
|
|
24238
|
-
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24239
|
-
if (!baseGen) {
|
|
24240
|
-
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
24241
|
-
}
|
|
24242
|
-
const lock = this.lockManager.read();
|
|
24243
|
-
const currentGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24244
|
-
const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
|
|
24245
|
-
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24246
|
-
const metadata = {
|
|
24247
|
-
patchId: patch.id,
|
|
24248
|
-
patchMessage: patch.original_message,
|
|
24249
|
-
baseGeneration: patch.base_generation,
|
|
24250
|
-
currentGeneration: lock.current_generation
|
|
24251
|
-
};
|
|
24252
|
-
let base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24253
|
-
let renameSourcePath;
|
|
24254
|
-
if (!base) {
|
|
24255
|
-
const renameSource = this.extractRenameSource(patch.patch_content, filePath);
|
|
24256
|
-
if (renameSource) {
|
|
24257
|
-
base = await this.git.showFile(baseGen.tree_hash, renameSource);
|
|
24258
|
-
renameSourcePath = renameSource;
|
|
24259
|
-
}
|
|
24260
|
-
}
|
|
24261
|
-
const oursPath = (0, import_path8.join)(this.outputDir, resolvedPath);
|
|
24262
|
-
const ours = await (0, import_promises3.readFile)(oursPath, "utf-8").catch(() => null);
|
|
24263
|
-
let ghostReconstructed = false;
|
|
24264
|
-
let theirs = null;
|
|
24265
|
-
if (!base && ours && !renameSourcePath) {
|
|
24266
|
-
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
24267
|
-
if (!treeReachable) {
|
|
24268
|
-
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
24269
|
-
if (fileDiff) {
|
|
24270
|
-
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
24271
|
-
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
24272
|
-
if (result) {
|
|
24273
|
-
base = result.base;
|
|
24274
|
-
theirs = result.theirs;
|
|
24275
|
-
ghostReconstructed = true;
|
|
24276
|
-
}
|
|
24277
|
-
}
|
|
24278
|
-
}
|
|
24279
|
-
}
|
|
24280
|
-
if (!ghostReconstructed) {
|
|
24281
|
-
theirs = await this.applyPatchToContent(
|
|
24282
|
-
base,
|
|
24283
|
-
patch.patch_content,
|
|
24284
|
-
filePath,
|
|
24285
|
-
tempGit,
|
|
24286
|
-
tempDir,
|
|
24287
|
-
renameSourcePath
|
|
24288
|
-
);
|
|
24289
|
-
}
|
|
24290
|
-
if (theirs) {
|
|
24291
|
-
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
24292
|
-
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
24293
|
-
if (theirsHasMarkers && !baseHasMarkers) {
|
|
24294
|
-
return {
|
|
24295
|
-
file: resolvedPath,
|
|
24296
|
-
status: "skipped",
|
|
24297
|
-
reason: "stale-conflict-markers"
|
|
24298
|
-
};
|
|
24299
|
-
}
|
|
24366
|
+
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
24367
|
+
if (theirs) {
|
|
24368
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
24369
|
+
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
24370
|
+
if (theirsHasMarkers && !baseHasMarkers) {
|
|
24371
|
+
if (accumulatorEntry) {
|
|
24372
|
+
theirs = null;
|
|
24373
|
+
} else {
|
|
24374
|
+
return {
|
|
24375
|
+
file: resolvedPath,
|
|
24376
|
+
status: "skipped",
|
|
24377
|
+
reason: "stale-conflict-markers"
|
|
24378
|
+
};
|
|
24379
|
+
}
|
|
24380
|
+
}
|
|
24300
24381
|
}
|
|
24301
24382
|
let useAccumulatorAsMergeBase = false;
|
|
24302
|
-
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
24303
24383
|
if (accumulatorEntry && (theirs == null || base == null)) {
|
|
24304
24384
|
theirs = await this.applyPatchToContent(
|
|
24305
24385
|
accumulatorEntry.content,
|
|
@@ -24355,16 +24435,16 @@ var init_dist3 = __esm({
|
|
|
24355
24435
|
content: effective_theirs,
|
|
24356
24436
|
baseGeneration: patch.base_generation
|
|
24357
24437
|
});
|
|
24358
|
-
const outDir2 = (0,
|
|
24359
|
-
await (0,
|
|
24360
|
-
await (0,
|
|
24438
|
+
const outDir2 = (0, import_path9.dirname)(oursPath);
|
|
24439
|
+
await (0, import_promises4.mkdir)(outDir2, { recursive: true });
|
|
24440
|
+
await (0, import_promises4.writeFile)(oursPath, effective_theirs);
|
|
24361
24441
|
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
24362
24442
|
}
|
|
24363
24443
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
24364
24444
|
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
24365
|
-
const outDir2 = (0,
|
|
24366
|
-
await (0,
|
|
24367
|
-
await (0,
|
|
24445
|
+
const outDir2 = (0, import_path9.dirname)(oursPath);
|
|
24446
|
+
await (0, import_promises4.mkdir)(outDir2, { recursive: true });
|
|
24447
|
+
await (0, import_promises4.writeFile)(oursPath, merged2.content);
|
|
24368
24448
|
if (merged2.hasConflicts) {
|
|
24369
24449
|
return {
|
|
24370
24450
|
file: resolvedPath,
|
|
@@ -24402,205 +24482,773 @@ var init_dist3 = __esm({
|
|
|
24402
24482
|
reason: "missing-content"
|
|
24403
24483
|
};
|
|
24404
24484
|
}
|
|
24405
|
-
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
24406
|
-
const outDir = (0,
|
|
24407
|
-
await (0,
|
|
24408
|
-
await (0,
|
|
24409
|
-
|
|
24410
|
-
|
|
24411
|
-
|
|
24412
|
-
|
|
24413
|
-
|
|
24485
|
+
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
24486
|
+
const outDir = (0, import_path9.dirname)(oursPath);
|
|
24487
|
+
await (0, import_promises4.mkdir)(outDir, { recursive: true });
|
|
24488
|
+
await (0, import_promises4.writeFile)(oursPath, merged.content);
|
|
24489
|
+
const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
|
|
24490
|
+
if (populateAccumulator) {
|
|
24491
|
+
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24492
|
+
content: effective_theirs,
|
|
24493
|
+
baseGeneration: patch.base_generation
|
|
24494
|
+
});
|
|
24495
|
+
}
|
|
24496
|
+
if (merged.hasConflicts) {
|
|
24497
|
+
return {
|
|
24498
|
+
file: resolvedPath,
|
|
24499
|
+
status: "conflict",
|
|
24500
|
+
conflicts: merged.conflicts,
|
|
24501
|
+
conflictReason: baseMismatchSkipped ? "base-generation-mismatch" : "same-line-edit",
|
|
24502
|
+
conflictMetadata: metadata
|
|
24503
|
+
};
|
|
24504
|
+
}
|
|
24505
|
+
return {
|
|
24506
|
+
file: resolvedPath,
|
|
24507
|
+
status: "merged",
|
|
24508
|
+
...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
|
|
24509
|
+
};
|
|
24510
|
+
} catch (error) {
|
|
24511
|
+
return {
|
|
24512
|
+
file: filePath,
|
|
24513
|
+
status: "skipped",
|
|
24514
|
+
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
24515
|
+
};
|
|
24516
|
+
}
|
|
24517
|
+
}
|
|
24518
|
+
async isTreeReachable(treeHash) {
|
|
24519
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
24520
|
+
if (result === void 0) {
|
|
24521
|
+
result = await this.git.treeExists(treeHash);
|
|
24522
|
+
this.treeExistsCache.set(treeHash, result);
|
|
24523
|
+
}
|
|
24524
|
+
return result;
|
|
24525
|
+
}
|
|
24526
|
+
isExcluded(patch) {
|
|
24527
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
24528
|
+
if (!config.exclude) return false;
|
|
24529
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
|
|
24530
|
+
}
|
|
24531
|
+
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
24532
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
24533
|
+
if (config.moves) {
|
|
24534
|
+
for (const move of config.moves) {
|
|
24535
|
+
if (minimatch(filePath, move.from) || filePath === move.from) {
|
|
24536
|
+
if (filePath === move.from) {
|
|
24537
|
+
return move.to;
|
|
24538
|
+
}
|
|
24539
|
+
const fromBase = move.from.replace(/\*\*.*$/, "");
|
|
24540
|
+
const toBase = move.to.replace(/\*\*.*$/, "");
|
|
24541
|
+
if (filePath.startsWith(fromBase)) {
|
|
24542
|
+
return toBase + filePath.slice(fromBase.length);
|
|
24543
|
+
}
|
|
24544
|
+
}
|
|
24545
|
+
}
|
|
24546
|
+
}
|
|
24547
|
+
const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
|
|
24548
|
+
let renames = this.renameCache.get(cacheKey);
|
|
24549
|
+
if (!renames) {
|
|
24550
|
+
renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
|
|
24551
|
+
this.renameCache.set(cacheKey, renames);
|
|
24552
|
+
}
|
|
24553
|
+
const gitRename = renames.find((r2) => r2.from === filePath);
|
|
24554
|
+
if (gitRename) {
|
|
24555
|
+
return gitRename.to;
|
|
24556
|
+
}
|
|
24557
|
+
return filePath;
|
|
24558
|
+
}
|
|
24559
|
+
async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
|
|
24560
|
+
if (!base) {
|
|
24561
|
+
return this.extractNewFileFromPatch(patchContent, filePath);
|
|
24562
|
+
}
|
|
24563
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
24564
|
+
if (!fileDiff) return null;
|
|
24565
|
+
try {
|
|
24566
|
+
if (sourceFilePath) {
|
|
24567
|
+
const tempSourcePath = (0, import_path9.join)(tempDir, sourceFilePath);
|
|
24568
|
+
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempSourcePath), { recursive: true });
|
|
24569
|
+
await (0, import_promises4.writeFile)(tempSourcePath, base);
|
|
24570
|
+
await tempGit.exec(["add", sourceFilePath]);
|
|
24571
|
+
await tempGit.exec([
|
|
24572
|
+
"commit",
|
|
24573
|
+
"-m",
|
|
24574
|
+
`base for rename ${sourceFilePath} -> ${filePath}`,
|
|
24575
|
+
"--allow-empty"
|
|
24576
|
+
]);
|
|
24577
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24578
|
+
const tempTargetPath = (0, import_path9.join)(tempDir, filePath);
|
|
24579
|
+
return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
|
|
24580
|
+
}
|
|
24581
|
+
const tempFilePath = (0, import_path9.join)(tempDir, filePath);
|
|
24582
|
+
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
|
|
24583
|
+
await (0, import_promises4.writeFile)(tempFilePath, base);
|
|
24584
|
+
await tempGit.exec(["add", filePath]);
|
|
24585
|
+
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
24586
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24587
|
+
return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
|
|
24588
|
+
} catch {
|
|
24589
|
+
return null;
|
|
24590
|
+
}
|
|
24591
|
+
}
|
|
24592
|
+
/**
|
|
24593
|
+
* Detects whether a patch's additions are already present in `content`.
|
|
24594
|
+
* Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
|
|
24595
|
+
* the forward patch is effectively a no-op (its +lines are already there and
|
|
24596
|
+
* its -lines are already gone). Used to distinguish "patch already applied"
|
|
24597
|
+
* from "patch base mismatch" after a forward-apply fails.
|
|
24598
|
+
*/
|
|
24599
|
+
async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
|
|
24600
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
24601
|
+
if (!fileDiff) return false;
|
|
24602
|
+
try {
|
|
24603
|
+
const tempFilePath = (0, import_path9.join)(tempDir, filePath);
|
|
24604
|
+
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
|
|
24605
|
+
await (0, import_promises4.writeFile)(tempFilePath, content);
|
|
24606
|
+
await tempGit.exec(["add", filePath]);
|
|
24607
|
+
await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
|
|
24608
|
+
await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
|
|
24609
|
+
return true;
|
|
24610
|
+
} catch {
|
|
24611
|
+
return false;
|
|
24612
|
+
}
|
|
24613
|
+
}
|
|
24614
|
+
extractFileDiff(patchContent, filePath) {
|
|
24615
|
+
const lines = patchContent.split("\n");
|
|
24616
|
+
const diffLines = [];
|
|
24617
|
+
let inTargetFile = false;
|
|
24618
|
+
for (const line of lines) {
|
|
24619
|
+
if (line.startsWith("diff --git")) {
|
|
24620
|
+
if (inTargetFile) {
|
|
24621
|
+
break;
|
|
24622
|
+
}
|
|
24623
|
+
if (isDiffLineForFile(line, filePath)) {
|
|
24624
|
+
inTargetFile = true;
|
|
24625
|
+
diffLines.push(line);
|
|
24626
|
+
}
|
|
24627
|
+
continue;
|
|
24628
|
+
}
|
|
24629
|
+
if (inTargetFile) {
|
|
24630
|
+
diffLines.push(line);
|
|
24631
|
+
}
|
|
24632
|
+
}
|
|
24633
|
+
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
24634
|
+
}
|
|
24635
|
+
extractRenameSource(patchContent, targetFilePath) {
|
|
24636
|
+
const lines = patchContent.split("\n");
|
|
24637
|
+
let inTargetFile = false;
|
|
24638
|
+
for (const line of lines) {
|
|
24639
|
+
if (line.startsWith("diff --git")) {
|
|
24640
|
+
if (inTargetFile) break;
|
|
24641
|
+
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
24642
|
+
continue;
|
|
24643
|
+
}
|
|
24644
|
+
if (!inTargetFile) continue;
|
|
24645
|
+
if (line.startsWith("@@")) break;
|
|
24646
|
+
if (line.startsWith("rename from ")) {
|
|
24647
|
+
return line.slice("rename from ".length);
|
|
24648
|
+
}
|
|
24649
|
+
}
|
|
24650
|
+
return null;
|
|
24651
|
+
}
|
|
24652
|
+
extractNewFileFromPatch(patchContent, filePath) {
|
|
24653
|
+
const lines = patchContent.split("\n");
|
|
24654
|
+
const addedLines = [];
|
|
24655
|
+
let inTargetFile = false;
|
|
24656
|
+
let inHunk = false;
|
|
24657
|
+
let isNewFile = false;
|
|
24658
|
+
let noTrailingNewline = false;
|
|
24659
|
+
for (const line of lines) {
|
|
24660
|
+
if (line.startsWith("diff --git")) {
|
|
24661
|
+
if (inTargetFile) break;
|
|
24662
|
+
inTargetFile = isDiffLineForFile(line, filePath);
|
|
24663
|
+
inHunk = false;
|
|
24664
|
+
isNewFile = false;
|
|
24665
|
+
continue;
|
|
24666
|
+
}
|
|
24667
|
+
if (!inTargetFile) continue;
|
|
24668
|
+
if (!inHunk) {
|
|
24669
|
+
if (line === "--- /dev/null" || line.startsWith("new file mode")) {
|
|
24670
|
+
isNewFile = true;
|
|
24671
|
+
}
|
|
24672
|
+
}
|
|
24673
|
+
if (line.startsWith("@@")) {
|
|
24674
|
+
if (!isNewFile) return null;
|
|
24675
|
+
inHunk = true;
|
|
24676
|
+
continue;
|
|
24677
|
+
}
|
|
24678
|
+
if (!inHunk) continue;
|
|
24679
|
+
if (line === "\") {
|
|
24680
|
+
noTrailingNewline = true;
|
|
24681
|
+
continue;
|
|
24682
|
+
}
|
|
24683
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
24684
|
+
addedLines.push(line.slice(1));
|
|
24685
|
+
}
|
|
24686
|
+
}
|
|
24687
|
+
if (addedLines.length === 0) return null;
|
|
24688
|
+
return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
|
|
24689
|
+
}
|
|
24690
|
+
};
|
|
24691
|
+
INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
24692
|
+
ReplayDetector = class {
|
|
24693
|
+
git;
|
|
24694
|
+
lockManager;
|
|
24695
|
+
sdkOutputDir;
|
|
24696
|
+
warnings = [];
|
|
24697
|
+
constructor(git, lockManager, sdkOutputDir) {
|
|
24698
|
+
this.git = git;
|
|
24699
|
+
this.lockManager = lockManager;
|
|
24700
|
+
this.sdkOutputDir = sdkOutputDir;
|
|
24701
|
+
}
|
|
24702
|
+
async detectNewPatches() {
|
|
24703
|
+
const lock = this.lockManager.read();
|
|
24704
|
+
const lastGen = this.getLastGeneration(lock);
|
|
24705
|
+
if (!lastGen) {
|
|
24706
|
+
return { patches: [], revertedPatchIds: [] };
|
|
24707
|
+
}
|
|
24708
|
+
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
24709
|
+
if (!exists2) {
|
|
24710
|
+
this.warnings.push(
|
|
24711
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
24712
|
+
);
|
|
24713
|
+
return this.detectPatchesViaTreeDiff(
|
|
24714
|
+
lastGen,
|
|
24715
|
+
/* commitKnownMissing */
|
|
24716
|
+
true
|
|
24717
|
+
);
|
|
24718
|
+
}
|
|
24719
|
+
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
24720
|
+
if (!isAncestor) {
|
|
24721
|
+
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
24722
|
+
}
|
|
24723
|
+
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
24724
|
+
}
|
|
24725
|
+
/**
|
|
24726
|
+
* FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
24727
|
+
*
|
|
24728
|
+
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
24729
|
+
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
24730
|
+
* merge-base is the commit on main from which the bot's branch was created —
|
|
24731
|
+
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
24732
|
+
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
24733
|
+
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
24734
|
+
* generation) get bundled as customer customizations.
|
|
24735
|
+
*
|
|
24736
|
+
* Falls through to the legacy composite path when no common ancestor exists
|
|
24737
|
+
* (truly disjoint history — orphan branches with no shared root).
|
|
24738
|
+
*/
|
|
24739
|
+
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
24740
|
+
let mergeBase = "";
|
|
24741
|
+
try {
|
|
24742
|
+
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
24743
|
+
} catch {
|
|
24744
|
+
}
|
|
24745
|
+
if (!mergeBase) {
|
|
24746
|
+
this.warnings.push(
|
|
24747
|
+
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
24748
|
+
);
|
|
24749
|
+
return this.detectPatchesViaTreeDiff(
|
|
24750
|
+
lastGen,
|
|
24751
|
+
/* commitKnownMissing */
|
|
24752
|
+
false
|
|
24753
|
+
);
|
|
24754
|
+
}
|
|
24755
|
+
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
24756
|
+
}
|
|
24757
|
+
/**
|
|
24758
|
+
* FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
24759
|
+
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
24760
|
+
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
24761
|
+
*
|
|
24762
|
+
* Patch `base_generation` is always `lastGen.commit_sha` — the
|
|
24763
|
+
* lockfile-tracked reference the applicator uses to fetch base file
|
|
24764
|
+
* content, regardless of where the walk actually started.
|
|
24765
|
+
*/
|
|
24766
|
+
async detectPatchesInRange(rangeStart, lastGen, lock) {
|
|
24767
|
+
const log = await this.git.exec([
|
|
24768
|
+
"log",
|
|
24769
|
+
"--first-parent",
|
|
24770
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
24771
|
+
`${rangeStart}..HEAD`,
|
|
24772
|
+
"--",
|
|
24773
|
+
this.sdkOutputDir
|
|
24774
|
+
]);
|
|
24775
|
+
if (!log.trim()) {
|
|
24776
|
+
return { patches: [], revertedPatchIds: [] };
|
|
24777
|
+
}
|
|
24778
|
+
const commits = this.parseGitLog(log);
|
|
24779
|
+
const newPatches = [];
|
|
24780
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
24781
|
+
for (const commit of commits) {
|
|
24782
|
+
if (isGenerationCommit(commit)) {
|
|
24783
|
+
continue;
|
|
24784
|
+
}
|
|
24785
|
+
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
24786
|
+
continue;
|
|
24787
|
+
}
|
|
24788
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
24789
|
+
if (parents.length > 1) {
|
|
24790
|
+
const compositePatch = await this.createMergeCompositePatch({
|
|
24791
|
+
mergeCommit: commit,
|
|
24792
|
+
firstParent: parents[0],
|
|
24793
|
+
lastGen,
|
|
24794
|
+
lock
|
|
24795
|
+
});
|
|
24796
|
+
if (compositePatch) {
|
|
24797
|
+
newPatches.push(compositePatch);
|
|
24798
|
+
}
|
|
24799
|
+
continue;
|
|
24800
|
+
}
|
|
24801
|
+
let patchContent;
|
|
24802
|
+
try {
|
|
24803
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
24804
|
+
} catch {
|
|
24805
|
+
this.warnings.push(
|
|
24806
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
24807
|
+
);
|
|
24808
|
+
continue;
|
|
24809
|
+
}
|
|
24810
|
+
let contentHash = this.computeContentHash(patchContent);
|
|
24811
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
24812
|
+
continue;
|
|
24813
|
+
}
|
|
24814
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
24815
|
+
const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
24816
|
+
const sensitiveFiles = allFiles.filter((f2) => isSensitiveFile(f2));
|
|
24817
|
+
const files = allFiles.filter((f2) => !isSensitiveFile(f2));
|
|
24818
|
+
if (sensitiveFiles.length > 0) {
|
|
24819
|
+
this.warnings.push(
|
|
24820
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
24821
|
+
);
|
|
24822
|
+
if (files.length > 0) {
|
|
24823
|
+
const parentSha = parents[0];
|
|
24824
|
+
if (parentSha) {
|
|
24825
|
+
try {
|
|
24826
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
24827
|
+
} catch {
|
|
24828
|
+
continue;
|
|
24829
|
+
}
|
|
24830
|
+
if (!patchContent.trim()) continue;
|
|
24831
|
+
contentHash = this.computeContentHash(patchContent);
|
|
24832
|
+
}
|
|
24833
|
+
}
|
|
24834
|
+
}
|
|
24835
|
+
if (files.length === 0) {
|
|
24836
|
+
continue;
|
|
24837
|
+
}
|
|
24838
|
+
const theirsSnapshot = {};
|
|
24839
|
+
for (const file of files) {
|
|
24840
|
+
if (isBinaryFile(file)) continue;
|
|
24841
|
+
const content = await this.git.showFile(commit.sha, file).catch(() => null);
|
|
24842
|
+
if (content != null) theirsSnapshot[file] = content;
|
|
24843
|
+
}
|
|
24844
|
+
newPatches.push({
|
|
24845
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
24846
|
+
content_hash: contentHash,
|
|
24847
|
+
original_commit: commit.sha,
|
|
24848
|
+
original_message: commit.message,
|
|
24849
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
24850
|
+
base_generation: lastGen.commit_sha,
|
|
24851
|
+
files,
|
|
24852
|
+
patch_content: patchContent,
|
|
24853
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
24854
|
+
...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
|
|
24855
|
+
});
|
|
24856
|
+
}
|
|
24857
|
+
const survivingPatches = await this.collapseNetZeroFiles(newPatches);
|
|
24858
|
+
survivingPatches.reverse();
|
|
24859
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
24860
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
24861
|
+
for (let i = 0; i < survivingPatches.length; i++) {
|
|
24862
|
+
const patch = survivingPatches[i];
|
|
24863
|
+
if (!isRevertCommit(patch.original_message)) continue;
|
|
24864
|
+
let body = "";
|
|
24865
|
+
try {
|
|
24866
|
+
body = await this.git.getCommitBody(patch.original_commit);
|
|
24867
|
+
} catch {
|
|
24868
|
+
}
|
|
24869
|
+
const revertedSha = parseRevertedSha(body);
|
|
24870
|
+
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
24871
|
+
let matchedExisting = false;
|
|
24872
|
+
if (revertedSha) {
|
|
24873
|
+
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
24874
|
+
if (existing) {
|
|
24875
|
+
revertedPatchIdSet.add(existing.id);
|
|
24876
|
+
revertIndicesToRemove.add(i);
|
|
24877
|
+
matchedExisting = true;
|
|
24878
|
+
}
|
|
24879
|
+
}
|
|
24880
|
+
if (!matchedExisting && revertedMessage) {
|
|
24881
|
+
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
24882
|
+
if (existing) {
|
|
24883
|
+
revertedPatchIdSet.add(existing.id);
|
|
24884
|
+
revertIndicesToRemove.add(i);
|
|
24885
|
+
matchedExisting = true;
|
|
24886
|
+
}
|
|
24887
|
+
}
|
|
24888
|
+
if (matchedExisting) continue;
|
|
24889
|
+
let matchedNew = false;
|
|
24890
|
+
if (revertedSha) {
|
|
24891
|
+
const idx = survivingPatches.findIndex(
|
|
24892
|
+
(p, j2) => j2 !== i && !revertIndicesToRemove.has(j2) && p.original_commit === revertedSha
|
|
24893
|
+
);
|
|
24894
|
+
if (idx !== -1) {
|
|
24895
|
+
revertIndicesToRemove.add(i);
|
|
24896
|
+
revertIndicesToRemove.add(idx);
|
|
24897
|
+
matchedNew = true;
|
|
24898
|
+
}
|
|
24899
|
+
}
|
|
24900
|
+
if (!matchedNew && revertedMessage) {
|
|
24901
|
+
const idx = survivingPatches.findIndex(
|
|
24902
|
+
(p, j2) => j2 !== i && !revertIndicesToRemove.has(j2) && p.original_message === revertedMessage
|
|
24903
|
+
);
|
|
24904
|
+
if (idx !== -1) {
|
|
24905
|
+
revertIndicesToRemove.add(i);
|
|
24906
|
+
revertIndicesToRemove.add(idx);
|
|
24907
|
+
}
|
|
24908
|
+
}
|
|
24909
|
+
if (!matchedExisting && !matchedNew) {
|
|
24910
|
+
revertIndicesToRemove.add(i);
|
|
24911
|
+
}
|
|
24912
|
+
}
|
|
24913
|
+
const filteredPatches = survivingPatches.filter((_2, i) => !revertIndicesToRemove.has(i));
|
|
24914
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
24915
|
+
}
|
|
24916
|
+
/**
|
|
24917
|
+
* Compute content hash for deduplication.
|
|
24918
|
+
*
|
|
24919
|
+
* Produces a format-agnostic hash so both `git format-patch` output
|
|
24920
|
+
* (email-wrapped) and plain `git diff` output hash to the same value
|
|
24921
|
+
* when the underlying diff hunks are identical.
|
|
24922
|
+
*
|
|
24923
|
+
* Normalization:
|
|
24924
|
+
* 1. Strip email wrapper: everything before the first `diff --git` line
|
|
24925
|
+
* (From:, Subject:, Date:, diffstat, blank separators).
|
|
24926
|
+
* 2. Strip `index` lines (blob SHA pairs change across rebases).
|
|
24927
|
+
* 3. Strip trailing git version marker (`-- \n<version>`).
|
|
24928
|
+
*
|
|
24929
|
+
* This ensures content hashes match across the no-patches and normal-
|
|
24930
|
+
* regeneration flows, which store formatPatch and git-diff formats
|
|
24931
|
+
* respectively (FER-9850, D5-D9).
|
|
24932
|
+
*/
|
|
24933
|
+
computeContentHash(patchContent) {
|
|
24934
|
+
const lines = patchContent.split("\n");
|
|
24935
|
+
const diffStart = lines.findIndex((l) => l.startsWith("diff --git "));
|
|
24936
|
+
const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
|
|
24937
|
+
const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
|
|
24938
|
+
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
24939
|
+
}
|
|
24940
|
+
/**
|
|
24941
|
+
* FER-9805 — Drop patches whose files were transiently created and then
|
|
24942
|
+
* deleted within the current linear detection window, and are absent from
|
|
24943
|
+
* HEAD at the end of the window.
|
|
24944
|
+
*
|
|
24945
|
+
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
24946
|
+
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
24947
|
+
* linear history each commit becomes its own patch, so a file that was
|
|
24948
|
+
* briefly added and later removed survives as a create+delete pair whose
|
|
24949
|
+
* cumulative diff is empty. We drop such patches before they reach the
|
|
24950
|
+
* lockfile.
|
|
24951
|
+
*
|
|
24952
|
+
* A file is "transient" when:
|
|
24953
|
+
* 1. some patch in the window sets `new file mode` for it (a creation), AND
|
|
24954
|
+
* 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
|
|
24955
|
+
* 3. it is absent from HEAD's tree.
|
|
24956
|
+
*
|
|
24957
|
+
* The HEAD guard (#3) prevents false positives when a file is deleted
|
|
24958
|
+
* and then recreated with different content — the cumulative diff of
|
|
24959
|
+
* such a pair is non-empty and must be preserved.
|
|
24960
|
+
*
|
|
24961
|
+
* This only drops patches whose `files` are a subset of the transient
|
|
24962
|
+
* set. A partial-transient patch (one that touches a transient file
|
|
24963
|
+
* alongside a persistent one) is left unmodified; surgical stripping of
|
|
24964
|
+
* single files from a multi-file patch would require a follow-up that
|
|
24965
|
+
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
24966
|
+
* current failing test exercises this, and leaving the patch intact
|
|
24967
|
+
* preserves today's behaviour. TODO(FER-9805): revisit if a future
|
|
24968
|
+
* adversarial test demands per-file stripping.
|
|
24969
|
+
*/
|
|
24970
|
+
async collapseNetZeroFiles(patches) {
|
|
24971
|
+
if (patches.length === 0) return patches;
|
|
24972
|
+
const stateByFile = /* @__PURE__ */ new Map();
|
|
24973
|
+
for (const patch of patches) {
|
|
24974
|
+
for (const header of parsePatchFileHeaders(patch.patch_content)) {
|
|
24975
|
+
if (!header.isCreate && !header.isDelete) continue;
|
|
24976
|
+
const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
|
|
24977
|
+
if (header.isCreate) prior.created = true;
|
|
24978
|
+
if (header.isDelete) prior.deleted = true;
|
|
24979
|
+
stateByFile.set(header.path, prior);
|
|
24980
|
+
}
|
|
24981
|
+
}
|
|
24982
|
+
let anyCandidate = false;
|
|
24983
|
+
for (const state of stateByFile.values()) {
|
|
24984
|
+
if (state.created && state.deleted) {
|
|
24985
|
+
anyCandidate = true;
|
|
24986
|
+
break;
|
|
24414
24987
|
}
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
24419
|
-
|
|
24420
|
-
|
|
24421
|
-
|
|
24422
|
-
};
|
|
24988
|
+
}
|
|
24989
|
+
if (!anyCandidate) return patches;
|
|
24990
|
+
const headFiles = await this.listHeadFiles();
|
|
24991
|
+
const transient = /* @__PURE__ */ new Set();
|
|
24992
|
+
for (const [file, state] of stateByFile) {
|
|
24993
|
+
if (state.created && state.deleted && !headFiles.has(file)) {
|
|
24994
|
+
transient.add(file);
|
|
24423
24995
|
}
|
|
24424
|
-
return { file: resolvedPath, status: "merged" };
|
|
24425
|
-
} catch (error) {
|
|
24426
|
-
return {
|
|
24427
|
-
file: filePath,
|
|
24428
|
-
status: "skipped",
|
|
24429
|
-
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
24430
|
-
};
|
|
24431
24996
|
}
|
|
24997
|
+
if (transient.size === 0) return patches;
|
|
24998
|
+
return patches.filter((patch) => !patch.files.every((f2) => transient.has(f2)));
|
|
24432
24999
|
}
|
|
24433
|
-
|
|
24434
|
-
|
|
24435
|
-
|
|
24436
|
-
|
|
24437
|
-
|
|
25000
|
+
/**
|
|
25001
|
+
* List every tracked path at HEAD (one `git ls-tree -r` invocation).
|
|
25002
|
+
* Returns an empty Set if HEAD is unborn or the invocation fails — the
|
|
25003
|
+
* caller then treats every candidate as "not in HEAD" and may collapse
|
|
25004
|
+
* more aggressively. On typical SDK repos this is a single sub-10ms call.
|
|
25005
|
+
*/
|
|
25006
|
+
async listHeadFiles() {
|
|
25007
|
+
try {
|
|
25008
|
+
const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
25009
|
+
return new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
25010
|
+
} catch {
|
|
25011
|
+
return /* @__PURE__ */ new Set();
|
|
24438
25012
|
}
|
|
24439
|
-
return result;
|
|
24440
|
-
}
|
|
24441
|
-
isExcluded(patch) {
|
|
24442
|
-
const config = this.lockManager.getCustomizationsConfig();
|
|
24443
|
-
if (!config.exclude) return false;
|
|
24444
|
-
return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
|
|
24445
25013
|
}
|
|
24446
|
-
|
|
24447
|
-
|
|
24448
|
-
|
|
24449
|
-
|
|
24450
|
-
|
|
24451
|
-
|
|
24452
|
-
|
|
24453
|
-
|
|
24454
|
-
|
|
24455
|
-
|
|
24456
|
-
|
|
24457
|
-
|
|
24458
|
-
|
|
24459
|
-
|
|
24460
|
-
|
|
25014
|
+
/**
|
|
25015
|
+
* Create a single composite patch for a merge commit by diffing the merge
|
|
25016
|
+
* commit against its first parent. This captures the net change of the
|
|
25017
|
+
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
25018
|
+
* zero-net-change ghost conflicts.
|
|
25019
|
+
*/
|
|
25020
|
+
async createMergeCompositePatch(input) {
|
|
25021
|
+
const { mergeCommit, firstParent, lastGen, lock } = input;
|
|
25022
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
25023
|
+
const filesOutput = await this.git.exec([
|
|
25024
|
+
"diff",
|
|
25025
|
+
"--name-only",
|
|
25026
|
+
firstParent,
|
|
25027
|
+
mergeCommit.sha
|
|
25028
|
+
]);
|
|
25029
|
+
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
25030
|
+
const sensitiveMergeFiles = allMergeFiles.filter((f2) => isSensitiveFile(f2));
|
|
25031
|
+
const files = allMergeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
25032
|
+
if (sensitiveMergeFiles.length > 0) {
|
|
25033
|
+
this.warnings.push(
|
|
25034
|
+
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25035
|
+
);
|
|
24461
25036
|
}
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
|
|
24465
|
-
|
|
24466
|
-
|
|
25037
|
+
if (files.length === 0) return null;
|
|
25038
|
+
const diff = await this.git.exec([
|
|
25039
|
+
"diff",
|
|
25040
|
+
firstParent,
|
|
25041
|
+
mergeCommit.sha,
|
|
25042
|
+
"--",
|
|
25043
|
+
...files
|
|
25044
|
+
]);
|
|
25045
|
+
if (!diff.trim()) return null;
|
|
25046
|
+
const contentHash = this.computeContentHash(diff);
|
|
25047
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
25048
|
+
return null;
|
|
24467
25049
|
}
|
|
24468
|
-
const
|
|
24469
|
-
|
|
24470
|
-
|
|
25050
|
+
const theirsSnapshot = {};
|
|
25051
|
+
for (const file of files) {
|
|
25052
|
+
if (isBinaryFile(file)) continue;
|
|
25053
|
+
const content = await this.git.showFile(mergeCommit.sha, file).catch(() => null);
|
|
25054
|
+
if (content != null) theirsSnapshot[file] = content;
|
|
24471
25055
|
}
|
|
24472
|
-
return
|
|
25056
|
+
return {
|
|
25057
|
+
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
25058
|
+
content_hash: contentHash,
|
|
25059
|
+
original_commit: mergeCommit.sha,
|
|
25060
|
+
original_message: mergeCommit.message,
|
|
25061
|
+
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
25062
|
+
base_generation: lastGen.commit_sha,
|
|
25063
|
+
files,
|
|
25064
|
+
patch_content: diff,
|
|
25065
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
25066
|
+
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
25067
|
+
};
|
|
24473
25068
|
}
|
|
24474
|
-
|
|
24475
|
-
|
|
24476
|
-
|
|
25069
|
+
/**
|
|
25070
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
25071
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
25072
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
25073
|
+
*/
|
|
25074
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
25075
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
25076
|
+
if (!diffBase) {
|
|
25077
|
+
return this.detectPatchesViaCommitScan();
|
|
24477
25078
|
}
|
|
24478
|
-
const
|
|
24479
|
-
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
|
|
24483
|
-
|
|
24484
|
-
|
|
24485
|
-
|
|
24486
|
-
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24491
|
-
|
|
24492
|
-
|
|
24493
|
-
|
|
24494
|
-
|
|
24495
|
-
|
|
24496
|
-
|
|
24497
|
-
|
|
24498
|
-
|
|
24499
|
-
await
|
|
24500
|
-
|
|
24501
|
-
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24502
|
-
return await (0, import_promises3.readFile)(tempFilePath, "utf-8");
|
|
24503
|
-
} catch {
|
|
24504
|
-
return null;
|
|
25079
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
25080
|
+
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
25081
|
+
const sensitiveTreeFiles = allTreeFiles.filter((f2) => isSensitiveFile(f2));
|
|
25082
|
+
const files = allTreeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
25083
|
+
if (sensitiveTreeFiles.length > 0) {
|
|
25084
|
+
this.warnings.push(
|
|
25085
|
+
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25086
|
+
);
|
|
25087
|
+
}
|
|
25088
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
25089
|
+
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
25090
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
25091
|
+
const contentHash = this.computeContentHash(diff);
|
|
25092
|
+
const lock = this.lockManager.read();
|
|
25093
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
25094
|
+
return { patches: [], revertedPatchIds: [] };
|
|
25095
|
+
}
|
|
25096
|
+
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
25097
|
+
const theirsSnapshot = {};
|
|
25098
|
+
for (const file of files) {
|
|
25099
|
+
if (isBinaryFile(file)) continue;
|
|
25100
|
+
const content = await this.git.showFile("HEAD", file).catch(() => null);
|
|
25101
|
+
if (content != null) theirsSnapshot[file] = content;
|
|
24505
25102
|
}
|
|
25103
|
+
const compositePatch = {
|
|
25104
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
25105
|
+
content_hash: contentHash,
|
|
25106
|
+
original_commit: headSha,
|
|
25107
|
+
original_message: "Customer customizations (composite)",
|
|
25108
|
+
original_author: "composite",
|
|
25109
|
+
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
25110
|
+
// reference to find base file content. diffBase may be the tree_hash.
|
|
25111
|
+
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
25112
|
+
files,
|
|
25113
|
+
patch_content: diff,
|
|
25114
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
25115
|
+
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
25116
|
+
};
|
|
25117
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
24506
25118
|
}
|
|
24507
25119
|
/**
|
|
24508
|
-
*
|
|
24509
|
-
*
|
|
24510
|
-
*
|
|
24511
|
-
*
|
|
24512
|
-
*
|
|
25120
|
+
* Last-resort detection when both generation commit and tree are unreachable.
|
|
25121
|
+
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
25122
|
+
* skips creation-only commits (squashed history after force push).
|
|
25123
|
+
*
|
|
25124
|
+
* Detected patches use the commit's parent as base_generation so the applicator
|
|
25125
|
+
* can find base file content from the parent's tree (which IS reachable).
|
|
24513
25126
|
*/
|
|
24514
|
-
async
|
|
24515
|
-
const
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
return false;
|
|
25127
|
+
async detectPatchesViaCommitScan() {
|
|
25128
|
+
const lock = this.lockManager.read();
|
|
25129
|
+
const log = await this.git.exec([
|
|
25130
|
+
"log",
|
|
25131
|
+
"--max-count=200",
|
|
25132
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
25133
|
+
"HEAD",
|
|
25134
|
+
"--",
|
|
25135
|
+
this.sdkOutputDir
|
|
25136
|
+
]);
|
|
25137
|
+
if (!log.trim()) {
|
|
25138
|
+
return { patches: [], revertedPatchIds: [] };
|
|
24527
25139
|
}
|
|
24528
|
-
|
|
24529
|
-
|
|
24530
|
-
const
|
|
24531
|
-
const
|
|
24532
|
-
|
|
24533
|
-
for (const
|
|
24534
|
-
if (
|
|
24535
|
-
if (inTargetFile) {
|
|
24536
|
-
break;
|
|
24537
|
-
}
|
|
24538
|
-
if (isDiffLineForFile(line, filePath)) {
|
|
24539
|
-
inTargetFile = true;
|
|
24540
|
-
diffLines.push(line);
|
|
24541
|
-
}
|
|
25140
|
+
const commits = this.parseGitLog(log);
|
|
25141
|
+
const newPatches = [];
|
|
25142
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
25143
|
+
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
25144
|
+
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
25145
|
+
for (const commit of commits) {
|
|
25146
|
+
if (isGenerationCommit(commit)) {
|
|
24542
25147
|
continue;
|
|
24543
25148
|
}
|
|
24544
|
-
|
|
24545
|
-
|
|
25149
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
25150
|
+
if (parents.length > 1) {
|
|
25151
|
+
continue;
|
|
24546
25152
|
}
|
|
24547
|
-
|
|
24548
|
-
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
24549
|
-
}
|
|
24550
|
-
extractRenameSource(patchContent, targetFilePath) {
|
|
24551
|
-
const lines = patchContent.split("\n");
|
|
24552
|
-
let inTargetFile = false;
|
|
24553
|
-
for (const line of lines) {
|
|
24554
|
-
if (line.startsWith("diff --git")) {
|
|
24555
|
-
if (inTargetFile) break;
|
|
24556
|
-
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
25153
|
+
if (existingCommits.has(commit.sha)) {
|
|
24557
25154
|
continue;
|
|
24558
25155
|
}
|
|
24559
|
-
|
|
24560
|
-
|
|
24561
|
-
|
|
24562
|
-
|
|
25156
|
+
let patchContent;
|
|
25157
|
+
try {
|
|
25158
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
25159
|
+
} catch {
|
|
25160
|
+
continue;
|
|
24563
25161
|
}
|
|
24564
|
-
|
|
24565
|
-
|
|
24566
|
-
}
|
|
24567
|
-
extractNewFileFromPatch(patchContent, filePath) {
|
|
24568
|
-
const lines = patchContent.split("\n");
|
|
24569
|
-
const addedLines = [];
|
|
24570
|
-
let inTargetFile = false;
|
|
24571
|
-
let inHunk = false;
|
|
24572
|
-
let isNewFile = false;
|
|
24573
|
-
let noTrailingNewline = false;
|
|
24574
|
-
for (const line of lines) {
|
|
24575
|
-
if (line.startsWith("diff --git")) {
|
|
24576
|
-
if (inTargetFile) break;
|
|
24577
|
-
inTargetFile = isDiffLineForFile(line, filePath);
|
|
24578
|
-
inHunk = false;
|
|
24579
|
-
isNewFile = false;
|
|
25162
|
+
let contentHash = this.computeContentHash(patchContent);
|
|
25163
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
24580
25164
|
continue;
|
|
24581
25165
|
}
|
|
24582
|
-
if (
|
|
24583
|
-
|
|
24584
|
-
|
|
24585
|
-
|
|
25166
|
+
if (this.isCreationOnlyPatch(patchContent)) {
|
|
25167
|
+
continue;
|
|
25168
|
+
}
|
|
25169
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
25170
|
+
const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2));
|
|
25171
|
+
const sensitiveScanFiles = allScanFiles.filter((f2) => isSensitiveFile(f2));
|
|
25172
|
+
const files = allScanFiles.filter((f2) => !isSensitiveFile(f2));
|
|
25173
|
+
if (sensitiveScanFiles.length > 0) {
|
|
25174
|
+
this.warnings.push(
|
|
25175
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25176
|
+
);
|
|
25177
|
+
if (files.length > 0) {
|
|
25178
|
+
if (parents.length === 0) {
|
|
25179
|
+
continue;
|
|
25180
|
+
}
|
|
25181
|
+
try {
|
|
25182
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
25183
|
+
} catch {
|
|
25184
|
+
continue;
|
|
25185
|
+
}
|
|
25186
|
+
if (!patchContent.trim()) continue;
|
|
25187
|
+
contentHash = this.computeContentHash(patchContent);
|
|
24586
25188
|
}
|
|
24587
25189
|
}
|
|
24588
|
-
if (
|
|
24589
|
-
if (!isNewFile) return null;
|
|
24590
|
-
inHunk = true;
|
|
25190
|
+
if (files.length === 0) {
|
|
24591
25191
|
continue;
|
|
24592
25192
|
}
|
|
24593
|
-
if (
|
|
24594
|
-
if (line === "\") {
|
|
24595
|
-
noTrailingNewline = true;
|
|
25193
|
+
if (parents.length === 0) {
|
|
24596
25194
|
continue;
|
|
24597
25195
|
}
|
|
24598
|
-
|
|
24599
|
-
|
|
25196
|
+
const parentSha = parents[0];
|
|
25197
|
+
const theirsSnapshot = {};
|
|
25198
|
+
for (const file of files) {
|
|
25199
|
+
if (isBinaryFile(file)) continue;
|
|
25200
|
+
const content = await this.git.showFile(commit.sha, file).catch(() => null);
|
|
25201
|
+
if (content != null) theirsSnapshot[file] = content;
|
|
24600
25202
|
}
|
|
25203
|
+
newPatches.push({
|
|
25204
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
25205
|
+
content_hash: contentHash,
|
|
25206
|
+
original_commit: commit.sha,
|
|
25207
|
+
original_message: commit.message,
|
|
25208
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
25209
|
+
base_generation: parentSha,
|
|
25210
|
+
files,
|
|
25211
|
+
patch_content: patchContent,
|
|
25212
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
25213
|
+
});
|
|
24601
25214
|
}
|
|
24602
|
-
|
|
24603
|
-
return
|
|
25215
|
+
newPatches.reverse();
|
|
25216
|
+
return { patches: newPatches, revertedPatchIds: [] };
|
|
25217
|
+
}
|
|
25218
|
+
/**
|
|
25219
|
+
* Check if a format-patch consists entirely of new-file creations.
|
|
25220
|
+
* Used to identify squashed commits after force push, which create all files
|
|
25221
|
+
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
25222
|
+
*/
|
|
25223
|
+
isCreationOnlyPatch(patchContent) {
|
|
25224
|
+
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
25225
|
+
if (diffOldHeaders.length === 0) {
|
|
25226
|
+
return false;
|
|
25227
|
+
}
|
|
25228
|
+
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
25229
|
+
}
|
|
25230
|
+
/**
|
|
25231
|
+
* Resolve the best available diff base for a generation record.
|
|
25232
|
+
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
25233
|
+
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
25234
|
+
*/
|
|
25235
|
+
async resolveDiffBase(gen, commitKnownMissing) {
|
|
25236
|
+
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
25237
|
+
return gen.commit_sha;
|
|
25238
|
+
}
|
|
25239
|
+
if (await this.git.treeExists(gen.tree_hash)) {
|
|
25240
|
+
return gen.tree_hash;
|
|
25241
|
+
}
|
|
25242
|
+
return null;
|
|
25243
|
+
}
|
|
25244
|
+
parseGitLog(log) {
|
|
25245
|
+
return log.trim().split("\n").map((line) => {
|
|
25246
|
+
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
25247
|
+
return { sha, authorName, authorEmail, message };
|
|
25248
|
+
});
|
|
25249
|
+
}
|
|
25250
|
+
getLastGeneration(lock) {
|
|
25251
|
+
return lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24604
25252
|
}
|
|
24605
25253
|
};
|
|
24606
25254
|
ReplayCommitter = class {
|
|
@@ -24632,13 +25280,46 @@ CLI Version: ${options.cliVersion}`;
|
|
|
24632
25280
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
24633
25281
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
24634
25282
|
}
|
|
24635
|
-
async commitReplay(_patchCount, patches, message) {
|
|
25283
|
+
async commitReplay(_patchCount, patches, message, options) {
|
|
24636
25284
|
await this.stageAll();
|
|
24637
25285
|
if (!await this.hasStagedChanges()) {
|
|
24638
25286
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
24639
25287
|
}
|
|
24640
25288
|
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
24641
|
-
|
|
25289
|
+
const buckets = options?.buckets;
|
|
25290
|
+
if (buckets) {
|
|
25291
|
+
if (buckets.applied.length > 0) {
|
|
25292
|
+
fullMessage += `
|
|
25293
|
+
|
|
25294
|
+
Patches applied (${buckets.applied.length}):`;
|
|
25295
|
+
for (const p of buckets.applied) {
|
|
25296
|
+
fullMessage += `
|
|
25297
|
+
- ${p.id}: ${p.original_message}`;
|
|
25298
|
+
}
|
|
25299
|
+
}
|
|
25300
|
+
if (buckets.unresolved.length > 0) {
|
|
25301
|
+
fullMessage += `
|
|
25302
|
+
|
|
25303
|
+
Patches with unresolved conflicts (${buckets.unresolved.length}):`;
|
|
25304
|
+
for (const p of buckets.unresolved) {
|
|
25305
|
+
fullMessage += `
|
|
25306
|
+
- ${p.id}: ${p.original_message}`;
|
|
25307
|
+
}
|
|
25308
|
+
fullMessage += `
|
|
25309
|
+
Run \`fern-replay resolve\` to apply these customizations.`;
|
|
25310
|
+
}
|
|
25311
|
+
if (buckets.absorbed.length > 0) {
|
|
25312
|
+
fullMessage += `
|
|
25313
|
+
|
|
25314
|
+
Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
25315
|
+
for (const p of buckets.absorbed) {
|
|
25316
|
+
fullMessage += `
|
|
25317
|
+
- ${p.id}: ${p.original_message}`;
|
|
25318
|
+
}
|
|
25319
|
+
fullMessage += `
|
|
25320
|
+
The generator now produces these customizations natively.`;
|
|
25321
|
+
}
|
|
25322
|
+
} else if (patches && patches.length > 0) {
|
|
24642
25323
|
fullMessage += "\n\nPatches replayed:";
|
|
24643
25324
|
for (const patch of patches) {
|
|
24644
25325
|
fullMessage += `
|
|
@@ -24672,6 +25353,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
24672
25353
|
}
|
|
24673
25354
|
};
|
|
24674
25355
|
init_GitClient();
|
|
25356
|
+
init_HybridReconstruction();
|
|
24675
25357
|
ReplayService = class {
|
|
24676
25358
|
git;
|
|
24677
25359
|
detector;
|
|
@@ -25065,6 +25747,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25065
25747
|
if (newPatches.length > 0) {
|
|
25066
25748
|
results = await this.applicator.applyPatches(newPatches);
|
|
25067
25749
|
this._lastApplyResults = results;
|
|
25750
|
+
this.recordDroppedContextLineWarnings(results);
|
|
25068
25751
|
this.revertConflictingFiles(results);
|
|
25069
25752
|
for (const result of results) {
|
|
25070
25753
|
if (result.status === "conflict") {
|
|
@@ -25083,7 +25766,8 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25083
25766
|
if (newPatches.length > 0) {
|
|
25084
25767
|
if (!options?.stageOnly) {
|
|
25085
25768
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
25086
|
-
|
|
25769
|
+
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
|
|
25770
|
+
await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
25087
25771
|
} else {
|
|
25088
25772
|
await this.committer.stageAll();
|
|
25089
25773
|
}
|
|
@@ -25146,12 +25830,16 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25146
25830
|
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
25147
25831
|
);
|
|
25148
25832
|
existingPatches = this.lockManager.getPatches();
|
|
25149
|
-
const seenHashes = /* @__PURE__ */ new
|
|
25833
|
+
const seenHashes = /* @__PURE__ */ new Map();
|
|
25150
25834
|
for (const p of existingPatches) {
|
|
25151
|
-
|
|
25835
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
25836
|
+
if (priorPatchId !== void 0) {
|
|
25152
25837
|
this.lockManager.removePatch(p.id);
|
|
25838
|
+
this.warnings.push(
|
|
25839
|
+
`Consolidated patch ${p.id} (commit ${(p.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
25840
|
+
);
|
|
25153
25841
|
} else {
|
|
25154
|
-
seenHashes.
|
|
25842
|
+
seenHashes.set(p.content_hash, p.id);
|
|
25155
25843
|
}
|
|
25156
25844
|
}
|
|
25157
25845
|
existingPatches = this.lockManager.getPatches();
|
|
@@ -25212,6 +25900,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25212
25900
|
const genSha = prep._prepared.genSha;
|
|
25213
25901
|
const results = await this.applicator.applyPatches(allPatches);
|
|
25214
25902
|
this._lastApplyResults = results;
|
|
25903
|
+
this.recordDroppedContextLineWarnings(results);
|
|
25215
25904
|
this.revertConflictingFiles(results);
|
|
25216
25905
|
for (const result of results) {
|
|
25217
25906
|
if (result.status === "conflict") {
|
|
@@ -25239,7 +25928,8 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25239
25928
|
await this.committer.stageAll();
|
|
25240
25929
|
} else {
|
|
25241
25930
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
25242
|
-
|
|
25931
|
+
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
|
|
25932
|
+
await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
25243
25933
|
}
|
|
25244
25934
|
const warnings = [...detectorWarnings, ...this.warnings];
|
|
25245
25935
|
return this.buildReport(
|
|
@@ -25263,7 +25953,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25263
25953
|
let repointed = 0;
|
|
25264
25954
|
let contentRebased = 0;
|
|
25265
25955
|
let keptAsUserOwned = 0;
|
|
25266
|
-
const seenContentHashes = /* @__PURE__ */ new
|
|
25956
|
+
const seenContentHashes = /* @__PURE__ */ new Map();
|
|
25267
25957
|
const absorbedPatchIds = /* @__PURE__ */ new Set();
|
|
25268
25958
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
25269
25959
|
for (const result of results) {
|
|
@@ -25277,6 +25967,23 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25277
25967
|
}
|
|
25278
25968
|
}
|
|
25279
25969
|
}
|
|
25970
|
+
const conflictedFilePaths = /* @__PURE__ */ new Set();
|
|
25971
|
+
for (const r2 of results) {
|
|
25972
|
+
if (r2.status !== "conflict") continue;
|
|
25973
|
+
if (r2.fileResults) {
|
|
25974
|
+
for (const fr of r2.fileResults) {
|
|
25975
|
+
if (fr.status !== "conflict") continue;
|
|
25976
|
+
conflictedFilePaths.add(fr.file);
|
|
25977
|
+
if (r2.resolvedFiles) {
|
|
25978
|
+
for (const [orig, resolved] of Object.entries(r2.resolvedFiles)) {
|
|
25979
|
+
if (resolved === fr.file) conflictedFilePaths.add(orig);
|
|
25980
|
+
}
|
|
25981
|
+
}
|
|
25982
|
+
}
|
|
25983
|
+
} else {
|
|
25984
|
+
for (const f2 of r2.patch.files) conflictedFilePaths.add(f2);
|
|
25985
|
+
}
|
|
25986
|
+
}
|
|
25280
25987
|
for (const result of results) {
|
|
25281
25988
|
if (result.status === "conflict" && result.fileResults) {
|
|
25282
25989
|
await this.trimAbsorbedFiles(result, currentGenSha);
|
|
@@ -25313,17 +26020,27 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25313
26020
|
if (allUserOwned && patch.files.length > 0) {
|
|
25314
26021
|
const baseChanged = patch.base_generation !== currentGenSha;
|
|
25315
26022
|
const userDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
26023
|
+
let userDiffSnapshot = null;
|
|
25316
26024
|
if (userDiff != null && userDiff.trim()) {
|
|
25317
26025
|
const newHash = this.detector.computeContentHash(userDiff);
|
|
25318
26026
|
patch.patch_content = userDiff;
|
|
25319
26027
|
patch.content_hash = newHash;
|
|
26028
|
+
const snap = await this.readSnapshotFromDisk(patch.files);
|
|
26029
|
+
if (Object.keys(snap).length > 0) {
|
|
26030
|
+
userDiffSnapshot = snap;
|
|
26031
|
+
patch.theirs_snapshot = snap;
|
|
26032
|
+
}
|
|
25320
26033
|
}
|
|
25321
26034
|
patch.base_generation = currentGenSha;
|
|
25322
26035
|
try {
|
|
25323
26036
|
this.lockManager.updatePatch(patch.id, {
|
|
25324
26037
|
base_generation: currentGenSha,
|
|
25325
26038
|
...!patch.user_owned ? { user_owned: true } : {},
|
|
25326
|
-
...userDiff != null && userDiff.trim() ? {
|
|
26039
|
+
...userDiff != null && userDiff.trim() ? {
|
|
26040
|
+
patch_content: patch.patch_content,
|
|
26041
|
+
content_hash: patch.content_hash,
|
|
26042
|
+
...userDiffSnapshot != null && Object.keys(userDiffSnapshot).length > 0 ? { theirs_snapshot: userDiffSnapshot } : {}
|
|
26043
|
+
} : {}
|
|
25327
26044
|
});
|
|
25328
26045
|
} catch {
|
|
25329
26046
|
}
|
|
@@ -25344,6 +26061,18 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25344
26061
|
keptAsUserOwned++;
|
|
25345
26062
|
continue;
|
|
25346
26063
|
}
|
|
26064
|
+
const overlapsConflicted = patch.files.some((f2) => conflictedFilePaths.has(f2));
|
|
26065
|
+
if (overlapsConflicted) {
|
|
26066
|
+
patch.status = "unresolved";
|
|
26067
|
+
try {
|
|
26068
|
+
this.lockManager.updatePatch(patch.id, { status: "unresolved" });
|
|
26069
|
+
} catch {
|
|
26070
|
+
}
|
|
26071
|
+
this.warnings.push(
|
|
26072
|
+
`Patch ${patch.id} (${patch.original_message}) was preserved as unresolved because an earlier patch conflicted on the same file(s) (${patch.files.join(", ")}). Run \`fern replay resolve\` to apply this customization manually.`
|
|
26073
|
+
);
|
|
26074
|
+
continue;
|
|
26075
|
+
}
|
|
25347
26076
|
this.lockManager.removePatch(patch.id);
|
|
25348
26077
|
absorbedPatchIds.add(patch.id);
|
|
25349
26078
|
absorbed++;
|
|
@@ -25359,21 +26088,34 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25359
26088
|
continue;
|
|
25360
26089
|
}
|
|
25361
26090
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
25362
|
-
|
|
26091
|
+
const priorPatchId = seenContentHashes.get(newContentHash);
|
|
26092
|
+
if (priorPatchId !== void 0) {
|
|
25363
26093
|
this.lockManager.removePatch(patch.id);
|
|
25364
26094
|
absorbedPatchIds.add(patch.id);
|
|
25365
26095
|
absorbed++;
|
|
26096
|
+
if (priorPatchId !== patch.id) {
|
|
26097
|
+
this.warnings.push(
|
|
26098
|
+
`Consolidated patch ${patch.id} (commit ${(patch.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content after rebase. Per-commit attribution preserved in git log.`
|
|
26099
|
+
);
|
|
26100
|
+
}
|
|
25366
26101
|
continue;
|
|
26102
|
+
} else {
|
|
26103
|
+
seenContentHashes.set(newContentHash, patch.id);
|
|
25367
26104
|
}
|
|
25368
|
-
seenContentHashes.add(newContentHash);
|
|
25369
26105
|
patch.base_generation = currentGenSha;
|
|
25370
26106
|
patch.patch_content = diff;
|
|
25371
26107
|
patch.content_hash = newContentHash;
|
|
26108
|
+
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
26109
|
+
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
26110
|
+
if (snapshotIsNonEmpty) {
|
|
26111
|
+
patch.theirs_snapshot = snapshot;
|
|
26112
|
+
}
|
|
25372
26113
|
try {
|
|
25373
26114
|
this.lockManager.updatePatch(patch.id, {
|
|
25374
26115
|
base_generation: currentGenSha,
|
|
25375
26116
|
patch_content: diff,
|
|
25376
|
-
content_hash: newContentHash
|
|
26117
|
+
content_hash: newContentHash,
|
|
26118
|
+
...snapshotIsNonEmpty ? { theirs_snapshot: snapshot } : {}
|
|
25377
26119
|
});
|
|
25378
26120
|
} catch {
|
|
25379
26121
|
}
|
|
@@ -25383,6 +26125,112 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25383
26125
|
}
|
|
25384
26126
|
return { absorbed, repointed, contentRebased, keptAsUserOwned, absorbedPatchIds };
|
|
25385
26127
|
}
|
|
26128
|
+
/**
|
|
26129
|
+
* Read THEIRS snapshot (post-customer-edit content) for a list of files
|
|
26130
|
+
* from the working tree on disk. Used by `rebasePatches` after a clean
|
|
26131
|
+
* apply — disk = correctly-merged customer state at the new generation.
|
|
26132
|
+
* Skips binary files (matches `mergeFile`/`populateAccumulatorForPatch`
|
|
26133
|
+
* policy — binary content as `utf-8` is lossy and snapshots of it would
|
|
26134
|
+
* never be used during apply anyway).
|
|
26135
|
+
*/
|
|
26136
|
+
async readSnapshotFromDisk(files) {
|
|
26137
|
+
const snapshot = {};
|
|
26138
|
+
for (const file of files) {
|
|
26139
|
+
if (isBinaryFile(file)) continue;
|
|
26140
|
+
try {
|
|
26141
|
+
const content = await (0, import_promises5.readFile)((0, import_path10.join)(this.outputDir, file), "utf-8");
|
|
26142
|
+
snapshot[file] = content;
|
|
26143
|
+
} catch {
|
|
26144
|
+
}
|
|
26145
|
+
}
|
|
26146
|
+
return snapshot;
|
|
26147
|
+
}
|
|
26148
|
+
/**
|
|
26149
|
+
* Read THEIRS snapshot from a git tree-ish (commit, branch, or tree).
|
|
26150
|
+
* Used when the diff that produced `patch_content` was relative to that
|
|
26151
|
+
* tree (e.g., `git diff currentGen HEAD --` → snapshot from HEAD).
|
|
26152
|
+
* Skips binary files.
|
|
26153
|
+
*/
|
|
26154
|
+
async readSnapshotFromTree(treeRef, files) {
|
|
26155
|
+
const snapshot = {};
|
|
26156
|
+
for (const file of files) {
|
|
26157
|
+
if (isBinaryFile(file)) continue;
|
|
26158
|
+
const content = await this.git.showFile(treeRef, file).catch(() => null);
|
|
26159
|
+
if (content != null) snapshot[file] = content;
|
|
26160
|
+
}
|
|
26161
|
+
return snapshot;
|
|
26162
|
+
}
|
|
26163
|
+
/**
|
|
26164
|
+
* One-shot auto-migration: backfill `theirs_snapshot` on existing
|
|
26165
|
+
* patches that predate the snapshot encoding. Computes the snapshot by
|
|
26166
|
+
* applying the patch's `patch_content` to its `base_generation` tree —
|
|
26167
|
+
* yielding the patch's INDIVIDUAL contribution.
|
|
26168
|
+
*
|
|
26169
|
+
* **Skips patches sharing files with other patches.** HEAD's content
|
|
26170
|
+
* for a file shared by N patches is cumulative across all of them, so
|
|
26171
|
+
* a HEAD-fallback would falsely encode other patches' contributions
|
|
26172
|
+
* into one patch's snapshot — breaking FER-9525 cross-patch isolation
|
|
26173
|
+
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
26174
|
+
* make sense when the patch owns its file. Multi-patch-same-file
|
|
26175
|
+
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
26176
|
+
* existing path); they're no worse than pre-upgrade.
|
|
26177
|
+
*
|
|
26178
|
+
* **Does not fall back to HEAD when BASE-reconstruction fails.** Same
|
|
26179
|
+
* reason — HEAD content can be cumulative. Patches whose
|
|
26180
|
+
* reconstruction fails just don't get a snapshot; legacy paths
|
|
26181
|
+
* (PR #73's by-association gate, accumulator fallback) handle them.
|
|
26182
|
+
*
|
|
26183
|
+
* Skips patches that already have a snapshot.
|
|
26184
|
+
*/
|
|
26185
|
+
async migratePatchesToSnapshot(patches) {
|
|
26186
|
+
const { applyPatchToContent: applyPatchToContent2 } = await Promise.resolve().then(() => (init_PatchApplyTheirs(), PatchApplyTheirs_exports));
|
|
26187
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
26188
|
+
for (const p of patches) {
|
|
26189
|
+
for (const f2 of p.files) {
|
|
26190
|
+
fileFreq.set(f2, (fileFreq.get(f2) ?? 0) + 1);
|
|
26191
|
+
}
|
|
26192
|
+
}
|
|
26193
|
+
let migrated = 0;
|
|
26194
|
+
let skipped = 0;
|
|
26195
|
+
for (const patch of patches) {
|
|
26196
|
+
if (patch.theirs_snapshot != null) continue;
|
|
26197
|
+
const sharesAnyFile = patch.files.some((f2) => (fileFreq.get(f2) ?? 0) > 1);
|
|
26198
|
+
if (sharesAnyFile) {
|
|
26199
|
+
skipped++;
|
|
26200
|
+
continue;
|
|
26201
|
+
}
|
|
26202
|
+
const snapshot = {};
|
|
26203
|
+
for (const file of patch.files) {
|
|
26204
|
+
if (isBinaryFile(file)) continue;
|
|
26205
|
+
try {
|
|
26206
|
+
const base = await this.git.showFile(patch.base_generation, file).catch(() => null);
|
|
26207
|
+
if (base != null) {
|
|
26208
|
+
const content = await applyPatchToContent2(base, patch.patch_content, file);
|
|
26209
|
+
if (content != null) snapshot[file] = content;
|
|
26210
|
+
}
|
|
26211
|
+
} catch {
|
|
26212
|
+
}
|
|
26213
|
+
}
|
|
26214
|
+
if (Object.keys(snapshot).length > 0) {
|
|
26215
|
+
patch.theirs_snapshot = snapshot;
|
|
26216
|
+
try {
|
|
26217
|
+
this.lockManager.updatePatch(patch.id, { theirs_snapshot: snapshot });
|
|
26218
|
+
migrated++;
|
|
26219
|
+
} catch {
|
|
26220
|
+
}
|
|
26221
|
+
}
|
|
26222
|
+
}
|
|
26223
|
+
if (migrated > 0) {
|
|
26224
|
+
this.warnings.push(
|
|
26225
|
+
`Migrated ${migrated} patch(es) to snapshot encoding. Future regens use 3-way merge with stored THEIRS, robust to generator structural changes.`
|
|
26226
|
+
);
|
|
26227
|
+
}
|
|
26228
|
+
if (skipped > 0) {
|
|
26229
|
+
this.warnings.push(
|
|
26230
|
+
`Skipped snapshot migration for ${skipped} patch(es) sharing files with other patches. These continue to use line-anchored reconstruction (the existing path).`
|
|
26231
|
+
);
|
|
26232
|
+
}
|
|
26233
|
+
}
|
|
25386
26234
|
/**
|
|
25387
26235
|
* Determine whether `file` is user-owned (never produced by the generator).
|
|
25388
26236
|
*
|
|
@@ -25503,6 +26351,20 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25503
26351
|
this.lockManager.clearReplaySkippedAt();
|
|
25504
26352
|
return { conflictResolved: 0, conflictAbsorbed: 0, contentRefreshed: 0 };
|
|
25505
26353
|
}
|
|
26354
|
+
await this.migratePatchesToSnapshot(patches);
|
|
26355
|
+
const headMessage = await this.git.exec(["log", "-1", "--format=%s%n%aN%n%ae", "HEAD"]).catch(() => "");
|
|
26356
|
+
const [headMsgLine, headAuthorName, headAuthorEmail] = headMessage.split("\n");
|
|
26357
|
+
const headIsGeneration = headMsgLine ? isGenerationCommit({
|
|
26358
|
+
sha: "HEAD",
|
|
26359
|
+
authorName: headAuthorName ?? "",
|
|
26360
|
+
authorEmail: headAuthorEmail ?? "",
|
|
26361
|
+
message: headMsgLine
|
|
26362
|
+
}) : false;
|
|
26363
|
+
const headParentChangedFiles = async (patchFiles) => {
|
|
26364
|
+
if (patchFiles.length === 0) return /* @__PURE__ */ new Set();
|
|
26365
|
+
const diff = await this.git.exec(["diff", "--name-only", "HEAD~1", "HEAD", "--", ...patchFiles]).catch(() => "");
|
|
26366
|
+
return new Set(diff.trim().split("\n").filter(Boolean));
|
|
26367
|
+
};
|
|
25506
26368
|
let conflictResolved = 0;
|
|
25507
26369
|
let conflictAbsorbed = 0;
|
|
25508
26370
|
let contentRefreshed = 0;
|
|
@@ -25539,10 +26401,23 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25539
26401
|
delete patch.status;
|
|
25540
26402
|
continue;
|
|
25541
26403
|
}
|
|
26404
|
+
if (headIsGeneration) {
|
|
26405
|
+
const parentChangedFiles = await headParentChangedFiles(patch.files);
|
|
26406
|
+
const headTouchedSource = patch.files.some((f2) => parentChangedFiles.has(f2));
|
|
26407
|
+
if (headTouchedSource) {
|
|
26408
|
+
continue;
|
|
26409
|
+
}
|
|
26410
|
+
}
|
|
25542
26411
|
if (patch.base_generation === currentGen) {
|
|
25543
26412
|
try {
|
|
25544
|
-
const
|
|
25545
|
-
|
|
26413
|
+
const ppResult = await computePerPatchDiffWithFallback(
|
|
26414
|
+
patch,
|
|
26415
|
+
(f2) => this.git.showFile(currentGen, f2),
|
|
26416
|
+
(f2) => this.git.showFile("HEAD", f2),
|
|
26417
|
+
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
26418
|
+
);
|
|
26419
|
+
if (ppResult === null) continue;
|
|
26420
|
+
const diff = ppResult.diff;
|
|
25546
26421
|
if (!diff.trim()) {
|
|
25547
26422
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
25548
26423
|
this.lockManager.removePatch(patch.id);
|
|
@@ -25569,10 +26444,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25569
26444
|
if (newFiles.length === 0) {
|
|
25570
26445
|
this.lockManager.removePatch(patch.id);
|
|
25571
26446
|
} else {
|
|
26447
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", newFiles);
|
|
25572
26448
|
this.lockManager.updatePatch(patch.id, {
|
|
25573
26449
|
patch_content: diff,
|
|
25574
26450
|
content_hash: newContentHash,
|
|
25575
|
-
files: newFiles
|
|
26451
|
+
files: newFiles,
|
|
26452
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
25576
26453
|
});
|
|
25577
26454
|
}
|
|
25578
26455
|
contentRefreshed++;
|
|
@@ -25584,8 +26461,14 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25584
26461
|
try {
|
|
25585
26462
|
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
25586
26463
|
if (markerFiles.trim()) continue;
|
|
25587
|
-
const
|
|
25588
|
-
|
|
26464
|
+
const ppResult = await computePerPatchDiffWithFallback(
|
|
26465
|
+
patch,
|
|
26466
|
+
(f2) => this.git.showFile(currentGen, f2),
|
|
26467
|
+
(f2) => this.git.showFile("HEAD", f2),
|
|
26468
|
+
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
26469
|
+
);
|
|
26470
|
+
if (ppResult === null) continue;
|
|
26471
|
+
const diff = ppResult.diff;
|
|
25589
26472
|
if (!diff.trim()) {
|
|
25590
26473
|
if (await allPatchFilesUserOwned(patch)) {
|
|
25591
26474
|
try {
|
|
@@ -25599,10 +26482,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25599
26482
|
continue;
|
|
25600
26483
|
}
|
|
25601
26484
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
26485
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", patch.files);
|
|
25602
26486
|
this.lockManager.updatePatch(patch.id, {
|
|
25603
26487
|
base_generation: currentGen,
|
|
25604
26488
|
patch_content: diff,
|
|
25605
|
-
content_hash: newContentHash
|
|
26489
|
+
content_hash: newContentHash,
|
|
26490
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
25606
26491
|
});
|
|
25607
26492
|
conflictResolved++;
|
|
25608
26493
|
} catch {
|
|
@@ -25610,6 +26495,32 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25610
26495
|
}
|
|
25611
26496
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
25612
26497
|
}
|
|
26498
|
+
/**
|
|
26499
|
+
* After applyPatches(), surface a warning for each (patch × file) where
|
|
26500
|
+
* diff3 silently dropped lines that were unchanged context in the
|
|
26501
|
+
* customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
|
|
26502
|
+
* but the customer must learn so they can re-add the lines if they
|
|
26503
|
+
* relied on them. This is the "surface or preserve, never silently drop"
|
|
26504
|
+
* contract for legitimate-deletion cases.
|
|
26505
|
+
*/
|
|
26506
|
+
recordDroppedContextLineWarnings(results) {
|
|
26507
|
+
const PREVIEW_COUNT = 5;
|
|
26508
|
+
for (const result of results) {
|
|
26509
|
+
if (!result.fileResults) continue;
|
|
26510
|
+
for (const fr of result.fileResults) {
|
|
26511
|
+
const dropped = fr.droppedContextLines;
|
|
26512
|
+
if (!dropped || dropped.length === 0) continue;
|
|
26513
|
+
const preview = dropped.slice(0, PREVIEW_COUNT).map((line) => ` ${line}`).join("\n");
|
|
26514
|
+
const more = dropped.length > PREVIEW_COUNT ? `
|
|
26515
|
+
... and ${dropped.length - PREVIEW_COUNT} more` : "";
|
|
26516
|
+
this.warnings.push(
|
|
26517
|
+
`${fr.file}: ${dropped.length} line(s) that appeared as unchanged context in your customization were deleted by the new generator output. The merge followed the deletion. First lines deleted:
|
|
26518
|
+
${preview}${more}
|
|
26519
|
+
If you want to keep these lines, restore them in a follow-up commit and re-run replay; otherwise this warning can be safely ignored.`
|
|
26520
|
+
);
|
|
26521
|
+
}
|
|
26522
|
+
}
|
|
26523
|
+
}
|
|
25613
26524
|
/**
|
|
25614
26525
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
25615
26526
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -25619,7 +26530,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25619
26530
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
25620
26531
|
for (const fileResult of result.fileResults) {
|
|
25621
26532
|
if (fileResult.status !== "conflict") continue;
|
|
25622
|
-
const filePath = (0,
|
|
26533
|
+
const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
|
|
25623
26534
|
try {
|
|
25624
26535
|
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
25625
26536
|
const stripped = stripConflictMarkers(content);
|
|
@@ -25649,7 +26560,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25649
26560
|
}
|
|
25650
26561
|
}
|
|
25651
26562
|
readFernignorePatterns() {
|
|
25652
|
-
const fernignorePath = (0,
|
|
26563
|
+
const fernignorePath = (0, import_path10.join)(this.outputDir, ".fernignore");
|
|
25653
26564
|
if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
|
|
25654
26565
|
return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
25655
26566
|
}
|
|
@@ -25667,6 +26578,9 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25667
26578
|
};
|
|
25668
26579
|
}).filter((d) => d.files.length > 0);
|
|
25669
26580
|
const partialCount = conflictDetails.filter((d) => d.cleanFiles && d.cleanFiles.length > 0).length;
|
|
26581
|
+
const unresolvedResults = results.filter(
|
|
26582
|
+
(r2) => r2.status === "conflict" || r2.patch.status === "unresolved"
|
|
26583
|
+
);
|
|
25670
26584
|
return {
|
|
25671
26585
|
flow,
|
|
25672
26586
|
patchesDetected: patches.length,
|
|
@@ -25683,7 +26597,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25683
26597
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
25684
26598
|
conflicts: conflictResults.flatMap((r2) => r2.fileResults?.filter((f2) => f2.status === "conflict") ?? []),
|
|
25685
26599
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
25686
|
-
unresolvedPatches:
|
|
26600
|
+
unresolvedPatches: unresolvedResults.length > 0 ? unresolvedResults.map((r2) => ({
|
|
25687
26601
|
patchId: r2.patch.id,
|
|
25688
26602
|
patchMessage: r2.patch.original_message,
|
|
25689
26603
|
files: r2.patch.files,
|
|
@@ -25704,10 +26618,10 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25704
26618
|
this.outputDir = outputDir;
|
|
25705
26619
|
}
|
|
25706
26620
|
fernignoreExists() {
|
|
25707
|
-
return (0, import_fs8.existsSync)((0,
|
|
26621
|
+
return (0, import_fs8.existsSync)((0, import_path12.join)(this.outputDir, ".fernignore"));
|
|
25708
26622
|
}
|
|
25709
26623
|
readFernignorePatterns() {
|
|
25710
|
-
const fernignorePath = (0,
|
|
26624
|
+
const fernignorePath = (0, import_path12.join)(this.outputDir, ".fernignore");
|
|
25711
26625
|
if (!(0, import_fs8.existsSync)(fernignorePath)) {
|
|
25712
26626
|
return [];
|
|
25713
26627
|
}
|
|
@@ -25759,6 +26673,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25759
26673
|
if (patchFiles.length > 0) {
|
|
25760
26674
|
const patchContent = diffParts.join("\n");
|
|
25761
26675
|
const contentHash = `sha256:${(0, import_crypto2.createHash)("sha256").update(patchContent).digest("hex")}`;
|
|
26676
|
+
const theirsSnapshot = {};
|
|
26677
|
+
for (const filePath of patchFiles) {
|
|
26678
|
+
if (isBinaryFile(filePath)) continue;
|
|
26679
|
+
const c2 = this.readFileContent(filePath);
|
|
26680
|
+
if (c2 != null) theirsSnapshot[filePath] = c2;
|
|
26681
|
+
}
|
|
25762
26682
|
syntheticPatches.push({
|
|
25763
26683
|
id: `patch-fernignore-${(0, import_crypto2.createHash)("sha256").update(pattern).digest("hex").slice(0, 8)}`,
|
|
25764
26684
|
content_hash: contentHash,
|
|
@@ -25767,7 +26687,17 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25767
26687
|
original_author: "Fern Replay <replay@buildwithfern.com>",
|
|
25768
26688
|
base_generation: currentGen.commit_sha,
|
|
25769
26689
|
files: patchFiles,
|
|
25770
|
-
patch_content: patchContent
|
|
26690
|
+
patch_content: patchContent,
|
|
26691
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
26692
|
+
// Synthetic patches captured from .fernignore-protected files
|
|
26693
|
+
// are user-owned by definition — the customer's content
|
|
26694
|
+
// diverges from the generator's pristine output, and the
|
|
26695
|
+
// generator never produced this divergent content. Without
|
|
26696
|
+
// this flag, removing the file from .fernignore later (so
|
|
26697
|
+
// it leaves the protection check in `isFileUserOwned`) would
|
|
26698
|
+
// expose the patch to absorption on the next regen,
|
|
26699
|
+
// silently losing the customization.
|
|
26700
|
+
user_owned: true
|
|
25771
26701
|
});
|
|
25772
26702
|
trackedByBoth.push({
|
|
25773
26703
|
file: pattern,
|
|
@@ -25800,7 +26730,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25800
26730
|
return results;
|
|
25801
26731
|
}
|
|
25802
26732
|
readFileContent(filePath) {
|
|
25803
|
-
const fullPath = (0,
|
|
26733
|
+
const fullPath = (0, import_path12.join)(this.outputDir, filePath);
|
|
25804
26734
|
if (!(0, import_fs8.existsSync)(fullPath)) {
|
|
25805
26735
|
return null;
|
|
25806
26736
|
}
|
|
@@ -25865,7 +26795,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25865
26795
|
};
|
|
25866
26796
|
}
|
|
25867
26797
|
movePatternsToReplayYml(patterns) {
|
|
25868
|
-
const replayYmlPath = (0,
|
|
26798
|
+
const replayYmlPath = (0, import_path12.join)(this.outputDir, ".fern", "replay.yml");
|
|
25869
26799
|
let config = {};
|
|
25870
26800
|
if ((0, import_fs8.existsSync)(replayYmlPath)) {
|
|
25871
26801
|
const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -25874,7 +26804,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
25874
26804
|
const existing = config.exclude ?? [];
|
|
25875
26805
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
25876
26806
|
config.exclude = merged;
|
|
25877
|
-
const dir = (0,
|
|
26807
|
+
const dir = (0, import_path12.dirname)(replayYmlPath);
|
|
25878
26808
|
if (!(0, import_fs8.existsSync)(dir)) {
|
|
25879
26809
|
(0, import_fs8.mkdirSync)(dir, { recursive: true });
|
|
25880
26810
|
}
|
|
@@ -26378,7 +27308,7 @@ var require_cross_spawn = __commonJS({
|
|
|
26378
27308
|
var cp2 = require("child_process");
|
|
26379
27309
|
var parse3 = require_parse2();
|
|
26380
27310
|
var enoent = require_enoent();
|
|
26381
|
-
function
|
|
27311
|
+
function spawn3(command, args, options) {
|
|
26382
27312
|
const parsed = parse3(command, args, options);
|
|
26383
27313
|
const spawned = cp2.spawn(parsed.command, parsed.args, parsed.options);
|
|
26384
27314
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -26390,8 +27320,8 @@ var require_cross_spawn = __commonJS({
|
|
|
26390
27320
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
26391
27321
|
return result;
|
|
26392
27322
|
}
|
|
26393
|
-
module2.exports =
|
|
26394
|
-
module2.exports.spawn =
|
|
27323
|
+
module2.exports = spawn3;
|
|
27324
|
+
module2.exports.spawn = spawn3;
|
|
26395
27325
|
module2.exports.sync = spawnSync;
|
|
26396
27326
|
module2.exports._parse = parse3;
|
|
26397
27327
|
module2.exports._enoent = enoent;
|
|
@@ -28430,8 +29360,8 @@ function resolve(...paths) {
|
|
|
28430
29360
|
|
|
28431
29361
|
// src/cli.ts
|
|
28432
29362
|
var import_fs15 = __toESM(require("fs"));
|
|
28433
|
-
var
|
|
28434
|
-
var
|
|
29363
|
+
var import_promises15 = require("fs/promises");
|
|
29364
|
+
var import_path21 = __toESM(require("path"));
|
|
28435
29365
|
|
|
28436
29366
|
// ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
|
|
28437
29367
|
var YError = class _YError extends Error {
|
|
@@ -38132,42 +39062,42 @@ function replayForget(params) {
|
|
|
38132
39062
|
// src/replay/replay-init.ts
|
|
38133
39063
|
init_dist3();
|
|
38134
39064
|
var import_fs11 = require("fs");
|
|
38135
|
-
var
|
|
39065
|
+
var import_path16 = require("path");
|
|
38136
39066
|
var import_tmp_promise2 = __toESM(require_tmp_promise());
|
|
38137
39067
|
|
|
38138
39068
|
// src/replay/fernignore.ts
|
|
38139
|
-
var
|
|
38140
|
-
var
|
|
39069
|
+
var import_promises8 = require("fs/promises");
|
|
39070
|
+
var import_path15 = require("path");
|
|
38141
39071
|
var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
38142
39072
|
var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
|
|
38143
39073
|
async function ensureReplayFernignoreEntries(outputDir) {
|
|
38144
|
-
const fernignorePath = (0,
|
|
39074
|
+
const fernignorePath = (0, import_path15.join)(outputDir, ".fernignore");
|
|
38145
39075
|
let content = "";
|
|
38146
39076
|
try {
|
|
38147
|
-
content = await (0,
|
|
39077
|
+
content = await (0, import_promises8.readFile)(fernignorePath, "utf-8");
|
|
38148
39078
|
} catch {
|
|
38149
39079
|
}
|
|
38150
39080
|
const lines = content.split("\n");
|
|
38151
39081
|
const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38152
39082
|
if (toAdd.length > 0) {
|
|
38153
39083
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38154
|
-
await (0,
|
|
39084
|
+
await (0, import_promises8.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38155
39085
|
return true;
|
|
38156
39086
|
}
|
|
38157
39087
|
return false;
|
|
38158
39088
|
}
|
|
38159
39089
|
async function ensureGitattributesEntries2(outputDir) {
|
|
38160
|
-
const gitattributesPath = (0,
|
|
39090
|
+
const gitattributesPath = (0, import_path15.join)(outputDir, ".gitattributes");
|
|
38161
39091
|
let content = "";
|
|
38162
39092
|
try {
|
|
38163
|
-
content = await (0,
|
|
39093
|
+
content = await (0, import_promises8.readFile)(gitattributesPath, "utf-8");
|
|
38164
39094
|
} catch {
|
|
38165
39095
|
}
|
|
38166
39096
|
const lines = content.split("\n");
|
|
38167
39097
|
const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38168
39098
|
if (toAdd.length > 0) {
|
|
38169
39099
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38170
|
-
await (0,
|
|
39100
|
+
await (0, import_promises8.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38171
39101
|
}
|
|
38172
39102
|
}
|
|
38173
39103
|
|
|
@@ -38194,7 +39124,7 @@ async function replayInit(params) {
|
|
|
38194
39124
|
if (dryRun) {
|
|
38195
39125
|
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
|
|
38196
39126
|
}
|
|
38197
|
-
const lockfilePath = (0,
|
|
39127
|
+
const lockfilePath = (0, import_path16.join)(repoPath, ".fern", "replay.lock");
|
|
38198
39128
|
const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
38199
39129
|
return {
|
|
38200
39130
|
bootstrap: bootstrapResult,
|
|
@@ -38315,9 +39245,9 @@ init_dist3();
|
|
|
38315
39245
|
|
|
38316
39246
|
// src/replay/replay-run.ts
|
|
38317
39247
|
init_dist3();
|
|
38318
|
-
var
|
|
39248
|
+
var import_child_process4 = require("child_process");
|
|
38319
39249
|
var import_fs12 = require("fs");
|
|
38320
|
-
var
|
|
39250
|
+
var import_path17 = require("path");
|
|
38321
39251
|
async function replayPrepare(params) {
|
|
38322
39252
|
const {
|
|
38323
39253
|
outputDir,
|
|
@@ -38328,7 +39258,7 @@ async function replayPrepare(params) {
|
|
|
38328
39258
|
skipApplication,
|
|
38329
39259
|
logger
|
|
38330
39260
|
} = params;
|
|
38331
|
-
const lockfilePath = (0,
|
|
39261
|
+
const lockfilePath = (0, import_path17.join)(outputDir, ".fern", "replay.lock");
|
|
38332
39262
|
if (!(0, import_fs12.existsSync)(lockfilePath)) {
|
|
38333
39263
|
return null;
|
|
38334
39264
|
}
|
|
@@ -38460,7 +39390,7 @@ async function replayRun(params) {
|
|
|
38460
39390
|
}
|
|
38461
39391
|
function isTagMergedIntoHead(cwd2, tagSha) {
|
|
38462
39392
|
try {
|
|
38463
|
-
(0,
|
|
39393
|
+
(0, import_child_process4.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
|
|
38464
39394
|
cwd: cwd2,
|
|
38465
39395
|
stdio: "pipe"
|
|
38466
39396
|
});
|
|
@@ -38472,7 +39402,7 @@ function isTagMergedIntoHead(cwd2, tagSha) {
|
|
|
38472
39402
|
return false;
|
|
38473
39403
|
}
|
|
38474
39404
|
try {
|
|
38475
|
-
const trees = (0,
|
|
39405
|
+
const trees = (0, import_child_process4.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
|
|
38476
39406
|
cwd: cwd2,
|
|
38477
39407
|
encoding: "utf-8",
|
|
38478
39408
|
stdio: "pipe"
|
|
@@ -38484,7 +39414,7 @@ function isTagMergedIntoHead(cwd2, tagSha) {
|
|
|
38484
39414
|
}
|
|
38485
39415
|
function gitRevParse(cwd2, rev) {
|
|
38486
39416
|
try {
|
|
38487
|
-
return (0,
|
|
39417
|
+
return (0, import_child_process4.execFileSync)("git", ["rev-parse", "--verify", rev], {
|
|
38488
39418
|
cwd: cwd2,
|
|
38489
39419
|
encoding: "utf-8",
|
|
38490
39420
|
stdio: "pipe"
|
|
@@ -38495,7 +39425,7 @@ function gitRevParse(cwd2, rev) {
|
|
|
38495
39425
|
}
|
|
38496
39426
|
function gitDiffNameOnly(cwd2, from, to) {
|
|
38497
39427
|
try {
|
|
38498
|
-
const output = (0,
|
|
39428
|
+
const output = (0, import_child_process4.execFileSync)("git", ["diff", "--name-only", from, to], {
|
|
38499
39429
|
cwd: cwd2,
|
|
38500
39430
|
encoding: "utf-8",
|
|
38501
39431
|
stdio: "pipe"
|
|
@@ -38513,29 +39443,29 @@ function replayStatus(params) {
|
|
|
38513
39443
|
}
|
|
38514
39444
|
|
|
38515
39445
|
// src/configuration/loadGitHubConfig.ts
|
|
38516
|
-
var
|
|
39446
|
+
var import_promises9 = require("fs/promises");
|
|
38517
39447
|
async function loadGitHubConfig({
|
|
38518
39448
|
absolutePathToConfig
|
|
38519
39449
|
}) {
|
|
38520
|
-
const rawContents = await (0,
|
|
39450
|
+
const rawContents = await (0, import_promises9.readFile)(absolutePathToConfig, "utf8");
|
|
38521
39451
|
return JSON.parse(rawContents);
|
|
38522
39452
|
}
|
|
38523
39453
|
|
|
38524
39454
|
// src/configuration/loadReadmeConfig.ts
|
|
38525
|
-
var
|
|
39455
|
+
var import_promises10 = require("fs/promises");
|
|
38526
39456
|
async function loadReadmeConfig({
|
|
38527
39457
|
absolutePathToConfig
|
|
38528
39458
|
}) {
|
|
38529
|
-
const rawContents = await (0,
|
|
39459
|
+
const rawContents = await (0, import_promises10.readFile)(absolutePathToConfig, "utf8");
|
|
38530
39460
|
return JSON.parse(rawContents);
|
|
38531
39461
|
}
|
|
38532
39462
|
|
|
38533
39463
|
// src/configuration/loadReferenceConfig.ts
|
|
38534
|
-
var
|
|
39464
|
+
var import_promises11 = require("fs/promises");
|
|
38535
39465
|
async function loadReferenceConfig({
|
|
38536
39466
|
absolutePathToConfig
|
|
38537
39467
|
}) {
|
|
38538
|
-
const rawContents = await (0,
|
|
39468
|
+
const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
|
|
38539
39469
|
return JSON.parse(rawContents);
|
|
38540
39470
|
}
|
|
38541
39471
|
|
|
@@ -38552,7 +39482,7 @@ var consolePipelineLogger = {
|
|
|
38552
39482
|
};
|
|
38553
39483
|
|
|
38554
39484
|
// src/pipeline/PostGenerationPipeline.ts
|
|
38555
|
-
var
|
|
39485
|
+
var import_child_process6 = require("child_process");
|
|
38556
39486
|
|
|
38557
39487
|
// src/pipeline/github/constants.ts
|
|
38558
39488
|
var FERN_BOT_NAME2 = "fern-api";
|
|
@@ -38560,10 +39490,10 @@ var FERN_BOT_EMAIL2 = "115122769+fern-api[bot]@users.noreply.github.com";
|
|
|
38560
39490
|
var FERN_BOT_LOGIN2 = "fern-api[bot]";
|
|
38561
39491
|
|
|
38562
39492
|
// src/pipeline/steps/AutoVersionStep.ts
|
|
38563
|
-
var
|
|
39493
|
+
var import_child_process5 = require("child_process");
|
|
38564
39494
|
var import_fs14 = require("fs");
|
|
38565
|
-
var
|
|
38566
|
-
var
|
|
39495
|
+
var import_promises13 = require("fs/promises");
|
|
39496
|
+
var import_path19 = require("path");
|
|
38567
39497
|
|
|
38568
39498
|
// ../commons/logging-execa/lib/loggingExeca.js
|
|
38569
39499
|
var import_execa = __toESM(require_execa(), 1);
|
|
@@ -38622,8 +39552,8 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
|
|
|
38622
39552
|
|
|
38623
39553
|
// src/autoversion/AutoVersioningService.ts
|
|
38624
39554
|
var import_fs13 = require("fs");
|
|
38625
|
-
var
|
|
38626
|
-
var
|
|
39555
|
+
var import_promises12 = require("fs/promises");
|
|
39556
|
+
var import_path18 = require("path");
|
|
38627
39557
|
var import_semver = __toESM(require_semver2());
|
|
38628
39558
|
var AutoVersioningException = class extends Error {
|
|
38629
39559
|
/**
|
|
@@ -39310,7 +40240,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39310
40240
|
);
|
|
39311
40241
|
return;
|
|
39312
40242
|
}
|
|
39313
|
-
const goModPath = (0,
|
|
40243
|
+
const goModPath = (0, import_path18.join)(workingDirectory, "go.mod");
|
|
39314
40244
|
if (!(0, import_fs13.existsSync)(goModPath)) {
|
|
39315
40245
|
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
39316
40246
|
return;
|
|
@@ -39344,7 +40274,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39344
40274
|
* Reads the module path from a go.mod file.
|
|
39345
40275
|
*/
|
|
39346
40276
|
async readGoModulePath(goModPath) {
|
|
39347
|
-
const content = await (0,
|
|
40277
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39348
40278
|
for (const line of content.split("\n")) {
|
|
39349
40279
|
const trimmed2 = line.trim();
|
|
39350
40280
|
if (trimmed2.startsWith("module ")) {
|
|
@@ -39357,7 +40287,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39357
40287
|
* Adds a version suffix to the module line in go.mod.
|
|
39358
40288
|
*/
|
|
39359
40289
|
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
39360
|
-
const content = await (0,
|
|
40290
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39361
40291
|
const lines = content.split("\n");
|
|
39362
40292
|
let changed = false;
|
|
39363
40293
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -39369,7 +40299,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39369
40299
|
}
|
|
39370
40300
|
}
|
|
39371
40301
|
if (changed) {
|
|
39372
|
-
await (0,
|
|
40302
|
+
await (0, import_promises12.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
39373
40303
|
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
39374
40304
|
}
|
|
39375
40305
|
}
|
|
@@ -39378,10 +40308,10 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39378
40308
|
*/
|
|
39379
40309
|
async walkDirectory(dir, filter2) {
|
|
39380
40310
|
const results = [];
|
|
39381
|
-
const entries2 = await (0,
|
|
40311
|
+
const entries2 = await (0, import_promises12.readdir)(dir);
|
|
39382
40312
|
for (const entry of entries2) {
|
|
39383
|
-
const fullPath = (0,
|
|
39384
|
-
const entryStat = await (0,
|
|
40313
|
+
const fullPath = (0, import_path18.join)(dir, entry);
|
|
40314
|
+
const entryStat = await (0, import_promises12.stat)(fullPath);
|
|
39385
40315
|
if (entryStat.isDirectory()) {
|
|
39386
40316
|
if (entry === ".git" || entry === "vendor") {
|
|
39387
40317
|
continue;
|
|
@@ -39398,7 +40328,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39398
40328
|
* Adds a version suffix to import paths in all .go files in the repository.
|
|
39399
40329
|
*/
|
|
39400
40330
|
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
39401
|
-
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0,
|
|
40331
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path18.extname)(filePath) === ".go");
|
|
39402
40332
|
for (const filePath of goFiles) {
|
|
39403
40333
|
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
39404
40334
|
}
|
|
@@ -39408,7 +40338,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39408
40338
|
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
39409
40339
|
*/
|
|
39410
40340
|
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
39411
|
-
const content = await (0,
|
|
40341
|
+
const content = await (0, import_promises12.readFile)(filePath, "utf-8");
|
|
39412
40342
|
const lines = content.split("\n");
|
|
39413
40343
|
let changed = false;
|
|
39414
40344
|
let inImportBlock = false;
|
|
@@ -39437,7 +40367,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39437
40367
|
}
|
|
39438
40368
|
}
|
|
39439
40369
|
if (changed) {
|
|
39440
|
-
await (0,
|
|
40370
|
+
await (0, import_promises12.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
39441
40371
|
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
39442
40372
|
}
|
|
39443
40373
|
}
|
|
@@ -39800,7 +40730,7 @@ var AutoVersionStep = class extends BaseStep {
|
|
|
39800
40730
|
}
|
|
39801
40731
|
async readVersionFromMetadata() {
|
|
39802
40732
|
try {
|
|
39803
|
-
const output = (0,
|
|
40733
|
+
const output = (0, import_child_process5.execFileSync)("git", ["show", "HEAD~1:.fern/metadata.json"], {
|
|
39804
40734
|
cwd: this.outputDir,
|
|
39805
40735
|
encoding: "utf-8",
|
|
39806
40736
|
stdio: "pipe"
|
|
@@ -39816,7 +40746,7 @@ var AutoVersionStep = class extends BaseStep {
|
|
|
39816
40746
|
return mappedMagicVersion.startsWith("v") ? `v${stripped}` : stripped;
|
|
39817
40747
|
}
|
|
39818
40748
|
gitDiff(from, to) {
|
|
39819
|
-
return (0,
|
|
40749
|
+
return (0, import_child_process5.execFileSync)("git", ["diff", from, to, "--", ".", ":(exclude).fern/metadata.json"], {
|
|
39820
40750
|
cwd: this.outputDir,
|
|
39821
40751
|
encoding: "utf-8",
|
|
39822
40752
|
stdio: "pipe",
|
|
@@ -39829,12 +40759,12 @@ var AutoVersionStep = class extends BaseStep {
|
|
|
39829
40759
|
const fullMessage = body.length > 0 ? `${COMMIT_MARKER} ${subject}
|
|
39830
40760
|
|
|
39831
40761
|
${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
39832
|
-
(0,
|
|
39833
|
-
(0,
|
|
40762
|
+
(0, import_child_process5.execFileSync)("git", ["add", "-A"], { cwd: this.outputDir, stdio: "pipe" });
|
|
40763
|
+
(0, import_child_process5.execFileSync)("git", ["commit", "--allow-empty", "-m", fullMessage], {
|
|
39834
40764
|
cwd: this.outputDir,
|
|
39835
40765
|
stdio: "pipe"
|
|
39836
40766
|
});
|
|
39837
|
-
return (0,
|
|
40767
|
+
return (0, import_child_process5.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
39838
40768
|
cwd: this.outputDir,
|
|
39839
40769
|
encoding: "utf-8",
|
|
39840
40770
|
stdio: "pipe"
|
|
@@ -39842,7 +40772,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
39842
40772
|
}
|
|
39843
40773
|
async prependChangelogEntry(params) {
|
|
39844
40774
|
const { version: version2, entry } = params;
|
|
39845
|
-
const changelogPath = (0,
|
|
40775
|
+
const changelogPath = (0, import_path19.join)(this.outputDir, "changelog.md");
|
|
39846
40776
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
39847
40777
|
const header = `## [${version2}] - ${now}
|
|
39848
40778
|
`;
|
|
@@ -39851,7 +40781,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
39851
40781
|
`;
|
|
39852
40782
|
let existing = "";
|
|
39853
40783
|
if ((0, import_fs14.existsSync)(changelogPath)) {
|
|
39854
|
-
existing = await (0,
|
|
40784
|
+
existing = await (0, import_promises13.readFile)(changelogPath, "utf-8");
|
|
39855
40785
|
}
|
|
39856
40786
|
let output;
|
|
39857
40787
|
if (existing.trim().length === 0) {
|
|
@@ -39868,7 +40798,7 @@ ${newBlock}${remainder}`;
|
|
|
39868
40798
|
} else {
|
|
39869
40799
|
output = `${newBlock}${existing}`;
|
|
39870
40800
|
}
|
|
39871
|
-
await (0,
|
|
40801
|
+
await (0, import_promises13.writeFile)(changelogPath, output, "utf-8");
|
|
39872
40802
|
}
|
|
39873
40803
|
brandMessage(message) {
|
|
39874
40804
|
if (this.config.isWhitelabel) {
|
|
@@ -40050,8 +40980,8 @@ var GenerationCommitStep = class extends BaseStep {
|
|
|
40050
40980
|
};
|
|
40051
40981
|
|
|
40052
40982
|
// src/pipeline/steps/GithubStep.ts
|
|
40053
|
-
var
|
|
40054
|
-
var
|
|
40983
|
+
var import_promises14 = require("fs/promises");
|
|
40984
|
+
var import_path20 = require("path");
|
|
40055
40985
|
|
|
40056
40986
|
// src/pipeline/github/createReplayBranch.ts
|
|
40057
40987
|
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
@@ -40668,13 +41598,13 @@ var GithubStep = class extends BaseStep {
|
|
|
40668
41598
|
}
|
|
40669
41599
|
async ensureFernignore() {
|
|
40670
41600
|
this.logger.debug("Checking for .fernignore file...");
|
|
40671
|
-
const fernignorePath = (0,
|
|
41601
|
+
const fernignorePath = (0, import_path20.join)(this.outputDir, ".fernignore");
|
|
40672
41602
|
try {
|
|
40673
|
-
await (0,
|
|
41603
|
+
await (0, import_promises14.access)(fernignorePath);
|
|
40674
41604
|
this.logger.debug(".fernignore already exists");
|
|
40675
41605
|
} catch {
|
|
40676
41606
|
this.logger.debug("Creating .fernignore file...");
|
|
40677
|
-
await (0,
|
|
41607
|
+
await (0, import_promises14.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
40678
41608
|
}
|
|
40679
41609
|
}
|
|
40680
41610
|
deriveSkipCommit(replayResult) {
|
|
@@ -40896,8 +41826,8 @@ var PostGenerationPipeline = class {
|
|
|
40896
41826
|
async run() {
|
|
40897
41827
|
if (this.config.github?.enabled) {
|
|
40898
41828
|
try {
|
|
40899
|
-
(0,
|
|
40900
|
-
(0,
|
|
41829
|
+
(0, import_child_process6.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME2], { cwd: this.config.outputDir });
|
|
41830
|
+
(0, import_child_process6.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL2], { cwd: this.config.outputDir });
|
|
40901
41831
|
} catch {
|
|
40902
41832
|
}
|
|
40903
41833
|
}
|
|
@@ -40967,7 +41897,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
40967
41897
|
const readmeConfig = await loadReadmeConfig({
|
|
40968
41898
|
absolutePathToConfig: resolve(wd, argv.config)
|
|
40969
41899
|
});
|
|
40970
|
-
const originalReadmeContent = argv.originalReadme != null ? await (0,
|
|
41900
|
+
const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises15.readFile)(argv.originalReadme, "utf8") : void 0;
|
|
40971
41901
|
await generateReadmeToStream({
|
|
40972
41902
|
originalReadmeContent,
|
|
40973
41903
|
readmeConfig,
|
|
@@ -41095,7 +42025,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
41095
42025
|
try {
|
|
41096
42026
|
const wd = cwd();
|
|
41097
42027
|
const configValue = argv.config;
|
|
41098
|
-
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0,
|
|
42028
|
+
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises15.readFile)(resolve(wd, configValue), "utf-8");
|
|
41099
42029
|
let config;
|
|
41100
42030
|
try {
|
|
41101
42031
|
config = JSON.parse(configContent);
|
|
@@ -41391,7 +42321,7 @@ async function createWriteStream(outputPath) {
|
|
|
41391
42321
|
}
|
|
41392
42322
|
async function createWriteStreamFromFile(filepath) {
|
|
41393
42323
|
if (!await doesPathExist(filepath)) {
|
|
41394
|
-
await (0,
|
|
42324
|
+
await (0, import_promises15.mkdir)(import_path21.default.dirname(filepath), { recursive: true });
|
|
41395
42325
|
}
|
|
41396
42326
|
return import_fs15.default.createWriteStream(filepath);
|
|
41397
42327
|
}
|