@fern-api/generator-cli 0.9.21 → 0.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1402 -1076
- 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, {
|
|
@@ -24905,171 +24521,734 @@ var init_dist3 = __esm({
|
|
|
24905
24521
|
result = await this.git.treeExists(treeHash);
|
|
24906
24522
|
this.treeExistsCache.set(treeHash, result);
|
|
24907
24523
|
}
|
|
24908
|
-
return result;
|
|
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] };
|
|
24909
24915
|
}
|
|
24910
|
-
|
|
24911
|
-
|
|
24912
|
-
|
|
24913
|
-
|
|
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")}`;
|
|
24914
24939
|
}
|
|
24915
|
-
|
|
24916
|
-
|
|
24917
|
-
|
|
24918
|
-
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
|
|
24923
|
-
|
|
24924
|
-
|
|
24925
|
-
|
|
24926
|
-
|
|
24927
|
-
|
|
24928
|
-
|
|
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);
|
|
24929
24980
|
}
|
|
24930
24981
|
}
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
|
|
24982
|
+
let anyCandidate = false;
|
|
24983
|
+
for (const state of stateByFile.values()) {
|
|
24984
|
+
if (state.created && state.deleted) {
|
|
24985
|
+
anyCandidate = true;
|
|
24986
|
+
break;
|
|
24987
|
+
}
|
|
24936
24988
|
}
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
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
|
+
}
|
|
24940
24996
|
}
|
|
24941
|
-
return
|
|
24997
|
+
if (transient.size === 0) return patches;
|
|
24998
|
+
return patches.filter((patch) => !patch.files.every((f2) => transient.has(f2)));
|
|
24942
24999
|
}
|
|
24943
|
-
|
|
24944
|
-
|
|
24945
|
-
|
|
24946
|
-
|
|
24947
|
-
|
|
24948
|
-
|
|
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() {
|
|
24949
25007
|
try {
|
|
24950
|
-
|
|
24951
|
-
|
|
24952
|
-
await (0, import_promises3.mkdir)((0, import_path8.dirname)(tempSourcePath), { recursive: true });
|
|
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");
|
|
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));
|
|
24972
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)) {
|
|
24973
25048
|
return null;
|
|
24974
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;
|
|
25055
|
+
}
|
|
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
|
+
};
|
|
24975
25068
|
}
|
|
24976
25069
|
/**
|
|
24977
|
-
*
|
|
24978
|
-
*
|
|
24979
|
-
*
|
|
24980
|
-
* its -lines are already gone). Used to distinguish "patch already applied"
|
|
24981
|
-
* from "patch base mismatch" after a forward-apply fails.
|
|
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.
|
|
24982
25073
|
*/
|
|
24983
|
-
async
|
|
24984
|
-
const
|
|
24985
|
-
if (!
|
|
24986
|
-
|
|
24987
|
-
|
|
24988
|
-
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
25074
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
25075
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
25076
|
+
if (!diffBase) {
|
|
25077
|
+
return this.detectPatchesViaCommitScan();
|
|
25078
|
+
}
|
|
25079
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
25080
|
+
const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
25081
|
+
const sensitiveTreeFiles = allTreeFiles.filter((f2) => isSensitiveFile(f2));
|
|
25082
|
+
const files = allTreeFiles.filter((f2) => !isSensitiveFile(f2));
|
|
25083
|
+
if (sensitiveTreeFiles.length > 0) {
|
|
25084
|
+
this.warnings.push(
|
|
25085
|
+
`Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25086
|
+
);
|
|
25087
|
+
}
|
|
25088
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
25089
|
+
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
25090
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
25091
|
+
const contentHash = this.computeContentHash(diff);
|
|
25092
|
+
const lock = this.lockManager.read();
|
|
25093
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
25094
|
+
return { patches: [], revertedPatchIds: [] };
|
|
25095
|
+
}
|
|
25096
|
+
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
25097
|
+
const theirsSnapshot = {};
|
|
25098
|
+
for (const file of files) {
|
|
25099
|
+
if (isBinaryFile(file)) continue;
|
|
25100
|
+
const content = await this.git.showFile("HEAD", file).catch(() => null);
|
|
25101
|
+
if (content != null) theirsSnapshot[file] = content;
|
|
24996
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: [] };
|
|
24997
25118
|
}
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
|
|
25003
|
-
|
|
25004
|
-
|
|
25005
|
-
|
|
25006
|
-
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25119
|
+
/**
|
|
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).
|
|
25126
|
+
*/
|
|
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: [] };
|
|
25139
|
+
}
|
|
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
|
}
|
|
@@ -29034,8 +29360,8 @@ function resolve(...paths) {
|
|
|
29034
29360
|
|
|
29035
29361
|
// src/cli.ts
|
|
29036
29362
|
var import_fs15 = __toESM(require("fs"));
|
|
29037
|
-
var
|
|
29038
|
-
var
|
|
29363
|
+
var import_promises15 = require("fs/promises");
|
|
29364
|
+
var import_path21 = __toESM(require("path"));
|
|
29039
29365
|
|
|
29040
29366
|
// ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
|
|
29041
29367
|
var YError = class _YError extends Error {
|
|
@@ -38736,42 +39062,42 @@ function replayForget(params) {
|
|
|
38736
39062
|
// src/replay/replay-init.ts
|
|
38737
39063
|
init_dist3();
|
|
38738
39064
|
var import_fs11 = require("fs");
|
|
38739
|
-
var
|
|
39065
|
+
var import_path16 = require("path");
|
|
38740
39066
|
var import_tmp_promise2 = __toESM(require_tmp_promise());
|
|
38741
39067
|
|
|
38742
39068
|
// src/replay/fernignore.ts
|
|
38743
|
-
var
|
|
38744
|
-
var
|
|
39069
|
+
var import_promises8 = require("fs/promises");
|
|
39070
|
+
var import_path15 = require("path");
|
|
38745
39071
|
var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
38746
39072
|
var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
|
|
38747
39073
|
async function ensureReplayFernignoreEntries(outputDir) {
|
|
38748
|
-
const fernignorePath = (0,
|
|
39074
|
+
const fernignorePath = (0, import_path15.join)(outputDir, ".fernignore");
|
|
38749
39075
|
let content = "";
|
|
38750
39076
|
try {
|
|
38751
|
-
content = await (0,
|
|
39077
|
+
content = await (0, import_promises8.readFile)(fernignorePath, "utf-8");
|
|
38752
39078
|
} catch {
|
|
38753
39079
|
}
|
|
38754
39080
|
const lines = content.split("\n");
|
|
38755
39081
|
const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38756
39082
|
if (toAdd.length > 0) {
|
|
38757
39083
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38758
|
-
await (0,
|
|
39084
|
+
await (0, import_promises8.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38759
39085
|
return true;
|
|
38760
39086
|
}
|
|
38761
39087
|
return false;
|
|
38762
39088
|
}
|
|
38763
39089
|
async function ensureGitattributesEntries2(outputDir) {
|
|
38764
|
-
const gitattributesPath = (0,
|
|
39090
|
+
const gitattributesPath = (0, import_path15.join)(outputDir, ".gitattributes");
|
|
38765
39091
|
let content = "";
|
|
38766
39092
|
try {
|
|
38767
|
-
content = await (0,
|
|
39093
|
+
content = await (0, import_promises8.readFile)(gitattributesPath, "utf-8");
|
|
38768
39094
|
} catch {
|
|
38769
39095
|
}
|
|
38770
39096
|
const lines = content.split("\n");
|
|
38771
39097
|
const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
38772
39098
|
if (toAdd.length > 0) {
|
|
38773
39099
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
38774
|
-
await (0,
|
|
39100
|
+
await (0, import_promises8.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
38775
39101
|
}
|
|
38776
39102
|
}
|
|
38777
39103
|
|
|
@@ -38798,7 +39124,7 @@ async function replayInit(params) {
|
|
|
38798
39124
|
if (dryRun) {
|
|
38799
39125
|
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
|
|
38800
39126
|
}
|
|
38801
|
-
const lockfilePath = (0,
|
|
39127
|
+
const lockfilePath = (0, import_path16.join)(repoPath, ".fern", "replay.lock");
|
|
38802
39128
|
const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
38803
39129
|
return {
|
|
38804
39130
|
bootstrap: bootstrapResult,
|
|
@@ -38921,7 +39247,7 @@ init_dist3();
|
|
|
38921
39247
|
init_dist3();
|
|
38922
39248
|
var import_child_process4 = require("child_process");
|
|
38923
39249
|
var import_fs12 = require("fs");
|
|
38924
|
-
var
|
|
39250
|
+
var import_path17 = require("path");
|
|
38925
39251
|
async function replayPrepare(params) {
|
|
38926
39252
|
const {
|
|
38927
39253
|
outputDir,
|
|
@@ -38932,7 +39258,7 @@ async function replayPrepare(params) {
|
|
|
38932
39258
|
skipApplication,
|
|
38933
39259
|
logger
|
|
38934
39260
|
} = params;
|
|
38935
|
-
const lockfilePath = (0,
|
|
39261
|
+
const lockfilePath = (0, import_path17.join)(outputDir, ".fern", "replay.lock");
|
|
38936
39262
|
if (!(0, import_fs12.existsSync)(lockfilePath)) {
|
|
38937
39263
|
return null;
|
|
38938
39264
|
}
|
|
@@ -39117,29 +39443,29 @@ function replayStatus(params) {
|
|
|
39117
39443
|
}
|
|
39118
39444
|
|
|
39119
39445
|
// src/configuration/loadGitHubConfig.ts
|
|
39120
|
-
var
|
|
39446
|
+
var import_promises9 = require("fs/promises");
|
|
39121
39447
|
async function loadGitHubConfig({
|
|
39122
39448
|
absolutePathToConfig
|
|
39123
39449
|
}) {
|
|
39124
|
-
const rawContents = await (0,
|
|
39450
|
+
const rawContents = await (0, import_promises9.readFile)(absolutePathToConfig, "utf8");
|
|
39125
39451
|
return JSON.parse(rawContents);
|
|
39126
39452
|
}
|
|
39127
39453
|
|
|
39128
39454
|
// src/configuration/loadReadmeConfig.ts
|
|
39129
|
-
var
|
|
39455
|
+
var import_promises10 = require("fs/promises");
|
|
39130
39456
|
async function loadReadmeConfig({
|
|
39131
39457
|
absolutePathToConfig
|
|
39132
39458
|
}) {
|
|
39133
|
-
const rawContents = await (0,
|
|
39459
|
+
const rawContents = await (0, import_promises10.readFile)(absolutePathToConfig, "utf8");
|
|
39134
39460
|
return JSON.parse(rawContents);
|
|
39135
39461
|
}
|
|
39136
39462
|
|
|
39137
39463
|
// src/configuration/loadReferenceConfig.ts
|
|
39138
|
-
var
|
|
39464
|
+
var import_promises11 = require("fs/promises");
|
|
39139
39465
|
async function loadReferenceConfig({
|
|
39140
39466
|
absolutePathToConfig
|
|
39141
39467
|
}) {
|
|
39142
|
-
const rawContents = await (0,
|
|
39468
|
+
const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
|
|
39143
39469
|
return JSON.parse(rawContents);
|
|
39144
39470
|
}
|
|
39145
39471
|
|
|
@@ -39166,8 +39492,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
|
|
|
39166
39492
|
// src/pipeline/steps/AutoVersionStep.ts
|
|
39167
39493
|
var import_child_process5 = require("child_process");
|
|
39168
39494
|
var import_fs14 = require("fs");
|
|
39169
|
-
var
|
|
39170
|
-
var
|
|
39495
|
+
var import_promises13 = require("fs/promises");
|
|
39496
|
+
var import_path19 = require("path");
|
|
39171
39497
|
|
|
39172
39498
|
// ../commons/logging-execa/lib/loggingExeca.js
|
|
39173
39499
|
var import_execa = __toESM(require_execa(), 1);
|
|
@@ -39226,8 +39552,8 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
|
|
|
39226
39552
|
|
|
39227
39553
|
// src/autoversion/AutoVersioningService.ts
|
|
39228
39554
|
var import_fs13 = require("fs");
|
|
39229
|
-
var
|
|
39230
|
-
var
|
|
39555
|
+
var import_promises12 = require("fs/promises");
|
|
39556
|
+
var import_path18 = require("path");
|
|
39231
39557
|
var import_semver = __toESM(require_semver2());
|
|
39232
39558
|
var AutoVersioningException = class extends Error {
|
|
39233
39559
|
/**
|
|
@@ -39914,7 +40240,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39914
40240
|
);
|
|
39915
40241
|
return;
|
|
39916
40242
|
}
|
|
39917
|
-
const goModPath = (0,
|
|
40243
|
+
const goModPath = (0, import_path18.join)(workingDirectory, "go.mod");
|
|
39918
40244
|
if (!(0, import_fs13.existsSync)(goModPath)) {
|
|
39919
40245
|
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
39920
40246
|
return;
|
|
@@ -39948,7 +40274,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39948
40274
|
* Reads the module path from a go.mod file.
|
|
39949
40275
|
*/
|
|
39950
40276
|
async readGoModulePath(goModPath) {
|
|
39951
|
-
const content = await (0,
|
|
40277
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39952
40278
|
for (const line of content.split("\n")) {
|
|
39953
40279
|
const trimmed2 = line.trim();
|
|
39954
40280
|
if (trimmed2.startsWith("module ")) {
|
|
@@ -39961,7 +40287,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39961
40287
|
* Adds a version suffix to the module line in go.mod.
|
|
39962
40288
|
*/
|
|
39963
40289
|
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
39964
|
-
const content = await (0,
|
|
40290
|
+
const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
|
|
39965
40291
|
const lines = content.split("\n");
|
|
39966
40292
|
let changed = false;
|
|
39967
40293
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -39973,7 +40299,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39973
40299
|
}
|
|
39974
40300
|
}
|
|
39975
40301
|
if (changed) {
|
|
39976
|
-
await (0,
|
|
40302
|
+
await (0, import_promises12.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
39977
40303
|
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
39978
40304
|
}
|
|
39979
40305
|
}
|
|
@@ -39982,10 +40308,10 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
39982
40308
|
*/
|
|
39983
40309
|
async walkDirectory(dir, filter2) {
|
|
39984
40310
|
const results = [];
|
|
39985
|
-
const entries2 = await (0,
|
|
40311
|
+
const entries2 = await (0, import_promises12.readdir)(dir);
|
|
39986
40312
|
for (const entry of entries2) {
|
|
39987
|
-
const fullPath = (0,
|
|
39988
|
-
const entryStat = await (0,
|
|
40313
|
+
const fullPath = (0, import_path18.join)(dir, entry);
|
|
40314
|
+
const entryStat = await (0, import_promises12.stat)(fullPath);
|
|
39989
40315
|
if (entryStat.isDirectory()) {
|
|
39990
40316
|
if (entry === ".git" || entry === "vendor") {
|
|
39991
40317
|
continue;
|
|
@@ -40002,7 +40328,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40002
40328
|
* Adds a version suffix to import paths in all .go files in the repository.
|
|
40003
40329
|
*/
|
|
40004
40330
|
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
40005
|
-
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0,
|
|
40331
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path18.extname)(filePath) === ".go");
|
|
40006
40332
|
for (const filePath of goFiles) {
|
|
40007
40333
|
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
40008
40334
|
}
|
|
@@ -40012,7 +40338,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40012
40338
|
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
40013
40339
|
*/
|
|
40014
40340
|
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
40015
|
-
const content = await (0,
|
|
40341
|
+
const content = await (0, import_promises12.readFile)(filePath, "utf-8");
|
|
40016
40342
|
const lines = content.split("\n");
|
|
40017
40343
|
let changed = false;
|
|
40018
40344
|
let inImportBlock = false;
|
|
@@ -40041,7 +40367,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40041
40367
|
}
|
|
40042
40368
|
}
|
|
40043
40369
|
if (changed) {
|
|
40044
|
-
await (0,
|
|
40370
|
+
await (0, import_promises12.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
40045
40371
|
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
40046
40372
|
}
|
|
40047
40373
|
}
|
|
@@ -40446,7 +40772,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
40446
40772
|
}
|
|
40447
40773
|
async prependChangelogEntry(params) {
|
|
40448
40774
|
const { version: version2, entry } = params;
|
|
40449
|
-
const changelogPath = (0,
|
|
40775
|
+
const changelogPath = (0, import_path19.join)(this.outputDir, "changelog.md");
|
|
40450
40776
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
40451
40777
|
const header = `## [${version2}] - ${now}
|
|
40452
40778
|
`;
|
|
@@ -40455,7 +40781,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
40455
40781
|
`;
|
|
40456
40782
|
let existing = "";
|
|
40457
40783
|
if ((0, import_fs14.existsSync)(changelogPath)) {
|
|
40458
|
-
existing = await (0,
|
|
40784
|
+
existing = await (0, import_promises13.readFile)(changelogPath, "utf-8");
|
|
40459
40785
|
}
|
|
40460
40786
|
let output;
|
|
40461
40787
|
if (existing.trim().length === 0) {
|
|
@@ -40472,7 +40798,7 @@ ${newBlock}${remainder}`;
|
|
|
40472
40798
|
} else {
|
|
40473
40799
|
output = `${newBlock}${existing}`;
|
|
40474
40800
|
}
|
|
40475
|
-
await (0,
|
|
40801
|
+
await (0, import_promises13.writeFile)(changelogPath, output, "utf-8");
|
|
40476
40802
|
}
|
|
40477
40803
|
brandMessage(message) {
|
|
40478
40804
|
if (this.config.isWhitelabel) {
|
|
@@ -40654,8 +40980,8 @@ var GenerationCommitStep = class extends BaseStep {
|
|
|
40654
40980
|
};
|
|
40655
40981
|
|
|
40656
40982
|
// src/pipeline/steps/GithubStep.ts
|
|
40657
|
-
var
|
|
40658
|
-
var
|
|
40983
|
+
var import_promises14 = require("fs/promises");
|
|
40984
|
+
var import_path20 = require("path");
|
|
40659
40985
|
|
|
40660
40986
|
// src/pipeline/github/createReplayBranch.ts
|
|
40661
40987
|
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
@@ -41272,13 +41598,13 @@ var GithubStep = class extends BaseStep {
|
|
|
41272
41598
|
}
|
|
41273
41599
|
async ensureFernignore() {
|
|
41274
41600
|
this.logger.debug("Checking for .fernignore file...");
|
|
41275
|
-
const fernignorePath = (0,
|
|
41601
|
+
const fernignorePath = (0, import_path20.join)(this.outputDir, ".fernignore");
|
|
41276
41602
|
try {
|
|
41277
|
-
await (0,
|
|
41603
|
+
await (0, import_promises14.access)(fernignorePath);
|
|
41278
41604
|
this.logger.debug(".fernignore already exists");
|
|
41279
41605
|
} catch {
|
|
41280
41606
|
this.logger.debug("Creating .fernignore file...");
|
|
41281
|
-
await (0,
|
|
41607
|
+
await (0, import_promises14.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
41282
41608
|
}
|
|
41283
41609
|
}
|
|
41284
41610
|
deriveSkipCommit(replayResult) {
|
|
@@ -41571,7 +41897,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
41571
41897
|
const readmeConfig = await loadReadmeConfig({
|
|
41572
41898
|
absolutePathToConfig: resolve(wd, argv.config)
|
|
41573
41899
|
});
|
|
41574
|
-
const originalReadmeContent = argv.originalReadme != null ? await (0,
|
|
41900
|
+
const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises15.readFile)(argv.originalReadme, "utf8") : void 0;
|
|
41575
41901
|
await generateReadmeToStream({
|
|
41576
41902
|
originalReadmeContent,
|
|
41577
41903
|
readmeConfig,
|
|
@@ -41699,7 +42025,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
41699
42025
|
try {
|
|
41700
42026
|
const wd = cwd();
|
|
41701
42027
|
const configValue = argv.config;
|
|
41702
|
-
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0,
|
|
42028
|
+
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises15.readFile)(resolve(wd, configValue), "utf-8");
|
|
41703
42029
|
let config;
|
|
41704
42030
|
try {
|
|
41705
42031
|
config = JSON.parse(configContent);
|
|
@@ -41995,7 +42321,7 @@ async function createWriteStream(outputPath) {
|
|
|
41995
42321
|
}
|
|
41996
42322
|
async function createWriteStreamFromFile(filepath) {
|
|
41997
42323
|
if (!await doesPathExist(filepath)) {
|
|
41998
|
-
await (0,
|
|
42324
|
+
await (0, import_promises15.mkdir)(import_path21.default.dirname(filepath), { recursive: true });
|
|
41999
42325
|
}
|
|
42000
42326
|
return import_fs15.default.createWriteStream(filepath);
|
|
42001
42327
|
}
|