@fern-api/generator-cli 0.9.21 → 0.9.23
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/api.d.ts +7 -1
- package/dist/api.js +45 -13
- package/dist/cli.js +1620 -1146
- 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) {
|
|
@@ -22028,7 +22028,7 @@ var init_diff3 = __esm({
|
|
|
22028
22028
|
}
|
|
22029
22029
|
});
|
|
22030
22030
|
|
|
22031
|
-
// ../../node_modules/.pnpm/@fern-api+replay@0.14.
|
|
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");
|
|
@@ -22495,76 +22574,8 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
22495
22574
|
}
|
|
22496
22575
|
return result;
|
|
22497
22576
|
}
|
|
22498
|
-
function trimCR(line) {
|
|
22499
|
-
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
22500
|
-
}
|
|
22501
|
-
function findConflictRanges(lines) {
|
|
22502
|
-
const ranges = [];
|
|
22503
|
-
let i = 0;
|
|
22504
|
-
while (i < lines.length) {
|
|
22505
|
-
if (trimCR(lines[i]) === CONFLICT_OPENER) {
|
|
22506
|
-
let separatorIdx = -1;
|
|
22507
|
-
let j2 = i + 1;
|
|
22508
|
-
let found = false;
|
|
22509
|
-
while (j2 < lines.length) {
|
|
22510
|
-
const trimmed2 = trimCR(lines[j2]);
|
|
22511
|
-
if (trimmed2 === CONFLICT_OPENER) {
|
|
22512
|
-
break;
|
|
22513
|
-
}
|
|
22514
|
-
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
22515
|
-
separatorIdx = j2;
|
|
22516
|
-
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
22517
|
-
ranges.push({ start: i, separator: separatorIdx, end: j2 });
|
|
22518
|
-
i = j2;
|
|
22519
|
-
found = true;
|
|
22520
|
-
break;
|
|
22521
|
-
}
|
|
22522
|
-
j2++;
|
|
22523
|
-
}
|
|
22524
|
-
if (!found) {
|
|
22525
|
-
i++;
|
|
22526
|
-
continue;
|
|
22527
|
-
}
|
|
22528
|
-
}
|
|
22529
|
-
i++;
|
|
22530
|
-
}
|
|
22531
|
-
return ranges;
|
|
22532
|
-
}
|
|
22533
|
-
function stripConflictMarkers(content) {
|
|
22534
|
-
const lines = content.split(/\r?\n/);
|
|
22535
|
-
const ranges = findConflictRanges(lines);
|
|
22536
|
-
if (ranges.length === 0) {
|
|
22537
|
-
return content;
|
|
22538
|
-
}
|
|
22539
|
-
const result = [];
|
|
22540
|
-
let rangeIdx = 0;
|
|
22541
|
-
for (let i = 0; i < lines.length; i++) {
|
|
22542
|
-
if (rangeIdx < ranges.length) {
|
|
22543
|
-
const range2 = ranges[rangeIdx];
|
|
22544
|
-
if (i === range2.start) {
|
|
22545
|
-
continue;
|
|
22546
|
-
}
|
|
22547
|
-
if (i > range2.start && i < range2.separator) {
|
|
22548
|
-
result.push(lines[i]);
|
|
22549
|
-
continue;
|
|
22550
|
-
}
|
|
22551
|
-
if (i === range2.separator) {
|
|
22552
|
-
continue;
|
|
22553
|
-
}
|
|
22554
|
-
if (i > range2.separator && i < range2.end) {
|
|
22555
|
-
continue;
|
|
22556
|
-
}
|
|
22557
|
-
if (i === range2.end) {
|
|
22558
|
-
rangeIdx++;
|
|
22559
|
-
continue;
|
|
22560
|
-
}
|
|
22561
|
-
}
|
|
22562
|
-
result.push(lines[i]);
|
|
22563
|
-
}
|
|
22564
|
-
return result.join("\n");
|
|
22565
|
-
}
|
|
22566
22577
|
function isBinaryFile(filePath) {
|
|
22567
|
-
const ext2 = (0,
|
|
22578
|
+
const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
|
|
22568
22579
|
return BINARY_EXTENSIONS.has(ext2);
|
|
22569
22580
|
}
|
|
22570
22581
|
function computeDroppedContextLinesForFile(base, theirs, finalContent) {
|
|
@@ -22595,6 +22606,37 @@ function isDiffLineForFile(diffLine, filePath) {
|
|
|
22595
22606
|
const match2 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
22596
22607
|
return match2 !== null && match2[1] === filePath;
|
|
22597
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;
|
|
22628
|
+
}
|
|
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;
|
|
22634
|
+
}
|
|
22635
|
+
}
|
|
22636
|
+
}
|
|
22637
|
+
flush2();
|
|
22638
|
+
return results;
|
|
22639
|
+
}
|
|
22598
22640
|
function normalizeHunkBody(hunk) {
|
|
22599
22641
|
let contextC = 0;
|
|
22600
22642
|
let removeC = 0;
|
|
@@ -22644,13 +22686,13 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
|
|
|
22644
22686
|
const hunksRaw = parseHunks(fileDiff).map(normalizeHunkBody);
|
|
22645
22687
|
const hunks = hunksRaw.filter((h) => {
|
|
22646
22688
|
if (h.lines.length === 0) return false;
|
|
22647
|
-
let ctx = 0,
|
|
22689
|
+
let ctx = 0, rm4 = 0, add = 0;
|
|
22648
22690
|
for (const ln of h.lines) {
|
|
22649
22691
|
if (ln.type === "context") ctx++;
|
|
22650
|
-
else if (ln.type === "remove")
|
|
22692
|
+
else if (ln.type === "remove") rm4++;
|
|
22651
22693
|
else if (ln.type === "add") add++;
|
|
22652
22694
|
}
|
|
22653
|
-
return ctx +
|
|
22695
|
+
return ctx + rm4 === h.oldCount && ctx + add === h.newCount;
|
|
22654
22696
|
});
|
|
22655
22697
|
if (hunks.length === 0) {
|
|
22656
22698
|
if (hunksRaw.length > 0) unlocatableFiles.push(file);
|
|
@@ -22799,17 +22841,17 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
22799
22841
|
}
|
|
22800
22842
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
22801
22843
|
if (beforeContent === afterContent) return "";
|
|
22802
|
-
const tmp3 = await (0,
|
|
22844
|
+
const tmp3 = await (0, import_promises6.mkdtemp)((0, import_path11.join)((0, import_os3.tmpdir)(), "fern-replay-pp-"));
|
|
22803
22845
|
try {
|
|
22804
|
-
const beforePath = (0,
|
|
22805
|
-
const afterPath = (0,
|
|
22806
|
-
await (0,
|
|
22807
|
-
await (0,
|
|
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");
|
|
22808
22850
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
22809
22851
|
if (!raw.trim()) return "";
|
|
22810
22852
|
return rewriteDiffHeaders(raw, filePath);
|
|
22811
22853
|
} finally {
|
|
22812
|
-
await (0,
|
|
22854
|
+
await (0, import_promises6.rm)(tmp3, { recursive: true, force: true });
|
|
22813
22855
|
}
|
|
22814
22856
|
}
|
|
22815
22857
|
function runGitDiffNoIndex(beforePath, afterPath) {
|
|
@@ -23071,7 +23113,7 @@ function parseGitLog(log) {
|
|
|
23071
23113
|
});
|
|
23072
23114
|
}
|
|
23073
23115
|
function ensureFernignoreEntries(outputDir) {
|
|
23074
|
-
const fernignorePath = (0,
|
|
23116
|
+
const fernignorePath = (0, import_path13.join)(outputDir, ".fernignore");
|
|
23075
23117
|
let content = "";
|
|
23076
23118
|
if ((0, import_fs9.existsSync)(fernignorePath)) {
|
|
23077
23119
|
content = (0, import_fs9.readFileSync)(fernignorePath, "utf-8");
|
|
@@ -23094,7 +23136,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
23094
23136
|
return true;
|
|
23095
23137
|
}
|
|
23096
23138
|
function ensureGitattributesEntries(outputDir) {
|
|
23097
|
-
const gitattributesPath = (0,
|
|
23139
|
+
const gitattributesPath = (0, import_path13.join)(outputDir, ".gitattributes");
|
|
23098
23140
|
let content = "";
|
|
23099
23141
|
if ((0, import_fs9.existsSync)(gitattributesPath)) {
|
|
23100
23142
|
content = (0, import_fs9.readFileSync)(gitattributesPath, "utf-8");
|
|
@@ -23317,36 +23359,78 @@ async function resolve7(outputDir, options) {
|
|
|
23317
23359
|
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
23318
23360
|
const warnings = [];
|
|
23319
23361
|
let patchesResolved = 0;
|
|
23362
|
+
const pass1 = [];
|
|
23320
23363
|
for (const patch of patchesToCommit) {
|
|
23321
23364
|
const result = await computePerPatchDiffWithFallback(
|
|
23322
23365
|
patch,
|
|
23323
23366
|
(f2) => git.showFile(currentGen, f2),
|
|
23324
|
-
(f2) => (0,
|
|
23367
|
+
(f2) => (0, import_promises7.readFile)((0, import_path14.join)(outputDir, f2), "utf-8").catch(() => null),
|
|
23325
23368
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
23326
23369
|
);
|
|
23327
23370
|
if (result === null) {
|
|
23328
23371
|
warnings.push(
|
|
23329
23372
|
`Patch ${patch.id}: cumulative-diff fallback failed for unlocatable files; preserving patch unchanged`
|
|
23330
23373
|
);
|
|
23374
|
+
pass1.push({ kind: "skip" });
|
|
23331
23375
|
continue;
|
|
23332
23376
|
}
|
|
23333
23377
|
const diff = result.diff;
|
|
23334
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];
|
|
23335
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
|
+
);
|
|
23336
23411
|
continue;
|
|
23337
23412
|
}
|
|
23338
|
-
if (
|
|
23413
|
+
if (r2.hadFallback) {
|
|
23339
23414
|
warnings.push(
|
|
23340
|
-
`Patch ${patch.id}: ${
|
|
23415
|
+
`Patch ${patch.id}: ${r2.fallbackFiles.join(", ")} could not be anchored; cumulative-diff fallback applied for those files`
|
|
23341
23416
|
);
|
|
23342
23417
|
}
|
|
23343
|
-
const
|
|
23344
|
-
const
|
|
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
|
+
}
|
|
23345
23428
|
lockManager.markPatchResolved(patch.id, {
|
|
23346
|
-
patch_content: diff,
|
|
23347
|
-
content_hash:
|
|
23429
|
+
patch_content: r2.diff,
|
|
23430
|
+
content_hash: r2.hash,
|
|
23348
23431
|
base_generation: currentGen,
|
|
23349
|
-
files:
|
|
23432
|
+
files: filesForSnapshot,
|
|
23433
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
23350
23434
|
});
|
|
23351
23435
|
patchesResolved++;
|
|
23352
23436
|
}
|
|
@@ -23446,39 +23530,43 @@ function status(outputDir) {
|
|
|
23446
23530
|
excludePatterns
|
|
23447
23531
|
};
|
|
23448
23532
|
}
|
|
23449
|
-
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;
|
|
23450
23534
|
var init_dist3 = __esm({
|
|
23451
|
-
"../../node_modules/.pnpm/@fern-api+replay@0.14.
|
|
23535
|
+
"../../node_modules/.pnpm/@fern-api+replay@0.14.1/node_modules/@fern-api/replay/dist/index.js"() {
|
|
23452
23536
|
"use strict";
|
|
23453
23537
|
init_esm4();
|
|
23454
|
-
import_fs6 = require("fs");
|
|
23455
|
-
import_path7 = require("path");
|
|
23456
|
-
import_yaml = __toESM(require_dist3(), 1);
|
|
23457
|
-
import_crypto = require("crypto");
|
|
23458
|
-
init_diff3();
|
|
23459
23538
|
import_promises3 = require("fs/promises");
|
|
23460
23539
|
import_os = require("os");
|
|
23540
|
+
import_path7 = require("path");
|
|
23541
|
+
import_fs6 = require("fs");
|
|
23461
23542
|
import_path8 = require("path");
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
import_path9 = require("path");
|
|
23465
|
-
init_esm3();
|
|
23543
|
+
import_yaml = __toESM(require_dist3(), 1);
|
|
23544
|
+
import_crypto = require("crypto");
|
|
23466
23545
|
import_promises4 = require("fs/promises");
|
|
23467
23546
|
import_os2 = require("os");
|
|
23547
|
+
import_path9 = require("path");
|
|
23548
|
+
init_esm3();
|
|
23549
|
+
init_diff3();
|
|
23550
|
+
import_fs7 = require("fs");
|
|
23551
|
+
import_promises5 = require("fs/promises");
|
|
23468
23552
|
import_path10 = require("path");
|
|
23553
|
+
init_esm3();
|
|
23554
|
+
import_promises6 = require("fs/promises");
|
|
23555
|
+
import_os3 = require("os");
|
|
23556
|
+
import_path11 = require("path");
|
|
23469
23557
|
import_child_process3 = require("child_process");
|
|
23470
23558
|
import_crypto2 = require("crypto");
|
|
23471
23559
|
import_fs8 = require("fs");
|
|
23472
|
-
|
|
23560
|
+
import_path12 = require("path");
|
|
23473
23561
|
init_esm3();
|
|
23474
23562
|
import_yaml2 = __toESM(require_dist3(), 1);
|
|
23475
23563
|
import_crypto3 = require("crypto");
|
|
23476
23564
|
import_fs9 = require("fs");
|
|
23477
|
-
|
|
23565
|
+
import_path13 = require("path");
|
|
23478
23566
|
init_esm3();
|
|
23479
23567
|
import_fs10 = require("fs");
|
|
23480
|
-
|
|
23481
|
-
|
|
23568
|
+
import_promises7 = require("fs/promises");
|
|
23569
|
+
import_path14 = require("path");
|
|
23482
23570
|
__defProp3 = Object.defineProperty;
|
|
23483
23571
|
__getOwnPropNames3 = Object.getOwnPropertyNames;
|
|
23484
23572
|
__esm3 = (fn, res) => function __init() {
|
|
@@ -23634,6 +23722,16 @@ var init_dist3 = __esm({
|
|
|
23634
23722
|
"use strict";
|
|
23635
23723
|
}
|
|
23636
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
|
+
});
|
|
23637
23735
|
CREDENTIAL_PATTERNS = [
|
|
23638
23736
|
{ name: "PRIVATE KEY block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
23639
23737
|
{ name: "OPENSSH key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
|
|
@@ -23673,10 +23771,10 @@ var init_dist3 = __esm({
|
|
|
23673
23771
|
this.outputDir = outputDir;
|
|
23674
23772
|
}
|
|
23675
23773
|
get lockfilePath() {
|
|
23676
|
-
return (0,
|
|
23774
|
+
return (0, import_path8.join)(this.outputDir, ".fern", "replay.lock");
|
|
23677
23775
|
}
|
|
23678
23776
|
get customizationsPath() {
|
|
23679
|
-
return (0,
|
|
23777
|
+
return (0, import_path8.join)(this.outputDir, ".fern", "replay.yml");
|
|
23680
23778
|
}
|
|
23681
23779
|
exists() {
|
|
23682
23780
|
return (0, import_fs6.existsSync)(this.lockfilePath);
|
|
@@ -23720,7 +23818,17 @@ var init_dist3 = __esm({
|
|
|
23720
23818
|
this.warnings.length = 0;
|
|
23721
23819
|
const cleanPatches = [];
|
|
23722
23820
|
for (const patch of this.lock.patches) {
|
|
23723
|
-
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))];
|
|
23724
23832
|
const sensitiveFiles = patch.files.filter((f2) => isSensitiveFile(f2));
|
|
23725
23833
|
if (credentialMatches.length > 0 || sensitiveFiles.length > 0) {
|
|
23726
23834
|
const reasons = [];
|
|
@@ -23738,7 +23846,7 @@ var init_dist3 = __esm({
|
|
|
23738
23846
|
}
|
|
23739
23847
|
}
|
|
23740
23848
|
this.lock.patches = cleanPatches;
|
|
23741
|
-
const dir = (0,
|
|
23849
|
+
const dir = (0, import_path8.dirname)(this.lockfilePath);
|
|
23742
23850
|
if (!(0, import_fs6.existsSync)(dir)) {
|
|
23743
23851
|
(0, import_fs6.mkdirSync)(dir, { recursive: true });
|
|
23744
23852
|
}
|
|
@@ -23838,716 +23946,210 @@ var init_dist3 = __esm({
|
|
|
23838
23946
|
}
|
|
23839
23947
|
}
|
|
23840
23948
|
};
|
|
23841
|
-
|
|
23842
|
-
|
|
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 {
|
|
23843
24002
|
git;
|
|
23844
24003
|
lockManager;
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
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) {
|
|
23848
24042
|
this.git = git;
|
|
23849
24043
|
this.lockManager = lockManager;
|
|
23850
|
-
this.
|
|
23851
|
-
}
|
|
23852
|
-
async detectNewPatches() {
|
|
23853
|
-
const lock = this.lockManager.read();
|
|
23854
|
-
const lastGen = this.getLastGeneration(lock);
|
|
23855
|
-
if (!lastGen) {
|
|
23856
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23857
|
-
}
|
|
23858
|
-
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
23859
|
-
if (!exists2) {
|
|
23860
|
-
this.warnings.push(
|
|
23861
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
23862
|
-
);
|
|
23863
|
-
return this.detectPatchesViaTreeDiff(
|
|
23864
|
-
lastGen,
|
|
23865
|
-
/* commitKnownMissing */
|
|
23866
|
-
true
|
|
23867
|
-
);
|
|
23868
|
-
}
|
|
23869
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
23870
|
-
if (!isAncestor) {
|
|
23871
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
23872
|
-
}
|
|
23873
|
-
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
24044
|
+
this.outputDir = outputDir;
|
|
23874
24045
|
}
|
|
23875
24046
|
/**
|
|
23876
|
-
*
|
|
23877
|
-
*
|
|
23878
|
-
*
|
|
23879
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
23880
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
23881
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
23882
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
23883
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
23884
|
-
* generation) get bundled as customer customizations.
|
|
23885
|
-
*
|
|
23886
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
23887
|
-
* (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).
|
|
23888
24050
|
*/
|
|
23889
|
-
async
|
|
23890
|
-
|
|
24051
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
24052
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24053
|
+
if (gen) return gen;
|
|
23891
24054
|
try {
|
|
23892
|
-
|
|
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
|
+
};
|
|
23893
24063
|
} catch {
|
|
24064
|
+
return void 0;
|
|
23894
24065
|
}
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
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 });
|
|
23904
24082
|
}
|
|
23905
|
-
return
|
|
24083
|
+
return snapshot;
|
|
23906
24084
|
}
|
|
23907
24085
|
/**
|
|
23908
|
-
*
|
|
23909
|
-
*
|
|
23910
|
-
* (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
|
|
23911
24088
|
*
|
|
23912
|
-
*
|
|
23913
|
-
*
|
|
23914
|
-
*
|
|
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.
|
|
23915
24101
|
*/
|
|
23916
|
-
async
|
|
23917
|
-
const
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23923
|
-
|
|
23924
|
-
]);
|
|
23925
|
-
if (!log.trim()) {
|
|
23926
|
-
return { patches: [], revertedPatchIds: [] };
|
|
23927
|
-
}
|
|
23928
|
-
const commits = this.parseGitLog(log);
|
|
23929
|
-
const newPatches = [];
|
|
23930
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
23931
|
-
for (const commit of commits) {
|
|
23932
|
-
if (isGenerationCommit(commit)) {
|
|
23933
|
-
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);
|
|
23934
24110
|
}
|
|
23935
|
-
|
|
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"
|
|
24123
|
+
});
|
|
23936
24124
|
continue;
|
|
23937
24125
|
}
|
|
23938
|
-
const
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23951
|
-
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
|
|
23955
|
-
|
|
23956
|
-
|
|
23957
|
-
|
|
23958
|
-
|
|
23959
|
-
|
|
23960
|
-
|
|
23961
|
-
|
|
23962
|
-
|
|
23963
|
-
|
|
23964
|
-
|
|
23965
|
-
const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
23966
|
-
const sensitiveFiles = allFiles.filter((f2) => isSensitiveFile(f2));
|
|
23967
|
-
const files = allFiles.filter((f2) => !isSensitiveFile(f2));
|
|
23968
|
-
if (sensitiveFiles.length > 0) {
|
|
23969
|
-
this.warnings.push(
|
|
23970
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
23971
|
-
);
|
|
23972
|
-
if (files.length > 0) {
|
|
23973
|
-
const parentSha = parents[0];
|
|
23974
|
-
if (parentSha) {
|
|
23975
|
-
try {
|
|
23976
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
23977
|
-
} catch {
|
|
23978
|
-
continue;
|
|
23979
|
-
}
|
|
23980
|
-
if (!patchContent.trim()) continue;
|
|
23981
|
-
contentHash = this.computeContentHash(patchContent);
|
|
23982
|
-
}
|
|
23983
|
-
}
|
|
23984
|
-
}
|
|
23985
|
-
if (files.length === 0) {
|
|
23986
|
-
continue;
|
|
23987
|
-
}
|
|
23988
|
-
newPatches.push({
|
|
23989
|
-
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
23990
|
-
content_hash: contentHash,
|
|
23991
|
-
original_commit: commit.sha,
|
|
23992
|
-
original_message: commit.message,
|
|
23993
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
23994
|
-
base_generation: lastGen.commit_sha,
|
|
23995
|
-
files,
|
|
23996
|
-
patch_content: patchContent,
|
|
23997
|
-
...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
|
|
23998
|
-
});
|
|
23999
|
-
}
|
|
24000
|
-
const survivingPatches = await this.collapseNetZeroFiles(newPatches);
|
|
24001
|
-
survivingPatches.reverse();
|
|
24002
|
-
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
24003
|
-
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
24004
|
-
for (let i = 0; i < survivingPatches.length; i++) {
|
|
24005
|
-
const patch = survivingPatches[i];
|
|
24006
|
-
if (!isRevertCommit(patch.original_message)) continue;
|
|
24007
|
-
let body = "";
|
|
24008
|
-
try {
|
|
24009
|
-
body = await this.git.getCommitBody(patch.original_commit);
|
|
24010
|
-
} catch {
|
|
24011
|
-
}
|
|
24012
|
-
const revertedSha = parseRevertedSha(body);
|
|
24013
|
-
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
24014
|
-
let matchedExisting = false;
|
|
24015
|
-
if (revertedSha) {
|
|
24016
|
-
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
24017
|
-
if (existing) {
|
|
24018
|
-
revertedPatchIdSet.add(existing.id);
|
|
24019
|
-
revertIndicesToRemove.add(i);
|
|
24020
|
-
matchedExisting = true;
|
|
24021
|
-
}
|
|
24022
|
-
}
|
|
24023
|
-
if (!matchedExisting && revertedMessage) {
|
|
24024
|
-
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
24025
|
-
if (existing) {
|
|
24026
|
-
revertedPatchIdSet.add(existing.id);
|
|
24027
|
-
revertIndicesToRemove.add(i);
|
|
24028
|
-
matchedExisting = true;
|
|
24029
|
-
}
|
|
24030
|
-
}
|
|
24031
|
-
if (matchedExisting) continue;
|
|
24032
|
-
let matchedNew = false;
|
|
24033
|
-
if (revertedSha) {
|
|
24034
|
-
const idx = survivingPatches.findIndex(
|
|
24035
|
-
(p, j2) => j2 !== i && !revertIndicesToRemove.has(j2) && p.original_commit === revertedSha
|
|
24036
|
-
);
|
|
24037
|
-
if (idx !== -1) {
|
|
24038
|
-
revertIndicesToRemove.add(i);
|
|
24039
|
-
revertIndicesToRemove.add(idx);
|
|
24040
|
-
matchedNew = true;
|
|
24041
|
-
}
|
|
24042
|
-
}
|
|
24043
|
-
if (!matchedNew && revertedMessage) {
|
|
24044
|
-
const idx = survivingPatches.findIndex(
|
|
24045
|
-
(p, j2) => j2 !== i && !revertIndicesToRemove.has(j2) && p.original_message === revertedMessage
|
|
24046
|
-
);
|
|
24047
|
-
if (idx !== -1) {
|
|
24048
|
-
revertIndicesToRemove.add(i);
|
|
24049
|
-
revertIndicesToRemove.add(idx);
|
|
24050
|
-
}
|
|
24051
|
-
}
|
|
24052
|
-
if (!matchedExisting && !matchedNew) {
|
|
24053
|
-
revertIndicesToRemove.add(i);
|
|
24054
|
-
}
|
|
24055
|
-
}
|
|
24056
|
-
const filteredPatches = survivingPatches.filter((_2, i) => !revertIndicesToRemove.has(i));
|
|
24057
|
-
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
24058
|
-
}
|
|
24059
|
-
/**
|
|
24060
|
-
* Compute content hash for deduplication.
|
|
24061
|
-
*
|
|
24062
|
-
* Produces a format-agnostic hash so both `git format-patch` output
|
|
24063
|
-
* (email-wrapped) and plain `git diff` output hash to the same value
|
|
24064
|
-
* when the underlying diff hunks are identical.
|
|
24065
|
-
*
|
|
24066
|
-
* Normalization:
|
|
24067
|
-
* 1. Strip email wrapper: everything before the first `diff --git` line
|
|
24068
|
-
* (From:, Subject:, Date:, diffstat, blank separators).
|
|
24069
|
-
* 2. Strip `index` lines (blob SHA pairs change across rebases).
|
|
24070
|
-
* 3. Strip trailing git version marker (`-- \n<version>`).
|
|
24071
|
-
*
|
|
24072
|
-
* This ensures content hashes match across the no-patches and normal-
|
|
24073
|
-
* regeneration flows, which store formatPatch and git-diff formats
|
|
24074
|
-
* respectively (FER-9850, D5-D9).
|
|
24075
|
-
*/
|
|
24076
|
-
computeContentHash(patchContent) {
|
|
24077
|
-
const lines = patchContent.split("\n");
|
|
24078
|
-
const diffStart = lines.findIndex((l) => l.startsWith("diff --git "));
|
|
24079
|
-
const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
|
|
24080
|
-
const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
|
|
24081
|
-
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
24082
|
-
}
|
|
24083
|
-
/**
|
|
24084
|
-
* FER-9805 — Drop patches whose files were transiently created and then
|
|
24085
|
-
* deleted within the current linear detection window, and are absent from
|
|
24086
|
-
* HEAD at the end of the window.
|
|
24087
|
-
*
|
|
24088
|
-
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
24089
|
-
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
24090
|
-
* linear history each commit becomes its own patch, so a file that was
|
|
24091
|
-
* briefly added and later removed survives as a create+delete pair whose
|
|
24092
|
-
* cumulative diff is empty. We drop such patches before they reach the
|
|
24093
|
-
* lockfile.
|
|
24094
|
-
*
|
|
24095
|
-
* A file is "transient" when:
|
|
24096
|
-
* 1. some patch in the window sets `new file mode` for it (a creation), AND
|
|
24097
|
-
* 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
|
|
24098
|
-
* 3. it is absent from HEAD's tree.
|
|
24099
|
-
*
|
|
24100
|
-
* The HEAD guard (#3) prevents false positives when a file is deleted
|
|
24101
|
-
* and then recreated with different content — the cumulative diff of
|
|
24102
|
-
* such a pair is non-empty and must be preserved.
|
|
24103
|
-
*
|
|
24104
|
-
* This only drops patches whose `files` are a subset of the transient
|
|
24105
|
-
* set. A partial-transient patch (one that touches a transient file
|
|
24106
|
-
* alongside a persistent one) is left unmodified; surgical stripping of
|
|
24107
|
-
* single files from a multi-file patch would require a follow-up that
|
|
24108
|
-
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
24109
|
-
* current failing test exercises this, and leaving the patch intact
|
|
24110
|
-
* preserves today's behaviour. TODO(FER-9805): revisit if a future
|
|
24111
|
-
* adversarial test demands per-file stripping.
|
|
24112
|
-
*/
|
|
24113
|
-
async collapseNetZeroFiles(patches) {
|
|
24114
|
-
if (patches.length === 0) return patches;
|
|
24115
|
-
const stateByFile = /* @__PURE__ */ new Map();
|
|
24116
|
-
for (const patch of patches) {
|
|
24117
|
-
for (const header of parsePatchFileHeaders(patch.patch_content)) {
|
|
24118
|
-
if (!header.isCreate && !header.isDelete) continue;
|
|
24119
|
-
const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
|
|
24120
|
-
if (header.isCreate) prior.created = true;
|
|
24121
|
-
if (header.isDelete) prior.deleted = true;
|
|
24122
|
-
stateByFile.set(header.path, prior);
|
|
24123
|
-
}
|
|
24124
|
-
}
|
|
24125
|
-
let anyCandidate = false;
|
|
24126
|
-
for (const state of stateByFile.values()) {
|
|
24127
|
-
if (state.created && state.deleted) {
|
|
24128
|
-
anyCandidate = true;
|
|
24129
|
-
break;
|
|
24130
|
-
}
|
|
24131
|
-
}
|
|
24132
|
-
if (!anyCandidate) return patches;
|
|
24133
|
-
const headFiles = await this.listHeadFiles();
|
|
24134
|
-
const transient = /* @__PURE__ */ new Set();
|
|
24135
|
-
for (const [file, state] of stateByFile) {
|
|
24136
|
-
if (state.created && state.deleted && !headFiles.has(file)) {
|
|
24137
|
-
transient.add(file);
|
|
24138
|
-
}
|
|
24139
|
-
}
|
|
24140
|
-
if (transient.size === 0) return patches;
|
|
24141
|
-
return patches.filter((patch) => !patch.files.every((f2) => transient.has(f2)));
|
|
24142
|
-
}
|
|
24143
|
-
/**
|
|
24144
|
-
* List every tracked path at HEAD (one `git ls-tree -r` invocation).
|
|
24145
|
-
* Returns an empty Set if HEAD is unborn or the invocation fails — the
|
|
24146
|
-
* caller then treats every candidate as "not in HEAD" and may collapse
|
|
24147
|
-
* more aggressively. On typical SDK repos this is a single sub-10ms call.
|
|
24148
|
-
*/
|
|
24149
|
-
async listHeadFiles() {
|
|
24150
|
-
try {
|
|
24151
|
-
const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
|
|
24152
|
-
return new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
24153
|
-
} catch {
|
|
24154
|
-
return /* @__PURE__ */ new Set();
|
|
24155
|
-
}
|
|
24156
|
-
}
|
|
24157
|
-
/**
|
|
24158
|
-
* Create a single composite patch for a merge commit by diffing the merge
|
|
24159
|
-
* commit against its first parent. This captures the net change of the
|
|
24160
|
-
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
24161
|
-
* zero-net-change ghost conflicts.
|
|
24162
|
-
*/
|
|
24163
|
-
async createMergeCompositePatch(input) {
|
|
24164
|
-
const { mergeCommit, firstParent, lastGen, lock } = input;
|
|
24165
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
24166
|
-
const filesOutput = await this.git.exec([
|
|
24167
|
-
"diff",
|
|
24168
|
-
"--name-only",
|
|
24169
|
-
firstParent,
|
|
24170
|
-
mergeCommit.sha
|
|
24171
|
-
]);
|
|
24172
|
-
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
24173
|
-
const sensitiveMergeFiles = allMergeFiles.filter((f2) => isSensitiveFile(f2));
|
|
24174
|
-
const files = allMergeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
24175
|
-
if (sensitiveMergeFiles.length > 0) {
|
|
24176
|
-
this.warnings.push(
|
|
24177
|
-
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
24178
|
-
);
|
|
24179
|
-
}
|
|
24180
|
-
if (files.length === 0) return null;
|
|
24181
|
-
const diff = await this.git.exec([
|
|
24182
|
-
"diff",
|
|
24183
|
-
firstParent,
|
|
24184
|
-
mergeCommit.sha,
|
|
24185
|
-
"--",
|
|
24186
|
-
...files
|
|
24187
|
-
]);
|
|
24188
|
-
if (!diff.trim()) return null;
|
|
24189
|
-
const contentHash = this.computeContentHash(diff);
|
|
24190
|
-
if (lock.patches.some((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
24191
|
-
return null;
|
|
24192
|
-
}
|
|
24193
|
-
return {
|
|
24194
|
-
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
24195
|
-
content_hash: contentHash,
|
|
24196
|
-
original_commit: mergeCommit.sha,
|
|
24197
|
-
original_message: mergeCommit.message,
|
|
24198
|
-
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
24199
|
-
base_generation: lastGen.commit_sha,
|
|
24200
|
-
files,
|
|
24201
|
-
patch_content: diff,
|
|
24202
|
-
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
24203
|
-
};
|
|
24204
|
-
}
|
|
24205
|
-
/**
|
|
24206
|
-
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
24207
|
-
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
24208
|
-
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
24209
|
-
*/
|
|
24210
|
-
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
24211
|
-
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
24212
|
-
if (!diffBase) {
|
|
24213
|
-
return this.detectPatchesViaCommitScan();
|
|
24214
|
-
}
|
|
24215
|
-
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
24216
|
-
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
24217
|
-
const sensitiveTreeFiles = allTreeFiles.filter((f2) => isSensitiveFile(f2));
|
|
24218
|
-
const files = allTreeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
24219
|
-
if (sensitiveTreeFiles.length > 0) {
|
|
24220
|
-
this.warnings.push(
|
|
24221
|
-
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
24222
|
-
);
|
|
24223
|
-
}
|
|
24224
|
-
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
24225
|
-
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
24226
|
-
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
24227
|
-
const contentHash = this.computeContentHash(diff);
|
|
24228
|
-
const lock = this.lockManager.read();
|
|
24229
|
-
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
24230
|
-
return { patches: [], revertedPatchIds: [] };
|
|
24231
|
-
}
|
|
24232
|
-
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
24233
|
-
const compositePatch = {
|
|
24234
|
-
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
24235
|
-
content_hash: contentHash,
|
|
24236
|
-
original_commit: headSha,
|
|
24237
|
-
original_message: "Customer customizations (composite)",
|
|
24238
|
-
original_author: "composite",
|
|
24239
|
-
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
24240
|
-
// reference to find base file content. diffBase may be the tree_hash.
|
|
24241
|
-
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
24242
|
-
files,
|
|
24243
|
-
patch_content: diff,
|
|
24244
|
-
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
24245
|
-
};
|
|
24246
|
-
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
24247
|
-
}
|
|
24248
|
-
/**
|
|
24249
|
-
* Last-resort detection when both generation commit and tree are unreachable.
|
|
24250
|
-
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
24251
|
-
* skips creation-only commits (squashed history after force push).
|
|
24252
|
-
*
|
|
24253
|
-
* Detected patches use the commit's parent as base_generation so the applicator
|
|
24254
|
-
* can find base file content from the parent's tree (which IS reachable).
|
|
24255
|
-
*/
|
|
24256
|
-
async detectPatchesViaCommitScan() {
|
|
24257
|
-
const lock = this.lockManager.read();
|
|
24258
|
-
const log = await this.git.exec([
|
|
24259
|
-
"log",
|
|
24260
|
-
"--max-count=200",
|
|
24261
|
-
"--format=%H%x00%an%x00%ae%x00%s",
|
|
24262
|
-
"HEAD",
|
|
24263
|
-
"--",
|
|
24264
|
-
this.sdkOutputDir
|
|
24265
|
-
]);
|
|
24266
|
-
if (!log.trim()) {
|
|
24267
|
-
return { patches: [], revertedPatchIds: [] };
|
|
24268
|
-
}
|
|
24269
|
-
const commits = this.parseGitLog(log);
|
|
24270
|
-
const newPatches = [];
|
|
24271
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
24272
|
-
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
24273
|
-
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
24274
|
-
for (const commit of commits) {
|
|
24275
|
-
if (isGenerationCommit(commit)) {
|
|
24276
|
-
continue;
|
|
24277
|
-
}
|
|
24278
|
-
const parents = await this.git.getCommitParents(commit.sha);
|
|
24279
|
-
if (parents.length > 1) {
|
|
24280
|
-
continue;
|
|
24281
|
-
}
|
|
24282
|
-
if (existingCommits.has(commit.sha)) {
|
|
24283
|
-
continue;
|
|
24284
|
-
}
|
|
24285
|
-
let patchContent;
|
|
24286
|
-
try {
|
|
24287
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
24288
|
-
} catch {
|
|
24289
|
-
continue;
|
|
24290
|
-
}
|
|
24291
|
-
let contentHash = this.computeContentHash(patchContent);
|
|
24292
|
-
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
24293
|
-
continue;
|
|
24294
|
-
}
|
|
24295
|
-
if (this.isCreationOnlyPatch(patchContent)) {
|
|
24296
|
-
continue;
|
|
24297
|
-
}
|
|
24298
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
24299
|
-
const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2));
|
|
24300
|
-
const sensitiveScanFiles = allScanFiles.filter((f2) => isSensitiveFile(f2));
|
|
24301
|
-
const files = allScanFiles.filter((f2) => !isSensitiveFile(f2));
|
|
24302
|
-
if (sensitiveScanFiles.length > 0) {
|
|
24303
|
-
this.warnings.push(
|
|
24304
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
|
|
24305
|
-
);
|
|
24306
|
-
if (files.length > 0) {
|
|
24307
|
-
if (parents.length === 0) {
|
|
24308
|
-
continue;
|
|
24309
|
-
}
|
|
24310
|
-
try {
|
|
24311
|
-
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
24312
|
-
} catch {
|
|
24313
|
-
continue;
|
|
24314
|
-
}
|
|
24315
|
-
if (!patchContent.trim()) continue;
|
|
24316
|
-
contentHash = this.computeContentHash(patchContent);
|
|
24317
|
-
}
|
|
24318
|
-
}
|
|
24319
|
-
if (files.length === 0) {
|
|
24320
|
-
continue;
|
|
24321
|
-
}
|
|
24322
|
-
if (parents.length === 0) {
|
|
24323
|
-
continue;
|
|
24324
|
-
}
|
|
24325
|
-
const parentSha = parents[0];
|
|
24326
|
-
newPatches.push({
|
|
24327
|
-
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
24328
|
-
content_hash: contentHash,
|
|
24329
|
-
original_commit: commit.sha,
|
|
24330
|
-
original_message: commit.message,
|
|
24331
|
-
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
24332
|
-
base_generation: parentSha,
|
|
24333
|
-
files,
|
|
24334
|
-
patch_content: patchContent
|
|
24335
|
-
});
|
|
24336
|
-
}
|
|
24337
|
-
newPatches.reverse();
|
|
24338
|
-
return { patches: newPatches, revertedPatchIds: [] };
|
|
24339
|
-
}
|
|
24340
|
-
/**
|
|
24341
|
-
* Check if a format-patch consists entirely of new-file creations.
|
|
24342
|
-
* Used to identify squashed commits after force push, which create all files
|
|
24343
|
-
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
24344
|
-
*/
|
|
24345
|
-
isCreationOnlyPatch(patchContent) {
|
|
24346
|
-
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
24347
|
-
if (diffOldHeaders.length === 0) {
|
|
24348
|
-
return false;
|
|
24349
|
-
}
|
|
24350
|
-
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
24351
|
-
}
|
|
24352
|
-
/**
|
|
24353
|
-
* Resolve the best available diff base for a generation record.
|
|
24354
|
-
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
24355
|
-
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
24356
|
-
*/
|
|
24357
|
-
async resolveDiffBase(gen, commitKnownMissing) {
|
|
24358
|
-
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
24359
|
-
return gen.commit_sha;
|
|
24360
|
-
}
|
|
24361
|
-
if (await this.git.treeExists(gen.tree_hash)) {
|
|
24362
|
-
return gen.tree_hash;
|
|
24363
|
-
}
|
|
24364
|
-
return null;
|
|
24365
|
-
}
|
|
24366
|
-
parseGitLog(log) {
|
|
24367
|
-
return log.trim().split("\n").map((line) => {
|
|
24368
|
-
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
24369
|
-
return { sha, authorName, authorEmail, message };
|
|
24370
|
-
});
|
|
24371
|
-
}
|
|
24372
|
-
getLastGeneration(lock) {
|
|
24373
|
-
return lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
24374
|
-
}
|
|
24375
|
-
};
|
|
24376
|
-
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
24377
|
-
CONFLICT_SEPARATOR = "=======";
|
|
24378
|
-
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
24379
|
-
BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
24380
|
-
".png",
|
|
24381
|
-
".jpg",
|
|
24382
|
-
".jpeg",
|
|
24383
|
-
".gif",
|
|
24384
|
-
".bmp",
|
|
24385
|
-
".ico",
|
|
24386
|
-
".webp",
|
|
24387
|
-
".svg",
|
|
24388
|
-
".pdf",
|
|
24389
|
-
".doc",
|
|
24390
|
-
".docx",
|
|
24391
|
-
".xls",
|
|
24392
|
-
".xlsx",
|
|
24393
|
-
".ppt",
|
|
24394
|
-
".pptx",
|
|
24395
|
-
".zip",
|
|
24396
|
-
".gz",
|
|
24397
|
-
".tar",
|
|
24398
|
-
".bz2",
|
|
24399
|
-
".7z",
|
|
24400
|
-
".rar",
|
|
24401
|
-
".jar",
|
|
24402
|
-
".war",
|
|
24403
|
-
".ear",
|
|
24404
|
-
".class",
|
|
24405
|
-
".exe",
|
|
24406
|
-
".dll",
|
|
24407
|
-
".so",
|
|
24408
|
-
".dylib",
|
|
24409
|
-
".o",
|
|
24410
|
-
".a",
|
|
24411
|
-
".woff",
|
|
24412
|
-
".woff2",
|
|
24413
|
-
".ttf",
|
|
24414
|
-
".eot",
|
|
24415
|
-
".otf",
|
|
24416
|
-
".mp3",
|
|
24417
|
-
".mp4",
|
|
24418
|
-
".avi",
|
|
24419
|
-
".mov",
|
|
24420
|
-
".wav",
|
|
24421
|
-
".flac",
|
|
24422
|
-
".sqlite",
|
|
24423
|
-
".db",
|
|
24424
|
-
".pyc",
|
|
24425
|
-
".pyo",
|
|
24426
|
-
".DS_Store"
|
|
24427
|
-
]);
|
|
24428
|
-
ReplayApplicator = class {
|
|
24429
|
-
git;
|
|
24430
|
-
lockManager;
|
|
24431
|
-
outputDir;
|
|
24432
|
-
renameCache = /* @__PURE__ */ new Map();
|
|
24433
|
-
treeExistsCache = /* @__PURE__ */ new Map();
|
|
24434
|
-
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
24435
|
-
/**
|
|
24436
|
-
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
24437
|
-
* of `applyPatches`, read by `mergeFile` to decide:
|
|
24438
|
-
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
24439
|
-
* - whether to populate the accumulator after a conflicted merge
|
|
24440
|
-
* (resolve mode populates so subsequent patches on the same file
|
|
24441
|
-
* can use patch A's THEIRS as a structurally-correct merge base
|
|
24442
|
-
* when their diff was authored against post-A structure)
|
|
24443
|
-
* - whether to retry THEIRS reconstruction against the accumulator
|
|
24444
|
-
* when the BASE-relative reconstruction produced markers from a
|
|
24445
|
-
* cross-patch context mismatch
|
|
24446
|
-
*/
|
|
24447
|
-
currentApplyMode = "replay";
|
|
24448
|
-
constructor(git, lockManager, outputDir) {
|
|
24449
|
-
this.git = git;
|
|
24450
|
-
this.lockManager = lockManager;
|
|
24451
|
-
this.outputDir = outputDir;
|
|
24452
|
-
}
|
|
24453
|
-
/**
|
|
24454
|
-
* Resolve the GenerationRecord for a patch's base_generation.
|
|
24455
|
-
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
24456
|
-
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
24457
|
-
*/
|
|
24458
|
-
async resolveBaseGeneration(baseGeneration) {
|
|
24459
|
-
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24460
|
-
if (gen) return gen;
|
|
24461
|
-
try {
|
|
24462
|
-
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
24463
|
-
return {
|
|
24464
|
-
commit_sha: baseGeneration,
|
|
24465
|
-
tree_hash: treeHash,
|
|
24466
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24467
|
-
cli_version: "unknown",
|
|
24468
|
-
generator_versions: {}
|
|
24469
|
-
};
|
|
24470
|
-
} catch {
|
|
24471
|
-
return void 0;
|
|
24472
|
-
}
|
|
24473
|
-
}
|
|
24474
|
-
/** Reset inter-patch accumulator for a new cycle. */
|
|
24475
|
-
resetAccumulator() {
|
|
24476
|
-
this.fileTheirsAccumulator.clear();
|
|
24477
|
-
}
|
|
24478
|
-
/**
|
|
24479
|
-
* @internal Test-only.
|
|
24480
|
-
* Returns a defensive copy of the inter-patch accumulator. Used by the
|
|
24481
|
-
* adversarial test suite (FER-9791) to assert invariant I2: after every
|
|
24482
|
-
* applied patch, the accumulator has an entry for each file the patch
|
|
24483
|
-
* touched, and the entry's content matches on-disk.
|
|
24484
|
-
*/
|
|
24485
|
-
getAccumulatorSnapshot() {
|
|
24486
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
24487
|
-
for (const [path5, entry] of this.fileTheirsAccumulator) {
|
|
24488
|
-
snapshot.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
24489
|
-
}
|
|
24490
|
-
return snapshot;
|
|
24491
|
-
}
|
|
24492
|
-
/**
|
|
24493
|
-
* Apply all patches, returning results for each.
|
|
24494
|
-
* Skips patches that match exclude patterns in replay.yml
|
|
24495
|
-
*
|
|
24496
|
-
* `applyMode` controls the post-conflict marker strategy:
|
|
24497
|
-
* - `"replay"` (default): strip conflict markers from disk between
|
|
24498
|
-
* iterations whenever a later patch touches the same file. Lets the
|
|
24499
|
-
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
24500
|
-
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
24501
|
-
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
24502
|
-
* any markers that survive.
|
|
24503
|
-
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
24504
|
-
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
24505
|
-
* A's markers and B's clean writes when their regions don't overlap;
|
|
24506
|
-
* when they do overlap, the customer gets nested markers and
|
|
24507
|
-
* resolves manually. Either way they aren't silently dropped.
|
|
24508
|
-
*/
|
|
24509
|
-
async applyPatches(patches, opts) {
|
|
24510
|
-
const applyMode = opts?.applyMode ?? "replay";
|
|
24511
|
-
this.currentApplyMode = applyMode;
|
|
24512
|
-
this.resetAccumulator();
|
|
24513
|
-
const results = [];
|
|
24514
|
-
for (let i = 0; i < patches.length; i++) {
|
|
24515
|
-
const patch = patches[i];
|
|
24516
|
-
if (this.isExcluded(patch)) {
|
|
24517
|
-
results.push({
|
|
24518
|
-
patch,
|
|
24519
|
-
status: "skipped",
|
|
24520
|
-
method: "git-am"
|
|
24521
|
-
});
|
|
24522
|
-
continue;
|
|
24523
|
-
}
|
|
24524
|
-
const result = await this.applyPatchWithFallback(patch);
|
|
24525
|
-
results.push(result);
|
|
24526
|
-
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
24527
|
-
const laterFiles = /* @__PURE__ */ new Set();
|
|
24528
|
-
for (let j2 = i + 1; j2 < patches.length; j2++) {
|
|
24529
|
-
for (const f2 of patches[j2].files) {
|
|
24530
|
-
laterFiles.add(f2);
|
|
24531
|
-
}
|
|
24532
|
-
}
|
|
24533
|
-
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
24534
|
-
if (result.resolvedFiles) {
|
|
24535
|
-
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
24536
|
-
resolvedToOriginal.set(resolved, orig);
|
|
24537
|
-
}
|
|
24538
|
-
}
|
|
24539
|
-
for (const fileResult of result.fileResults) {
|
|
24540
|
-
if (fileResult.status !== "conflict") continue;
|
|
24541
|
-
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
24542
|
-
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
24543
|
-
const filePath = (0, import_path8.join)(this.outputDir, fileResult.file);
|
|
24544
|
-
try {
|
|
24545
|
-
const content = await (0, import_promises3.readFile)(filePath, "utf-8");
|
|
24546
|
-
const stripped = stripConflictMarkers(content);
|
|
24547
|
-
await (0, import_promises3.writeFile)(filePath, stripped);
|
|
24548
|
-
} catch {
|
|
24549
|
-
}
|
|
24550
|
-
}
|
|
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);
|
|
24146
|
+
try {
|
|
24147
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf-8");
|
|
24148
|
+
const stripped = stripConflictMarkers(content);
|
|
24149
|
+
await (0, import_promises4.writeFile)(filePath, stripped);
|
|
24150
|
+
} catch {
|
|
24151
|
+
}
|
|
24152
|
+
}
|
|
24551
24153
|
}
|
|
24552
24154
|
}
|
|
24553
24155
|
}
|
|
@@ -24564,7 +24166,7 @@ var init_dist3 = __esm({
|
|
|
24564
24166
|
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
24565
24167
|
const fileResults = [];
|
|
24566
24168
|
if (!baseGen) return fileResults;
|
|
24567
|
-
const tempDir = await (0,
|
|
24169
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-acc-"));
|
|
24568
24170
|
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24569
24171
|
const tempGit = new GitClient2(tempDir);
|
|
24570
24172
|
await tempGit.exec(["init"]);
|
|
@@ -24575,8 +24177,10 @@ var init_dist3 = __esm({
|
|
|
24575
24177
|
if (isBinaryFile(filePath)) continue;
|
|
24576
24178
|
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24577
24179
|
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24578
|
-
const
|
|
24579
|
-
const
|
|
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);
|
|
24580
24184
|
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
24581
24185
|
if (effectiveTheirs != null) {
|
|
24582
24186
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
@@ -24596,7 +24200,7 @@ var init_dist3 = __esm({
|
|
|
24596
24200
|
}
|
|
24597
24201
|
}
|
|
24598
24202
|
} finally {
|
|
24599
|
-
await (0,
|
|
24203
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24600
24204
|
});
|
|
24601
24205
|
}
|
|
24602
24206
|
return fileResults;
|
|
@@ -24625,8 +24229,8 @@ var init_dist3 = __esm({
|
|
|
24625
24229
|
if (!needsAccumulation && !hasGeneratorRename) {
|
|
24626
24230
|
const snapshots = /* @__PURE__ */ new Map();
|
|
24627
24231
|
for (const filePath of patch.files) {
|
|
24628
|
-
const fullPath = (0,
|
|
24629
|
-
snapshots.set(filePath, await (0,
|
|
24232
|
+
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24233
|
+
snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
|
|
24630
24234
|
}
|
|
24631
24235
|
try {
|
|
24632
24236
|
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
@@ -24640,11 +24244,11 @@ var init_dist3 = __esm({
|
|
|
24640
24244
|
};
|
|
24641
24245
|
} catch {
|
|
24642
24246
|
for (const [filePath, content] of snapshots) {
|
|
24643
|
-
const fullPath = (0,
|
|
24247
|
+
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24644
24248
|
if (content != null) {
|
|
24645
|
-
await (0,
|
|
24249
|
+
await (0, import_promises4.writeFile)(fullPath, content);
|
|
24646
24250
|
} else {
|
|
24647
|
-
await (0,
|
|
24251
|
+
await (0, import_promises4.unlink)(fullPath).catch(() => {
|
|
24648
24252
|
});
|
|
24649
24253
|
}
|
|
24650
24254
|
}
|
|
@@ -24655,7 +24259,7 @@ var init_dist3 = __esm({
|
|
|
24655
24259
|
async applyWithThreeWayMerge(patch) {
|
|
24656
24260
|
const fileResults = [];
|
|
24657
24261
|
const resolvedFiles = {};
|
|
24658
|
-
const tempDir = await (0,
|
|
24262
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-"));
|
|
24659
24263
|
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24660
24264
|
const tempGit = new GitClient2(tempDir);
|
|
24661
24265
|
await tempGit.exec(["init"]);
|
|
@@ -24678,7 +24282,7 @@ var init_dist3 = __esm({
|
|
|
24678
24282
|
fileResults.push(result);
|
|
24679
24283
|
}
|
|
24680
24284
|
} finally {
|
|
24681
|
-
await (0,
|
|
24285
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24682
24286
|
});
|
|
24683
24287
|
}
|
|
24684
24288
|
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
@@ -24718,8 +24322,8 @@ var init_dist3 = __esm({
|
|
|
24718
24322
|
renameSourcePath = renameSource;
|
|
24719
24323
|
}
|
|
24720
24324
|
}
|
|
24721
|
-
const oursPath = (0,
|
|
24722
|
-
const ours = await (0,
|
|
24325
|
+
const oursPath = (0, import_path9.join)(this.outputDir, resolvedPath);
|
|
24326
|
+
const ours = await (0, import_promises4.readFile)(oursPath, "utf-8").catch(() => null);
|
|
24723
24327
|
let ghostReconstructed = false;
|
|
24724
24328
|
let theirs = null;
|
|
24725
24329
|
if (!base && ours && !renameSourcePath) {
|
|
@@ -24737,15 +24341,27 @@ var init_dist3 = __esm({
|
|
|
24737
24341
|
}
|
|
24738
24342
|
}
|
|
24739
24343
|
}
|
|
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;
|
|
24740
24347
|
if (!ghostReconstructed) {
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
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
|
+
}
|
|
24364
|
+
}
|
|
24749
24365
|
}
|
|
24750
24366
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
24751
24367
|
if (theirs) {
|
|
@@ -24819,16 +24435,16 @@ var init_dist3 = __esm({
|
|
|
24819
24435
|
content: effective_theirs,
|
|
24820
24436
|
baseGeneration: patch.base_generation
|
|
24821
24437
|
});
|
|
24822
|
-
const outDir2 = (0,
|
|
24823
|
-
await (0,
|
|
24824
|
-
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);
|
|
24825
24441
|
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
24826
24442
|
}
|
|
24827
24443
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
24828
24444
|
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
24829
|
-
const outDir2 = (0,
|
|
24830
|
-
await (0,
|
|
24831
|
-
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);
|
|
24832
24448
|
if (merged2.hasConflicts) {
|
|
24833
24449
|
return {
|
|
24834
24450
|
file: resolvedPath,
|
|
@@ -24867,9 +24483,9 @@ var init_dist3 = __esm({
|
|
|
24867
24483
|
};
|
|
24868
24484
|
}
|
|
24869
24485
|
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
24870
|
-
const outDir = (0,
|
|
24871
|
-
await (0,
|
|
24872
|
-
await (0,
|
|
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);
|
|
24873
24489
|
const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
|
|
24874
24490
|
if (populateAccumulator) {
|
|
24875
24491
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
@@ -24898,178 +24514,741 @@ var init_dist3 = __esm({
|
|
|
24898
24514
|
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
24899
24515
|
};
|
|
24900
24516
|
}
|
|
24901
|
-
}
|
|
24902
|
-
async isTreeReachable(treeHash) {
|
|
24903
|
-
let result = this.treeExistsCache.get(treeHash);
|
|
24904
|
-
if (result === void 0) {
|
|
24905
|
-
result = await this.git.treeExists(treeHash);
|
|
24906
|
-
this.treeExistsCache.set(treeHash, result);
|
|
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;
|
|
24987
|
+
}
|
|
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);
|
|
24995
|
+
}
|
|
24996
|
+
}
|
|
24997
|
+
if (transient.size === 0) return patches;
|
|
24998
|
+
return patches.filter((patch) => !patch.files.every((f2) => transient.has(f2)));
|
|
24999
|
+
}
|
|
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();
|
|
25012
|
+
}
|
|
25013
|
+
}
|
|
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
|
+
);
|
|
25036
|
+
}
|
|
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;
|
|
25049
|
+
}
|
|
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;
|
|
24907
25055
|
}
|
|
24908
|
-
return
|
|
24909
|
-
|
|
24910
|
-
|
|
24911
|
-
|
|
24912
|
-
|
|
24913
|
-
|
|
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
|
+
};
|
|
24914
25068
|
}
|
|
24915
|
-
|
|
24916
|
-
|
|
24917
|
-
|
|
24918
|
-
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
|
|
24923
|
-
|
|
24924
|
-
const toBase = move.to.replace(/\*\*.*$/, "");
|
|
24925
|
-
if (filePath.startsWith(fromBase)) {
|
|
24926
|
-
return toBase + filePath.slice(fromBase.length);
|
|
24927
|
-
}
|
|
24928
|
-
}
|
|
24929
|
-
}
|
|
24930
|
-
}
|
|
24931
|
-
const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
|
|
24932
|
-
let renames = this.renameCache.get(cacheKey);
|
|
24933
|
-
if (!renames) {
|
|
24934
|
-
renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
|
|
24935
|
-
this.renameCache.set(cacheKey, renames);
|
|
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();
|
|
24936
25078
|
}
|
|
24937
|
-
const
|
|
24938
|
-
|
|
24939
|
-
|
|
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
|
+
);
|
|
24940
25087
|
}
|
|
24941
|
-
return
|
|
24942
|
-
|
|
24943
|
-
|
|
24944
|
-
|
|
24945
|
-
|
|
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: [] };
|
|
24946
25095
|
}
|
|
24947
|
-
const
|
|
24948
|
-
|
|
24949
|
-
|
|
24950
|
-
if (
|
|
24951
|
-
|
|
24952
|
-
|
|
24953
|
-
await (0, import_promises3.writeFile)(tempSourcePath, base);
|
|
24954
|
-
await tempGit.exec(["add", sourceFilePath]);
|
|
24955
|
-
await tempGit.exec([
|
|
24956
|
-
"commit",
|
|
24957
|
-
"-m",
|
|
24958
|
-
`base for rename ${sourceFilePath} -> ${filePath}`,
|
|
24959
|
-
"--allow-empty"
|
|
24960
|
-
]);
|
|
24961
|
-
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24962
|
-
const tempTargetPath = (0, import_path8.join)(tempDir, filePath);
|
|
24963
|
-
return await (0, import_promises3.readFile)(tempTargetPath, "utf-8");
|
|
24964
|
-
}
|
|
24965
|
-
const tempFilePath = (0, import_path8.join)(tempDir, filePath);
|
|
24966
|
-
await (0, import_promises3.mkdir)((0, import_path8.dirname)(tempFilePath), { recursive: true });
|
|
24967
|
-
await (0, import_promises3.writeFile)(tempFilePath, base);
|
|
24968
|
-
await tempGit.exec(["add", filePath]);
|
|
24969
|
-
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
24970
|
-
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24971
|
-
return await (0, import_promises3.readFile)(tempFilePath, "utf-8");
|
|
24972
|
-
} catch {
|
|
24973
|
-
return null;
|
|
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;
|
|
24974
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: [] };
|
|
24975
25118
|
}
|
|
24976
25119
|
/**
|
|
24977
|
-
*
|
|
24978
|
-
*
|
|
24979
|
-
*
|
|
24980
|
-
*
|
|
24981
|
-
*
|
|
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).
|
|
24982
25126
|
*/
|
|
24983
|
-
async
|
|
24984
|
-
const
|
|
24985
|
-
|
|
24986
|
-
|
|
24987
|
-
|
|
24988
|
-
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
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: [] };
|
|
24996
25139
|
}
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
const
|
|
25000
|
-
const
|
|
25001
|
-
|
|
25002
|
-
for (const
|
|
25003
|
-
if (
|
|
25004
|
-
if (inTargetFile) {
|
|
25005
|
-
break;
|
|
25006
|
-
}
|
|
25007
|
-
if (isDiffLineForFile(line, filePath)) {
|
|
25008
|
-
inTargetFile = true;
|
|
25009
|
-
diffLines.push(line);
|
|
25010
|
-
}
|
|
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)) {
|
|
25011
25147
|
continue;
|
|
25012
25148
|
}
|
|
25013
|
-
|
|
25014
|
-
|
|
25149
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
25150
|
+
if (parents.length > 1) {
|
|
25151
|
+
continue;
|
|
25015
25152
|
}
|
|
25016
|
-
|
|
25017
|
-
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
25018
|
-
}
|
|
25019
|
-
extractRenameSource(patchContent, targetFilePath) {
|
|
25020
|
-
const lines = patchContent.split("\n");
|
|
25021
|
-
let inTargetFile = false;
|
|
25022
|
-
for (const line of lines) {
|
|
25023
|
-
if (line.startsWith("diff --git")) {
|
|
25024
|
-
if (inTargetFile) break;
|
|
25025
|
-
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
25153
|
+
if (existingCommits.has(commit.sha)) {
|
|
25026
25154
|
continue;
|
|
25027
25155
|
}
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25156
|
+
let patchContent;
|
|
25157
|
+
try {
|
|
25158
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
25159
|
+
} catch {
|
|
25160
|
+
continue;
|
|
25032
25161
|
}
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
}
|
|
25036
|
-
extractNewFileFromPatch(patchContent, filePath) {
|
|
25037
|
-
const lines = patchContent.split("\n");
|
|
25038
|
-
const addedLines = [];
|
|
25039
|
-
let inTargetFile = false;
|
|
25040
|
-
let inHunk = false;
|
|
25041
|
-
let isNewFile = false;
|
|
25042
|
-
let noTrailingNewline = false;
|
|
25043
|
-
for (const line of lines) {
|
|
25044
|
-
if (line.startsWith("diff --git")) {
|
|
25045
|
-
if (inTargetFile) break;
|
|
25046
|
-
inTargetFile = isDiffLineForFile(line, filePath);
|
|
25047
|
-
inHunk = false;
|
|
25048
|
-
isNewFile = false;
|
|
25162
|
+
let contentHash = this.computeContentHash(patchContent);
|
|
25163
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
25049
25164
|
continue;
|
|
25050
25165
|
}
|
|
25051
|
-
if (
|
|
25052
|
-
|
|
25053
|
-
|
|
25054
|
-
|
|
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);
|
|
25055
25188
|
}
|
|
25056
25189
|
}
|
|
25057
|
-
if (
|
|
25058
|
-
if (!isNewFile) return null;
|
|
25059
|
-
inHunk = true;
|
|
25190
|
+
if (files.length === 0) {
|
|
25060
25191
|
continue;
|
|
25061
25192
|
}
|
|
25062
|
-
if (
|
|
25063
|
-
if (line === "\") {
|
|
25064
|
-
noTrailingNewline = true;
|
|
25193
|
+
if (parents.length === 0) {
|
|
25065
25194
|
continue;
|
|
25066
25195
|
}
|
|
25067
|
-
|
|
25068
|
-
|
|
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;
|
|
25069
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
|
+
});
|
|
25070
25214
|
}
|
|
25071
|
-
|
|
25072
|
-
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);
|
|
25073
25252
|
}
|
|
25074
25253
|
};
|
|
25075
25254
|
ReplayCommitter = class {
|
|
@@ -25653,19 +25832,14 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25653
25832
|
existingPatches = this.lockManager.getPatches();
|
|
25654
25833
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
25655
25834
|
for (const p of existingPatches) {
|
|
25656
|
-
const
|
|
25657
|
-
if (
|
|
25658
|
-
|
|
25659
|
-
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
} else {
|
|
25663
|
-
this.warnings.push(
|
|
25664
|
-
`Preserving patch ${p.id} (commit ${(p.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
25665
|
-
);
|
|
25666
|
-
}
|
|
25835
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
25836
|
+
if (priorPatchId !== void 0) {
|
|
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
|
+
);
|
|
25667
25841
|
} else {
|
|
25668
|
-
seenHashes.set(p.content_hash, p.
|
|
25842
|
+
seenHashes.set(p.content_hash, p.id);
|
|
25669
25843
|
}
|
|
25670
25844
|
}
|
|
25671
25845
|
existingPatches = this.lockManager.getPatches();
|
|
@@ -25846,17 +26020,27 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25846
26020
|
if (allUserOwned && patch.files.length > 0) {
|
|
25847
26021
|
const baseChanged = patch.base_generation !== currentGenSha;
|
|
25848
26022
|
const userDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
26023
|
+
let userDiffSnapshot = null;
|
|
25849
26024
|
if (userDiff != null && userDiff.trim()) {
|
|
25850
26025
|
const newHash = this.detector.computeContentHash(userDiff);
|
|
25851
26026
|
patch.patch_content = userDiff;
|
|
25852
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
|
+
}
|
|
25853
26033
|
}
|
|
25854
26034
|
patch.base_generation = currentGenSha;
|
|
25855
26035
|
try {
|
|
25856
26036
|
this.lockManager.updatePatch(patch.id, {
|
|
25857
26037
|
base_generation: currentGenSha,
|
|
25858
26038
|
...!patch.user_owned ? { user_owned: true } : {},
|
|
25859
|
-
...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
|
+
} : {}
|
|
25860
26044
|
});
|
|
25861
26045
|
} catch {
|
|
25862
26046
|
}
|
|
@@ -25904,30 +26088,34 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25904
26088
|
continue;
|
|
25905
26089
|
}
|
|
25906
26090
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
25907
|
-
const
|
|
25908
|
-
if (
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
|
|
25914
|
-
|
|
25915
|
-
|
|
26091
|
+
const priorPatchId = seenContentHashes.get(newContentHash);
|
|
26092
|
+
if (priorPatchId !== void 0) {
|
|
26093
|
+
this.lockManager.removePatch(patch.id);
|
|
26094
|
+
absorbedPatchIds.add(patch.id);
|
|
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
|
+
);
|
|
25916
26100
|
}
|
|
25917
|
-
|
|
25918
|
-
`Preserving patch ${patch.id} (commit ${(patch.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
25919
|
-
);
|
|
26101
|
+
continue;
|
|
25920
26102
|
} else {
|
|
25921
|
-
seenContentHashes.set(newContentHash, patch.
|
|
26103
|
+
seenContentHashes.set(newContentHash, patch.id);
|
|
25922
26104
|
}
|
|
25923
26105
|
patch.base_generation = currentGenSha;
|
|
25924
26106
|
patch.patch_content = diff;
|
|
25925
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
|
+
}
|
|
25926
26113
|
try {
|
|
25927
26114
|
this.lockManager.updatePatch(patch.id, {
|
|
25928
26115
|
base_generation: currentGenSha,
|
|
25929
26116
|
patch_content: diff,
|
|
25930
|
-
content_hash: newContentHash
|
|
26117
|
+
content_hash: newContentHash,
|
|
26118
|
+
...snapshotIsNonEmpty ? { theirs_snapshot: snapshot } : {}
|
|
25931
26119
|
});
|
|
25932
26120
|
} catch {
|
|
25933
26121
|
}
|
|
@@ -25937,6 +26125,112 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25937
26125
|
}
|
|
25938
26126
|
return { absorbed, repointed, contentRebased, keptAsUserOwned, absorbedPatchIds };
|
|
25939
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
|
+
}
|
|
25940
26234
|
/**
|
|
25941
26235
|
* Determine whether `file` is user-owned (never produced by the generator).
|
|
25942
26236
|
*
|
|
@@ -26057,6 +26351,20 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26057
26351
|
this.lockManager.clearReplaySkippedAt();
|
|
26058
26352
|
return { conflictResolved: 0, conflictAbsorbed: 0, contentRefreshed: 0 };
|
|
26059
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
|
+
};
|
|
26060
26368
|
let conflictResolved = 0;
|
|
26061
26369
|
let conflictAbsorbed = 0;
|
|
26062
26370
|
let contentRefreshed = 0;
|
|
@@ -26093,6 +26401,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26093
26401
|
delete patch.status;
|
|
26094
26402
|
continue;
|
|
26095
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
|
+
}
|
|
26096
26411
|
if (patch.base_generation === currentGen) {
|
|
26097
26412
|
try {
|
|
26098
26413
|
const ppResult = await computePerPatchDiffWithFallback(
|
|
@@ -26129,10 +26444,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26129
26444
|
if (newFiles.length === 0) {
|
|
26130
26445
|
this.lockManager.removePatch(patch.id);
|
|
26131
26446
|
} else {
|
|
26447
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", newFiles);
|
|
26132
26448
|
this.lockManager.updatePatch(patch.id, {
|
|
26133
26449
|
patch_content: diff,
|
|
26134
26450
|
content_hash: newContentHash,
|
|
26135
|
-
files: newFiles
|
|
26451
|
+
files: newFiles,
|
|
26452
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
26136
26453
|
});
|
|
26137
26454
|
}
|
|
26138
26455
|
contentRefreshed++;
|
|
@@ -26165,10 +26482,12 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26165
26482
|
continue;
|
|
26166
26483
|
}
|
|
26167
26484
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
26485
|
+
const snapshot = await this.readSnapshotFromTree("HEAD", patch.files);
|
|
26168
26486
|
this.lockManager.updatePatch(patch.id, {
|
|
26169
26487
|
base_generation: currentGen,
|
|
26170
26488
|
patch_content: diff,
|
|
26171
|
-
content_hash: newContentHash
|
|
26489
|
+
content_hash: newContentHash,
|
|
26490
|
+
...Object.keys(snapshot).length > 0 ? { theirs_snapshot: snapshot } : {}
|
|
26172
26491
|
});
|
|
26173
26492
|
conflictResolved++;
|
|
26174
26493
|
} catch {
|
|
@@ -26211,7 +26530,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26211
26530
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
26212
26531
|
for (const fileResult of result.fileResults) {
|
|
26213
26532
|
if (fileResult.status !== "conflict") continue;
|
|
26214
|
-
const filePath = (0,
|
|
26533
|
+
const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
|
|
26215
26534
|
try {
|
|
26216
26535
|
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
26217
26536
|
const stripped = stripConflictMarkers(content);
|
|
@@ -26241,7 +26560,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26241
26560
|
}
|
|
26242
26561
|
}
|
|
26243
26562
|
readFernignorePatterns() {
|
|
26244
|
-
const fernignorePath = (0,
|
|
26563
|
+
const fernignorePath = (0, import_path10.join)(this.outputDir, ".fernignore");
|
|
26245
26564
|
if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
|
|
26246
26565
|
return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
26247
26566
|
}
|
|
@@ -26299,10 +26618,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26299
26618
|
this.outputDir = outputDir;
|
|
26300
26619
|
}
|
|
26301
26620
|
fernignoreExists() {
|
|
26302
|
-
return (0, import_fs8.existsSync)((0,
|
|
26621
|
+
return (0, import_fs8.existsSync)((0, import_path12.join)(this.outputDir, ".fernignore"));
|
|
26303
26622
|
}
|
|
26304
26623
|
readFernignorePatterns() {
|
|
26305
|
-
const fernignorePath = (0,
|
|
26624
|
+
const fernignorePath = (0, import_path12.join)(this.outputDir, ".fernignore");
|
|
26306
26625
|
if (!(0, import_fs8.existsSync)(fernignorePath)) {
|
|
26307
26626
|
return [];
|
|
26308
26627
|
}
|
|
@@ -26354,6 +26673,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26354
26673
|
if (patchFiles.length > 0) {
|
|
26355
26674
|
const patchContent = diffParts.join("\n");
|
|
26356
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
|
+
}
|
|
26357
26682
|
syntheticPatches.push({
|
|
26358
26683
|
id: `patch-fernignore-${(0, import_crypto2.createHash)("sha256").update(pattern).digest("hex").slice(0, 8)}`,
|
|
26359
26684
|
content_hash: contentHash,
|
|
@@ -26363,6 +26688,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26363
26688
|
base_generation: currentGen.commit_sha,
|
|
26364
26689
|
files: patchFiles,
|
|
26365
26690
|
patch_content: patchContent,
|
|
26691
|
+
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
26366
26692
|
// Synthetic patches captured from .fernignore-protected files
|
|
26367
26693
|
// are user-owned by definition — the customer's content
|
|
26368
26694
|
// diverges from the generator's pristine output, and the
|
|
@@ -26404,7 +26730,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26404
26730
|
return results;
|
|
26405
26731
|
}
|
|
26406
26732
|
readFileContent(filePath) {
|
|
26407
|
-
const fullPath = (0,
|
|
26733
|
+
const fullPath = (0, import_path12.join)(this.outputDir, filePath);
|
|
26408
26734
|
if (!(0, import_fs8.existsSync)(fullPath)) {
|
|
26409
26735
|
return null;
|
|
26410
26736
|
}
|
|
@@ -26469,7 +26795,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26469
26795
|
};
|
|
26470
26796
|
}
|
|
26471
26797
|
movePatternsToReplayYml(patterns) {
|
|
26472
|
-
const replayYmlPath = (0,
|
|
26798
|
+
const replayYmlPath = (0, import_path12.join)(this.outputDir, ".fern", "replay.yml");
|
|
26473
26799
|
let config = {};
|
|
26474
26800
|
if ((0, import_fs8.existsSync)(replayYmlPath)) {
|
|
26475
26801
|
const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -26478,7 +26804,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26478
26804
|
const existing = config.exclude ?? [];
|
|
26479
26805
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
26480
26806
|
config.exclude = merged;
|
|
26481
|
-
const dir = (0,
|
|
26807
|
+
const dir = (0, import_path12.dirname)(replayYmlPath);
|
|
26482
26808
|
if (!(0, import_fs8.existsSync)(dir)) {
|
|
26483
26809
|
(0, import_fs8.mkdirSync)(dir, { recursive: true });
|
|
26484
26810
|
}
|
|
@@ -28885,11 +29211,11 @@ var VersionBump2;
|
|
|
28885
29211
|
var init_types = __esm({
|
|
28886
29212
|
"../cli/ai/lib/baml_client/types.js"() {
|
|
28887
29213
|
"use strict";
|
|
28888
|
-
(function(
|
|
28889
|
-
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
|
|
29214
|
+
(function(VersionBump4) {
|
|
29215
|
+
VersionBump4["MAJOR"] = "MAJOR";
|
|
29216
|
+
VersionBump4["MINOR"] = "MINOR";
|
|
29217
|
+
VersionBump4["PATCH"] = "PATCH";
|
|
29218
|
+
VersionBump4["NO_CHANGE"] = "NO_CHANGE";
|
|
28893
29219
|
})(VersionBump2 || (VersionBump2 = {}));
|
|
28894
29220
|
}
|
|
28895
29221
|
});
|
|
@@ -28919,6 +29245,24 @@ var init_watchers = __esm({
|
|
|
28919
29245
|
});
|
|
28920
29246
|
|
|
28921
29247
|
// ../cli/ai/lib/baml_client/index.js
|
|
29248
|
+
var baml_client_exports = {};
|
|
29249
|
+
__export(baml_client_exports, {
|
|
29250
|
+
BamlClientFinishReasonError: () => import_baml6.BamlClientFinishReasonError,
|
|
29251
|
+
BamlClientHttpError: () => import_baml6.BamlClientHttpError,
|
|
29252
|
+
BamlValidationError: () => import_baml6.BamlValidationError,
|
|
29253
|
+
VersionBump: () => VersionBump2,
|
|
29254
|
+
all_succeeded: () => all_succeeded,
|
|
29255
|
+
b: () => b2,
|
|
29256
|
+
flush: () => flush,
|
|
29257
|
+
get_checks: () => get_checks,
|
|
29258
|
+
onLogEvent: () => onLogEvent,
|
|
29259
|
+
resetBamlEnvVars: () => resetBamlEnvVars,
|
|
29260
|
+
setTags: () => setTags,
|
|
29261
|
+
traceAsync: () => traceAsync,
|
|
29262
|
+
traceSync: () => traceSync,
|
|
29263
|
+
version: () => version,
|
|
29264
|
+
watchers: () => watchers_exports
|
|
29265
|
+
});
|
|
28922
29266
|
var import_baml5, import_baml6, version;
|
|
28923
29267
|
var init_baml_client = __esm({
|
|
28924
29268
|
"../cli/ai/lib/baml_client/index.js"() {
|
|
@@ -28936,6 +29280,10 @@ var init_baml_client = __esm({
|
|
|
28936
29280
|
});
|
|
28937
29281
|
|
|
28938
29282
|
// ../cli/ai/lib/configureBamlClient.js
|
|
29283
|
+
var configureBamlClient_exports = {};
|
|
29284
|
+
__export(configureBamlClient_exports, {
|
|
29285
|
+
configureBamlClient: () => configureBamlClient
|
|
29286
|
+
});
|
|
28939
29287
|
function configureBamlClient(config) {
|
|
28940
29288
|
const registry = new import_baml7.ClientRegistry();
|
|
28941
29289
|
const clientOptions = {
|
|
@@ -28956,31 +29304,59 @@ var init_configureBamlClient = __esm({
|
|
|
28956
29304
|
}
|
|
28957
29305
|
});
|
|
28958
29306
|
|
|
29307
|
+
// ../cli/ai/lib/loader.js
|
|
29308
|
+
async function loadBamlDependencies() {
|
|
29309
|
+
try {
|
|
29310
|
+
const [baml, bamlClient, config] = await Promise.all([
|
|
29311
|
+
import("@boundaryml/baml"),
|
|
29312
|
+
Promise.resolve().then(() => (init_baml_client(), baml_client_exports)),
|
|
29313
|
+
Promise.resolve().then(() => (init_configureBamlClient(), configureBamlClient_exports))
|
|
29314
|
+
]);
|
|
29315
|
+
return {
|
|
29316
|
+
ClientRegistry: baml.ClientRegistry,
|
|
29317
|
+
BamlClient: bamlClient.b,
|
|
29318
|
+
configureBamlClient: config.configureBamlClient
|
|
29319
|
+
};
|
|
29320
|
+
} catch (error) {
|
|
29321
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
29322
|
+
throw new Error(`${INSTALL_MESSAGE}
|
|
29323
|
+
|
|
29324
|
+
Original error: ${original}`);
|
|
29325
|
+
}
|
|
29326
|
+
}
|
|
29327
|
+
var INSTALL_MESSAGE;
|
|
29328
|
+
var init_loader = __esm({
|
|
29329
|
+
"../cli/ai/lib/loader.js"() {
|
|
29330
|
+
"use strict";
|
|
29331
|
+
INSTALL_MESSAGE = "@boundaryml/baml is required for AI features (auto-versioning, sdk-diff).\nInstall it with:\n npm install @boundaryml/baml\n pnpm add @boundaryml/baml";
|
|
29332
|
+
}
|
|
29333
|
+
});
|
|
29334
|
+
|
|
29335
|
+
// ../cli/ai/lib/types.js
|
|
29336
|
+
var VersionBump3;
|
|
29337
|
+
var init_types2 = __esm({
|
|
29338
|
+
"../cli/ai/lib/types.js"() {
|
|
29339
|
+
"use strict";
|
|
29340
|
+
(function(VersionBump4) {
|
|
29341
|
+
VersionBump4["MAJOR"] = "MAJOR";
|
|
29342
|
+
VersionBump4["MINOR"] = "MINOR";
|
|
29343
|
+
VersionBump4["PATCH"] = "PATCH";
|
|
29344
|
+
VersionBump4["NO_CHANGE"] = "NO_CHANGE";
|
|
29345
|
+
})(VersionBump3 || (VersionBump3 = {}));
|
|
29346
|
+
}
|
|
29347
|
+
});
|
|
29348
|
+
|
|
28959
29349
|
// ../cli/ai/lib/index.js
|
|
28960
29350
|
var lib_exports = {};
|
|
28961
29351
|
__export(lib_exports, {
|
|
28962
|
-
|
|
28963
|
-
|
|
28964
|
-
BamlValidationError: () => import_baml6.BamlValidationError,
|
|
28965
|
-
VersionBump: () => VersionBump2,
|
|
28966
|
-
all_succeeded: () => all_succeeded,
|
|
28967
|
-
b: () => b2,
|
|
28968
|
-
configureBamlClient: () => configureBamlClient,
|
|
28969
|
-
flush: () => flush,
|
|
28970
|
-
get_checks: () => get_checks,
|
|
28971
|
-
onLogEvent: () => onLogEvent,
|
|
28972
|
-
resetBamlEnvVars: () => resetBamlEnvVars,
|
|
28973
|
-
setTags: () => setTags,
|
|
28974
|
-
traceAsync: () => traceAsync,
|
|
28975
|
-
traceSync: () => traceSync,
|
|
28976
|
-
version: () => version,
|
|
28977
|
-
watchers: () => watchers_exports
|
|
29352
|
+
VersionBump: () => VersionBump3,
|
|
29353
|
+
loadBamlDependencies: () => loadBamlDependencies
|
|
28978
29354
|
});
|
|
28979
29355
|
var init_lib = __esm({
|
|
28980
29356
|
"../cli/ai/lib/index.js"() {
|
|
28981
29357
|
"use strict";
|
|
28982
|
-
|
|
28983
|
-
|
|
29358
|
+
init_loader();
|
|
29359
|
+
init_types2();
|
|
28984
29360
|
}
|
|
28985
29361
|
});
|
|
28986
29362
|
|
|
@@ -29029,13 +29405,13 @@ async function doesPathExist(filepath, mode) {
|
|
|
29029
29405
|
// ../commons/fs-utils/lib/resolve.js
|
|
29030
29406
|
var import_path = __toESM(require("path"), 1);
|
|
29031
29407
|
function resolve(...paths) {
|
|
29032
|
-
return import_path.default.resolve(...paths.map(convertToOsPath));
|
|
29408
|
+
return convertToOsPath(import_path.default.resolve(...paths.map(convertToOsPath)));
|
|
29033
29409
|
}
|
|
29034
29410
|
|
|
29035
29411
|
// src/cli.ts
|
|
29036
29412
|
var import_fs15 = __toESM(require("fs"));
|
|
29037
|
-
var
|
|
29038
|
-
var
|
|
29413
|
+
var import_promises15 = require("fs/promises");
|
|
29414
|
+
var import_path21 = __toESM(require("path"));
|
|
29039
29415
|
|
|
29040
29416
|
// ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
|
|
29041
29417
|
var YError = class _YError extends Error {
|
|
@@ -31021,7 +31397,9 @@ function newRepositoryReference({ remote, owner, repo }) {
|
|
|
31021
31397
|
repoUrl,
|
|
31022
31398
|
cloneUrl,
|
|
31023
31399
|
getAuthedCloneUrl: (installationToken) => {
|
|
31024
|
-
|
|
31400
|
+
const isInstallationToken = installationToken.startsWith("ghs_") || installationToken.startsWith("ghu_");
|
|
31401
|
+
const userPrefix = isInstallationToken ? "x-access-token:" : "";
|
|
31402
|
+
return cloneUrl.replace("https://", `https://${userPrefix}${installationToken}@`);
|
|
31025
31403
|
}
|
|
31026
31404
|
};
|
|
31027
31405
|
}
|
|
@@ -38736,42 +39114,42 @@ function replayForget(params) {
|
|
|
38736
39114
|
// src/replay/replay-init.ts
|
|
38737
39115
|
init_dist3();
|
|
38738
39116
|
var import_fs11 = require("fs");
|
|
38739
|
-
var
|
|
39117
|
+
var import_path16 = require("path");
|
|
38740
39118
|
var import_tmp_promise2 = __toESM(require_tmp_promise());
|
|
38741
39119
|
|
|
38742
39120
|
// src/replay/fernignore.ts
|
|
38743
|
-
var
|
|
38744
|
-
var
|
|
39121
|
+
var import_promises8 = require("fs/promises");
|
|
39122
|
+
var import_path15 = require("path");
|
|
38745
39123
|
var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
38746
39124
|
var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
|
|
38747
39125
|
async function ensureReplayFernignoreEntries(outputDir) {
|
|
38748
|
-
const fernignorePath = (0,
|
|
39126
|
+
const fernignorePath = (0, import_path15.join)(outputDir, ".fernignore");
|
|
38749
39127
|
let content = "";
|
|
38750
39128
|
try {
|
|
38751
|
-
content = await (0,
|
|
39129
|
+
content = await (0, import_promises8.readFile)(fernignorePath, "utf-8");
|
|
38752
39130
|
} catch {
|
|
38753
39131
|
}
|
|
38754
39132
|
const lines = content.split("\n");
|
|
38755
39133
|
const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38756
39134
|
if (toAdd.length > 0) {
|
|
38757
39135
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38758
|
-
await (0,
|
|
39136
|
+
await (0, import_promises8.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38759
39137
|
return true;
|
|
38760
39138
|
}
|
|
38761
39139
|
return false;
|
|
38762
39140
|
}
|
|
38763
39141
|
async function ensureGitattributesEntries2(outputDir) {
|
|
38764
|
-
const gitattributesPath = (0,
|
|
39142
|
+
const gitattributesPath = (0, import_path15.join)(outputDir, ".gitattributes");
|
|
38765
39143
|
let content = "";
|
|
38766
39144
|
try {
|
|
38767
|
-
content = await (0,
|
|
39145
|
+
content = await (0, import_promises8.readFile)(gitattributesPath, "utf-8");
|
|
38768
39146
|
} catch {
|
|
38769
39147
|
}
|
|
38770
39148
|
const lines = content.split("\n");
|
|
38771
39149
|
const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38772
39150
|
if (toAdd.length > 0) {
|
|
38773
39151
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38774
|
-
await (0,
|
|
39152
|
+
await (0, import_promises8.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38775
39153
|
}
|
|
38776
39154
|
}
|
|
38777
39155
|
|
|
@@ -38798,7 +39176,7 @@ async function replayInit(params) {
|
|
|
38798
39176
|
if (dryRun) {
|
|
38799
39177
|
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
|
|
38800
39178
|
}
|
|
38801
|
-
const lockfilePath = (0,
|
|
39179
|
+
const lockfilePath = (0, import_path16.join)(repoPath, ".fern", "replay.lock");
|
|
38802
39180
|
const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
38803
39181
|
return {
|
|
38804
39182
|
bootstrap: bootstrapResult,
|
|
@@ -38921,7 +39299,7 @@ init_dist3();
|
|
|
38921
39299
|
init_dist3();
|
|
38922
39300
|
var import_child_process4 = require("child_process");
|
|
38923
39301
|
var import_fs12 = require("fs");
|
|
38924
|
-
var
|
|
39302
|
+
var import_path17 = require("path");
|
|
38925
39303
|
async function replayPrepare(params) {
|
|
38926
39304
|
const {
|
|
38927
39305
|
outputDir,
|
|
@@ -38932,7 +39310,7 @@ async function replayPrepare(params) {
|
|
|
38932
39310
|
skipApplication,
|
|
38933
39311
|
logger
|
|
38934
39312
|
} = params;
|
|
38935
|
-
const lockfilePath = (0,
|
|
39313
|
+
const lockfilePath = (0, import_path17.join)(outputDir, ".fern", "replay.lock");
|
|
38936
39314
|
if (!(0, import_fs12.existsSync)(lockfilePath)) {
|
|
38937
39315
|
return null;
|
|
38938
39316
|
}
|
|
@@ -38972,12 +39350,17 @@ async function replayPrepare(params) {
|
|
|
38972
39350
|
`fern-generation-base tag ${tagSha} is not reachable from HEAD \u2014 skipping divergent-merge sync. The tag likely points at an unmerged generation (PR closed without merge).`
|
|
38973
39351
|
);
|
|
38974
39352
|
} else {
|
|
38975
|
-
|
|
38976
|
-
|
|
38977
|
-
|
|
38978
|
-
|
|
38979
|
-
|
|
38980
|
-
|
|
39353
|
+
try {
|
|
39354
|
+
const syncService = new ReplayService(outputDir, { enabled: true });
|
|
39355
|
+
await syncService.syncFromDivergentMerge(tagSha, {
|
|
39356
|
+
cliVersion,
|
|
39357
|
+
generatorVersions,
|
|
39358
|
+
baseBranchHead: baseBranchHead ?? void 0
|
|
39359
|
+
});
|
|
39360
|
+
} catch (error) {
|
|
39361
|
+
logger?.warn("Replay divergent-merge sync failed, continuing without sync: " + String(error));
|
|
39362
|
+
throw new ReplayPrepareError(String(error), error);
|
|
39363
|
+
}
|
|
38981
39364
|
try {
|
|
38982
39365
|
const freshLockManager = new LockfileManager(outputDir);
|
|
38983
39366
|
if (freshLockManager.exists()) {
|
|
@@ -38999,8 +39382,8 @@ async function replayPrepare(params) {
|
|
|
38999
39382
|
baseBranchHead: baseBranchHead ?? void 0
|
|
39000
39383
|
});
|
|
39001
39384
|
} catch (error) {
|
|
39002
|
-
logger?.warn("Replay failed, continuing without replay: " + String(error));
|
|
39003
|
-
|
|
39385
|
+
logger?.warn("Replay prepare failed, continuing without replay: " + String(error));
|
|
39386
|
+
throw new ReplayPrepareError(String(error), error);
|
|
39004
39387
|
}
|
|
39005
39388
|
return {
|
|
39006
39389
|
_service: service,
|
|
@@ -39018,13 +39401,14 @@ async function replayApply(prepared, params = {}) {
|
|
|
39018
39401
|
try {
|
|
39019
39402
|
report = await prepared._service.applyPreparedReplay(prepared._preparation, { stageOnly });
|
|
39020
39403
|
} catch (error) {
|
|
39021
|
-
logger?.warn("Replay failed, continuing without replay: " + String(error));
|
|
39404
|
+
logger?.warn("Replay apply failed, continuing without replay: " + String(error));
|
|
39022
39405
|
return {
|
|
39023
39406
|
report: null,
|
|
39024
39407
|
fernignoreUpdated: false,
|
|
39025
39408
|
previousGenerationSha: prepared.previousGenerationSha,
|
|
39026
39409
|
currentGenerationSha: prepared.currentGenerationSha,
|
|
39027
|
-
baseBranchHead: prepared.baseBranchHead
|
|
39410
|
+
baseBranchHead: prepared.baseBranchHead,
|
|
39411
|
+
failureReason: String(error)
|
|
39028
39412
|
};
|
|
39029
39413
|
}
|
|
39030
39414
|
let resolvedBaseBranchHead = prepared.baseBranchHead;
|
|
@@ -39050,7 +39434,19 @@ async function replayApply(prepared, params = {}) {
|
|
|
39050
39434
|
};
|
|
39051
39435
|
}
|
|
39052
39436
|
async function replayRun(params) {
|
|
39053
|
-
|
|
39437
|
+
let prepared;
|
|
39438
|
+
try {
|
|
39439
|
+
prepared = await replayPrepare(params);
|
|
39440
|
+
} catch (error) {
|
|
39441
|
+
return {
|
|
39442
|
+
report: null,
|
|
39443
|
+
fernignoreUpdated: false,
|
|
39444
|
+
previousGenerationSha: null,
|
|
39445
|
+
currentGenerationSha: null,
|
|
39446
|
+
baseBranchHead: null,
|
|
39447
|
+
failureReason: error instanceof ReplayPrepareError ? error.reason : String(error)
|
|
39448
|
+
};
|
|
39449
|
+
}
|
|
39054
39450
|
if (prepared == null) {
|
|
39055
39451
|
return {
|
|
39056
39452
|
report: null,
|
|
@@ -39062,6 +39458,18 @@ async function replayRun(params) {
|
|
|
39062
39458
|
}
|
|
39063
39459
|
return replayApply(prepared, { stageOnly: params.stageOnly, logger: params.logger });
|
|
39064
39460
|
}
|
|
39461
|
+
var ReplayPrepareError = class extends Error {
|
|
39462
|
+
reason;
|
|
39463
|
+
constructor(reason, cause) {
|
|
39464
|
+
super(`Replay prepare failed: ${reason}`, { cause });
|
|
39465
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39466
|
+
if (Error.captureStackTrace) {
|
|
39467
|
+
Error.captureStackTrace(this, this.constructor);
|
|
39468
|
+
}
|
|
39469
|
+
this.name = this.constructor.name;
|
|
39470
|
+
this.reason = reason;
|
|
39471
|
+
}
|
|
39472
|
+
};
|
|
39065
39473
|
function isTagMergedIntoHead(cwd2, tagSha) {
|
|
39066
39474
|
try {
|
|
39067
39475
|
(0, import_child_process4.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
|
|
@@ -39117,29 +39525,29 @@ function replayStatus(params) {
|
|
|
39117
39525
|
}
|
|
39118
39526
|
|
|
39119
39527
|
// src/configuration/loadGitHubConfig.ts
|
|
39120
|
-
var
|
|
39528
|
+
var import_promises9 = require("fs/promises");
|
|
39121
39529
|
async function loadGitHubConfig({
|
|
39122
39530
|
absolutePathToConfig
|
|
39123
39531
|
}) {
|
|
39124
|
-
const rawContents = await (0,
|
|
39532
|
+
const rawContents = await (0, import_promises9.readFile)(absolutePathToConfig, "utf8");
|
|
39125
39533
|
return JSON.parse(rawContents);
|
|
39126
39534
|
}
|
|
39127
39535
|
|
|
39128
39536
|
// src/configuration/loadReadmeConfig.ts
|
|
39129
|
-
var
|
|
39537
|
+
var import_promises10 = require("fs/promises");
|
|
39130
39538
|
async function loadReadmeConfig({
|
|
39131
39539
|
absolutePathToConfig
|
|
39132
39540
|
}) {
|
|
39133
|
-
const rawContents = await (0,
|
|
39541
|
+
const rawContents = await (0, import_promises10.readFile)(absolutePathToConfig, "utf8");
|
|
39134
39542
|
return JSON.parse(rawContents);
|
|
39135
39543
|
}
|
|
39136
39544
|
|
|
39137
39545
|
// src/configuration/loadReferenceConfig.ts
|
|
39138
|
-
var
|
|
39546
|
+
var import_promises11 = require("fs/promises");
|
|
39139
39547
|
async function loadReferenceConfig({
|
|
39140
39548
|
absolutePathToConfig
|
|
39141
39549
|
}) {
|
|
39142
|
-
const rawContents = await (0,
|
|
39550
|
+
const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
|
|
39143
39551
|
return JSON.parse(rawContents);
|
|
39144
39552
|
}
|
|
39145
39553
|
|
|
@@ -39156,7 +39564,7 @@ var consolePipelineLogger = {
|
|
|
39156
39564
|
};
|
|
39157
39565
|
|
|
39158
39566
|
// src/pipeline/PostGenerationPipeline.ts
|
|
39159
|
-
var
|
|
39567
|
+
var import_child_process7 = require("child_process");
|
|
39160
39568
|
|
|
39161
39569
|
// src/pipeline/github/constants.ts
|
|
39162
39570
|
var FERN_BOT_NAME2 = "fern-api";
|
|
@@ -39166,8 +39574,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
|
|
|
39166
39574
|
// src/pipeline/steps/AutoVersionStep.ts
|
|
39167
39575
|
var import_child_process5 = require("child_process");
|
|
39168
39576
|
var import_fs14 = require("fs");
|
|
39169
|
-
var
|
|
39170
|
-
var
|
|
39577
|
+
var import_promises13 = require("fs/promises");
|
|
39578
|
+
var import_path19 = require("path");
|
|
39171
39579
|
|
|
39172
39580
|
// ../commons/logging-execa/lib/loggingExeca.js
|
|
39173
39581
|
var import_execa = __toESM(require_execa(), 1);
|
|
@@ -39226,8 +39634,8 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
|
|
|
39226
39634
|
|
|
39227
39635
|
// src/autoversion/AutoVersioningService.ts
|
|
39228
39636
|
var import_fs13 = require("fs");
|
|
39229
|
-
var
|
|
39230
|
-
var
|
|
39637
|
+
var import_promises12 = require("fs/promises");
|
|
39638
|
+
var import_path18 = require("path");
|
|
39231
39639
|
var import_semver = __toESM(require_semver2());
|
|
39232
39640
|
var AutoVersioningException = class extends Error {
|
|
39233
39641
|
/**
|
|
@@ -39914,7 +40322,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39914
40322
|
);
|
|
39915
40323
|
return;
|
|
39916
40324
|
}
|
|
39917
|
-
const goModPath = (0,
|
|
40325
|
+
const goModPath = (0, import_path18.join)(workingDirectory, "go.mod");
|
|
39918
40326
|
if (!(0, import_fs13.existsSync)(goModPath)) {
|
|
39919
40327
|
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
39920
40328
|
return;
|
|
@@ -39948,7 +40356,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39948
40356
|
* Reads the module path from a go.mod file.
|
|
39949
40357
|
*/
|
|
39950
40358
|
async readGoModulePath(goModPath) {
|
|
39951
|
-
const content = await (0,
|
|
40359
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39952
40360
|
for (const line of content.split("\n")) {
|
|
39953
40361
|
const trimmed2 = line.trim();
|
|
39954
40362
|
if (trimmed2.startsWith("module ")) {
|
|
@@ -39961,7 +40369,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39961
40369
|
* Adds a version suffix to the module line in go.mod.
|
|
39962
40370
|
*/
|
|
39963
40371
|
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
39964
|
-
const content = await (0,
|
|
40372
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39965
40373
|
const lines = content.split("\n");
|
|
39966
40374
|
let changed = false;
|
|
39967
40375
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -39973,7 +40381,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39973
40381
|
}
|
|
39974
40382
|
}
|
|
39975
40383
|
if (changed) {
|
|
39976
|
-
await (0,
|
|
40384
|
+
await (0, import_promises12.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
39977
40385
|
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
39978
40386
|
}
|
|
39979
40387
|
}
|
|
@@ -39982,10 +40390,10 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39982
40390
|
*/
|
|
39983
40391
|
async walkDirectory(dir, filter2) {
|
|
39984
40392
|
const results = [];
|
|
39985
|
-
const entries2 = await (0,
|
|
40393
|
+
const entries2 = await (0, import_promises12.readdir)(dir);
|
|
39986
40394
|
for (const entry of entries2) {
|
|
39987
|
-
const fullPath = (0,
|
|
39988
|
-
const entryStat = await (0,
|
|
40395
|
+
const fullPath = (0, import_path18.join)(dir, entry);
|
|
40396
|
+
const entryStat = await (0, import_promises12.stat)(fullPath);
|
|
39989
40397
|
if (entryStat.isDirectory()) {
|
|
39990
40398
|
if (entry === ".git" || entry === "vendor") {
|
|
39991
40399
|
continue;
|
|
@@ -40002,7 +40410,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40002
40410
|
* Adds a version suffix to import paths in all .go files in the repository.
|
|
40003
40411
|
*/
|
|
40004
40412
|
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
40005
|
-
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0,
|
|
40413
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path18.extname)(filePath) === ".go");
|
|
40006
40414
|
for (const filePath of goFiles) {
|
|
40007
40415
|
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
40008
40416
|
}
|
|
@@ -40012,7 +40420,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40012
40420
|
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
40013
40421
|
*/
|
|
40014
40422
|
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
40015
|
-
const content = await (0,
|
|
40423
|
+
const content = await (0, import_promises12.readFile)(filePath, "utf-8");
|
|
40016
40424
|
const lines = content.split("\n");
|
|
40017
40425
|
let changed = false;
|
|
40018
40426
|
let inImportBlock = false;
|
|
@@ -40041,7 +40449,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40041
40449
|
}
|
|
40042
40450
|
}
|
|
40043
40451
|
if (changed) {
|
|
40044
|
-
await (0,
|
|
40452
|
+
await (0, import_promises12.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
40045
40453
|
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
40046
40454
|
}
|
|
40047
40455
|
}
|
|
@@ -40446,7 +40854,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
40446
40854
|
}
|
|
40447
40855
|
async prependChangelogEntry(params) {
|
|
40448
40856
|
const { version: version2, entry } = params;
|
|
40449
|
-
const changelogPath = (0,
|
|
40857
|
+
const changelogPath = (0, import_path19.join)(this.outputDir, "changelog.md");
|
|
40450
40858
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
40451
40859
|
const header = `## [${version2}] - ${now}
|
|
40452
40860
|
`;
|
|
@@ -40455,7 +40863,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
40455
40863
|
`;
|
|
40456
40864
|
let existing = "";
|
|
40457
40865
|
if ((0, import_fs14.existsSync)(changelogPath)) {
|
|
40458
|
-
existing = await (0,
|
|
40866
|
+
existing = await (0, import_promises13.readFile)(changelogPath, "utf-8");
|
|
40459
40867
|
}
|
|
40460
40868
|
let output;
|
|
40461
40869
|
if (existing.trim().length === 0) {
|
|
@@ -40472,7 +40880,7 @@ ${newBlock}${remainder}`;
|
|
|
40472
40880
|
} else {
|
|
40473
40881
|
output = `${newBlock}${existing}`;
|
|
40474
40882
|
}
|
|
40475
|
-
await (0,
|
|
40883
|
+
await (0, import_promises13.writeFile)(changelogPath, output, "utf-8");
|
|
40476
40884
|
}
|
|
40477
40885
|
brandMessage(message) {
|
|
40478
40886
|
if (this.config.isWhitelabel) {
|
|
@@ -40484,7 +40892,7 @@ ${newBlock}${remainder}`;
|
|
|
40484
40892
|
return `${message.trimEnd()}${FERN_TRAILER}`;
|
|
40485
40893
|
}
|
|
40486
40894
|
async analyzeSingle(cleanedDiff, language, previousVersion) {
|
|
40487
|
-
const { client, VersionBump:
|
|
40895
|
+
const { client, VersionBump: VersionBump4 } = await this.loadBaml();
|
|
40488
40896
|
const result = await client.AnalyzeSdkDiff(
|
|
40489
40897
|
cleanedDiff,
|
|
40490
40898
|
language,
|
|
@@ -40492,7 +40900,7 @@ ${newBlock}${remainder}`;
|
|
|
40492
40900
|
this.config.priorChangelog ?? "",
|
|
40493
40901
|
this.config.specCommitMessage ?? ""
|
|
40494
40902
|
);
|
|
40495
|
-
if (result.version_bump ===
|
|
40903
|
+
if (result.version_bump === VersionBump4.NO_CHANGE) {
|
|
40496
40904
|
return null;
|
|
40497
40905
|
}
|
|
40498
40906
|
return {
|
|
@@ -40503,8 +40911,8 @@ ${newBlock}${remainder}`;
|
|
|
40503
40911
|
};
|
|
40504
40912
|
}
|
|
40505
40913
|
async analyzeChunks(chunks, language, previousVersion) {
|
|
40506
|
-
const { client, VersionBump:
|
|
40507
|
-
let bestBump =
|
|
40914
|
+
const { client, VersionBump: VersionBump4 } = await this.loadBaml();
|
|
40915
|
+
let bestBump = VersionBump4.NO_CHANGE;
|
|
40508
40916
|
let bestMessage = "";
|
|
40509
40917
|
let bestVersionBumpReason;
|
|
40510
40918
|
const changelogEntries = [];
|
|
@@ -40520,7 +40928,7 @@ ${newBlock}${remainder}`;
|
|
|
40520
40928
|
this.config.priorChangelog ?? "",
|
|
40521
40929
|
this.config.specCommitMessage ?? ""
|
|
40522
40930
|
);
|
|
40523
|
-
if (analysis.version_bump ===
|
|
40931
|
+
if (analysis.version_bump === VersionBump4.NO_CHANGE) {
|
|
40524
40932
|
continue;
|
|
40525
40933
|
}
|
|
40526
40934
|
const prev = bestBump;
|
|
@@ -40534,7 +40942,7 @@ ${newBlock}${remainder}`;
|
|
|
40534
40942
|
changelogEntries.push(entry);
|
|
40535
40943
|
}
|
|
40536
40944
|
}
|
|
40537
|
-
if (bestBump ===
|
|
40945
|
+
if (bestBump === VersionBump4.NO_CHANGE) {
|
|
40538
40946
|
return null;
|
|
40539
40947
|
}
|
|
40540
40948
|
if (changelogEntries.length <= 1) {
|
|
@@ -40581,11 +40989,12 @@ ${newBlock}${remainder}`;
|
|
|
40581
40989
|
if (this.config.ai == null) {
|
|
40582
40990
|
throw new Error("AutoVersionStep: ai config is missing. Set autoVersion.ai to a BAML provider+model pair.");
|
|
40583
40991
|
}
|
|
40584
|
-
const
|
|
40585
|
-
const
|
|
40992
|
+
const { loadBamlDependencies: loadBamlDependencies2, VersionBump: VersionBump4 } = await Promise.resolve().then(() => (init_lib(), lib_exports));
|
|
40993
|
+
const { BamlClient, configureBamlClient: configureBamlClient2 } = await loadBamlDependencies2();
|
|
40994
|
+
const registry = configureBamlClient2(this.config.ai);
|
|
40586
40995
|
return {
|
|
40587
|
-
client:
|
|
40588
|
-
VersionBump:
|
|
40996
|
+
client: BamlClient.withOptions({ clientRegistry: registry }),
|
|
40997
|
+
VersionBump: VersionBump4
|
|
40589
40998
|
};
|
|
40590
40999
|
}
|
|
40591
41000
|
/**
|
|
@@ -40612,6 +41021,7 @@ ${newBlock}${remainder}`;
|
|
|
40612
41021
|
};
|
|
40613
41022
|
|
|
40614
41023
|
// src/pipeline/steps/GenerationCommitStep.ts
|
|
41024
|
+
var import_child_process6 = require("child_process");
|
|
40615
41025
|
var GenerationCommitStep = class extends BaseStep {
|
|
40616
41026
|
constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
|
|
40617
41027
|
super(outputDir, logger);
|
|
@@ -40626,14 +41036,35 @@ var GenerationCommitStep = class extends BaseStep {
|
|
|
40626
41036
|
generatorName;
|
|
40627
41037
|
name = "generationCommit";
|
|
40628
41038
|
async execute(_context) {
|
|
40629
|
-
const
|
|
40630
|
-
|
|
40631
|
-
|
|
40632
|
-
|
|
40633
|
-
|
|
40634
|
-
|
|
40635
|
-
|
|
40636
|
-
|
|
41039
|
+
const headBeforePrepare = tryRevParse(this.outputDir, "HEAD");
|
|
41040
|
+
let prepared;
|
|
41041
|
+
try {
|
|
41042
|
+
prepared = await replayPrepare({
|
|
41043
|
+
outputDir: this.outputDir,
|
|
41044
|
+
cliVersion: this.cliVersion,
|
|
41045
|
+
generatorVersions: this.generatorVersions,
|
|
41046
|
+
generatorName: this.generatorName,
|
|
41047
|
+
skipApplication: this.config.skipApplication,
|
|
41048
|
+
logger: this.logger
|
|
41049
|
+
});
|
|
41050
|
+
} catch (error) {
|
|
41051
|
+
const reason = error instanceof ReplayPrepareError ? error.reason : String(error);
|
|
41052
|
+
if (headBeforePrepare != null) {
|
|
41053
|
+
try {
|
|
41054
|
+
(0, import_child_process6.execFileSync)("git", ["reset", "--hard", headBeforePrepare], {
|
|
41055
|
+
cwd: this.outputDir,
|
|
41056
|
+
stdio: "pipe"
|
|
41057
|
+
});
|
|
41058
|
+
} catch {
|
|
41059
|
+
}
|
|
41060
|
+
}
|
|
41061
|
+
return {
|
|
41062
|
+
executed: true,
|
|
41063
|
+
success: true,
|
|
41064
|
+
errorMessage: reason,
|
|
41065
|
+
preparedReplay: null
|
|
41066
|
+
};
|
|
41067
|
+
}
|
|
40637
41068
|
if (prepared == null) {
|
|
40638
41069
|
return {
|
|
40639
41070
|
executed: true,
|
|
@@ -40652,10 +41083,21 @@ var GenerationCommitStep = class extends BaseStep {
|
|
|
40652
41083
|
};
|
|
40653
41084
|
}
|
|
40654
41085
|
};
|
|
41086
|
+
function tryRevParse(cwd2, rev) {
|
|
41087
|
+
try {
|
|
41088
|
+
return (0, import_child_process6.execFileSync)("git", ["rev-parse", "--verify", rev], {
|
|
41089
|
+
cwd: cwd2,
|
|
41090
|
+
encoding: "utf-8",
|
|
41091
|
+
stdio: "pipe"
|
|
41092
|
+
}).trim();
|
|
41093
|
+
} catch {
|
|
41094
|
+
return null;
|
|
41095
|
+
}
|
|
41096
|
+
}
|
|
40655
41097
|
|
|
40656
41098
|
// src/pipeline/steps/GithubStep.ts
|
|
40657
|
-
var
|
|
40658
|
-
var
|
|
41099
|
+
var import_promises14 = require("fs/promises");
|
|
41100
|
+
var import_path20 = require("path");
|
|
40659
41101
|
|
|
40660
41102
|
// src/pipeline/github/createReplayBranch.ts
|
|
40661
41103
|
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
@@ -41272,13 +41714,13 @@ var GithubStep = class extends BaseStep {
|
|
|
41272
41714
|
}
|
|
41273
41715
|
async ensureFernignore() {
|
|
41274
41716
|
this.logger.debug("Checking for .fernignore file...");
|
|
41275
|
-
const fernignorePath = (0,
|
|
41717
|
+
const fernignorePath = (0, import_path20.join)(this.outputDir, ".fernignore");
|
|
41276
41718
|
try {
|
|
41277
|
-
await (0,
|
|
41719
|
+
await (0, import_promises14.access)(fernignorePath);
|
|
41278
41720
|
this.logger.debug(".fernignore already exists");
|
|
41279
41721
|
} catch {
|
|
41280
41722
|
this.logger.debug("Creating .fernignore file...");
|
|
41281
|
-
await (0,
|
|
41723
|
+
await (0, import_promises14.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
41282
41724
|
}
|
|
41283
41725
|
}
|
|
41284
41726
|
deriveSkipCommit(replayResult) {
|
|
@@ -41382,6 +41824,18 @@ var ReplayStep = class extends BaseStep {
|
|
|
41382
41824
|
async execute(context) {
|
|
41383
41825
|
const generationCommit = context.previousStepResults.generationCommit;
|
|
41384
41826
|
const prepared = generationCommit?.preparedReplay;
|
|
41827
|
+
if (generationCommit != null && generationCommit.errorMessage != null && prepared == null) {
|
|
41828
|
+
return {
|
|
41829
|
+
executed: true,
|
|
41830
|
+
success: true,
|
|
41831
|
+
replayCrashed: true,
|
|
41832
|
+
errorMessage: generationCommit.errorMessage,
|
|
41833
|
+
flow: "normal-regeneration",
|
|
41834
|
+
patchesDetected: 0,
|
|
41835
|
+
patchesApplied: 0,
|
|
41836
|
+
patchesWithConflicts: 0
|
|
41837
|
+
};
|
|
41838
|
+
}
|
|
41385
41839
|
if (generationCommit != null && prepared == null) {
|
|
41386
41840
|
return {
|
|
41387
41841
|
executed: true,
|
|
@@ -41404,6 +41858,21 @@ var ReplayStep = class extends BaseStep {
|
|
|
41404
41858
|
skipApplication: this.config.skipApplication,
|
|
41405
41859
|
logger: this.logger
|
|
41406
41860
|
});
|
|
41861
|
+
if (result.failureReason != null) {
|
|
41862
|
+
return {
|
|
41863
|
+
executed: true,
|
|
41864
|
+
success: true,
|
|
41865
|
+
replayCrashed: true,
|
|
41866
|
+
errorMessage: result.failureReason,
|
|
41867
|
+
previousGenerationSha: result.previousGenerationSha ?? void 0,
|
|
41868
|
+
currentGenerationSha: result.currentGenerationSha ?? void 0,
|
|
41869
|
+
baseBranchHead: result.baseBranchHead ?? void 0,
|
|
41870
|
+
flow: "normal-regeneration",
|
|
41871
|
+
patchesDetected: 0,
|
|
41872
|
+
patchesApplied: 0,
|
|
41873
|
+
patchesWithConflicts: 0
|
|
41874
|
+
};
|
|
41875
|
+
}
|
|
41407
41876
|
if (result.report == null) {
|
|
41408
41877
|
return {
|
|
41409
41878
|
executed: true,
|
|
@@ -41432,6 +41901,11 @@ var ReplayStep = class extends BaseStep {
|
|
|
41432
41901
|
patchesRepointed: report.patchesRepointed,
|
|
41433
41902
|
patchesContentRebased: report.patchesContentRebased,
|
|
41434
41903
|
patchesKeptAsUserOwned: report.patchesKeptAsUserOwned,
|
|
41904
|
+
patchesSkipped: report.patchesSkipped,
|
|
41905
|
+
patchesPartiallyApplied: report.patchesPartiallyApplied,
|
|
41906
|
+
patchesConflictResolved: report.patchesConflictResolved,
|
|
41907
|
+
patchesReverted: report.patchesReverted,
|
|
41908
|
+
patchesRefreshed: report.patchesRefreshed,
|
|
41435
41909
|
unresolvedPatches: report.unresolvedPatches?.map((info) => ({
|
|
41436
41910
|
patchId: info.patchId,
|
|
41437
41911
|
patchMessage: info.patchMessage,
|
|
@@ -41500,8 +41974,8 @@ var PostGenerationPipeline = class {
|
|
|
41500
41974
|
async run() {
|
|
41501
41975
|
if (this.config.github?.enabled) {
|
|
41502
41976
|
try {
|
|
41503
|
-
(0,
|
|
41504
|
-
(0,
|
|
41977
|
+
(0, import_child_process7.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME2], { cwd: this.config.outputDir });
|
|
41978
|
+
(0, import_child_process7.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL2], { cwd: this.config.outputDir });
|
|
41505
41979
|
} catch {
|
|
41506
41980
|
}
|
|
41507
41981
|
}
|
|
@@ -41571,7 +42045,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
41571
42045
|
const readmeConfig = await loadReadmeConfig({
|
|
41572
42046
|
absolutePathToConfig: resolve(wd, argv.config)
|
|
41573
42047
|
});
|
|
41574
|
-
const originalReadmeContent = argv.originalReadme != null ? await (0,
|
|
42048
|
+
const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises15.readFile)(argv.originalReadme, "utf8") : void 0;
|
|
41575
42049
|
await generateReadmeToStream({
|
|
41576
42050
|
originalReadmeContent,
|
|
41577
42051
|
readmeConfig,
|
|
@@ -41699,7 +42173,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
41699
42173
|
try {
|
|
41700
42174
|
const wd = cwd();
|
|
41701
42175
|
const configValue = argv.config;
|
|
41702
|
-
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0,
|
|
42176
|
+
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises15.readFile)(resolve(wd, configValue), "utf-8");
|
|
41703
42177
|
let config;
|
|
41704
42178
|
try {
|
|
41705
42179
|
config = JSON.parse(configContent);
|
|
@@ -41995,7 +42469,7 @@ async function createWriteStream(outputPath) {
|
|
|
41995
42469
|
}
|
|
41996
42470
|
async function createWriteStreamFromFile(filepath) {
|
|
41997
42471
|
if (!await doesPathExist(filepath)) {
|
|
41998
|
-
await (0,
|
|
42472
|
+
await (0, import_promises15.mkdir)(import_path21.default.dirname(filepath), { recursive: true });
|
|
41999
42473
|
}
|
|
42000
42474
|
return import_fs15.default.createWriteStream(filepath);
|
|
42001
42475
|
}
|