@fern-api/generator-cli 0.9.28 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +5 -2
- package/dist/cli.js +1615 -1428
- 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: dirname6, 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 tmp4, stats = statSync4(dir);
|
|
7039
7039
|
if (!stats.isDirectory()) {
|
|
7040
|
-
dir =
|
|
7040
|
+
dir = dirname6(dir);
|
|
7041
7041
|
}
|
|
7042
7042
|
while (true) {
|
|
7043
7043
|
tmp4 = callback(dir, readdirSync2(dir));
|
|
7044
7044
|
if (tmp4) return resolve8(dir, tmp4);
|
|
7045
|
-
dir =
|
|
7045
|
+
dir = dirname6(tmp4 = dir);
|
|
7046
7046
|
if (tmp4 === 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 join17 = require("path").join;
|
|
7082
7082
|
var resolve8 = require("path").resolve;
|
|
7083
|
-
var
|
|
7083
|
+
var dirname6 = 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 ? dirname6(m.filename) : resolve8(dirname6(m.filename), path5);
|
|
7117
7117
|
fs3.readdirSync(path5).forEach(function(filename) {
|
|
7118
|
-
var joined =
|
|
7118
|
+
var joined = join17(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) {
|
|
@@ -22126,7 +22126,7 @@ var init_diff3 = __esm({
|
|
|
22126
22126
|
}
|
|
22127
22127
|
});
|
|
22128
22128
|
|
|
22129
|
-
// ../../node_modules/.pnpm/@fern-api+replay@0.15.
|
|
22129
|
+
// ../../node_modules/.pnpm/@fern-api+replay@0.15.2/node_modules/@fern-api/replay/dist/index.js
|
|
22130
22130
|
var dist_exports = {};
|
|
22131
22131
|
__export(dist_exports, {
|
|
22132
22132
|
CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
|
|
@@ -22468,73 +22468,55 @@ function parseRevertedMessage(subject) {
|
|
|
22468
22468
|
const match2 = subject.match(/^Revert "(.+)"$/);
|
|
22469
22469
|
return match2?.[1];
|
|
22470
22470
|
}
|
|
22471
|
-
function
|
|
22472
|
-
|
|
22471
|
+
function isBinaryFile(filePath) {
|
|
22472
|
+
const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
|
|
22473
|
+
return BINARY_EXTENSIONS.has(ext2);
|
|
22473
22474
|
}
|
|
22474
|
-
function
|
|
22475
|
-
const
|
|
22476
|
-
let
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
break;
|
|
22494
|
-
}
|
|
22495
|
-
j2++;
|
|
22475
|
+
function parsePatchFileHeaders(patchContent) {
|
|
22476
|
+
const results = [];
|
|
22477
|
+
let currentPath = null;
|
|
22478
|
+
let currentIsCreate = false;
|
|
22479
|
+
let currentIsDelete = false;
|
|
22480
|
+
const flush2 = () => {
|
|
22481
|
+
if (currentPath !== null) {
|
|
22482
|
+
results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
|
|
22483
|
+
}
|
|
22484
|
+
};
|
|
22485
|
+
for (const line of patchContent.split("\n")) {
|
|
22486
|
+
if (line.startsWith("diff --git ")) {
|
|
22487
|
+
flush2();
|
|
22488
|
+
currentPath = null;
|
|
22489
|
+
currentIsCreate = false;
|
|
22490
|
+
currentIsDelete = false;
|
|
22491
|
+
const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
22492
|
+
if (match2) {
|
|
22493
|
+
currentPath = match2[2] ?? null;
|
|
22496
22494
|
}
|
|
22497
|
-
|
|
22498
|
-
|
|
22499
|
-
|
|
22495
|
+
} else if (currentPath !== null) {
|
|
22496
|
+
if (line.startsWith("new file mode ")) {
|
|
22497
|
+
currentIsCreate = true;
|
|
22498
|
+
} else if (line.startsWith("deleted file mode ")) {
|
|
22499
|
+
currentIsDelete = true;
|
|
22500
22500
|
}
|
|
22501
22501
|
}
|
|
22502
|
-
i2++;
|
|
22503
22502
|
}
|
|
22504
|
-
|
|
22503
|
+
flush2();
|
|
22504
|
+
return results;
|
|
22505
22505
|
}
|
|
22506
|
-
function
|
|
22507
|
-
const
|
|
22508
|
-
const
|
|
22509
|
-
|
|
22510
|
-
|
|
22506
|
+
async function capturePatchSnapshot(git, files, treeish) {
|
|
22507
|
+
const snapshot = {};
|
|
22508
|
+
for (const file of files) {
|
|
22509
|
+
if (isBinaryFile(file)) continue;
|
|
22510
|
+
const content = await git.showFile(treeish, file).catch(() => null);
|
|
22511
|
+
if (content != null) snapshot[file] = content;
|
|
22511
22512
|
}
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
}
|
|
22520
|
-
if (i2 > range2.start && i2 < range2.separator) {
|
|
22521
|
-
result.push(lines[i2]);
|
|
22522
|
-
continue;
|
|
22523
|
-
}
|
|
22524
|
-
if (i2 === range2.separator) {
|
|
22525
|
-
continue;
|
|
22526
|
-
}
|
|
22527
|
-
if (i2 > range2.separator && i2 < range2.end) {
|
|
22528
|
-
continue;
|
|
22529
|
-
}
|
|
22530
|
-
if (i2 === range2.end) {
|
|
22531
|
-
rangeIdx++;
|
|
22532
|
-
continue;
|
|
22533
|
-
}
|
|
22534
|
-
}
|
|
22535
|
-
result.push(lines[i2]);
|
|
22536
|
-
}
|
|
22537
|
-
return result.join("\n");
|
|
22513
|
+
return snapshot;
|
|
22514
|
+
}
|
|
22515
|
+
function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
|
|
22516
|
+
const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
22517
|
+
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
22518
|
+
const files = allFiles.filter((f) => !isSensitiveFile(f));
|
|
22519
|
+
return { files, sensitiveFiles };
|
|
22538
22520
|
}
|
|
22539
22521
|
function threeWayMerge(base, ours, theirs) {
|
|
22540
22522
|
const baseLines = base.split("\n");
|
|
@@ -22672,9 +22654,331 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
22672
22654
|
}
|
|
22673
22655
|
return result;
|
|
22674
22656
|
}
|
|
22675
|
-
function
|
|
22676
|
-
const
|
|
22677
|
-
|
|
22657
|
+
async function resolveInputs(args) {
|
|
22658
|
+
const {
|
|
22659
|
+
patch,
|
|
22660
|
+
filePath,
|
|
22661
|
+
git,
|
|
22662
|
+
lockManager,
|
|
22663
|
+
outputDir,
|
|
22664
|
+
isTreeReachable,
|
|
22665
|
+
resolveBaseGeneration,
|
|
22666
|
+
resolveFilePath,
|
|
22667
|
+
extractRenameSource,
|
|
22668
|
+
extractFileDiff: extractFileDiff2
|
|
22669
|
+
} = args;
|
|
22670
|
+
const baseGen = await resolveBaseGeneration(patch.base_generation);
|
|
22671
|
+
if (!baseGen) {
|
|
22672
|
+
return {
|
|
22673
|
+
resolvedPath: filePath,
|
|
22674
|
+
metadata: {
|
|
22675
|
+
patchId: patch.id,
|
|
22676
|
+
patchMessage: patch.original_message,
|
|
22677
|
+
baseGeneration: patch.base_generation,
|
|
22678
|
+
currentGeneration: lockManager.read().current_generation
|
|
22679
|
+
},
|
|
22680
|
+
base: null,
|
|
22681
|
+
ours: null,
|
|
22682
|
+
oursPath: (0, import_path11.join)(outputDir, filePath),
|
|
22683
|
+
renameSourcePath: void 0,
|
|
22684
|
+
ghostTheirs: null,
|
|
22685
|
+
earlyExit: { status: "skipped", reason: "base-generation-not-found" }
|
|
22686
|
+
};
|
|
22687
|
+
}
|
|
22688
|
+
const lock = lockManager.read();
|
|
22689
|
+
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
22690
|
+
const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
|
|
22691
|
+
const resolvedPath = await resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
22692
|
+
const metadata = {
|
|
22693
|
+
patchId: patch.id,
|
|
22694
|
+
patchMessage: patch.original_message,
|
|
22695
|
+
baseGeneration: patch.base_generation,
|
|
22696
|
+
currentGeneration: lock.current_generation
|
|
22697
|
+
};
|
|
22698
|
+
let base = await git.showFile(baseGen.tree_hash, filePath);
|
|
22699
|
+
let renameSourcePath;
|
|
22700
|
+
if (!base) {
|
|
22701
|
+
const renameSource = extractRenameSource(patch.patch_content, filePath);
|
|
22702
|
+
if (renameSource) {
|
|
22703
|
+
base = await git.showFile(baseGen.tree_hash, renameSource);
|
|
22704
|
+
renameSourcePath = renameSource;
|
|
22705
|
+
}
|
|
22706
|
+
}
|
|
22707
|
+
const oursPath = (0, import_path11.join)(outputDir, resolvedPath);
|
|
22708
|
+
const ours = await (0, import_promises5.readFile)(oursPath, "utf-8").catch(() => null);
|
|
22709
|
+
let ghostTheirs = null;
|
|
22710
|
+
if (!base && ours && !renameSourcePath) {
|
|
22711
|
+
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
22712
|
+
if (!treeReachable) {
|
|
22713
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
22714
|
+
if (fileDiff) {
|
|
22715
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
22716
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
22717
|
+
if (result) {
|
|
22718
|
+
base = result.base;
|
|
22719
|
+
ghostTheirs = result.theirs;
|
|
22720
|
+
}
|
|
22721
|
+
}
|
|
22722
|
+
}
|
|
22723
|
+
}
|
|
22724
|
+
return {
|
|
22725
|
+
resolvedPath,
|
|
22726
|
+
metadata,
|
|
22727
|
+
base,
|
|
22728
|
+
ours,
|
|
22729
|
+
oursPath,
|
|
22730
|
+
renameSourcePath,
|
|
22731
|
+
ghostTheirs
|
|
22732
|
+
};
|
|
22733
|
+
}
|
|
22734
|
+
function trimCR(line) {
|
|
22735
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
22736
|
+
}
|
|
22737
|
+
function findConflictRanges(lines) {
|
|
22738
|
+
const ranges = [];
|
|
22739
|
+
let i2 = 0;
|
|
22740
|
+
while (i2 < lines.length) {
|
|
22741
|
+
if (trimCR(lines[i2]) === CONFLICT_OPENER) {
|
|
22742
|
+
let separatorIdx = -1;
|
|
22743
|
+
let j2 = i2 + 1;
|
|
22744
|
+
let found = false;
|
|
22745
|
+
while (j2 < lines.length) {
|
|
22746
|
+
const trimmed2 = trimCR(lines[j2]);
|
|
22747
|
+
if (trimmed2 === CONFLICT_OPENER) {
|
|
22748
|
+
break;
|
|
22749
|
+
}
|
|
22750
|
+
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
22751
|
+
separatorIdx = j2;
|
|
22752
|
+
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
22753
|
+
ranges.push({ start: i2, separator: separatorIdx, end: j2 });
|
|
22754
|
+
i2 = j2;
|
|
22755
|
+
found = true;
|
|
22756
|
+
break;
|
|
22757
|
+
}
|
|
22758
|
+
j2++;
|
|
22759
|
+
}
|
|
22760
|
+
if (!found) {
|
|
22761
|
+
i2++;
|
|
22762
|
+
continue;
|
|
22763
|
+
}
|
|
22764
|
+
}
|
|
22765
|
+
i2++;
|
|
22766
|
+
}
|
|
22767
|
+
return ranges;
|
|
22768
|
+
}
|
|
22769
|
+
function stripConflictMarkers(content) {
|
|
22770
|
+
const lines = content.split(/\r?\n/);
|
|
22771
|
+
const ranges = findConflictRanges(lines);
|
|
22772
|
+
if (ranges.length === 0) {
|
|
22773
|
+
return content;
|
|
22774
|
+
}
|
|
22775
|
+
const result = [];
|
|
22776
|
+
let rangeIdx = 0;
|
|
22777
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
22778
|
+
if (rangeIdx < ranges.length) {
|
|
22779
|
+
const range2 = ranges[rangeIdx];
|
|
22780
|
+
if (i2 === range2.start) {
|
|
22781
|
+
continue;
|
|
22782
|
+
}
|
|
22783
|
+
if (i2 > range2.start && i2 < range2.separator) {
|
|
22784
|
+
result.push(lines[i2]);
|
|
22785
|
+
continue;
|
|
22786
|
+
}
|
|
22787
|
+
if (i2 === range2.separator) {
|
|
22788
|
+
continue;
|
|
22789
|
+
}
|
|
22790
|
+
if (i2 > range2.separator && i2 < range2.end) {
|
|
22791
|
+
continue;
|
|
22792
|
+
}
|
|
22793
|
+
if (i2 === range2.end) {
|
|
22794
|
+
rangeIdx++;
|
|
22795
|
+
continue;
|
|
22796
|
+
}
|
|
22797
|
+
}
|
|
22798
|
+
result.push(lines[i2]);
|
|
22799
|
+
}
|
|
22800
|
+
return result.join("\n");
|
|
22801
|
+
}
|
|
22802
|
+
function hasConflictMarkerStrings(content) {
|
|
22803
|
+
if (content == null) return false;
|
|
22804
|
+
return content.includes(CONFLICT_OPENER) || content.includes(CONFLICT_CLOSER);
|
|
22805
|
+
}
|
|
22806
|
+
async function buildMergePlan(args) {
|
|
22807
|
+
const {
|
|
22808
|
+
inputs,
|
|
22809
|
+
patch,
|
|
22810
|
+
filePath,
|
|
22811
|
+
accumulator,
|
|
22812
|
+
tempGit,
|
|
22813
|
+
tempDir,
|
|
22814
|
+
applyPatchToContent: applyPatchToContent2,
|
|
22815
|
+
isPatchAlreadyApplied
|
|
22816
|
+
} = args;
|
|
22817
|
+
const { base, ours, resolvedPath, renameSourcePath, ghostTheirs } = inputs;
|
|
22818
|
+
let theirs = ghostTheirs;
|
|
22819
|
+
const ghostReconstructed = ghostTheirs != null;
|
|
22820
|
+
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
22821
|
+
const fileIsShared = accumulator.isShared(resolvedPath) || accumulator.isShared(filePath);
|
|
22822
|
+
const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
|
|
22823
|
+
let primarySource = ghostReconstructed ? "ghost" : "reconstructed";
|
|
22824
|
+
if (!ghostReconstructed) {
|
|
22825
|
+
if (useSnapshotAsPrimary) {
|
|
22826
|
+
theirs = snapshotForFile;
|
|
22827
|
+
primarySource = "snapshot";
|
|
22828
|
+
} else {
|
|
22829
|
+
theirs = await applyPatchToContent2(
|
|
22830
|
+
base,
|
|
22831
|
+
patch.patch_content,
|
|
22832
|
+
filePath,
|
|
22833
|
+
tempGit,
|
|
22834
|
+
tempDir,
|
|
22835
|
+
renameSourcePath
|
|
22836
|
+
);
|
|
22837
|
+
const reconstructionHasMarkers = hasConflictMarkerStrings(theirs);
|
|
22838
|
+
const baseHadMarkers = hasConflictMarkerStrings(base);
|
|
22839
|
+
if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
|
|
22840
|
+
theirs = snapshotForFile;
|
|
22841
|
+
}
|
|
22842
|
+
}
|
|
22843
|
+
}
|
|
22844
|
+
const accumulatorEntry = accumulator.lookup(resolvedPath);
|
|
22845
|
+
if (theirs) {
|
|
22846
|
+
const theirsHasMarkers = hasConflictMarkerStrings(theirs);
|
|
22847
|
+
const baseHasMarkers = hasConflictMarkerStrings(base);
|
|
22848
|
+
if (theirsHasMarkers && !baseHasMarkers) {
|
|
22849
|
+
if (accumulatorEntry) {
|
|
22850
|
+
theirs = null;
|
|
22851
|
+
} else {
|
|
22852
|
+
return { kind: "skipped", reason: "stale-conflict-markers" };
|
|
22853
|
+
}
|
|
22854
|
+
}
|
|
22855
|
+
}
|
|
22856
|
+
let useAccumulatorAsMergeBase = false;
|
|
22857
|
+
if (accumulatorEntry && (theirs == null || base == null)) {
|
|
22858
|
+
theirs = await applyPatchToContent2(
|
|
22859
|
+
accumulatorEntry.content,
|
|
22860
|
+
patch.patch_content,
|
|
22861
|
+
filePath,
|
|
22862
|
+
tempGit,
|
|
22863
|
+
tempDir
|
|
22864
|
+
);
|
|
22865
|
+
if (theirs != null) {
|
|
22866
|
+
useAccumulatorAsMergeBase = true;
|
|
22867
|
+
} else if (await isPatchAlreadyApplied(
|
|
22868
|
+
patch.patch_content,
|
|
22869
|
+
filePath,
|
|
22870
|
+
accumulatorEntry.content,
|
|
22871
|
+
tempGit,
|
|
22872
|
+
tempDir
|
|
22873
|
+
)) {
|
|
22874
|
+
theirs = accumulatorEntry.content;
|
|
22875
|
+
useAccumulatorAsMergeBase = true;
|
|
22876
|
+
}
|
|
22877
|
+
}
|
|
22878
|
+
if (theirs) {
|
|
22879
|
+
const theirsHasMarkers = hasConflictMarkerStrings(theirs);
|
|
22880
|
+
const accBaseHasMarkers = accumulatorEntry != null && hasConflictMarkerStrings(accumulatorEntry.content);
|
|
22881
|
+
if (theirsHasMarkers && !accBaseHasMarkers && !hasConflictMarkerStrings(base)) {
|
|
22882
|
+
return { kind: "skipped", reason: "stale-conflict-markers" };
|
|
22883
|
+
}
|
|
22884
|
+
}
|
|
22885
|
+
let effective_theirs = theirs;
|
|
22886
|
+
let conflictReasonHint;
|
|
22887
|
+
if (theirs != null && base != null && !useAccumulatorAsMergeBase) {
|
|
22888
|
+
if (accumulatorEntry && accumulatorEntry.baseGeneration === patch.base_generation) {
|
|
22889
|
+
try {
|
|
22890
|
+
const preMerged = threeWayMerge(base, accumulatorEntry.content, theirs);
|
|
22891
|
+
if (!preMerged.hasConflicts) {
|
|
22892
|
+
effective_theirs = preMerged.content;
|
|
22893
|
+
} else {
|
|
22894
|
+
effective_theirs = theirs;
|
|
22895
|
+
}
|
|
22896
|
+
} catch {
|
|
22897
|
+
effective_theirs = theirs;
|
|
22898
|
+
}
|
|
22899
|
+
} else if (accumulatorEntry) {
|
|
22900
|
+
conflictReasonHint = "base-generation-mismatch";
|
|
22901
|
+
}
|
|
22902
|
+
}
|
|
22903
|
+
if (base == null && ours == null && effective_theirs != null) {
|
|
22904
|
+
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
22905
|
+
}
|
|
22906
|
+
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
22907
|
+
return { kind: "new-file-both", theirs: effective_theirs };
|
|
22908
|
+
}
|
|
22909
|
+
if (effective_theirs == null) {
|
|
22910
|
+
return { kind: "skipped", reason: "missing-content" };
|
|
22911
|
+
}
|
|
22912
|
+
if (base == null && !useAccumulatorAsMergeBase || ours == null) {
|
|
22913
|
+
return { kind: "skipped", reason: "missing-content" };
|
|
22914
|
+
}
|
|
22915
|
+
if (useAccumulatorAsMergeBase) {
|
|
22916
|
+
const mergeBase = accumulatorEntry?.content ?? base;
|
|
22917
|
+
if (mergeBase == null) {
|
|
22918
|
+
return { kind: "skipped", reason: "missing-content" };
|
|
22919
|
+
}
|
|
22920
|
+
return {
|
|
22921
|
+
kind: "accumulator-base",
|
|
22922
|
+
theirs: effective_theirs,
|
|
22923
|
+
mergeBase,
|
|
22924
|
+
conflictReasonHint
|
|
22925
|
+
};
|
|
22926
|
+
}
|
|
22927
|
+
return {
|
|
22928
|
+
kind: primarySource,
|
|
22929
|
+
base,
|
|
22930
|
+
theirs: effective_theirs,
|
|
22931
|
+
mergeBase: base,
|
|
22932
|
+
conflictReasonHint
|
|
22933
|
+
};
|
|
22934
|
+
}
|
|
22935
|
+
async function runMergeAndRecord(args) {
|
|
22936
|
+
const { plan, inputs, patch, accumulator } = args;
|
|
22937
|
+
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
22938
|
+
if (plan.kind === "skipped") {
|
|
22939
|
+
return { file: resolvedPath, status: "skipped", reason: plan.reason };
|
|
22940
|
+
}
|
|
22941
|
+
if (plan.kind === "new-file-only-user") {
|
|
22942
|
+
accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
|
|
22943
|
+
await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
|
|
22944
|
+
await (0, import_promises6.writeFile)(oursPath, plan.theirs);
|
|
22945
|
+
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
22946
|
+
}
|
|
22947
|
+
if (plan.kind === "new-file-both") {
|
|
22948
|
+
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
22949
|
+
await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
|
|
22950
|
+
await (0, import_promises6.writeFile)(oursPath, merged2.content);
|
|
22951
|
+
if (merged2.hasConflicts) {
|
|
22952
|
+
return {
|
|
22953
|
+
file: resolvedPath,
|
|
22954
|
+
status: "conflict",
|
|
22955
|
+
conflicts: merged2.conflicts,
|
|
22956
|
+
conflictReason: "new-file-both",
|
|
22957
|
+
conflictMetadata: metadata
|
|
22958
|
+
};
|
|
22959
|
+
}
|
|
22960
|
+
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
22961
|
+
return { file: resolvedPath, status: "merged" };
|
|
22962
|
+
}
|
|
22963
|
+
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
22964
|
+
await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
|
|
22965
|
+
await (0, import_promises6.writeFile)(oursPath, merged.content);
|
|
22966
|
+
if (merged.hasConflicts) {
|
|
22967
|
+
accumulator.recordConflict(resolvedPath, plan.theirs, patch.base_generation);
|
|
22968
|
+
return {
|
|
22969
|
+
file: resolvedPath,
|
|
22970
|
+
status: "conflict",
|
|
22971
|
+
conflicts: merged.conflicts,
|
|
22972
|
+
conflictReason: plan.conflictReasonHint ?? "same-line-edit",
|
|
22973
|
+
conflictMetadata: metadata
|
|
22974
|
+
};
|
|
22975
|
+
}
|
|
22976
|
+
accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
|
|
22977
|
+
return {
|
|
22978
|
+
file: resolvedPath,
|
|
22979
|
+
status: "merged",
|
|
22980
|
+
...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
|
|
22981
|
+
};
|
|
22678
22982
|
}
|
|
22679
22983
|
function computeDroppedContextLinesForFile(base, theirs, finalContent) {
|
|
22680
22984
|
const baseLines = base.split("\n");
|
|
@@ -22704,37 +23008,6 @@ function isDiffLineForFile(diffLine, filePath) {
|
|
|
22704
23008
|
const match2 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
22705
23009
|
return match2 !== null && match2[1] === filePath;
|
|
22706
23010
|
}
|
|
22707
|
-
function parsePatchFileHeaders(patchContent) {
|
|
22708
|
-
const results = [];
|
|
22709
|
-
let currentPath = null;
|
|
22710
|
-
let currentIsCreate = false;
|
|
22711
|
-
let currentIsDelete = false;
|
|
22712
|
-
const flush2 = () => {
|
|
22713
|
-
if (currentPath !== null) {
|
|
22714
|
-
results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
|
|
22715
|
-
}
|
|
22716
|
-
};
|
|
22717
|
-
for (const line of patchContent.split("\n")) {
|
|
22718
|
-
if (line.startsWith("diff --git ")) {
|
|
22719
|
-
flush2();
|
|
22720
|
-
currentPath = null;
|
|
22721
|
-
currentIsCreate = false;
|
|
22722
|
-
currentIsDelete = false;
|
|
22723
|
-
const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
22724
|
-
if (match2) {
|
|
22725
|
-
currentPath = match2[2] ?? null;
|
|
22726
|
-
}
|
|
22727
|
-
} else if (currentPath !== null) {
|
|
22728
|
-
if (line.startsWith("new file mode ")) {
|
|
22729
|
-
currentIsCreate = true;
|
|
22730
|
-
} else if (line.startsWith("deleted file mode ")) {
|
|
22731
|
-
currentIsDelete = true;
|
|
22732
|
-
}
|
|
22733
|
-
}
|
|
22734
|
-
}
|
|
22735
|
-
flush2();
|
|
22736
|
-
return results;
|
|
22737
|
-
}
|
|
22738
23011
|
function normalizeHunkBody(hunk) {
|
|
22739
23012
|
let contextC = 0;
|
|
22740
23013
|
let removeC = 0;
|
|
@@ -22939,17 +23212,17 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
22939
23212
|
}
|
|
22940
23213
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
22941
23214
|
if (beforeContent === afterContent) return "";
|
|
22942
|
-
const tmp4 = await (0,
|
|
23215
|
+
const tmp4 = await (0, import_promises8.mkdtemp)((0, import_path14.join)((0, import_os3.tmpdir)(), "fern-replay-pp-"));
|
|
22943
23216
|
try {
|
|
22944
|
-
const beforePath = (0,
|
|
22945
|
-
const afterPath = (0,
|
|
22946
|
-
await (0,
|
|
22947
|
-
await (0,
|
|
23217
|
+
const beforePath = (0, import_path14.join)(tmp4, "before");
|
|
23218
|
+
const afterPath = (0, import_path14.join)(tmp4, "after");
|
|
23219
|
+
await (0, import_promises8.writeFile)(beforePath, beforeContent, "utf-8");
|
|
23220
|
+
await (0, import_promises8.writeFile)(afterPath, afterContent, "utf-8");
|
|
22948
23221
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
22949
23222
|
if (!raw.trim()) return "";
|
|
22950
23223
|
return rewriteDiffHeaders(raw, filePath);
|
|
22951
23224
|
} finally {
|
|
22952
|
-
await (0,
|
|
23225
|
+
await (0, import_promises8.rm)(tmp4, { recursive: true, force: true });
|
|
22953
23226
|
}
|
|
22954
23227
|
}
|
|
22955
23228
|
function runGitDiffNoIndex(beforePath, afterPath) {
|
|
@@ -23020,6 +23293,37 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
23020
23293
|
const trailer = hasTrailingNewline ? "" : "\n\";
|
|
23021
23294
|
return header + "\n" + body + trailer + "\n";
|
|
23022
23295
|
}
|
|
23296
|
+
function fileIsCreationInPatchContent(patchContent, file) {
|
|
23297
|
+
const lines = patchContent.split("\n");
|
|
23298
|
+
let inFileSection = false;
|
|
23299
|
+
for (const line of lines) {
|
|
23300
|
+
if (line.startsWith("diff --git ")) {
|
|
23301
|
+
inFileSection = line.includes(` b/${file}`) || line.endsWith(` b/${file}`);
|
|
23302
|
+
continue;
|
|
23303
|
+
}
|
|
23304
|
+
if (inFileSection && line.startsWith("--- ")) {
|
|
23305
|
+
return line === "--- /dev/null";
|
|
23306
|
+
}
|
|
23307
|
+
}
|
|
23308
|
+
return false;
|
|
23309
|
+
}
|
|
23310
|
+
function firstGenerationReport() {
|
|
23311
|
+
return {
|
|
23312
|
+
flow: "first-generation",
|
|
23313
|
+
patchesDetected: 0,
|
|
23314
|
+
patchesApplied: 0,
|
|
23315
|
+
patchesWithConflicts: 0,
|
|
23316
|
+
patchesSkipped: 0,
|
|
23317
|
+
conflicts: []
|
|
23318
|
+
};
|
|
23319
|
+
}
|
|
23320
|
+
function assertActive(prep) {
|
|
23321
|
+
if (prep._prepared.kind !== "active") {
|
|
23322
|
+
throw new Error(
|
|
23323
|
+
`Flow.apply() called with terminal preparation (kind=${prep._prepared.kind}); terminal cases must be handled by applyPreparedReplay's dispatcher`
|
|
23324
|
+
);
|
|
23325
|
+
}
|
|
23326
|
+
}
|
|
23023
23327
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
23024
23328
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
23025
23329
|
for (const r2 of results) resultByPatchId.set(r2.patch.id, r2);
|
|
@@ -23211,7 +23515,7 @@ function parseGitLog(log) {
|
|
|
23211
23515
|
});
|
|
23212
23516
|
}
|
|
23213
23517
|
function ensureFernignoreEntries(outputDir) {
|
|
23214
|
-
const fernignorePath = (0,
|
|
23518
|
+
const fernignorePath = (0, import_path16.join)(outputDir, ".fernignore");
|
|
23215
23519
|
let content = "";
|
|
23216
23520
|
if ((0, import_fs9.existsSync)(fernignorePath)) {
|
|
23217
23521
|
content = (0, import_fs9.readFileSync)(fernignorePath, "utf-8");
|
|
@@ -23234,7 +23538,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
23234
23538
|
return true;
|
|
23235
23539
|
}
|
|
23236
23540
|
function ensureGitattributesEntries(outputDir) {
|
|
23237
|
-
const gitattributesPath = (0,
|
|
23541
|
+
const gitattributesPath = (0, import_path16.join)(outputDir, ".gitattributes");
|
|
23238
23542
|
let content = "";
|
|
23239
23543
|
if ((0, import_fs9.existsSync)(gitattributesPath)) {
|
|
23240
23544
|
content = (0, import_fs9.readFileSync)(gitattributesPath, "utf-8");
|
|
@@ -23462,7 +23766,7 @@ async function resolve7(outputDir, options) {
|
|
|
23462
23766
|
const result = await computePerPatchDiffWithFallback(
|
|
23463
23767
|
patch,
|
|
23464
23768
|
(f) => git.showFile(currentGen, f),
|
|
23465
|
-
(f) => (0,
|
|
23769
|
+
(f) => (0, import_promises9.readFile)((0, import_path17.join)(outputDir, f), "utf-8").catch(() => null),
|
|
23466
23770
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
23467
23771
|
);
|
|
23468
23772
|
if (result === null) {
|
|
@@ -23518,7 +23822,7 @@ async function resolve7(outputDir, options) {
|
|
|
23518
23822
|
const theirsSnapshot = {};
|
|
23519
23823
|
for (const file of filesForSnapshot) {
|
|
23520
23824
|
if (isBinaryFile(file)) continue;
|
|
23521
|
-
const content = await (0,
|
|
23825
|
+
const content = await (0, import_promises9.readFile)((0, import_path17.join)(outputDir, file), "utf-8").catch(() => null);
|
|
23522
23826
|
if (content != null) {
|
|
23523
23827
|
theirsSnapshot[file] = content;
|
|
23524
23828
|
}
|
|
@@ -23628,9 +23932,9 @@ function status(outputDir) {
|
|
|
23628
23932
|
excludePatterns
|
|
23629
23933
|
};
|
|
23630
23934
|
}
|
|
23631
|
-
var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_promises4, import_os2,
|
|
23935
|
+
var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_path9, import_promises4, import_os2, import_path10, import_promises5, import_path11, import_promises6, import_path12, import_fs7, import_promises7, import_path13, import_promises8, import_os3, import_path14, import_child_process3, import_crypto2, import_fs8, import_path15, import_yaml2, import_crypto3, import_fs9, import_path16, import_fs10, import_promises9, import_path17, __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, BINARY_EXTENSIONS, INFRASTRUCTURE_FILES, ReplayDetector, MergeAccumulator, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, ReplayApplicator, ReplayCommitter, FileOwnership, FirstGenerationFlow, SkipApplicationFlow, NoPatchesFlow, NormalRegenerationFlow, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
|
|
23632
23936
|
var init_dist3 = __esm({
|
|
23633
|
-
"../../node_modules/.pnpm/@fern-api+replay@0.15.
|
|
23937
|
+
"../../node_modules/.pnpm/@fern-api+replay@0.15.2/node_modules/@fern-api/replay/dist/index.js"() {
|
|
23634
23938
|
"use strict";
|
|
23635
23939
|
init_esm4();
|
|
23636
23940
|
import_promises3 = require("fs/promises");
|
|
@@ -23640,31 +23944,37 @@ var init_dist3 = __esm({
|
|
|
23640
23944
|
import_path8 = require("path");
|
|
23641
23945
|
import_yaml = __toESM(require_dist3(), 1);
|
|
23642
23946
|
import_crypto = require("crypto");
|
|
23643
|
-
import_promises4 = require("fs/promises");
|
|
23644
|
-
import_os2 = require("os");
|
|
23645
23947
|
import_path9 = require("path");
|
|
23646
|
-
init_esm3();
|
|
23647
23948
|
init_diff3();
|
|
23648
|
-
|
|
23649
|
-
|
|
23949
|
+
import_promises4 = require("fs/promises");
|
|
23950
|
+
import_os2 = require("os");
|
|
23650
23951
|
import_path10 = require("path");
|
|
23651
23952
|
init_esm3();
|
|
23953
|
+
import_promises5 = require("fs/promises");
|
|
23954
|
+
import_path11 = require("path");
|
|
23652
23955
|
import_promises6 = require("fs/promises");
|
|
23956
|
+
import_path12 = require("path");
|
|
23957
|
+
import_fs7 = require("fs");
|
|
23958
|
+
import_promises7 = require("fs/promises");
|
|
23959
|
+
import_path13 = require("path");
|
|
23960
|
+
init_esm3();
|
|
23961
|
+
import_promises8 = require("fs/promises");
|
|
23653
23962
|
import_os3 = require("os");
|
|
23654
|
-
|
|
23963
|
+
import_path14 = require("path");
|
|
23655
23964
|
import_child_process3 = require("child_process");
|
|
23965
|
+
init_esm3();
|
|
23656
23966
|
import_crypto2 = require("crypto");
|
|
23657
23967
|
import_fs8 = require("fs");
|
|
23658
|
-
|
|
23968
|
+
import_path15 = require("path");
|
|
23659
23969
|
init_esm3();
|
|
23660
23970
|
import_yaml2 = __toESM(require_dist3(), 1);
|
|
23661
23971
|
import_crypto3 = require("crypto");
|
|
23662
23972
|
import_fs9 = require("fs");
|
|
23663
|
-
|
|
23973
|
+
import_path16 = require("path");
|
|
23664
23974
|
init_esm3();
|
|
23665
23975
|
import_fs10 = require("fs");
|
|
23666
|
-
|
|
23667
|
-
|
|
23976
|
+
import_promises9 = require("fs/promises");
|
|
23977
|
+
import_path17 = require("path");
|
|
23668
23978
|
__defProp3 = Object.defineProperty;
|
|
23669
23979
|
__getOwnPropNames3 = Object.getOwnPropertyNames;
|
|
23670
23980
|
__esm3 = (fn, res) => function __init() {
|
|
@@ -24044,9 +24354,6 @@ var init_dist3 = __esm({
|
|
|
24044
24354
|
}
|
|
24045
24355
|
}
|
|
24046
24356
|
};
|
|
24047
|
-
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
24048
|
-
CONFLICT_SEPARATOR = "=======";
|
|
24049
|
-
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
24050
24357
|
BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
24051
24358
|
".png",
|
|
24052
24359
|
".jpg",
|
|
@@ -24096,696 +24403,6 @@ var init_dist3 = __esm({
|
|
|
24096
24403
|
".pyo",
|
|
24097
24404
|
".DS_Store"
|
|
24098
24405
|
]);
|
|
24099
|
-
ReplayApplicator = class {
|
|
24100
|
-
git;
|
|
24101
|
-
lockManager;
|
|
24102
|
-
outputDir;
|
|
24103
|
-
renameCache = /* @__PURE__ */ new Map();
|
|
24104
|
-
treeExistsCache = /* @__PURE__ */ new Map();
|
|
24105
|
-
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
24106
|
-
/**
|
|
24107
|
-
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
24108
|
-
* of `applyPatches`, read by `mergeFile` to decide:
|
|
24109
|
-
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
24110
|
-
* - whether to populate the accumulator after a conflicted merge
|
|
24111
|
-
* (resolve mode populates so subsequent patches on the same file
|
|
24112
|
-
* can use patch A's THEIRS as a structurally-correct merge base
|
|
24113
|
-
* when their diff was authored against post-A structure)
|
|
24114
|
-
* - whether to retry THEIRS reconstruction against the accumulator
|
|
24115
|
-
* when the BASE-relative reconstruction produced markers from a
|
|
24116
|
-
* cross-patch context mismatch
|
|
24117
|
-
*/
|
|
24118
|
-
currentApplyMode = "replay";
|
|
24119
|
-
/**
|
|
24120
|
-
* Set of files that appear in 2+ patches in the current `applyPatches`
|
|
24121
|
-
* invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
|
|
24122
|
-
* and `populateAccumulatorForPatch` to decide whether to use the patch's
|
|
24123
|
-
* `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
|
|
24124
|
-
* back to line-anchored reconstruction (FER-9525 cross-patch isolation).
|
|
24125
|
-
*
|
|
24126
|
-
* Snapshot-as-primary is correct when the patch owns its file (no other
|
|
24127
|
-
* patch in this run touches it): the snapshot is what the customer
|
|
24128
|
-
* intends for that file post-regen, regardless of generator structural
|
|
24129
|
-
* changes that would invalidate the diff's line anchors.
|
|
24130
|
-
*
|
|
24131
|
-
* When a file IS shared, snapshots are CUMULATIVE across the patches
|
|
24132
|
-
* touching it (each one's snapshot includes prior patches' contributions).
|
|
24133
|
-
* Using a later patch's cumulative snapshot as its individual THEIRS
|
|
24134
|
-
* would propagate prior patches' edits into the merge — surfacing nested
|
|
24135
|
-
* conflicts at regions the patch alone never touched. Reconstruction
|
|
24136
|
-
* via `applyPatchToContent` is required there.
|
|
24137
|
-
*/
|
|
24138
|
-
sharedFiles = /* @__PURE__ */ new Set();
|
|
24139
|
-
constructor(git, lockManager, outputDir) {
|
|
24140
|
-
this.git = git;
|
|
24141
|
-
this.lockManager = lockManager;
|
|
24142
|
-
this.outputDir = outputDir;
|
|
24143
|
-
}
|
|
24144
|
-
/**
|
|
24145
|
-
* Resolve the GenerationRecord for a patch's base_generation.
|
|
24146
|
-
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
24147
|
-
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
24148
|
-
*/
|
|
24149
|
-
async resolveBaseGeneration(baseGeneration) {
|
|
24150
|
-
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24151
|
-
if (gen) return gen;
|
|
24152
|
-
try {
|
|
24153
|
-
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
24154
|
-
return {
|
|
24155
|
-
commit_sha: baseGeneration,
|
|
24156
|
-
tree_hash: treeHash,
|
|
24157
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24158
|
-
cli_version: "unknown",
|
|
24159
|
-
generator_versions: {}
|
|
24160
|
-
};
|
|
24161
|
-
} catch {
|
|
24162
|
-
return void 0;
|
|
24163
|
-
}
|
|
24164
|
-
}
|
|
24165
|
-
/** Reset inter-patch accumulator for a new cycle. */
|
|
24166
|
-
resetAccumulator() {
|
|
24167
|
-
this.fileTheirsAccumulator.clear();
|
|
24168
|
-
}
|
|
24169
|
-
/**
|
|
24170
|
-
* @internal Test-only.
|
|
24171
|
-
* Returns a defensive copy of the inter-patch accumulator. Used by the
|
|
24172
|
-
* adversarial test suite (FER-9791) to assert invariant I2: after every
|
|
24173
|
-
* applied patch, the accumulator has an entry for each file the patch
|
|
24174
|
-
* touched, and the entry's content matches on-disk.
|
|
24175
|
-
*/
|
|
24176
|
-
getAccumulatorSnapshot() {
|
|
24177
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
24178
|
-
for (const [path5, entry] of this.fileTheirsAccumulator) {
|
|
24179
|
-
snapshot.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
24180
|
-
}
|
|
24181
|
-
return snapshot;
|
|
24182
|
-
}
|
|
24183
|
-
/**
|
|
24184
|
-
* Apply all patches, returning results for each.
|
|
24185
|
-
* Skips patches that match exclude patterns in replay.yml
|
|
24186
|
-
*
|
|
24187
|
-
* `applyMode` controls the post-conflict marker strategy:
|
|
24188
|
-
* - `"replay"` (default): strip conflict markers from disk between
|
|
24189
|
-
* iterations whenever a later patch touches the same file. Lets the
|
|
24190
|
-
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
24191
|
-
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
24192
|
-
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
24193
|
-
* any markers that survive.
|
|
24194
|
-
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
24195
|
-
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
24196
|
-
* A's markers and B's clean writes when their regions don't overlap;
|
|
24197
|
-
* when they do overlap, the customer gets nested markers and
|
|
24198
|
-
* resolves manually. Either way they aren't silently dropped.
|
|
24199
|
-
*/
|
|
24200
|
-
async applyPatches(patches, opts) {
|
|
24201
|
-
const applyMode = opts?.applyMode ?? "replay";
|
|
24202
|
-
this.currentApplyMode = applyMode;
|
|
24203
|
-
this.resetAccumulator();
|
|
24204
|
-
const fileFreq = /* @__PURE__ */ new Map();
|
|
24205
|
-
for (const p2 of patches) {
|
|
24206
|
-
for (const f of p2.files) {
|
|
24207
|
-
fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
|
|
24208
|
-
}
|
|
24209
|
-
}
|
|
24210
|
-
this.sharedFiles = new Set(
|
|
24211
|
-
Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
|
|
24212
|
-
);
|
|
24213
|
-
const results = [];
|
|
24214
|
-
for (let i2 = 0; i2 < patches.length; i2++) {
|
|
24215
|
-
const patch = patches[i2];
|
|
24216
|
-
if (this.isExcluded(patch)) {
|
|
24217
|
-
results.push({
|
|
24218
|
-
patch,
|
|
24219
|
-
status: "skipped",
|
|
24220
|
-
method: "git-am"
|
|
24221
|
-
});
|
|
24222
|
-
continue;
|
|
24223
|
-
}
|
|
24224
|
-
const result = await this.applyPatchWithFallback(patch);
|
|
24225
|
-
results.push(result);
|
|
24226
|
-
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
24227
|
-
const laterFiles = /* @__PURE__ */ new Set();
|
|
24228
|
-
for (let j2 = i2 + 1; j2 < patches.length; j2++) {
|
|
24229
|
-
for (const f of patches[j2].files) {
|
|
24230
|
-
laterFiles.add(f);
|
|
24231
|
-
}
|
|
24232
|
-
}
|
|
24233
|
-
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
24234
|
-
if (result.resolvedFiles) {
|
|
24235
|
-
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
24236
|
-
resolvedToOriginal.set(resolved, orig);
|
|
24237
|
-
}
|
|
24238
|
-
}
|
|
24239
|
-
for (const fileResult of result.fileResults) {
|
|
24240
|
-
if (fileResult.status !== "conflict") continue;
|
|
24241
|
-
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
24242
|
-
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
24243
|
-
const filePath = (0, import_path9.join)(this.outputDir, fileResult.file);
|
|
24244
|
-
try {
|
|
24245
|
-
const content = await (0, import_promises4.readFile)(filePath, "utf-8");
|
|
24246
|
-
const stripped = stripConflictMarkers(content);
|
|
24247
|
-
await (0, import_promises4.writeFile)(filePath, stripped);
|
|
24248
|
-
} catch {
|
|
24249
|
-
}
|
|
24250
|
-
}
|
|
24251
|
-
}
|
|
24252
|
-
}
|
|
24253
|
-
}
|
|
24254
|
-
return results;
|
|
24255
|
-
}
|
|
24256
|
-
/**
|
|
24257
|
-
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
24258
|
-
* results including any "dropped context lines" — lines that were
|
|
24259
|
-
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
24260
|
-
* final on-disk state because OURS deleted them. This is the fast-path
|
|
24261
|
-
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
24262
|
-
* 3-way slow path; both must surface the same warning.
|
|
24263
|
-
*/
|
|
24264
|
-
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
24265
|
-
const fileResults = [];
|
|
24266
|
-
if (!baseGen) return fileResults;
|
|
24267
|
-
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-acc-"));
|
|
24268
|
-
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24269
|
-
const tempGit = new GitClient2(tempDir);
|
|
24270
|
-
await tempGit.exec(["init"]);
|
|
24271
|
-
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24272
|
-
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24273
|
-
try {
|
|
24274
|
-
for (const filePath of patch.files) {
|
|
24275
|
-
if (isBinaryFile(filePath)) continue;
|
|
24276
|
-
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24277
|
-
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24278
|
-
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
24279
|
-
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
24280
|
-
const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
24281
|
-
const finalOnDisk = await (0, import_promises4.readFile)((0, import_path9.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
24282
|
-
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
24283
|
-
if (effectiveTheirs != null) {
|
|
24284
|
-
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24285
|
-
content: effectiveTheirs,
|
|
24286
|
-
baseGeneration: patch.base_generation
|
|
24287
|
-
});
|
|
24288
|
-
}
|
|
24289
|
-
if (base != null && effectiveTheirs != null && finalOnDisk != null) {
|
|
24290
|
-
const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
|
|
24291
|
-
if (dropped.length > 0) {
|
|
24292
|
-
fileResults.push({
|
|
24293
|
-
file: resolvedPath,
|
|
24294
|
-
status: "merged",
|
|
24295
|
-
droppedContextLines: dropped
|
|
24296
|
-
});
|
|
24297
|
-
}
|
|
24298
|
-
}
|
|
24299
|
-
}
|
|
24300
|
-
} finally {
|
|
24301
|
-
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24302
|
-
});
|
|
24303
|
-
}
|
|
24304
|
-
return fileResults;
|
|
24305
|
-
}
|
|
24306
|
-
async applyPatchWithFallback(patch) {
|
|
24307
|
-
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24308
|
-
const lock = this.lockManager.read();
|
|
24309
|
-
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
24310
|
-
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
24311
|
-
const needsAccumulation = await Promise.all(
|
|
24312
|
-
patch.files.map(async (f) => {
|
|
24313
|
-
if (!baseGen) return false;
|
|
24314
|
-
const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
|
|
24315
|
-
return this.fileTheirsAccumulator.has(resolved);
|
|
24316
|
-
})
|
|
24317
|
-
).then((results) => results.some(Boolean));
|
|
24318
|
-
const resolvedFiles = {};
|
|
24319
|
-
for (const filePath of patch.files) {
|
|
24320
|
-
const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
|
|
24321
|
-
if (resolvedPath !== filePath) {
|
|
24322
|
-
resolvedFiles[filePath] = resolvedPath;
|
|
24323
|
-
}
|
|
24324
|
-
}
|
|
24325
|
-
const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
|
|
24326
|
-
const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
|
|
24327
|
-
if (!needsAccumulation && !hasGeneratorRename) {
|
|
24328
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
24329
|
-
for (const filePath of patch.files) {
|
|
24330
|
-
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24331
|
-
snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
|
|
24332
|
-
}
|
|
24333
|
-
try {
|
|
24334
|
-
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
24335
|
-
const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
24336
|
-
return {
|
|
24337
|
-
patch,
|
|
24338
|
-
status: "applied",
|
|
24339
|
-
method: "git-am",
|
|
24340
|
-
...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
|
|
24341
|
-
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24342
|
-
};
|
|
24343
|
-
} catch {
|
|
24344
|
-
for (const [filePath, content] of snapshots) {
|
|
24345
|
-
const fullPath = (0, import_path9.join)(this.outputDir, filePath);
|
|
24346
|
-
if (content != null) {
|
|
24347
|
-
await (0, import_promises4.writeFile)(fullPath, content);
|
|
24348
|
-
} else {
|
|
24349
|
-
await (0, import_promises4.unlink)(fullPath).catch(() => {
|
|
24350
|
-
});
|
|
24351
|
-
}
|
|
24352
|
-
}
|
|
24353
|
-
}
|
|
24354
|
-
}
|
|
24355
|
-
return this.applyWithThreeWayMerge(patch);
|
|
24356
|
-
}
|
|
24357
|
-
async applyWithThreeWayMerge(patch) {
|
|
24358
|
-
const fileResults = [];
|
|
24359
|
-
const resolvedFiles = {};
|
|
24360
|
-
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-"));
|
|
24361
|
-
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
24362
|
-
const tempGit = new GitClient2(tempDir);
|
|
24363
|
-
await tempGit.exec(["init"]);
|
|
24364
|
-
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
24365
|
-
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
24366
|
-
try {
|
|
24367
|
-
for (const filePath of patch.files) {
|
|
24368
|
-
if (isBinaryFile(filePath)) {
|
|
24369
|
-
fileResults.push({
|
|
24370
|
-
file: filePath,
|
|
24371
|
-
status: "skipped",
|
|
24372
|
-
reason: "binary-file"
|
|
24373
|
-
});
|
|
24374
|
-
continue;
|
|
24375
|
-
}
|
|
24376
|
-
const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
|
|
24377
|
-
if (result.file !== filePath) {
|
|
24378
|
-
resolvedFiles[filePath] = result.file;
|
|
24379
|
-
}
|
|
24380
|
-
fileResults.push(result);
|
|
24381
|
-
}
|
|
24382
|
-
} finally {
|
|
24383
|
-
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
24384
|
-
});
|
|
24385
|
-
}
|
|
24386
|
-
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
24387
|
-
const hasConflicts = conflictFiles.length > 0;
|
|
24388
|
-
const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
|
|
24389
|
-
return {
|
|
24390
|
-
patch,
|
|
24391
|
-
status: hasConflicts ? "conflict" : "applied",
|
|
24392
|
-
method: "3way-merge",
|
|
24393
|
-
fileResults,
|
|
24394
|
-
conflictReason,
|
|
24395
|
-
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
24396
|
-
};
|
|
24397
|
-
}
|
|
24398
|
-
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
24399
|
-
try {
|
|
24400
|
-
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
24401
|
-
if (!baseGen) {
|
|
24402
|
-
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
24403
|
-
}
|
|
24404
|
-
const lock = this.lockManager.read();
|
|
24405
|
-
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
24406
|
-
const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
|
|
24407
|
-
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
24408
|
-
const metadata = {
|
|
24409
|
-
patchId: patch.id,
|
|
24410
|
-
patchMessage: patch.original_message,
|
|
24411
|
-
baseGeneration: patch.base_generation,
|
|
24412
|
-
currentGeneration: lock.current_generation
|
|
24413
|
-
};
|
|
24414
|
-
let base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
24415
|
-
let renameSourcePath;
|
|
24416
|
-
if (!base) {
|
|
24417
|
-
const renameSource = this.extractRenameSource(patch.patch_content, filePath);
|
|
24418
|
-
if (renameSource) {
|
|
24419
|
-
base = await this.git.showFile(baseGen.tree_hash, renameSource);
|
|
24420
|
-
renameSourcePath = renameSource;
|
|
24421
|
-
}
|
|
24422
|
-
}
|
|
24423
|
-
const oursPath = (0, import_path9.join)(this.outputDir, resolvedPath);
|
|
24424
|
-
const ours = await (0, import_promises4.readFile)(oursPath, "utf-8").catch(() => null);
|
|
24425
|
-
let ghostReconstructed = false;
|
|
24426
|
-
let theirs = null;
|
|
24427
|
-
if (!base && ours && !renameSourcePath) {
|
|
24428
|
-
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
24429
|
-
if (!treeReachable) {
|
|
24430
|
-
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
24431
|
-
if (fileDiff) {
|
|
24432
|
-
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
24433
|
-
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
24434
|
-
if (result) {
|
|
24435
|
-
base = result.base;
|
|
24436
|
-
theirs = result.theirs;
|
|
24437
|
-
ghostReconstructed = true;
|
|
24438
|
-
}
|
|
24439
|
-
}
|
|
24440
|
-
}
|
|
24441
|
-
}
|
|
24442
|
-
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
24443
|
-
const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
|
|
24444
|
-
const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
|
|
24445
|
-
if (!ghostReconstructed) {
|
|
24446
|
-
if (useSnapshotAsPrimary) {
|
|
24447
|
-
theirs = snapshotForFile;
|
|
24448
|
-
} else {
|
|
24449
|
-
theirs = await this.applyPatchToContent(
|
|
24450
|
-
base,
|
|
24451
|
-
patch.patch_content,
|
|
24452
|
-
filePath,
|
|
24453
|
-
tempGit,
|
|
24454
|
-
tempDir,
|
|
24455
|
-
renameSourcePath
|
|
24456
|
-
);
|
|
24457
|
-
const reconstructionHasMarkers = theirs != null && (theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization"));
|
|
24458
|
-
const baseHadMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
24459
|
-
if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
|
|
24460
|
-
theirs = snapshotForFile;
|
|
24461
|
-
}
|
|
24462
|
-
}
|
|
24463
|
-
}
|
|
24464
|
-
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
24465
|
-
if (theirs) {
|
|
24466
|
-
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
24467
|
-
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
24468
|
-
if (theirsHasMarkers && !baseHasMarkers) {
|
|
24469
|
-
if (accumulatorEntry) {
|
|
24470
|
-
theirs = null;
|
|
24471
|
-
} else {
|
|
24472
|
-
return {
|
|
24473
|
-
file: resolvedPath,
|
|
24474
|
-
status: "skipped",
|
|
24475
|
-
reason: "stale-conflict-markers"
|
|
24476
|
-
};
|
|
24477
|
-
}
|
|
24478
|
-
}
|
|
24479
|
-
}
|
|
24480
|
-
let useAccumulatorAsMergeBase = false;
|
|
24481
|
-
if (accumulatorEntry && (theirs == null || base == null)) {
|
|
24482
|
-
theirs = await this.applyPatchToContent(
|
|
24483
|
-
accumulatorEntry.content,
|
|
24484
|
-
patch.patch_content,
|
|
24485
|
-
filePath,
|
|
24486
|
-
tempGit,
|
|
24487
|
-
tempDir
|
|
24488
|
-
);
|
|
24489
|
-
if (theirs != null) {
|
|
24490
|
-
useAccumulatorAsMergeBase = true;
|
|
24491
|
-
} else if (await this.isPatchAlreadyApplied(
|
|
24492
|
-
patch.patch_content,
|
|
24493
|
-
filePath,
|
|
24494
|
-
accumulatorEntry.content,
|
|
24495
|
-
tempGit,
|
|
24496
|
-
tempDir
|
|
24497
|
-
)) {
|
|
24498
|
-
theirs = accumulatorEntry.content;
|
|
24499
|
-
useAccumulatorAsMergeBase = true;
|
|
24500
|
-
}
|
|
24501
|
-
}
|
|
24502
|
-
if (theirs) {
|
|
24503
|
-
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
24504
|
-
const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
|
|
24505
|
-
if (theirsHasMarkers && !accBaseHasMarkers && !(base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization")))) {
|
|
24506
|
-
return {
|
|
24507
|
-
file: resolvedPath,
|
|
24508
|
-
status: "skipped",
|
|
24509
|
-
reason: "stale-conflict-markers"
|
|
24510
|
-
};
|
|
24511
|
-
}
|
|
24512
|
-
}
|
|
24513
|
-
let effective_theirs = theirs;
|
|
24514
|
-
let baseMismatchSkipped = false;
|
|
24515
|
-
if (theirs != null && base != null && !useAccumulatorAsMergeBase) {
|
|
24516
|
-
if (accumulatorEntry && accumulatorEntry.baseGeneration === patch.base_generation) {
|
|
24517
|
-
try {
|
|
24518
|
-
const preMerged = threeWayMerge(base, accumulatorEntry.content, theirs);
|
|
24519
|
-
if (!preMerged.hasConflicts) {
|
|
24520
|
-
effective_theirs = preMerged.content;
|
|
24521
|
-
} else {
|
|
24522
|
-
effective_theirs = theirs;
|
|
24523
|
-
}
|
|
24524
|
-
} catch {
|
|
24525
|
-
effective_theirs = theirs;
|
|
24526
|
-
}
|
|
24527
|
-
} else if (accumulatorEntry) {
|
|
24528
|
-
baseMismatchSkipped = true;
|
|
24529
|
-
}
|
|
24530
|
-
}
|
|
24531
|
-
if (base == null && ours == null && effective_theirs != null) {
|
|
24532
|
-
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24533
|
-
content: effective_theirs,
|
|
24534
|
-
baseGeneration: patch.base_generation
|
|
24535
|
-
});
|
|
24536
|
-
const outDir2 = (0, import_path9.dirname)(oursPath);
|
|
24537
|
-
await (0, import_promises4.mkdir)(outDir2, { recursive: true });
|
|
24538
|
-
await (0, import_promises4.writeFile)(oursPath, effective_theirs);
|
|
24539
|
-
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
24540
|
-
}
|
|
24541
|
-
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
24542
|
-
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
24543
|
-
const outDir2 = (0, import_path9.dirname)(oursPath);
|
|
24544
|
-
await (0, import_promises4.mkdir)(outDir2, { recursive: true });
|
|
24545
|
-
await (0, import_promises4.writeFile)(oursPath, merged2.content);
|
|
24546
|
-
if (merged2.hasConflicts) {
|
|
24547
|
-
return {
|
|
24548
|
-
file: resolvedPath,
|
|
24549
|
-
status: "conflict",
|
|
24550
|
-
conflicts: merged2.conflicts,
|
|
24551
|
-
conflictReason: "new-file-both",
|
|
24552
|
-
conflictMetadata: metadata
|
|
24553
|
-
};
|
|
24554
|
-
}
|
|
24555
|
-
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24556
|
-
content: merged2.content,
|
|
24557
|
-
baseGeneration: patch.base_generation
|
|
24558
|
-
});
|
|
24559
|
-
return { file: resolvedPath, status: "merged" };
|
|
24560
|
-
}
|
|
24561
|
-
if (effective_theirs == null) {
|
|
24562
|
-
return {
|
|
24563
|
-
file: resolvedPath,
|
|
24564
|
-
status: "skipped",
|
|
24565
|
-
reason: "missing-content"
|
|
24566
|
-
};
|
|
24567
|
-
}
|
|
24568
|
-
if (base == null && !useAccumulatorAsMergeBase || ours == null) {
|
|
24569
|
-
return {
|
|
24570
|
-
file: resolvedPath,
|
|
24571
|
-
status: "skipped",
|
|
24572
|
-
reason: "missing-content"
|
|
24573
|
-
};
|
|
24574
|
-
}
|
|
24575
|
-
const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
|
|
24576
|
-
if (mergeBase == null) {
|
|
24577
|
-
return {
|
|
24578
|
-
file: resolvedPath,
|
|
24579
|
-
status: "skipped",
|
|
24580
|
-
reason: "missing-content"
|
|
24581
|
-
};
|
|
24582
|
-
}
|
|
24583
|
-
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
24584
|
-
const outDir = (0, import_path9.dirname)(oursPath);
|
|
24585
|
-
await (0, import_promises4.mkdir)(outDir, { recursive: true });
|
|
24586
|
-
await (0, import_promises4.writeFile)(oursPath, merged.content);
|
|
24587
|
-
const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
|
|
24588
|
-
if (populateAccumulator) {
|
|
24589
|
-
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
24590
|
-
content: effective_theirs,
|
|
24591
|
-
baseGeneration: patch.base_generation
|
|
24592
|
-
});
|
|
24593
|
-
}
|
|
24594
|
-
if (merged.hasConflicts) {
|
|
24595
|
-
return {
|
|
24596
|
-
file: resolvedPath,
|
|
24597
|
-
status: "conflict",
|
|
24598
|
-
conflicts: merged.conflicts,
|
|
24599
|
-
conflictReason: baseMismatchSkipped ? "base-generation-mismatch" : "same-line-edit",
|
|
24600
|
-
conflictMetadata: metadata
|
|
24601
|
-
};
|
|
24602
|
-
}
|
|
24603
|
-
return {
|
|
24604
|
-
file: resolvedPath,
|
|
24605
|
-
status: "merged",
|
|
24606
|
-
...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
|
|
24607
|
-
};
|
|
24608
|
-
} catch (error) {
|
|
24609
|
-
return {
|
|
24610
|
-
file: filePath,
|
|
24611
|
-
status: "skipped",
|
|
24612
|
-
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
24613
|
-
};
|
|
24614
|
-
}
|
|
24615
|
-
}
|
|
24616
|
-
async isTreeReachable(treeHash) {
|
|
24617
|
-
let result = this.treeExistsCache.get(treeHash);
|
|
24618
|
-
if (result === void 0) {
|
|
24619
|
-
result = await this.git.treeExists(treeHash);
|
|
24620
|
-
this.treeExistsCache.set(treeHash, result);
|
|
24621
|
-
}
|
|
24622
|
-
return result;
|
|
24623
|
-
}
|
|
24624
|
-
isExcluded(patch) {
|
|
24625
|
-
const config = this.lockManager.getCustomizationsConfig();
|
|
24626
|
-
if (!config.exclude) return false;
|
|
24627
|
-
return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
|
|
24628
|
-
}
|
|
24629
|
-
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
24630
|
-
const config = this.lockManager.getCustomizationsConfig();
|
|
24631
|
-
if (config.moves) {
|
|
24632
|
-
for (const move of config.moves) {
|
|
24633
|
-
if (minimatch(filePath, move.from) || filePath === move.from) {
|
|
24634
|
-
if (filePath === move.from) {
|
|
24635
|
-
return move.to;
|
|
24636
|
-
}
|
|
24637
|
-
const fromBase = move.from.replace(/\*\*.*$/, "");
|
|
24638
|
-
const toBase = move.to.replace(/\*\*.*$/, "");
|
|
24639
|
-
if (filePath.startsWith(fromBase)) {
|
|
24640
|
-
return toBase + filePath.slice(fromBase.length);
|
|
24641
|
-
}
|
|
24642
|
-
}
|
|
24643
|
-
}
|
|
24644
|
-
}
|
|
24645
|
-
const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
|
|
24646
|
-
let renames = this.renameCache.get(cacheKey);
|
|
24647
|
-
if (!renames) {
|
|
24648
|
-
renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
|
|
24649
|
-
this.renameCache.set(cacheKey, renames);
|
|
24650
|
-
}
|
|
24651
|
-
const gitRename = renames.find((r2) => r2.from === filePath);
|
|
24652
|
-
if (gitRename) {
|
|
24653
|
-
return gitRename.to;
|
|
24654
|
-
}
|
|
24655
|
-
return filePath;
|
|
24656
|
-
}
|
|
24657
|
-
async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
|
|
24658
|
-
if (!base) {
|
|
24659
|
-
return this.extractNewFileFromPatch(patchContent, filePath);
|
|
24660
|
-
}
|
|
24661
|
-
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
24662
|
-
if (!fileDiff) return null;
|
|
24663
|
-
try {
|
|
24664
|
-
if (sourceFilePath) {
|
|
24665
|
-
const tempSourcePath = (0, import_path9.join)(tempDir, sourceFilePath);
|
|
24666
|
-
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempSourcePath), { recursive: true });
|
|
24667
|
-
await (0, import_promises4.writeFile)(tempSourcePath, base);
|
|
24668
|
-
await tempGit.exec(["add", sourceFilePath]);
|
|
24669
|
-
await tempGit.exec([
|
|
24670
|
-
"commit",
|
|
24671
|
-
"-m",
|
|
24672
|
-
`base for rename ${sourceFilePath} -> ${filePath}`,
|
|
24673
|
-
"--allow-empty"
|
|
24674
|
-
]);
|
|
24675
|
-
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24676
|
-
const tempTargetPath = (0, import_path9.join)(tempDir, filePath);
|
|
24677
|
-
return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
|
|
24678
|
-
}
|
|
24679
|
-
const tempFilePath = (0, import_path9.join)(tempDir, filePath);
|
|
24680
|
-
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
|
|
24681
|
-
await (0, import_promises4.writeFile)(tempFilePath, base);
|
|
24682
|
-
await tempGit.exec(["add", filePath]);
|
|
24683
|
-
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
24684
|
-
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
24685
|
-
return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
|
|
24686
|
-
} catch {
|
|
24687
|
-
return null;
|
|
24688
|
-
}
|
|
24689
|
-
}
|
|
24690
|
-
/**
|
|
24691
|
-
* Detects whether a patch's additions are already present in `content`.
|
|
24692
|
-
* Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
|
|
24693
|
-
* the forward patch is effectively a no-op (its +lines are already there and
|
|
24694
|
-
* its -lines are already gone). Used to distinguish "patch already applied"
|
|
24695
|
-
* from "patch base mismatch" after a forward-apply fails.
|
|
24696
|
-
*/
|
|
24697
|
-
async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
|
|
24698
|
-
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
24699
|
-
if (!fileDiff) return false;
|
|
24700
|
-
try {
|
|
24701
|
-
const tempFilePath = (0, import_path9.join)(tempDir, filePath);
|
|
24702
|
-
await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
|
|
24703
|
-
await (0, import_promises4.writeFile)(tempFilePath, content);
|
|
24704
|
-
await tempGit.exec(["add", filePath]);
|
|
24705
|
-
await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
|
|
24706
|
-
await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
|
|
24707
|
-
return true;
|
|
24708
|
-
} catch {
|
|
24709
|
-
return false;
|
|
24710
|
-
}
|
|
24711
|
-
}
|
|
24712
|
-
extractFileDiff(patchContent, filePath) {
|
|
24713
|
-
const lines = patchContent.split("\n");
|
|
24714
|
-
const diffLines = [];
|
|
24715
|
-
let inTargetFile = false;
|
|
24716
|
-
for (const line of lines) {
|
|
24717
|
-
if (line.startsWith("diff --git")) {
|
|
24718
|
-
if (inTargetFile) {
|
|
24719
|
-
break;
|
|
24720
|
-
}
|
|
24721
|
-
if (isDiffLineForFile(line, filePath)) {
|
|
24722
|
-
inTargetFile = true;
|
|
24723
|
-
diffLines.push(line);
|
|
24724
|
-
}
|
|
24725
|
-
continue;
|
|
24726
|
-
}
|
|
24727
|
-
if (inTargetFile) {
|
|
24728
|
-
diffLines.push(line);
|
|
24729
|
-
}
|
|
24730
|
-
}
|
|
24731
|
-
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
24732
|
-
}
|
|
24733
|
-
extractRenameSource(patchContent, targetFilePath) {
|
|
24734
|
-
const lines = patchContent.split("\n");
|
|
24735
|
-
let inTargetFile = false;
|
|
24736
|
-
for (const line of lines) {
|
|
24737
|
-
if (line.startsWith("diff --git")) {
|
|
24738
|
-
if (inTargetFile) break;
|
|
24739
|
-
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
24740
|
-
continue;
|
|
24741
|
-
}
|
|
24742
|
-
if (!inTargetFile) continue;
|
|
24743
|
-
if (line.startsWith("@@")) break;
|
|
24744
|
-
if (line.startsWith("rename from ")) {
|
|
24745
|
-
return line.slice("rename from ".length);
|
|
24746
|
-
}
|
|
24747
|
-
}
|
|
24748
|
-
return null;
|
|
24749
|
-
}
|
|
24750
|
-
extractNewFileFromPatch(patchContent, filePath) {
|
|
24751
|
-
const lines = patchContent.split("\n");
|
|
24752
|
-
const addedLines = [];
|
|
24753
|
-
let inTargetFile = false;
|
|
24754
|
-
let inHunk = false;
|
|
24755
|
-
let isNewFile = false;
|
|
24756
|
-
let noTrailingNewline = false;
|
|
24757
|
-
for (const line of lines) {
|
|
24758
|
-
if (line.startsWith("diff --git")) {
|
|
24759
|
-
if (inTargetFile) break;
|
|
24760
|
-
inTargetFile = isDiffLineForFile(line, filePath);
|
|
24761
|
-
inHunk = false;
|
|
24762
|
-
isNewFile = false;
|
|
24763
|
-
continue;
|
|
24764
|
-
}
|
|
24765
|
-
if (!inTargetFile) continue;
|
|
24766
|
-
if (!inHunk) {
|
|
24767
|
-
if (line === "--- /dev/null" || line.startsWith("new file mode")) {
|
|
24768
|
-
isNewFile = true;
|
|
24769
|
-
}
|
|
24770
|
-
}
|
|
24771
|
-
if (line.startsWith("@@")) {
|
|
24772
|
-
if (!isNewFile) return null;
|
|
24773
|
-
inHunk = true;
|
|
24774
|
-
continue;
|
|
24775
|
-
}
|
|
24776
|
-
if (!inHunk) continue;
|
|
24777
|
-
if (line === "\") {
|
|
24778
|
-
noTrailingNewline = true;
|
|
24779
|
-
continue;
|
|
24780
|
-
}
|
|
24781
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
24782
|
-
addedLines.push(line.slice(1));
|
|
24783
|
-
}
|
|
24784
|
-
}
|
|
24785
|
-
if (addedLines.length === 0) return null;
|
|
24786
|
-
return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
|
|
24787
|
-
}
|
|
24788
|
-
};
|
|
24789
24406
|
INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
24790
24407
|
ReplayDetector = class {
|
|
24791
24408
|
git;
|
|
@@ -24821,7 +24438,7 @@ var init_dist3 = __esm({
|
|
|
24821
24438
|
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
24822
24439
|
}
|
|
24823
24440
|
/**
|
|
24824
|
-
*
|
|
24441
|
+
* Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
24825
24442
|
*
|
|
24826
24443
|
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
24827
24444
|
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
@@ -24853,7 +24470,7 @@ var init_dist3 = __esm({
|
|
|
24853
24470
|
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
24854
24471
|
}
|
|
24855
24472
|
/**
|
|
24856
|
-
*
|
|
24473
|
+
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
24857
24474
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
24858
24475
|
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
24859
24476
|
*
|
|
@@ -24864,7 +24481,7 @@ var init_dist3 = __esm({
|
|
|
24864
24481
|
async detectPatchesInRange(rangeStart, lastGen, lock) {
|
|
24865
24482
|
const log = await this.git.exec([
|
|
24866
24483
|
"log",
|
|
24867
|
-
"--
|
|
24484
|
+
"--no-merges",
|
|
24868
24485
|
"--format=%H%x00%an%x00%ae%x00%s",
|
|
24869
24486
|
`${rangeStart}..HEAD`,
|
|
24870
24487
|
"--",
|
|
@@ -24884,18 +24501,6 @@ var init_dist3 = __esm({
|
|
|
24884
24501
|
continue;
|
|
24885
24502
|
}
|
|
24886
24503
|
const parents = await this.git.getCommitParents(commit.sha);
|
|
24887
|
-
if (parents.length > 1) {
|
|
24888
|
-
const compositePatch = await this.createMergeCompositePatch({
|
|
24889
|
-
mergeCommit: commit,
|
|
24890
|
-
firstParent: parents[0],
|
|
24891
|
-
lastGen,
|
|
24892
|
-
lock
|
|
24893
|
-
});
|
|
24894
|
-
if (compositePatch) {
|
|
24895
|
-
newPatches.push(compositePatch);
|
|
24896
|
-
}
|
|
24897
|
-
continue;
|
|
24898
|
-
}
|
|
24899
24504
|
let patchContent;
|
|
24900
24505
|
try {
|
|
24901
24506
|
patchContent = await this.git.formatPatch(commit.sha);
|
|
@@ -24910,9 +24515,7 @@ var init_dist3 = __esm({
|
|
|
24910
24515
|
continue;
|
|
24911
24516
|
}
|
|
24912
24517
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
24913
|
-
const
|
|
24914
|
-
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
24915
|
-
const files = allFiles.filter((f) => !isSensitiveFile(f));
|
|
24518
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24916
24519
|
if (sensitiveFiles.length > 0) {
|
|
24917
24520
|
this.warnings.push(
|
|
24918
24521
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
@@ -24933,12 +24536,7 @@ var init_dist3 = __esm({
|
|
|
24933
24536
|
if (files.length === 0) {
|
|
24934
24537
|
continue;
|
|
24935
24538
|
}
|
|
24936
|
-
const theirsSnapshot =
|
|
24937
|
-
for (const file of files) {
|
|
24938
|
-
if (isBinaryFile(file)) continue;
|
|
24939
|
-
const content = await this.git.showFile(commit.sha, file).catch(() => null);
|
|
24940
|
-
if (content != null) theirsSnapshot[file] = content;
|
|
24941
|
-
}
|
|
24539
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
24942
24540
|
newPatches.push({
|
|
24943
24541
|
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
24944
24542
|
content_hash: contentHash,
|
|
@@ -25026,7 +24624,7 @@ var init_dist3 = __esm({
|
|
|
25026
24624
|
*
|
|
25027
24625
|
* This ensures content hashes match across the no-patches and normal-
|
|
25028
24626
|
* regeneration flows, which store formatPatch and git-diff formats
|
|
25029
|
-
* respectively
|
|
24627
|
+
* respectively.
|
|
25030
24628
|
*/
|
|
25031
24629
|
computeContentHash(patchContent) {
|
|
25032
24630
|
const lines = patchContent.split("\n");
|
|
@@ -25036,13 +24634,11 @@ var init_dist3 = __esm({
|
|
|
25036
24634
|
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
25037
24635
|
}
|
|
25038
24636
|
/**
|
|
25039
|
-
*
|
|
25040
|
-
*
|
|
24637
|
+
* Drop patches whose files were transiently created and then deleted
|
|
24638
|
+
* within the current linear detection window, and are absent from
|
|
25041
24639
|
* HEAD at the end of the window.
|
|
25042
24640
|
*
|
|
25043
|
-
* Rationale:
|
|
25044
|
-
* firstParent..mergeCommit so net-zero files never enter the composite. On
|
|
25045
|
-
* linear history each commit becomes its own patch, so a file that was
|
|
24641
|
+
* Rationale: each commit becomes its own patch, so a file that was
|
|
25046
24642
|
* briefly added and later removed survives as a create+delete pair whose
|
|
25047
24643
|
* cumulative diff is empty. We drop such patches before they reach the
|
|
25048
24644
|
* lockfile.
|
|
@@ -25062,8 +24658,8 @@ var init_dist3 = __esm({
|
|
|
25062
24658
|
* single files from a multi-file patch would require a follow-up that
|
|
25063
24659
|
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
25064
24660
|
* current failing test exercises this, and leaving the patch intact
|
|
25065
|
-
* preserves today's behaviour.
|
|
25066
|
-
*
|
|
24661
|
+
* preserves today's behaviour. Revisit if a future adversarial test
|
|
24662
|
+
* demands per-file stripping.
|
|
25067
24663
|
*/
|
|
25068
24664
|
async collapseNetZeroFiles(patches) {
|
|
25069
24665
|
if (patches.length === 0) return patches;
|
|
@@ -25110,77 +24706,33 @@ var init_dist3 = __esm({
|
|
|
25110
24706
|
}
|
|
25111
24707
|
}
|
|
25112
24708
|
/**
|
|
25113
|
-
*
|
|
25114
|
-
*
|
|
25115
|
-
*
|
|
25116
|
-
* zero-net-change ghost conflicts.
|
|
25117
|
-
*/
|
|
25118
|
-
async createMergeCompositePatch(input) {
|
|
25119
|
-
const { mergeCommit, firstParent, lastGen, lock } = input;
|
|
25120
|
-
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
25121
|
-
const filesOutput = await this.git.exec([
|
|
25122
|
-
"diff",
|
|
25123
|
-
"--name-only",
|
|
25124
|
-
firstParent,
|
|
25125
|
-
mergeCommit.sha
|
|
25126
|
-
]);
|
|
25127
|
-
const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
25128
|
-
const sensitiveMergeFiles = allMergeFiles.filter((f) => isSensitiveFile(f));
|
|
25129
|
-
const files = allMergeFiles.filter((f) => !isSensitiveFile(f));
|
|
25130
|
-
if (sensitiveMergeFiles.length > 0) {
|
|
25131
|
-
this.warnings.push(
|
|
25132
|
-
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25133
|
-
);
|
|
25134
|
-
}
|
|
25135
|
-
if (files.length === 0) return null;
|
|
25136
|
-
const diff = await this.git.exec([
|
|
25137
|
-
"diff",
|
|
25138
|
-
firstParent,
|
|
25139
|
-
mergeCommit.sha,
|
|
25140
|
-
"--",
|
|
25141
|
-
...files
|
|
25142
|
-
]);
|
|
25143
|
-
if (!diff.trim()) return null;
|
|
25144
|
-
const contentHash = this.computeContentHash(diff);
|
|
25145
|
-
if (lock.patches.some((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
25146
|
-
return null;
|
|
25147
|
-
}
|
|
25148
|
-
const theirsSnapshot = {};
|
|
25149
|
-
for (const file of files) {
|
|
25150
|
-
if (isBinaryFile(file)) continue;
|
|
25151
|
-
const content = await this.git.showFile(mergeCommit.sha, file).catch(() => null);
|
|
25152
|
-
if (content != null) theirsSnapshot[file] = content;
|
|
25153
|
-
}
|
|
25154
|
-
return {
|
|
25155
|
-
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
25156
|
-
content_hash: contentHash,
|
|
25157
|
-
original_commit: mergeCommit.sha,
|
|
25158
|
-
original_message: mergeCommit.message,
|
|
25159
|
-
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
25160
|
-
base_generation: lastGen.commit_sha,
|
|
25161
|
-
files,
|
|
25162
|
-
patch_content: diff,
|
|
25163
|
-
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
25164
|
-
...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
|
|
25165
|
-
};
|
|
25166
|
-
}
|
|
25167
|
-
/**
|
|
25168
|
-
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
25169
|
-
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
25170
|
-
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
24709
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
24710
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
24711
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
25171
24712
|
*/
|
|
25172
24713
|
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
25173
24714
|
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
25174
24715
|
if (!diffBase) {
|
|
25175
24716
|
return this.detectPatchesViaCommitScan();
|
|
25176
24717
|
}
|
|
24718
|
+
const lockForGuard = this.lockManager.read();
|
|
24719
|
+
const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
|
|
24720
|
+
let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
|
|
24721
|
+
if (commitKnownMissing && !knownGenerations.has(diffBase)) {
|
|
24722
|
+
const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
|
|
24723
|
+
if (!fallback) {
|
|
24724
|
+
this.warnings.push(
|
|
24725
|
+
`Composite-patch fallback skipped: diff base ${diffBase.slice(0, 7)} is not a recorded generation and the lockfile has no recorded generations to anchor on. Customer customizations on disk are preserved; tracked patches will resume on the next regeneration.`
|
|
24726
|
+
);
|
|
24727
|
+
return { patches: [], revertedPatchIds: [] };
|
|
24728
|
+
}
|
|
24729
|
+
resolvedBaseGeneration = fallback;
|
|
24730
|
+
}
|
|
25177
24731
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
25178
|
-
const
|
|
25179
|
-
|
|
25180
|
-
const files = allTreeFiles.filter((f) => !isSensitiveFile(f));
|
|
25181
|
-
if (sensitiveTreeFiles.length > 0) {
|
|
24732
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24733
|
+
if (sensitiveFiles.length > 0) {
|
|
25182
24734
|
this.warnings.push(
|
|
25183
|
-
`Sensitive file(s) excluded from composite patch: ${
|
|
24735
|
+
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25184
24736
|
);
|
|
25185
24737
|
}
|
|
25186
24738
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
@@ -25192,21 +24744,19 @@ var init_dist3 = __esm({
|
|
|
25192
24744
|
return { patches: [], revertedPatchIds: [] };
|
|
25193
24745
|
}
|
|
25194
24746
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
25195
|
-
const theirsSnapshot =
|
|
25196
|
-
for (const file of files) {
|
|
25197
|
-
if (isBinaryFile(file)) continue;
|
|
25198
|
-
const content = await this.git.showFile("HEAD", file).catch(() => null);
|
|
25199
|
-
if (content != null) theirsSnapshot[file] = content;
|
|
25200
|
-
}
|
|
24747
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
|
|
25201
24748
|
const compositePatch = {
|
|
25202
24749
|
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
25203
24750
|
content_hash: contentHash,
|
|
25204
24751
|
original_commit: headSha,
|
|
25205
24752
|
original_message: "Customer customizations (composite)",
|
|
25206
24753
|
original_author: "composite",
|
|
25207
|
-
//
|
|
25208
|
-
//
|
|
25209
|
-
|
|
24754
|
+
// Anchor `base_generation` on a SHA the applicator's
|
|
24755
|
+
// `resolveBaseGeneration` can find — always a member of
|
|
24756
|
+
// `lock.generations`. When `diffBase` is a recorded
|
|
24757
|
+
// generation, use it directly; otherwise the guard above
|
|
24758
|
+
// has already mapped it onto `current_generation`.
|
|
24759
|
+
base_generation: resolvedBaseGeneration,
|
|
25210
24760
|
files,
|
|
25211
24761
|
patch_content: diff,
|
|
25212
24762
|
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
@@ -25224,6 +24774,8 @@ var init_dist3 = __esm({
|
|
|
25224
24774
|
*/
|
|
25225
24775
|
async detectPatchesViaCommitScan() {
|
|
25226
24776
|
const lock = this.lockManager.read();
|
|
24777
|
+
const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
|
|
24778
|
+
const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
|
|
25227
24779
|
const log = await this.git.exec([
|
|
25228
24780
|
"log",
|
|
25229
24781
|
"--max-count=200",
|
|
@@ -25265,12 +24817,10 @@ var init_dist3 = __esm({
|
|
|
25265
24817
|
continue;
|
|
25266
24818
|
}
|
|
25267
24819
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
25268
|
-
const
|
|
25269
|
-
|
|
25270
|
-
const files = allScanFiles.filter((f) => !isSensitiveFile(f));
|
|
25271
|
-
if (sensitiveScanFiles.length > 0) {
|
|
24820
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24821
|
+
if (sensitiveFiles.length > 0) {
|
|
25272
24822
|
this.warnings.push(
|
|
25273
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${
|
|
24823
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25274
24824
|
);
|
|
25275
24825
|
if (files.length > 0) {
|
|
25276
24826
|
if (parents.length === 0) {
|
|
@@ -25292,19 +24842,25 @@ var init_dist3 = __esm({
|
|
|
25292
24842
|
continue;
|
|
25293
24843
|
}
|
|
25294
24844
|
const parentSha = parents[0];
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
25299
|
-
|
|
24845
|
+
let baseGeneration;
|
|
24846
|
+
if (knownGenerations.has(parentSha)) {
|
|
24847
|
+
baseGeneration = parentSha;
|
|
24848
|
+
} else if (fallbackAnchor) {
|
|
24849
|
+
baseGeneration = fallbackAnchor;
|
|
24850
|
+
} else {
|
|
24851
|
+
this.warnings.push(
|
|
24852
|
+
`Skipping commit ${commit.sha.slice(0, 7)} (${commit.message.split("\n")[0]}): no recorded generation in the lockfile to anchor on. The customer's state on disk is preserved; tracked patches will resume on the next regeneration.`
|
|
24853
|
+
);
|
|
24854
|
+
continue;
|
|
25300
24855
|
}
|
|
24856
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
25301
24857
|
newPatches.push({
|
|
25302
24858
|
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
25303
24859
|
content_hash: contentHash,
|
|
25304
24860
|
original_commit: commit.sha,
|
|
25305
24861
|
original_message: commit.message,
|
|
25306
24862
|
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
25307
|
-
base_generation:
|
|
24863
|
+
base_generation: baseGeneration,
|
|
25308
24864
|
files,
|
|
25309
24865
|
patch_content: patchContent,
|
|
25310
24866
|
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
@@ -25349,6 +24905,541 @@ var init_dist3 = __esm({
|
|
|
25349
24905
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
25350
24906
|
}
|
|
25351
24907
|
};
|
|
24908
|
+
MergeAccumulator = class {
|
|
24909
|
+
constructor(mode, sharedFiles) {
|
|
24910
|
+
this.mode = mode;
|
|
24911
|
+
this.sharedFiles = sharedFiles;
|
|
24912
|
+
}
|
|
24913
|
+
entries = /* @__PURE__ */ new Map();
|
|
24914
|
+
/** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
|
|
24915
|
+
isShared(path5) {
|
|
24916
|
+
return this.sharedFiles.has(path5);
|
|
24917
|
+
}
|
|
24918
|
+
/** Has any prior patch already recorded a result for this path? */
|
|
24919
|
+
has(path5) {
|
|
24920
|
+
return this.entries.has(path5);
|
|
24921
|
+
}
|
|
24922
|
+
/** Look up the accumulated content + base generation for this path. */
|
|
24923
|
+
lookup(path5) {
|
|
24924
|
+
return this.entries.get(path5);
|
|
24925
|
+
}
|
|
24926
|
+
/**
|
|
24927
|
+
* True if any of the patch's files have a prior accumulator entry — in
|
|
24928
|
+
* which case the fast path (`git apply --3way`) cannot honor prior
|
|
24929
|
+
* patches' contributions and we must route through the slow path.
|
|
24930
|
+
*/
|
|
24931
|
+
shouldSkipFastPath(patch) {
|
|
24932
|
+
return patch.files.some((f) => this.entries.has(f));
|
|
24933
|
+
}
|
|
24934
|
+
/** Record a successful (clean) merge result. */
|
|
24935
|
+
recordMerge(path5, content, baseGeneration) {
|
|
24936
|
+
this.entries.set(path5, { content, baseGeneration });
|
|
24937
|
+
}
|
|
24938
|
+
/**
|
|
24939
|
+
* Record a conflicted merge result. Populates only in `"resolve"` mode
|
|
24940
|
+
* — replay mode keeps the accumulator clean of marker-laden content
|
|
24941
|
+
* since markers will be stripped from disk after the apply loop.
|
|
24942
|
+
*/
|
|
24943
|
+
recordConflict(path5, content, baseGeneration) {
|
|
24944
|
+
if (this.mode === "resolve") {
|
|
24945
|
+
this.entries.set(path5, { content, baseGeneration });
|
|
24946
|
+
}
|
|
24947
|
+
}
|
|
24948
|
+
/**
|
|
24949
|
+
* Defensive copy for test introspection. Used by invariant I2 to assert
|
|
24950
|
+
* that after every applied patch, the accumulator has an entry for each
|
|
24951
|
+
* file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
|
|
24952
|
+
*
|
|
24953
|
+
* @internal
|
|
24954
|
+
*/
|
|
24955
|
+
snapshot() {
|
|
24956
|
+
const copy = /* @__PURE__ */ new Map();
|
|
24957
|
+
for (const [path5, entry] of this.entries) {
|
|
24958
|
+
copy.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
24959
|
+
}
|
|
24960
|
+
return copy;
|
|
24961
|
+
}
|
|
24962
|
+
};
|
|
24963
|
+
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
24964
|
+
CONFLICT_SEPARATOR = "=======";
|
|
24965
|
+
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
24966
|
+
ReplayApplicator = class {
|
|
24967
|
+
git;
|
|
24968
|
+
lockManager;
|
|
24969
|
+
outputDir;
|
|
24970
|
+
renameCache = /* @__PURE__ */ new Map();
|
|
24971
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
24972
|
+
/**
|
|
24973
|
+
* Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
|
|
24974
|
+
* invocation with the run's apply mode + precomputed shared-file set.
|
|
24975
|
+
* See `src/accumulator/MergeAccumulator.ts` for the full contract.
|
|
24976
|
+
*
|
|
24977
|
+
* Initialized to an empty default in the constructor so type narrowing
|
|
24978
|
+
* works for callers that read it before any `applyPatches` invocation
|
|
24979
|
+
* (e.g., the `getAccumulatorSnapshot` test accessor).
|
|
24980
|
+
*/
|
|
24981
|
+
accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
|
|
24982
|
+
constructor(git, lockManager, outputDir) {
|
|
24983
|
+
this.git = git;
|
|
24984
|
+
this.lockManager = lockManager;
|
|
24985
|
+
this.outputDir = outputDir;
|
|
24986
|
+
}
|
|
24987
|
+
/**
|
|
24988
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
24989
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
24990
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
24991
|
+
*/
|
|
24992
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
24993
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
24994
|
+
if (gen) return gen;
|
|
24995
|
+
try {
|
|
24996
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
24997
|
+
return {
|
|
24998
|
+
commit_sha: baseGeneration,
|
|
24999
|
+
tree_hash: treeHash,
|
|
25000
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25001
|
+
cli_version: "unknown",
|
|
25002
|
+
generator_versions: {}
|
|
25003
|
+
};
|
|
25004
|
+
} catch {
|
|
25005
|
+
return void 0;
|
|
25006
|
+
}
|
|
25007
|
+
}
|
|
25008
|
+
/**
|
|
25009
|
+
* @internal Test-only.
|
|
25010
|
+
* Returns a defensive copy of the inter-patch accumulator. Used by
|
|
25011
|
+
* invariant I2 to assert that after every applied patch, the
|
|
25012
|
+
* accumulator has an entry for each file the patch touched, and the
|
|
25013
|
+
* entry's content matches on-disk.
|
|
25014
|
+
*/
|
|
25015
|
+
getAccumulatorSnapshot() {
|
|
25016
|
+
return this.accumulator.snapshot();
|
|
25017
|
+
}
|
|
25018
|
+
/**
|
|
25019
|
+
* Apply all patches, returning results for each.
|
|
25020
|
+
* Skips patches that match exclude patterns in replay.yml
|
|
25021
|
+
*
|
|
25022
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
25023
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
25024
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
25025
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
25026
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
25027
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
25028
|
+
* any markers that survive.
|
|
25029
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
25030
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
25031
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
25032
|
+
* when they do overlap, the customer gets nested markers and
|
|
25033
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
25034
|
+
*/
|
|
25035
|
+
async applyPatches(patches, opts) {
|
|
25036
|
+
const applyMode = opts?.applyMode ?? "replay";
|
|
25037
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
25038
|
+
for (const p2 of patches) {
|
|
25039
|
+
for (const f of p2.files) {
|
|
25040
|
+
fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
|
|
25041
|
+
}
|
|
25042
|
+
}
|
|
25043
|
+
const sharedFiles = new Set(
|
|
25044
|
+
Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
|
|
25045
|
+
);
|
|
25046
|
+
this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
|
|
25047
|
+
const results = [];
|
|
25048
|
+
for (let i2 = 0; i2 < patches.length; i2++) {
|
|
25049
|
+
const patch = patches[i2];
|
|
25050
|
+
if (this.isExcluded(patch)) {
|
|
25051
|
+
results.push({
|
|
25052
|
+
patch,
|
|
25053
|
+
status: "skipped",
|
|
25054
|
+
method: "git-am"
|
|
25055
|
+
});
|
|
25056
|
+
continue;
|
|
25057
|
+
}
|
|
25058
|
+
const result = await this.applyPatchWithFallback(patch);
|
|
25059
|
+
results.push(result);
|
|
25060
|
+
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
25061
|
+
const laterFiles = /* @__PURE__ */ new Set();
|
|
25062
|
+
for (let j2 = i2 + 1; j2 < patches.length; j2++) {
|
|
25063
|
+
for (const f of patches[j2].files) {
|
|
25064
|
+
laterFiles.add(f);
|
|
25065
|
+
}
|
|
25066
|
+
}
|
|
25067
|
+
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
25068
|
+
if (result.resolvedFiles) {
|
|
25069
|
+
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
25070
|
+
resolvedToOriginal.set(resolved, orig);
|
|
25071
|
+
}
|
|
25072
|
+
}
|
|
25073
|
+
for (const fileResult of result.fileResults) {
|
|
25074
|
+
if (fileResult.status !== "conflict") continue;
|
|
25075
|
+
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
25076
|
+
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
25077
|
+
const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
|
|
25078
|
+
try {
|
|
25079
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf-8");
|
|
25080
|
+
const stripped = stripConflictMarkers(content);
|
|
25081
|
+
await (0, import_promises4.writeFile)(filePath, stripped);
|
|
25082
|
+
} catch {
|
|
25083
|
+
}
|
|
25084
|
+
}
|
|
25085
|
+
}
|
|
25086
|
+
}
|
|
25087
|
+
}
|
|
25088
|
+
return results;
|
|
25089
|
+
}
|
|
25090
|
+
/**
|
|
25091
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
25092
|
+
* results including any "dropped context lines" — lines that were
|
|
25093
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
25094
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
25095
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
25096
|
+
* 3-way slow path; both must surface the same warning.
|
|
25097
|
+
*/
|
|
25098
|
+
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
25099
|
+
const fileResults = [];
|
|
25100
|
+
if (!baseGen) return fileResults;
|
|
25101
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-acc-"));
|
|
25102
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
25103
|
+
const tempGit = new GitClient2(tempDir);
|
|
25104
|
+
await tempGit.exec(["init"]);
|
|
25105
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
25106
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
25107
|
+
try {
|
|
25108
|
+
for (const filePath of patch.files) {
|
|
25109
|
+
if (isBinaryFile(filePath)) continue;
|
|
25110
|
+
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
25111
|
+
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
25112
|
+
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
25113
|
+
const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
|
|
25114
|
+
const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
25115
|
+
const finalOnDisk = await (0, import_promises4.readFile)((0, import_path10.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
25116
|
+
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
25117
|
+
if (effectiveTheirs != null) {
|
|
25118
|
+
this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch.base_generation);
|
|
25119
|
+
}
|
|
25120
|
+
if (base != null && effectiveTheirs != null && finalOnDisk != null) {
|
|
25121
|
+
const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
|
|
25122
|
+
if (dropped.length > 0) {
|
|
25123
|
+
fileResults.push({
|
|
25124
|
+
file: resolvedPath,
|
|
25125
|
+
status: "merged",
|
|
25126
|
+
droppedContextLines: dropped
|
|
25127
|
+
});
|
|
25128
|
+
}
|
|
25129
|
+
}
|
|
25130
|
+
}
|
|
25131
|
+
} finally {
|
|
25132
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
25133
|
+
});
|
|
25134
|
+
}
|
|
25135
|
+
return fileResults;
|
|
25136
|
+
}
|
|
25137
|
+
async applyPatchWithFallback(patch) {
|
|
25138
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
25139
|
+
const lock = this.lockManager.read();
|
|
25140
|
+
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
25141
|
+
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
25142
|
+
const needsAccumulation = await Promise.all(
|
|
25143
|
+
patch.files.map(async (f) => {
|
|
25144
|
+
if (!baseGen) return false;
|
|
25145
|
+
const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
|
|
25146
|
+
return this.accumulator.has(resolved);
|
|
25147
|
+
})
|
|
25148
|
+
).then((results) => results.some(Boolean));
|
|
25149
|
+
const resolvedFiles = {};
|
|
25150
|
+
for (const filePath of patch.files) {
|
|
25151
|
+
const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
|
|
25152
|
+
if (resolvedPath !== filePath) {
|
|
25153
|
+
resolvedFiles[filePath] = resolvedPath;
|
|
25154
|
+
}
|
|
25155
|
+
}
|
|
25156
|
+
const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
|
|
25157
|
+
const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
|
|
25158
|
+
if (!needsAccumulation && !hasGeneratorRename) {
|
|
25159
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
25160
|
+
for (const filePath of patch.files) {
|
|
25161
|
+
const fullPath = (0, import_path10.join)(this.outputDir, filePath);
|
|
25162
|
+
snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
|
|
25163
|
+
}
|
|
25164
|
+
try {
|
|
25165
|
+
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
25166
|
+
const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
25167
|
+
return {
|
|
25168
|
+
patch,
|
|
25169
|
+
status: "applied",
|
|
25170
|
+
method: "git-am",
|
|
25171
|
+
...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
|
|
25172
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
25173
|
+
};
|
|
25174
|
+
} catch {
|
|
25175
|
+
for (const [filePath, content] of snapshots) {
|
|
25176
|
+
const fullPath = (0, import_path10.join)(this.outputDir, filePath);
|
|
25177
|
+
if (content != null) {
|
|
25178
|
+
await (0, import_promises4.writeFile)(fullPath, content);
|
|
25179
|
+
} else {
|
|
25180
|
+
await (0, import_promises4.unlink)(fullPath).catch(() => {
|
|
25181
|
+
});
|
|
25182
|
+
}
|
|
25183
|
+
}
|
|
25184
|
+
}
|
|
25185
|
+
}
|
|
25186
|
+
return this.applyWithThreeWayMerge(patch);
|
|
25187
|
+
}
|
|
25188
|
+
async applyWithThreeWayMerge(patch) {
|
|
25189
|
+
const fileResults = [];
|
|
25190
|
+
const resolvedFiles = {};
|
|
25191
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-"));
|
|
25192
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
25193
|
+
const tempGit = new GitClient2(tempDir);
|
|
25194
|
+
await tempGit.exec(["init"]);
|
|
25195
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
25196
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
25197
|
+
try {
|
|
25198
|
+
for (const filePath of patch.files) {
|
|
25199
|
+
if (isBinaryFile(filePath)) {
|
|
25200
|
+
fileResults.push({
|
|
25201
|
+
file: filePath,
|
|
25202
|
+
status: "skipped",
|
|
25203
|
+
reason: "binary-file"
|
|
25204
|
+
});
|
|
25205
|
+
continue;
|
|
25206
|
+
}
|
|
25207
|
+
const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
|
|
25208
|
+
if (result.file !== filePath) {
|
|
25209
|
+
resolvedFiles[filePath] = result.file;
|
|
25210
|
+
}
|
|
25211
|
+
fileResults.push(result);
|
|
25212
|
+
}
|
|
25213
|
+
} finally {
|
|
25214
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
25215
|
+
});
|
|
25216
|
+
}
|
|
25217
|
+
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
25218
|
+
const hasConflicts = conflictFiles.length > 0;
|
|
25219
|
+
const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
|
|
25220
|
+
return {
|
|
25221
|
+
patch,
|
|
25222
|
+
status: hasConflicts ? "conflict" : "applied",
|
|
25223
|
+
method: "3way-merge",
|
|
25224
|
+
fileResults,
|
|
25225
|
+
conflictReason,
|
|
25226
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
25227
|
+
};
|
|
25228
|
+
}
|
|
25229
|
+
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
25230
|
+
try {
|
|
25231
|
+
const inputs = await resolveInputs({
|
|
25232
|
+
patch,
|
|
25233
|
+
filePath,
|
|
25234
|
+
git: this.git,
|
|
25235
|
+
lockManager: this.lockManager,
|
|
25236
|
+
outputDir: this.outputDir,
|
|
25237
|
+
isTreeReachable: (h2) => this.isTreeReachable(h2),
|
|
25238
|
+
resolveBaseGeneration: (s) => this.resolveBaseGeneration(s),
|
|
25239
|
+
resolveFilePath: (f, b3, c3) => this.resolveFilePath(f, b3, c3),
|
|
25240
|
+
extractRenameSource: (p2, f) => this.extractRenameSource(p2, f),
|
|
25241
|
+
extractFileDiff: (p2, f) => this.extractFileDiff(p2, f)
|
|
25242
|
+
});
|
|
25243
|
+
if (inputs.earlyExit) {
|
|
25244
|
+
return { file: filePath, ...inputs.earlyExit };
|
|
25245
|
+
}
|
|
25246
|
+
const plan = await buildMergePlan({
|
|
25247
|
+
inputs,
|
|
25248
|
+
patch,
|
|
25249
|
+
filePath,
|
|
25250
|
+
accumulator: this.accumulator,
|
|
25251
|
+
tempGit,
|
|
25252
|
+
tempDir,
|
|
25253
|
+
applyPatchToContent: (b3, p2, f, g, d, s) => this.applyPatchToContent(b3, p2, f, g, d, s),
|
|
25254
|
+
isPatchAlreadyApplied: (p2, f, c3, g, d) => this.isPatchAlreadyApplied(p2, f, c3, g, d)
|
|
25255
|
+
});
|
|
25256
|
+
return await runMergeAndRecord({
|
|
25257
|
+
plan,
|
|
25258
|
+
inputs,
|
|
25259
|
+
patch,
|
|
25260
|
+
accumulator: this.accumulator
|
|
25261
|
+
});
|
|
25262
|
+
} catch (error) {
|
|
25263
|
+
return {
|
|
25264
|
+
file: filePath,
|
|
25265
|
+
status: "skipped",
|
|
25266
|
+
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
25267
|
+
};
|
|
25268
|
+
}
|
|
25269
|
+
}
|
|
25270
|
+
async isTreeReachable(treeHash) {
|
|
25271
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
25272
|
+
if (result === void 0) {
|
|
25273
|
+
result = await this.git.treeExists(treeHash);
|
|
25274
|
+
this.treeExistsCache.set(treeHash, result);
|
|
25275
|
+
}
|
|
25276
|
+
return result;
|
|
25277
|
+
}
|
|
25278
|
+
isExcluded(patch) {
|
|
25279
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
25280
|
+
if (!config.exclude) return false;
|
|
25281
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
|
|
25282
|
+
}
|
|
25283
|
+
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
25284
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
25285
|
+
if (config.moves) {
|
|
25286
|
+
for (const move of config.moves) {
|
|
25287
|
+
if (minimatch(filePath, move.from) || filePath === move.from) {
|
|
25288
|
+
if (filePath === move.from) {
|
|
25289
|
+
return move.to;
|
|
25290
|
+
}
|
|
25291
|
+
const fromBase = move.from.replace(/\*\*.*$/, "");
|
|
25292
|
+
const toBase = move.to.replace(/\*\*.*$/, "");
|
|
25293
|
+
if (filePath.startsWith(fromBase)) {
|
|
25294
|
+
return toBase + filePath.slice(fromBase.length);
|
|
25295
|
+
}
|
|
25296
|
+
}
|
|
25297
|
+
}
|
|
25298
|
+
}
|
|
25299
|
+
const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
|
|
25300
|
+
let renames = this.renameCache.get(cacheKey);
|
|
25301
|
+
if (!renames) {
|
|
25302
|
+
renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
|
|
25303
|
+
this.renameCache.set(cacheKey, renames);
|
|
25304
|
+
}
|
|
25305
|
+
const gitRename = renames.find((r2) => r2.from === filePath);
|
|
25306
|
+
if (gitRename) {
|
|
25307
|
+
return gitRename.to;
|
|
25308
|
+
}
|
|
25309
|
+
return filePath;
|
|
25310
|
+
}
|
|
25311
|
+
async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
|
|
25312
|
+
if (!base) {
|
|
25313
|
+
return this.extractNewFileFromPatch(patchContent, filePath);
|
|
25314
|
+
}
|
|
25315
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
25316
|
+
if (!fileDiff) return null;
|
|
25317
|
+
try {
|
|
25318
|
+
if (sourceFilePath) {
|
|
25319
|
+
const tempSourcePath = (0, import_path10.join)(tempDir, sourceFilePath);
|
|
25320
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempSourcePath), { recursive: true });
|
|
25321
|
+
await (0, import_promises4.writeFile)(tempSourcePath, base);
|
|
25322
|
+
await tempGit.exec(["add", sourceFilePath]);
|
|
25323
|
+
await tempGit.exec([
|
|
25324
|
+
"commit",
|
|
25325
|
+
"-m",
|
|
25326
|
+
`base for rename ${sourceFilePath} -> ${filePath}`,
|
|
25327
|
+
"--allow-empty"
|
|
25328
|
+
]);
|
|
25329
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
25330
|
+
const tempTargetPath = (0, import_path10.join)(tempDir, filePath);
|
|
25331
|
+
return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
|
|
25332
|
+
}
|
|
25333
|
+
const tempFilePath = (0, import_path10.join)(tempDir, filePath);
|
|
25334
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
|
|
25335
|
+
await (0, import_promises4.writeFile)(tempFilePath, base);
|
|
25336
|
+
await tempGit.exec(["add", filePath]);
|
|
25337
|
+
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
25338
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
25339
|
+
return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
|
|
25340
|
+
} catch {
|
|
25341
|
+
return null;
|
|
25342
|
+
}
|
|
25343
|
+
}
|
|
25344
|
+
/**
|
|
25345
|
+
* Detects whether a patch's additions are already present in `content`.
|
|
25346
|
+
* Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
|
|
25347
|
+
* the forward patch is effectively a no-op (its +lines are already there and
|
|
25348
|
+
* its -lines are already gone). Used to distinguish "patch already applied"
|
|
25349
|
+
* from "patch base mismatch" after a forward-apply fails.
|
|
25350
|
+
*/
|
|
25351
|
+
async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
|
|
25352
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
25353
|
+
if (!fileDiff) return false;
|
|
25354
|
+
try {
|
|
25355
|
+
const tempFilePath = (0, import_path10.join)(tempDir, filePath);
|
|
25356
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
|
|
25357
|
+
await (0, import_promises4.writeFile)(tempFilePath, content);
|
|
25358
|
+
await tempGit.exec(["add", filePath]);
|
|
25359
|
+
await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
|
|
25360
|
+
await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
|
|
25361
|
+
return true;
|
|
25362
|
+
} catch {
|
|
25363
|
+
return false;
|
|
25364
|
+
}
|
|
25365
|
+
}
|
|
25366
|
+
extractFileDiff(patchContent, filePath) {
|
|
25367
|
+
const lines = patchContent.split("\n");
|
|
25368
|
+
const diffLines = [];
|
|
25369
|
+
let inTargetFile = false;
|
|
25370
|
+
for (const line of lines) {
|
|
25371
|
+
if (line.startsWith("diff --git")) {
|
|
25372
|
+
if (inTargetFile) {
|
|
25373
|
+
break;
|
|
25374
|
+
}
|
|
25375
|
+
if (isDiffLineForFile(line, filePath)) {
|
|
25376
|
+
inTargetFile = true;
|
|
25377
|
+
diffLines.push(line);
|
|
25378
|
+
}
|
|
25379
|
+
continue;
|
|
25380
|
+
}
|
|
25381
|
+
if (inTargetFile) {
|
|
25382
|
+
diffLines.push(line);
|
|
25383
|
+
}
|
|
25384
|
+
}
|
|
25385
|
+
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
25386
|
+
}
|
|
25387
|
+
extractRenameSource(patchContent, targetFilePath) {
|
|
25388
|
+
const lines = patchContent.split("\n");
|
|
25389
|
+
let inTargetFile = false;
|
|
25390
|
+
for (const line of lines) {
|
|
25391
|
+
if (line.startsWith("diff --git")) {
|
|
25392
|
+
if (inTargetFile) break;
|
|
25393
|
+
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
25394
|
+
continue;
|
|
25395
|
+
}
|
|
25396
|
+
if (!inTargetFile) continue;
|
|
25397
|
+
if (line.startsWith("@@")) break;
|
|
25398
|
+
if (line.startsWith("rename from ")) {
|
|
25399
|
+
return line.slice("rename from ".length);
|
|
25400
|
+
}
|
|
25401
|
+
}
|
|
25402
|
+
return null;
|
|
25403
|
+
}
|
|
25404
|
+
extractNewFileFromPatch(patchContent, filePath) {
|
|
25405
|
+
const lines = patchContent.split("\n");
|
|
25406
|
+
const addedLines = [];
|
|
25407
|
+
let inTargetFile = false;
|
|
25408
|
+
let inHunk = false;
|
|
25409
|
+
let isNewFile = false;
|
|
25410
|
+
let noTrailingNewline = false;
|
|
25411
|
+
for (const line of lines) {
|
|
25412
|
+
if (line.startsWith("diff --git")) {
|
|
25413
|
+
if (inTargetFile) break;
|
|
25414
|
+
inTargetFile = isDiffLineForFile(line, filePath);
|
|
25415
|
+
inHunk = false;
|
|
25416
|
+
isNewFile = false;
|
|
25417
|
+
continue;
|
|
25418
|
+
}
|
|
25419
|
+
if (!inTargetFile) continue;
|
|
25420
|
+
if (!inHunk) {
|
|
25421
|
+
if (line === "--- /dev/null" || line.startsWith("new file mode")) {
|
|
25422
|
+
isNewFile = true;
|
|
25423
|
+
}
|
|
25424
|
+
}
|
|
25425
|
+
if (line.startsWith("@@")) {
|
|
25426
|
+
if (!isNewFile) return null;
|
|
25427
|
+
inHunk = true;
|
|
25428
|
+
continue;
|
|
25429
|
+
}
|
|
25430
|
+
if (!inHunk) continue;
|
|
25431
|
+
if (line === "\") {
|
|
25432
|
+
noTrailingNewline = true;
|
|
25433
|
+
continue;
|
|
25434
|
+
}
|
|
25435
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
25436
|
+
addedLines.push(line.slice(1));
|
|
25437
|
+
}
|
|
25438
|
+
}
|
|
25439
|
+
if (addedLines.length === 0) return null;
|
|
25440
|
+
return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
|
|
25441
|
+
}
|
|
25442
|
+
};
|
|
25352
25443
|
ReplayCommitter = class {
|
|
25353
25444
|
git;
|
|
25354
25445
|
outputDir;
|
|
@@ -25452,210 +25543,66 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25452
25543
|
};
|
|
25453
25544
|
init_GitClient();
|
|
25454
25545
|
init_HybridReconstruction();
|
|
25455
|
-
|
|
25456
|
-
git
|
|
25457
|
-
detector;
|
|
25458
|
-
applicator;
|
|
25459
|
-
committer;
|
|
25460
|
-
lockManager;
|
|
25461
|
-
outputDir;
|
|
25462
|
-
/** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
|
|
25463
|
-
_lastApplyResults = [];
|
|
25464
|
-
/**
|
|
25465
|
-
* Service-level warnings accumulated during a single runReplay() call.
|
|
25466
|
-
* Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
|
|
25467
|
-
* Populated by `isFileUserOwned` when a patch's base generation tree is unreachable
|
|
25468
|
-
* (shallow clone / aggressive `git gc --prune`) and we fall back to a conservative
|
|
25469
|
-
* heuristic. Cleared at the top of `runReplay()`.
|
|
25470
|
-
*/
|
|
25471
|
-
warnings = [];
|
|
25472
|
-
/**
|
|
25473
|
-
* @internal Test-only accessor.
|
|
25474
|
-
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
25475
|
-
* to inspect the inter-patch accumulator after runReplay() completes (FER-9791, I2).
|
|
25476
|
-
*/
|
|
25477
|
-
get applicatorRef() {
|
|
25478
|
-
return this.applicator;
|
|
25479
|
-
}
|
|
25480
|
-
/**
|
|
25481
|
-
* @internal Test-only accessor.
|
|
25482
|
-
* Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
|
|
25483
|
-
* this to filter applied-vs-conflicted-vs-absorbed patches for invariant assertions
|
|
25484
|
-
* (FER-9791). Returns an empty array if no patches have been applied yet.
|
|
25485
|
-
*/
|
|
25486
|
-
getLastResults() {
|
|
25487
|
-
return this._lastApplyResults;
|
|
25488
|
-
}
|
|
25489
|
-
constructor(outputDir, _config) {
|
|
25490
|
-
const git = new GitClient(outputDir);
|
|
25546
|
+
FileOwnership = class {
|
|
25547
|
+
constructor(git) {
|
|
25491
25548
|
this.git = git;
|
|
25492
|
-
this.outputDir = outputDir;
|
|
25493
|
-
this.lockManager = new LockfileManager(outputDir);
|
|
25494
|
-
this.detector = new ReplayDetector(git, this.lockManager, outputDir);
|
|
25495
|
-
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
25496
|
-
this.committer = new ReplayCommitter(git, outputDir);
|
|
25497
25549
|
}
|
|
25498
25550
|
/**
|
|
25499
|
-
*
|
|
25500
|
-
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
25501
|
-
* the generation commit (or unchanged for dry-run).
|
|
25502
|
-
*
|
|
25503
|
-
* Does NOT apply patches — the returned handle must be passed to
|
|
25504
|
-
* `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
|
|
25505
|
-
* happen here; lockfile persistence is deferred to phase 2.
|
|
25551
|
+
* Canonical resolution. Returns true if `file` is user-owned per:
|
|
25506
25552
|
*
|
|
25507
|
-
*
|
|
25508
|
-
* `
|
|
25553
|
+
* 1. `patch.user_owned === true` — authoritative, persisted flag.
|
|
25554
|
+
* 2. `file === ".fernignore"` — always user-owned.
|
|
25555
|
+
* 3. File matches a `.fernignore` pattern (via minimatch).
|
|
25556
|
+
* 4. Patch content shows `--- /dev/null` for this file (legacy-safe
|
|
25557
|
+
* signal for patches predating `user_owned` or whose flag was lost).
|
|
25558
|
+
* 5. Tree-based check against `patch.base_generation` when reachable
|
|
25559
|
+
* AND distinct from `currentGenSha`. Unreachable base → conservative
|
|
25560
|
+
* user-owned (silent-absorption safety net), with a one-time warning
|
|
25561
|
+
* per base SHA.
|
|
25562
|
+
* 6. Final fallback: check `currentGenSha` when no usable base exists
|
|
25563
|
+
* (legacy one-gen lockfile).
|
|
25509
25564
|
*/
|
|
25510
|
-
async
|
|
25511
|
-
|
|
25512
|
-
|
|
25513
|
-
if (
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
switch (flow) {
|
|
25518
|
-
case "first-generation":
|
|
25519
|
-
return this._prepareFirstGeneration(options);
|
|
25520
|
-
case "no-patches":
|
|
25521
|
-
return this._prepareNoPatchesRegeneration(options);
|
|
25522
|
-
case "normal-regeneration":
|
|
25523
|
-
return this._prepareNormalRegeneration(options);
|
|
25524
|
-
}
|
|
25525
|
-
}
|
|
25526
|
-
/**
|
|
25527
|
-
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
25528
|
-
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
25529
|
-
* under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
|
|
25530
|
-
* respected.
|
|
25531
|
-
*
|
|
25532
|
-
* For terminal handles (dry-run, first-gen has no patch work, skip-app does
|
|
25533
|
-
* its own post-commit logic), this returns the precomputed report.
|
|
25534
|
-
*/
|
|
25535
|
-
async applyPreparedReplay(prep, options) {
|
|
25536
|
-
if (prep._prepared.terminal) {
|
|
25537
|
-
return prep._prepared.preparedReport;
|
|
25538
|
-
}
|
|
25539
|
-
switch (prep._prepared.flow) {
|
|
25540
|
-
case "first-generation":
|
|
25541
|
-
return this._applyFirstGeneration(prep);
|
|
25542
|
-
case "skip-application":
|
|
25543
|
-
return this._applySkipApplication(prep, options);
|
|
25544
|
-
case "no-patches":
|
|
25545
|
-
return this._applyNoPatchesRegeneration(prep, options);
|
|
25546
|
-
case "normal-regeneration":
|
|
25547
|
-
return this._applyNormalRegeneration(prep, options);
|
|
25548
|
-
}
|
|
25549
|
-
}
|
|
25550
|
-
/**
|
|
25551
|
-
* Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
|
|
25552
|
-
* Behavior is byte-identical to the pre-split implementation for all existing callers.
|
|
25553
|
-
*/
|
|
25554
|
-
async runReplay(options) {
|
|
25555
|
-
const prep = await this.prepareReplay(options);
|
|
25556
|
-
return this.applyPreparedReplay(prep, options);
|
|
25557
|
-
}
|
|
25558
|
-
/**
|
|
25559
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
25560
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
25561
|
-
*
|
|
25562
|
-
* After updating the generation record, re-detects customer patches via
|
|
25563
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
25564
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
25565
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
25566
|
-
* was restored from the base branch during conflict PR creation.
|
|
25567
|
-
*/
|
|
25568
|
-
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
25569
|
-
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
25570
|
-
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
25571
|
-
const record2 = {
|
|
25572
|
-
commit_sha: generationCommitSha,
|
|
25573
|
-
tree_hash: treeHash,
|
|
25574
|
-
timestamp,
|
|
25575
|
-
cli_version: options?.cliVersion ?? "unknown",
|
|
25576
|
-
generator_versions: options?.generatorVersions ?? {},
|
|
25577
|
-
base_branch_head: options?.baseBranchHead
|
|
25578
|
-
};
|
|
25579
|
-
let resolvedPatches;
|
|
25580
|
-
if (!this.lockManager.exists()) {
|
|
25581
|
-
this.lockManager.initializeInMemory(record2);
|
|
25582
|
-
} else {
|
|
25583
|
-
this.lockManager.read();
|
|
25584
|
-
const unresolvedPatches = [
|
|
25585
|
-
...this.lockManager.getUnresolvedPatches(),
|
|
25586
|
-
...this.lockManager.getResolvingPatches()
|
|
25587
|
-
];
|
|
25588
|
-
resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
|
|
25589
|
-
this.lockManager.addGeneration(record2);
|
|
25590
|
-
this.lockManager.clearPatches();
|
|
25591
|
-
for (const patch of unresolvedPatches) {
|
|
25592
|
-
this.lockManager.addPatch(patch);
|
|
25593
|
-
}
|
|
25565
|
+
async resolveCanonical(ctx) {
|
|
25566
|
+
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
25567
|
+
if (patch.user_owned) return true;
|
|
25568
|
+
if (file === ".fernignore") return true;
|
|
25569
|
+
if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
|
|
25570
|
+
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
25571
|
+
return true;
|
|
25594
25572
|
}
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
}
|
|
25605
|
-
for (const patch of redetectedPatches) {
|
|
25606
|
-
this.lockManager.addPatch(patch);
|
|
25573
|
+
const base = patch.base_generation;
|
|
25574
|
+
if (base && base !== currentGenSha) {
|
|
25575
|
+
const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
|
|
25576
|
+
if (!reachable) {
|
|
25577
|
+
if (!warnedGens.has(base)) {
|
|
25578
|
+
warnedGens.add(base);
|
|
25579
|
+
warnings.push(
|
|
25580
|
+
`Base generation ${base.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
|
|
25581
|
+
);
|
|
25607
25582
|
}
|
|
25583
|
+
return true;
|
|
25608
25584
|
}
|
|
25609
|
-
|
|
25610
|
-
for (const patch of resolvedPatches ?? []) {
|
|
25611
|
-
this.lockManager.addPatch(patch);
|
|
25612
|
-
}
|
|
25613
|
-
this.detector.warnings.push(
|
|
25614
|
-
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
25615
|
-
);
|
|
25616
|
-
}
|
|
25617
|
-
this.lockManager.save();
|
|
25618
|
-
this.detector.warnings.push(...this.lockManager.warnings);
|
|
25619
|
-
}
|
|
25620
|
-
determineFlow() {
|
|
25621
|
-
try {
|
|
25622
|
-
const lock = this.lockManager.read();
|
|
25623
|
-
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
25624
|
-
} catch (error) {
|
|
25625
|
-
if (error instanceof LockfileNotFoundError) {
|
|
25626
|
-
return "first-generation";
|
|
25627
|
-
}
|
|
25628
|
-
throw error;
|
|
25585
|
+
return await this.git.showFile(base, originalFileName ?? file) === null;
|
|
25629
25586
|
}
|
|
25587
|
+
return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
|
|
25630
25588
|
}
|
|
25631
|
-
|
|
25632
|
-
|
|
25633
|
-
|
|
25634
|
-
|
|
25635
|
-
patchesApplied: 0,
|
|
25636
|
-
patchesWithConflicts: 0,
|
|
25637
|
-
patchesSkipped: 0,
|
|
25638
|
-
conflicts: []
|
|
25639
|
-
};
|
|
25589
|
+
};
|
|
25590
|
+
FirstGenerationFlow = class {
|
|
25591
|
+
constructor(service) {
|
|
25592
|
+
this.service = service;
|
|
25640
25593
|
}
|
|
25641
|
-
async
|
|
25594
|
+
async prepare(options) {
|
|
25642
25595
|
if (options?.dryRun) {
|
|
25643
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25596
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25644
25597
|
return {
|
|
25645
25598
|
flow: "first-generation",
|
|
25646
25599
|
previousGenerationSha: null,
|
|
25647
25600
|
currentGenerationSha: headSha,
|
|
25648
25601
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25649
25602
|
_prepared: {
|
|
25603
|
+
kind: "terminal",
|
|
25650
25604
|
flow: "first-generation",
|
|
25651
|
-
|
|
25652
|
-
preparedReport: this.firstGenerationReport(),
|
|
25653
|
-
patchesToApply: [],
|
|
25654
|
-
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25655
|
-
detectorWarnings: [],
|
|
25656
|
-
revertedCount: 0,
|
|
25657
|
-
genSha: headSha,
|
|
25658
|
-
optionsSnapshot: options
|
|
25605
|
+
preparedReport: firstGenerationReport()
|
|
25659
25606
|
}
|
|
25660
25607
|
};
|
|
25661
25608
|
}
|
|
@@ -25664,17 +25611,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25664
25611
|
generatorVersions: options.generatorVersions ?? {},
|
|
25665
25612
|
baseBranchHead: options.baseBranchHead
|
|
25666
25613
|
} : void 0;
|
|
25667
|
-
await this.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
25668
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25669
|
-
this.lockManager.initializeInMemory(genRecord);
|
|
25614
|
+
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
25615
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25616
|
+
this.service.lockManager.initializeInMemory(genRecord);
|
|
25670
25617
|
return {
|
|
25671
25618
|
flow: "first-generation",
|
|
25672
25619
|
previousGenerationSha: null,
|
|
25673
25620
|
currentGenerationSha: genRecord.commit_sha,
|
|
25674
25621
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25675
25622
|
_prepared: {
|
|
25623
|
+
kind: "active",
|
|
25676
25624
|
flow: "first-generation",
|
|
25677
|
-
terminal: false,
|
|
25678
25625
|
patchesToApply: [],
|
|
25679
25626
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25680
25627
|
detectorWarnings: [],
|
|
@@ -25684,47 +25631,45 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25684
25631
|
}
|
|
25685
25632
|
};
|
|
25686
25633
|
}
|
|
25687
|
-
async
|
|
25688
|
-
this.lockManager.save();
|
|
25689
|
-
return
|
|
25634
|
+
async apply(_prep) {
|
|
25635
|
+
this.service.lockManager.save();
|
|
25636
|
+
return firstGenerationReport();
|
|
25690
25637
|
}
|
|
25691
|
-
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
*/
|
|
25698
|
-
async _prepareSkipApplication(options) {
|
|
25638
|
+
};
|
|
25639
|
+
SkipApplicationFlow = class {
|
|
25640
|
+
constructor(service) {
|
|
25641
|
+
this.service = service;
|
|
25642
|
+
}
|
|
25643
|
+
async prepare(options) {
|
|
25699
25644
|
const commitOpts = options ? {
|
|
25700
25645
|
cliVersion: options.cliVersion ?? "unknown",
|
|
25701
25646
|
generatorVersions: options.generatorVersions ?? {},
|
|
25702
25647
|
baseBranchHead: options.baseBranchHead
|
|
25703
25648
|
} : void 0;
|
|
25704
|
-
await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
25705
|
-
await this.cleanupStaleConflictMarkers();
|
|
25706
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25649
|
+
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
25650
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25651
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25707
25652
|
let previousGenerationSha = null;
|
|
25708
25653
|
try {
|
|
25709
|
-
const lock = this.lockManager.read();
|
|
25654
|
+
const lock = this.service.lockManager.read();
|
|
25710
25655
|
previousGenerationSha = lock.current_generation ?? null;
|
|
25711
|
-
this.lockManager.addGeneration(genRecord);
|
|
25656
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25712
25657
|
} catch (error) {
|
|
25713
25658
|
if (error instanceof LockfileNotFoundError) {
|
|
25714
|
-
this.lockManager.initializeInMemory(genRecord);
|
|
25659
|
+
this.service.lockManager.initializeInMemory(genRecord);
|
|
25715
25660
|
} else {
|
|
25716
25661
|
throw error;
|
|
25717
25662
|
}
|
|
25718
25663
|
}
|
|
25719
|
-
this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
25664
|
+
this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
25720
25665
|
return {
|
|
25721
25666
|
flow: "skip-application",
|
|
25722
25667
|
previousGenerationSha,
|
|
25723
25668
|
currentGenerationSha: genRecord.commit_sha,
|
|
25724
25669
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25725
25670
|
_prepared: {
|
|
25671
|
+
kind: "active",
|
|
25726
25672
|
flow: "skip-application",
|
|
25727
|
-
terminal: false,
|
|
25728
25673
|
patchesToApply: [],
|
|
25729
25674
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25730
25675
|
detectorWarnings: [],
|
|
@@ -25734,13 +25679,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25734
25679
|
}
|
|
25735
25680
|
};
|
|
25736
25681
|
}
|
|
25737
|
-
async
|
|
25738
|
-
this.lockManager.save();
|
|
25739
|
-
const credentialWarnings = [...this.lockManager.warnings];
|
|
25682
|
+
async apply(_prep, options) {
|
|
25683
|
+
this.service.lockManager.save();
|
|
25684
|
+
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
25740
25685
|
if (!options?.stageOnly) {
|
|
25741
|
-
await this.committer.commitReplay(0);
|
|
25686
|
+
await this.service.committer.commitReplay(0);
|
|
25742
25687
|
} else {
|
|
25743
|
-
await this.committer.stageAll();
|
|
25688
|
+
await this.service.committer.stageAll();
|
|
25744
25689
|
}
|
|
25745
25690
|
return {
|
|
25746
25691
|
flow: "skip-application",
|
|
@@ -25752,13 +25697,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25752
25697
|
warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
|
|
25753
25698
|
};
|
|
25754
25699
|
}
|
|
25755
|
-
|
|
25756
|
-
|
|
25700
|
+
};
|
|
25701
|
+
NoPatchesFlow = class {
|
|
25702
|
+
constructor(service) {
|
|
25703
|
+
this.service = service;
|
|
25704
|
+
}
|
|
25705
|
+
async prepare(options) {
|
|
25706
|
+
const preLock = this.service.lockManager.read();
|
|
25757
25707
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
25758
|
-
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
25759
|
-
const detectorWarnings = [...this.detector.warnings];
|
|
25708
|
+
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
25709
|
+
const detectorWarnings = [...this.service.detector.warnings];
|
|
25760
25710
|
if (options?.dryRun) {
|
|
25761
|
-
const dryRunWarnings = [...detectorWarnings, ...this.warnings];
|
|
25711
|
+
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
25762
25712
|
const preparedReport = {
|
|
25763
25713
|
flow: "no-patches",
|
|
25764
25714
|
patchesDetected: newPatches.length,
|
|
@@ -25770,22 +25720,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25770
25720
|
wouldApply: newPatches,
|
|
25771
25721
|
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
25772
25722
|
};
|
|
25773
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25723
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25774
25724
|
return {
|
|
25775
25725
|
flow: "no-patches",
|
|
25776
25726
|
previousGenerationSha,
|
|
25777
25727
|
currentGenerationSha: headSha,
|
|
25778
25728
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25779
25729
|
_prepared: {
|
|
25730
|
+
kind: "terminal",
|
|
25780
25731
|
flow: "no-patches",
|
|
25781
|
-
|
|
25782
|
-
preparedReport,
|
|
25783
|
-
patchesToApply: [],
|
|
25784
|
-
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25785
|
-
detectorWarnings,
|
|
25786
|
-
revertedCount: revertedPatchIds.length,
|
|
25787
|
-
genSha: headSha,
|
|
25788
|
-
optionsSnapshot: options
|
|
25732
|
+
preparedReport
|
|
25789
25733
|
}
|
|
25790
25734
|
};
|
|
25791
25735
|
}
|
|
@@ -25794,7 +25738,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25794
25738
|
const uniqueFiles = [...new Set(newPatches.flatMap((p2) => p2.files))];
|
|
25795
25739
|
const absorbedFiles = /* @__PURE__ */ new Set();
|
|
25796
25740
|
for (const file of uniqueFiles) {
|
|
25797
|
-
const diff = await this.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
|
|
25741
|
+
const diff = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
|
|
25798
25742
|
if (diff !== null && !diff.trim()) {
|
|
25799
25743
|
absorbedFiles.add(file);
|
|
25800
25744
|
}
|
|
@@ -25807,7 +25751,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25807
25751
|
}
|
|
25808
25752
|
for (const id of revertedPatchIds) {
|
|
25809
25753
|
try {
|
|
25810
|
-
this.lockManager.removePatch(id);
|
|
25754
|
+
this.service.lockManager.removePatch(id);
|
|
25811
25755
|
} catch {
|
|
25812
25756
|
}
|
|
25813
25757
|
}
|
|
@@ -25816,18 +25760,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25816
25760
|
generatorVersions: options.generatorVersions ?? {},
|
|
25817
25761
|
baseBranchHead: options.baseBranchHead
|
|
25818
25762
|
} : void 0;
|
|
25819
|
-
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
25820
|
-
await this.cleanupStaleConflictMarkers();
|
|
25821
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25822
|
-
this.lockManager.addGeneration(genRecord);
|
|
25763
|
+
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
25764
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25765
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25766
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25823
25767
|
return {
|
|
25824
25768
|
flow: "no-patches",
|
|
25825
25769
|
previousGenerationSha,
|
|
25826
25770
|
currentGenerationSha: genRecord.commit_sha,
|
|
25827
25771
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25828
25772
|
_prepared: {
|
|
25773
|
+
kind: "active",
|
|
25829
25774
|
flow: "no-patches",
|
|
25830
|
-
terminal: false,
|
|
25831
25775
|
patchesToApply: newPatches,
|
|
25832
25776
|
newPatchIds: new Set(newPatches.map((p2) => p2.id)),
|
|
25833
25777
|
detectorWarnings,
|
|
@@ -25837,41 +25781,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25837
25781
|
}
|
|
25838
25782
|
};
|
|
25839
25783
|
}
|
|
25840
|
-
async
|
|
25784
|
+
async apply(prep, options) {
|
|
25785
|
+
assertActive(prep);
|
|
25841
25786
|
const newPatches = prep._prepared.patchesToApply;
|
|
25842
25787
|
const detectorWarnings = prep._prepared.detectorWarnings;
|
|
25843
25788
|
const genSha = prep._prepared.genSha;
|
|
25844
25789
|
let results = [];
|
|
25845
25790
|
if (newPatches.length > 0) {
|
|
25846
|
-
results = await this.applicator.applyPatches(newPatches);
|
|
25847
|
-
this._lastApplyResults = results;
|
|
25848
|
-
this.recordDroppedContextLineWarnings(results);
|
|
25849
|
-
this.revertConflictingFiles(results);
|
|
25791
|
+
results = await this.service.applicator.applyPatches(newPatches);
|
|
25792
|
+
this.service._lastApplyResults = results;
|
|
25793
|
+
this.service.recordDroppedContextLineWarnings(results);
|
|
25794
|
+
this.service.revertConflictingFiles(results);
|
|
25850
25795
|
for (const result of results) {
|
|
25851
25796
|
if (result.status === "conflict") {
|
|
25852
25797
|
result.patch.status = "unresolved";
|
|
25853
25798
|
}
|
|
25854
25799
|
}
|
|
25855
25800
|
}
|
|
25856
|
-
const rebaseCounts = await this.rebasePatches(results, genSha);
|
|
25801
|
+
const rebaseCounts = await this.service.rebasePatches(results, genSha);
|
|
25857
25802
|
for (const patch of newPatches) {
|
|
25858
25803
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
25859
|
-
this.lockManager.addPatch(patch);
|
|
25804
|
+
this.service.lockManager.addPatch(patch);
|
|
25860
25805
|
}
|
|
25861
25806
|
}
|
|
25862
|
-
this.lockManager.save();
|
|
25863
|
-
this.warnings.push(...this.lockManager.warnings);
|
|
25807
|
+
this.service.lockManager.save();
|
|
25808
|
+
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
25864
25809
|
if (newPatches.length > 0) {
|
|
25865
25810
|
if (!options?.stageOnly) {
|
|
25866
25811
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
25867
25812
|
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
|
|
25868
|
-
await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
25813
|
+
await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
25869
25814
|
} else {
|
|
25870
|
-
await this.committer.stageAll();
|
|
25815
|
+
await this.service.committer.stageAll();
|
|
25871
25816
|
}
|
|
25872
25817
|
}
|
|
25873
|
-
const warnings = [...detectorWarnings, ...this.warnings];
|
|
25874
|
-
return this.buildReport(
|
|
25818
|
+
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
25819
|
+
return this.service.buildReport(
|
|
25875
25820
|
"no-patches",
|
|
25876
25821
|
newPatches,
|
|
25877
25822
|
results,
|
|
@@ -25882,13 +25827,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25882
25827
|
prep._prepared.revertedCount
|
|
25883
25828
|
);
|
|
25884
25829
|
}
|
|
25885
|
-
|
|
25886
|
-
|
|
25830
|
+
};
|
|
25831
|
+
NormalRegenerationFlow = class {
|
|
25832
|
+
constructor(service) {
|
|
25833
|
+
this.service = service;
|
|
25834
|
+
}
|
|
25835
|
+
async prepare(options) {
|
|
25836
|
+
const preLock = this.service.lockManager.read();
|
|
25887
25837
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
25888
25838
|
if (options?.dryRun) {
|
|
25889
|
-
const existingPatches2 = this.lockManager.getPatches();
|
|
25890
|
-
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
25891
|
-
const warnings = [...this.detector.warnings, ...this.warnings];
|
|
25839
|
+
const existingPatches2 = this.service.lockManager.getPatches();
|
|
25840
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
25841
|
+
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
25892
25842
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
25893
25843
|
const preparedReport = {
|
|
25894
25844
|
flow: "normal-regeneration",
|
|
@@ -25901,48 +25851,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25901
25851
|
wouldApply: allPatches2,
|
|
25902
25852
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
25903
25853
|
};
|
|
25904
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25854
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25905
25855
|
return {
|
|
25906
25856
|
flow: "normal-regeneration",
|
|
25907
25857
|
previousGenerationSha,
|
|
25908
25858
|
currentGenerationSha: headSha,
|
|
25909
25859
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25910
25860
|
_prepared: {
|
|
25861
|
+
kind: "terminal",
|
|
25911
25862
|
flow: "normal-regeneration",
|
|
25912
|
-
|
|
25913
|
-
preparedReport,
|
|
25914
|
-
patchesToApply: [],
|
|
25915
|
-
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25916
|
-
detectorWarnings: [...this.detector.warnings],
|
|
25917
|
-
revertedCount: dryRunReverted.length,
|
|
25918
|
-
genSha: headSha,
|
|
25919
|
-
optionsSnapshot: options
|
|
25863
|
+
preparedReport
|
|
25920
25864
|
}
|
|
25921
25865
|
};
|
|
25922
25866
|
}
|
|
25923
|
-
let existingPatches = this.lockManager.getPatches();
|
|
25867
|
+
let existingPatches = this.service.lockManager.getPatches();
|
|
25924
25868
|
const preRebasePatchIds = new Set(existingPatches.map((p2) => p2.id));
|
|
25925
|
-
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
25926
|
-
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p2) => p2.id));
|
|
25869
|
+
const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
|
|
25870
|
+
const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p2) => p2.id));
|
|
25927
25871
|
const removedByPreRebase = existingPatches.filter(
|
|
25928
25872
|
(p2) => preRebasePatchIds.has(p2.id) && !postRebasePatchIds.has(p2.id)
|
|
25929
25873
|
);
|
|
25930
|
-
existingPatches = this.lockManager.getPatches();
|
|
25874
|
+
existingPatches = this.service.lockManager.getPatches();
|
|
25931
25875
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
25932
25876
|
for (const p2 of existingPatches) {
|
|
25933
25877
|
const priorPatchId = seenHashes.get(p2.content_hash);
|
|
25934
25878
|
if (priorPatchId !== void 0) {
|
|
25935
|
-
this.lockManager.removePatch(p2.id);
|
|
25936
|
-
this.warnings.push(
|
|
25879
|
+
this.service.lockManager.removePatch(p2.id);
|
|
25880
|
+
this.service.warnings.push(
|
|
25937
25881
|
`Consolidated patch ${p2.id} (commit ${(p2.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.`
|
|
25938
25882
|
);
|
|
25939
25883
|
} else {
|
|
25940
25884
|
seenHashes.set(p2.content_hash, p2.id);
|
|
25941
25885
|
}
|
|
25942
25886
|
}
|
|
25943
|
-
existingPatches = this.lockManager.getPatches();
|
|
25944
|
-
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
25945
|
-
const detectorWarnings = [...this.detector.warnings];
|
|
25887
|
+
existingPatches = this.service.lockManager.getPatches();
|
|
25888
|
+
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
25889
|
+
const detectorWarnings = [...this.service.detector.warnings];
|
|
25946
25890
|
if (removedByPreRebase.length > 0) {
|
|
25947
25891
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p2) => p2.original_commit));
|
|
25948
25892
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p2) => p2.original_message));
|
|
@@ -25957,7 +25901,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25957
25901
|
}
|
|
25958
25902
|
for (const id of revertedPatchIds) {
|
|
25959
25903
|
try {
|
|
25960
|
-
this.lockManager.removePatch(id);
|
|
25904
|
+
this.service.lockManager.removePatch(id);
|
|
25961
25905
|
} catch {
|
|
25962
25906
|
}
|
|
25963
25907
|
}
|
|
@@ -25969,18 +25913,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25969
25913
|
generatorVersions: options.generatorVersions ?? {},
|
|
25970
25914
|
baseBranchHead: options.baseBranchHead
|
|
25971
25915
|
} : void 0;
|
|
25972
|
-
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
25973
|
-
await this.cleanupStaleConflictMarkers();
|
|
25974
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25975
|
-
this.lockManager.addGeneration(genRecord);
|
|
25916
|
+
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
25917
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25918
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25919
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25976
25920
|
return {
|
|
25977
25921
|
flow: "normal-regeneration",
|
|
25978
25922
|
previousGenerationSha,
|
|
25979
25923
|
currentGenerationSha: genRecord.commit_sha,
|
|
25980
25924
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25981
25925
|
_prepared: {
|
|
25926
|
+
kind: "active",
|
|
25982
25927
|
flow: "normal-regeneration",
|
|
25983
|
-
terminal: false,
|
|
25984
25928
|
patchesToApply: allPatches,
|
|
25985
25929
|
newPatchIds: new Set(newPatches.map((p2) => p2.id)),
|
|
25986
25930
|
preRebaseCounts,
|
|
@@ -25991,46 +25935,47 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25991
25935
|
}
|
|
25992
25936
|
};
|
|
25993
25937
|
}
|
|
25994
|
-
async
|
|
25938
|
+
async apply(prep, options) {
|
|
25939
|
+
assertActive(prep);
|
|
25995
25940
|
const allPatches = prep._prepared.patchesToApply;
|
|
25996
25941
|
const newPatchIds = prep._prepared.newPatchIds;
|
|
25997
25942
|
const detectorWarnings = prep._prepared.detectorWarnings;
|
|
25998
25943
|
const genSha = prep._prepared.genSha;
|
|
25999
|
-
const results = await this.applicator.applyPatches(allPatches);
|
|
26000
|
-
this._lastApplyResults = results;
|
|
26001
|
-
this.recordDroppedContextLineWarnings(results);
|
|
26002
|
-
this.revertConflictingFiles(results);
|
|
25944
|
+
const results = await this.service.applicator.applyPatches(allPatches);
|
|
25945
|
+
this.service._lastApplyResults = results;
|
|
25946
|
+
this.service.recordDroppedContextLineWarnings(results);
|
|
25947
|
+
this.service.revertConflictingFiles(results);
|
|
26003
25948
|
for (const result of results) {
|
|
26004
25949
|
if (result.status === "conflict") {
|
|
26005
25950
|
result.patch.status = "unresolved";
|
|
26006
25951
|
}
|
|
26007
25952
|
}
|
|
26008
|
-
const rebaseCounts = await this.rebasePatches(results, genSha);
|
|
25953
|
+
const rebaseCounts = await this.service.rebasePatches(results, genSha);
|
|
26009
25954
|
const newPatches = allPatches.filter((p2) => newPatchIds.has(p2.id));
|
|
26010
25955
|
for (const patch of newPatches) {
|
|
26011
25956
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
26012
|
-
this.lockManager.addPatch(patch);
|
|
25957
|
+
this.service.lockManager.addPatch(patch);
|
|
26013
25958
|
}
|
|
26014
25959
|
}
|
|
26015
25960
|
for (const result of results) {
|
|
26016
25961
|
if (result.status === "conflict") {
|
|
26017
25962
|
try {
|
|
26018
|
-
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
25963
|
+
this.service.lockManager.markPatchUnresolved(result.patch.id);
|
|
26019
25964
|
} catch {
|
|
26020
25965
|
}
|
|
26021
25966
|
}
|
|
26022
25967
|
}
|
|
26023
|
-
this.lockManager.save();
|
|
26024
|
-
this.warnings.push(...this.lockManager.warnings);
|
|
25968
|
+
this.service.lockManager.save();
|
|
25969
|
+
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
26025
25970
|
if (options?.stageOnly) {
|
|
26026
|
-
await this.committer.stageAll();
|
|
25971
|
+
await this.service.committer.stageAll();
|
|
26027
25972
|
} else {
|
|
26028
25973
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
26029
25974
|
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
|
|
26030
|
-
await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
25975
|
+
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
26031
25976
|
}
|
|
26032
|
-
const warnings = [...detectorWarnings, ...this.warnings];
|
|
26033
|
-
return this.buildReport(
|
|
25977
|
+
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
25978
|
+
return this.service.buildReport(
|
|
26034
25979
|
"normal-regeneration",
|
|
26035
25980
|
allPatches,
|
|
26036
25981
|
results,
|
|
@@ -26041,10 +25986,204 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26041
25986
|
prep._prepared.revertedCount
|
|
26042
25987
|
);
|
|
26043
25988
|
}
|
|
25989
|
+
};
|
|
25990
|
+
ReplayService = class {
|
|
25991
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
25992
|
+
git;
|
|
25993
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
25994
|
+
detector;
|
|
25995
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
25996
|
+
applicator;
|
|
25997
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
25998
|
+
committer;
|
|
25999
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26000
|
+
lockManager;
|
|
26001
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26002
|
+
fileOwnership;
|
|
26003
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26004
|
+
outputDir;
|
|
26005
|
+
/** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
|
|
26006
|
+
_lastApplyResults = [];
|
|
26007
|
+
/**
|
|
26008
|
+
* Service-level warnings accumulated during a single runReplay() call.
|
|
26009
|
+
* Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
|
|
26010
|
+
* Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
|
|
26011
|
+
* is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
|
|
26012
|
+
* conservative heuristic. Cleared at the top of `runReplay()`.
|
|
26013
|
+
*
|
|
26014
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
26015
|
+
*/
|
|
26016
|
+
warnings = [];
|
|
26017
|
+
/**
|
|
26018
|
+
* @internal Test-only accessor.
|
|
26019
|
+
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
26020
|
+
* to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
|
|
26021
|
+
*/
|
|
26022
|
+
get applicatorRef() {
|
|
26023
|
+
return this.applicator;
|
|
26024
|
+
}
|
|
26025
|
+
/**
|
|
26026
|
+
* @internal Test-only accessor.
|
|
26027
|
+
* Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
|
|
26028
|
+
* this to filter applied-vs-conflicted-vs-absorbed patches for invariant
|
|
26029
|
+
* assertions. Returns an empty array if no patches have been applied yet.
|
|
26030
|
+
*/
|
|
26031
|
+
getLastResults() {
|
|
26032
|
+
return this._lastApplyResults;
|
|
26033
|
+
}
|
|
26034
|
+
constructor(outputDir, _config) {
|
|
26035
|
+
const git = new GitClient(outputDir);
|
|
26036
|
+
this.git = git;
|
|
26037
|
+
this.outputDir = outputDir;
|
|
26038
|
+
this.lockManager = new LockfileManager(outputDir);
|
|
26039
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir);
|
|
26040
|
+
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
26041
|
+
this.committer = new ReplayCommitter(git, outputDir);
|
|
26042
|
+
this.fileOwnership = new FileOwnership(git);
|
|
26043
|
+
}
|
|
26044
|
+
/**
|
|
26045
|
+
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
26046
|
+
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
26047
|
+
* the generation commit (or unchanged for dry-run).
|
|
26048
|
+
*
|
|
26049
|
+
* Does NOT apply patches — the returned handle must be passed to
|
|
26050
|
+
* `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
|
|
26051
|
+
* happen here; lockfile persistence is deferred to phase 2.
|
|
26052
|
+
*
|
|
26053
|
+
* Callers may land additional commits on HEAD between `prepareReplay` and
|
|
26054
|
+
* `applyPreparedReplay` (e.g., `[fern-autoversion]`).
|
|
26055
|
+
*/
|
|
26056
|
+
async prepareReplay(options) {
|
|
26057
|
+
this.warnings = [];
|
|
26058
|
+
this.detector.warnings.length = 0;
|
|
26059
|
+
return this.selectFlow(options).prepare(options);
|
|
26060
|
+
}
|
|
26061
|
+
/**
|
|
26062
|
+
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
26063
|
+
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
26064
|
+
* under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
|
|
26065
|
+
* respected.
|
|
26066
|
+
*
|
|
26067
|
+
* For terminal handles (dry-run, first-gen has no patch work, skip-app does
|
|
26068
|
+
* its own post-commit logic), this returns the precomputed report.
|
|
26069
|
+
*/
|
|
26070
|
+
async applyPreparedReplay(prep, options) {
|
|
26071
|
+
if (prep._prepared.kind === "terminal") {
|
|
26072
|
+
return prep._prepared.preparedReport;
|
|
26073
|
+
}
|
|
26074
|
+
return this.flowForKind(prep._prepared.flow).apply(prep, options);
|
|
26075
|
+
}
|
|
26076
|
+
/**
|
|
26077
|
+
* Construct the flow for the current run. `selectFlow` is called from
|
|
26078
|
+
* `prepareReplay` and decides based on options + lockfile state.
|
|
26079
|
+
*/
|
|
26080
|
+
selectFlow(options) {
|
|
26081
|
+
if (options?.skipApplication) return new SkipApplicationFlow(this);
|
|
26082
|
+
return this.flowForKind(this.determineFlow());
|
|
26083
|
+
}
|
|
26084
|
+
/**
|
|
26085
|
+
* Construct a flow given its kind. Used by `applyPreparedReplay` to
|
|
26086
|
+
* route to the same kind that was selected in phase 1.
|
|
26087
|
+
*/
|
|
26088
|
+
flowForKind(kind) {
|
|
26089
|
+
switch (kind) {
|
|
26090
|
+
case "first-generation":
|
|
26091
|
+
return new FirstGenerationFlow(this);
|
|
26092
|
+
case "no-patches":
|
|
26093
|
+
return new NoPatchesFlow(this);
|
|
26094
|
+
case "normal-regeneration":
|
|
26095
|
+
return new NormalRegenerationFlow(this);
|
|
26096
|
+
case "skip-application":
|
|
26097
|
+
return new SkipApplicationFlow(this);
|
|
26098
|
+
}
|
|
26099
|
+
}
|
|
26100
|
+
/**
|
|
26101
|
+
* Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
|
|
26102
|
+
* Behavior is byte-identical to the pre-split implementation for all existing callers.
|
|
26103
|
+
*/
|
|
26104
|
+
async runReplay(options) {
|
|
26105
|
+
const prep = await this.prepareReplay(options);
|
|
26106
|
+
return this.applyPreparedReplay(prep, options);
|
|
26107
|
+
}
|
|
26108
|
+
/**
|
|
26109
|
+
* Sync the lockfile after a divergent PR was squash-merged.
|
|
26110
|
+
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
26111
|
+
*
|
|
26112
|
+
* After updating the generation record, re-detects customer patches via
|
|
26113
|
+
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
26114
|
+
* (which includes customer customizations after squash merge). This ensures
|
|
26115
|
+
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
26116
|
+
* was restored from the base branch during conflict PR creation.
|
|
26117
|
+
*/
|
|
26118
|
+
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
26119
|
+
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
26120
|
+
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
26121
|
+
const record2 = {
|
|
26122
|
+
commit_sha: generationCommitSha,
|
|
26123
|
+
tree_hash: treeHash,
|
|
26124
|
+
timestamp,
|
|
26125
|
+
cli_version: options?.cliVersion ?? "unknown",
|
|
26126
|
+
generator_versions: options?.generatorVersions ?? {},
|
|
26127
|
+
base_branch_head: options?.baseBranchHead
|
|
26128
|
+
};
|
|
26129
|
+
let resolvedPatches;
|
|
26130
|
+
if (!this.lockManager.exists()) {
|
|
26131
|
+
this.lockManager.initializeInMemory(record2);
|
|
26132
|
+
} else {
|
|
26133
|
+
this.lockManager.read();
|
|
26134
|
+
const unresolvedPatches = [
|
|
26135
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
26136
|
+
...this.lockManager.getResolvingPatches()
|
|
26137
|
+
];
|
|
26138
|
+
resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
|
|
26139
|
+
this.lockManager.addGeneration(record2);
|
|
26140
|
+
this.lockManager.clearPatches();
|
|
26141
|
+
for (const patch of unresolvedPatches) {
|
|
26142
|
+
this.lockManager.addPatch(patch);
|
|
26143
|
+
}
|
|
26144
|
+
}
|
|
26145
|
+
try {
|
|
26146
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
26147
|
+
if (redetectedPatches.length > 0) {
|
|
26148
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p2) => p2.files));
|
|
26149
|
+
const currentPatches = this.lockManager.getPatches();
|
|
26150
|
+
for (const patch of currentPatches) {
|
|
26151
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
26152
|
+
this.lockManager.removePatch(patch.id);
|
|
26153
|
+
}
|
|
26154
|
+
}
|
|
26155
|
+
for (const patch of redetectedPatches) {
|
|
26156
|
+
this.lockManager.addPatch(patch);
|
|
26157
|
+
}
|
|
26158
|
+
}
|
|
26159
|
+
} catch (error) {
|
|
26160
|
+
for (const patch of resolvedPatches ?? []) {
|
|
26161
|
+
this.lockManager.addPatch(patch);
|
|
26162
|
+
}
|
|
26163
|
+
this.detector.warnings.push(
|
|
26164
|
+
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
26165
|
+
);
|
|
26166
|
+
}
|
|
26167
|
+
this.lockManager.save();
|
|
26168
|
+
this.detector.warnings.push(...this.lockManager.warnings);
|
|
26169
|
+
}
|
|
26170
|
+
determineFlow() {
|
|
26171
|
+
try {
|
|
26172
|
+
const lock = this.lockManager.read();
|
|
26173
|
+
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
26174
|
+
} catch (error) {
|
|
26175
|
+
if (error instanceof LockfileNotFoundError) {
|
|
26176
|
+
return "first-generation";
|
|
26177
|
+
}
|
|
26178
|
+
throw error;
|
|
26179
|
+
}
|
|
26180
|
+
}
|
|
26044
26181
|
/**
|
|
26045
26182
|
* Rebase cleanly applied patches so they are relative to the current generation.
|
|
26046
26183
|
* This prevents recurring conflicts on subsequent regenerations.
|
|
26047
26184
|
* Returns the number of patches rebased.
|
|
26185
|
+
*
|
|
26186
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
26048
26187
|
*/
|
|
26049
26188
|
async rebasePatches(results, currentGenSha) {
|
|
26050
26189
|
let absorbed = 0;
|
|
@@ -26054,6 +26193,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26054
26193
|
const seenContentHashes = /* @__PURE__ */ new Map();
|
|
26055
26194
|
const absorbedPatchIds = /* @__PURE__ */ new Set();
|
|
26056
26195
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
26196
|
+
const warnedGens = /* @__PURE__ */ new Set();
|
|
26057
26197
|
for (const result of results) {
|
|
26058
26198
|
if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
|
|
26059
26199
|
const patch = result.patch;
|
|
@@ -26098,18 +26238,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26098
26238
|
}
|
|
26099
26239
|
}
|
|
26100
26240
|
const isUserOwnedPerFile = await Promise.all(
|
|
26101
|
-
patch.files.map(
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26106
|
-
|
|
26107
|
-
|
|
26108
|
-
|
|
26109
|
-
|
|
26110
|
-
|
|
26111
|
-
|
|
26112
|
-
})
|
|
26241
|
+
patch.files.map(
|
|
26242
|
+
(file) => this.fileOwnership.resolveCanonical({
|
|
26243
|
+
file,
|
|
26244
|
+
originalFileName: originalByResolved[file],
|
|
26245
|
+
patch,
|
|
26246
|
+
currentGenSha,
|
|
26247
|
+
fernignorePatterns,
|
|
26248
|
+
warnedGens,
|
|
26249
|
+
warnings: this.warnings
|
|
26250
|
+
})
|
|
26251
|
+
)
|
|
26113
26252
|
);
|
|
26114
26253
|
const hasProtectedFiles = patch.files.some(
|
|
26115
26254
|
(file) => file === ".fernignore" || fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))
|
|
@@ -26236,7 +26375,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26236
26375
|
for (const file of files) {
|
|
26237
26376
|
if (isBinaryFile(file)) continue;
|
|
26238
26377
|
try {
|
|
26239
|
-
const content = await (0,
|
|
26378
|
+
const content = await (0, import_promises7.readFile)((0, import_path13.join)(this.outputDir, file), "utf-8");
|
|
26240
26379
|
snapshot[file] = content;
|
|
26241
26380
|
} catch {
|
|
26242
26381
|
}
|
|
@@ -26244,10 +26383,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26244
26383
|
return snapshot;
|
|
26245
26384
|
}
|
|
26246
26385
|
/**
|
|
26247
|
-
*
|
|
26248
|
-
* `fromSha` and `toSha` touched any
|
|
26249
|
-
*
|
|
26250
|
-
*
|
|
26386
|
+
* Detects autoversion contamination — returns true when a
|
|
26387
|
+
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
|
26388
|
+
* of the patch's `files`. Re-classifies each grep match via
|
|
26389
|
+
* `isGenerationCommit` so a customer commit that merely quotes the
|
|
26390
|
+
* marker in its body doesn't false-positive.
|
|
26391
|
+
*
|
|
26392
|
+
* Used by preGenerationRebase to skip the customer-content rebuild for
|
|
26393
|
+
* patches whose files were last touched by an autoversion step — a
|
|
26394
|
+
* naive `git diff currentGen HEAD` there would capture the
|
|
26395
|
+
* placeholder→semver swap as customer customization.
|
|
26251
26396
|
*/
|
|
26252
26397
|
async hasAutoversionContamination(fromSha, toSha, files) {
|
|
26253
26398
|
if (files.length === 0) return false;
|
|
@@ -26276,10 +26421,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26276
26421
|
return false;
|
|
26277
26422
|
}
|
|
26278
26423
|
/**
|
|
26279
|
-
*
|
|
26280
|
-
*
|
|
26281
|
-
*
|
|
26282
|
-
*
|
|
26424
|
+
* Rebuild patch_content from `theirs_snapshot` (captured at detection
|
|
26425
|
+
* time, predates pipeline content) rather than a HEAD-relative diff.
|
|
26426
|
+
* Returns `null` when snapshot is missing or doesn't cover every file
|
|
26427
|
+
* in `patch.files` — caller leaves the patch unchanged.
|
|
26428
|
+
*
|
|
26429
|
+
* Used as the autoversion-contamination escape hatch: when the
|
|
26430
|
+
* HEAD-relative diff would capture pipeline content (autoversion's
|
|
26431
|
+
* placeholder swap) as customer customization, the snapshot path
|
|
26432
|
+
* preserves the customer's actual intent.
|
|
26283
26433
|
*/
|
|
26284
26434
|
async computeSnapshotBasedPatchDiff(patch, currentGen) {
|
|
26285
26435
|
if (!patch.theirs_snapshot) return null;
|
|
@@ -26327,7 +26477,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26327
26477
|
* **Skips patches sharing files with other patches.** HEAD's content
|
|
26328
26478
|
* for a file shared by N patches is cumulative across all of them, so
|
|
26329
26479
|
* a HEAD-fallback would falsely encode other patches' contributions
|
|
26330
|
-
* into one patch's snapshot — breaking
|
|
26480
|
+
* into one patch's snapshot — breaking cross-patch isolation
|
|
26331
26481
|
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
26332
26482
|
* make sense when the patch owns its file. Multi-patch-same-file
|
|
26333
26483
|
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
@@ -26389,72 +26539,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26389
26539
|
);
|
|
26390
26540
|
}
|
|
26391
26541
|
}
|
|
26392
|
-
/**
|
|
26393
|
-
* Determine whether `file` is user-owned (never produced by the generator).
|
|
26394
|
-
*
|
|
26395
|
-
* Resolution order (first match wins):
|
|
26396
|
-
* 1. `patch.user_owned === true` — authoritative, persisted across cycles.
|
|
26397
|
-
* 2. `.fernignore` itself, or file matches a .fernignore pattern.
|
|
26398
|
-
* 3. Legacy-safe signal: `patch_content` shows this file as a `--- /dev/null`
|
|
26399
|
-
* creation. Works for patches that predate the `user_owned` field or
|
|
26400
|
-
* whose flag was lost. Immune to rename tricks — relies on raw patch text.
|
|
26401
|
-
* 4. Tree-based check against `patch.base_generation` when it is reachable
|
|
26402
|
-
* AND distinct from `currentGenSha`. If the base tree is unreachable
|
|
26403
|
-
* (shallow clone / aggressive `git gc --prune`), emit a one-time
|
|
26404
|
-
* customer-actionable warning (deduplicated via warnedGens) and
|
|
26405
|
-
* conservatively treat as user-owned — strictly safer than silent
|
|
26406
|
-
* absorption (FER-9809).
|
|
26407
|
-
* 5. Final fallback — check `currentGenSha` when there is no usable base
|
|
26408
|
-
* (legacy one-gen lockfile). Preserves pre-fix behavior for that edge.
|
|
26409
|
-
*
|
|
26410
|
-
* `originalFileName` is the pre-rename path when the generator moved the
|
|
26411
|
-
* file between the patch's base and the current generation. Tree lookups
|
|
26412
|
-
* at ancestor generations use the original path — that's where the
|
|
26413
|
-
* generator produced the content.
|
|
26414
|
-
*/
|
|
26415
|
-
async isFileUserOwned(file, patch, currentGenSha, fernignorePatterns, warnedGens, originalFileName) {
|
|
26416
|
-
if (patch.user_owned) return true;
|
|
26417
|
-
if (file === ".fernignore") return true;
|
|
26418
|
-
if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
|
|
26419
|
-
if (this.fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
26420
|
-
return true;
|
|
26421
|
-
}
|
|
26422
|
-
const base = patch.base_generation;
|
|
26423
|
-
if (base && base !== currentGenSha) {
|
|
26424
|
-
const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
|
|
26425
|
-
if (!reachable) {
|
|
26426
|
-
if (!warnedGens.has(base)) {
|
|
26427
|
-
warnedGens.add(base);
|
|
26428
|
-
this.warnings.push(
|
|
26429
|
-
`Base generation ${base.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
|
|
26430
|
-
);
|
|
26431
|
-
}
|
|
26432
|
-
return true;
|
|
26433
|
-
}
|
|
26434
|
-
return await this.git.showFile(base, originalFileName ?? file) === null;
|
|
26435
|
-
}
|
|
26436
|
-
return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
|
|
26437
|
-
}
|
|
26438
|
-
/**
|
|
26439
|
-
* Returns true if `file`'s diff section in `patchContent` starts with
|
|
26440
|
-
* `--- /dev/null`, meaning this file was introduced by the patch (user
|
|
26441
|
-
* creation). Used as a legacy-safe fallback when `patch.user_owned` is
|
|
26442
|
-
* absent or cleared.
|
|
26443
|
-
*/
|
|
26444
|
-
fileIsCreationInPatchContent(patchContent, file) {
|
|
26445
|
-
const lines = patchContent.split("\n");
|
|
26446
|
-
let inFileSection = false;
|
|
26447
|
-
for (const line of lines) {
|
|
26448
|
-
if (line.startsWith("diff --git ")) {
|
|
26449
|
-
inFileSection = line.includes(` b/${file}`) || line.endsWith(` b/${file}`);
|
|
26450
|
-
continue;
|
|
26451
|
-
}
|
|
26452
|
-
if (inFileSection && line.startsWith("--- ")) {
|
|
26453
|
-
return line === "--- /dev/null";
|
|
26454
|
-
}
|
|
26455
|
-
}
|
|
26456
|
-
return false;
|
|
26457
|
-
}
|
|
26458
26542
|
/**
|
|
26459
26543
|
* For conflict patches with mixed results (some files merged, some conflicted),
|
|
26460
26544
|
* check if the cleanly merged files were absorbed by the generator (empty diff).
|
|
@@ -26473,7 +26557,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26473
26557
|
const warnedGens = /* @__PURE__ */ new Set();
|
|
26474
26558
|
const generatorCleanFiles = [];
|
|
26475
26559
|
for (const file of cleanFiles) {
|
|
26476
|
-
|
|
26560
|
+
const userOwned = await this.fileOwnership.resolveCanonical({
|
|
26561
|
+
file,
|
|
26562
|
+
patch,
|
|
26563
|
+
currentGenSha,
|
|
26564
|
+
fernignorePatterns,
|
|
26565
|
+
warnedGens,
|
|
26566
|
+
warnings: this.warnings
|
|
26567
|
+
});
|
|
26568
|
+
if (userOwned) {
|
|
26477
26569
|
continue;
|
|
26478
26570
|
}
|
|
26479
26571
|
generatorCleanFiles.push(file);
|
|
@@ -26502,6 +26594,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26502
26594
|
* Pre-generation rebase: update patches using the customer's current state.
|
|
26503
26595
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
26504
26596
|
*/
|
|
26597
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26505
26598
|
async preGenerationRebase(patches) {
|
|
26506
26599
|
const lock = this.lockManager.read();
|
|
26507
26600
|
const currentGen = lock.current_generation;
|
|
@@ -26540,7 +26633,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26540
26633
|
if (patch.user_owned) return true;
|
|
26541
26634
|
if (patch.files.length === 0) return false;
|
|
26542
26635
|
for (const file of patch.files) {
|
|
26543
|
-
|
|
26636
|
+
const userOwned = await this.fileOwnership.resolveCanonical({
|
|
26637
|
+
file,
|
|
26638
|
+
patch,
|
|
26639
|
+
currentGenSha: currentGen,
|
|
26640
|
+
fernignorePatterns,
|
|
26641
|
+
warnedGens,
|
|
26642
|
+
warnings: this.warnings
|
|
26643
|
+
});
|
|
26644
|
+
if (!userOwned) {
|
|
26544
26645
|
return false;
|
|
26545
26646
|
}
|
|
26546
26647
|
}
|
|
@@ -26732,6 +26833,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26732
26833
|
* relied on them. This is the "surface or preserve, never silently drop"
|
|
26733
26834
|
* contract for legitimate-deletion cases.
|
|
26734
26835
|
*/
|
|
26836
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26735
26837
|
recordDroppedContextLineWarnings(results) {
|
|
26736
26838
|
const PREVIEW_COUNT = 5;
|
|
26737
26839
|
for (const result of results) {
|
|
@@ -26754,12 +26856,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26754
26856
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
26755
26857
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
26756
26858
|
*/
|
|
26859
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26757
26860
|
revertConflictingFiles(results) {
|
|
26758
26861
|
for (const result of results) {
|
|
26759
26862
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
26760
26863
|
for (const fileResult of result.fileResults) {
|
|
26761
26864
|
if (fileResult.status !== "conflict") continue;
|
|
26762
|
-
const filePath = (0,
|
|
26865
|
+
const filePath = (0, import_path13.join)(this.outputDir, fileResult.file);
|
|
26763
26866
|
try {
|
|
26764
26867
|
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
26765
26868
|
const stripped = stripConflictMarkers(content);
|
|
@@ -26775,6 +26878,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26775
26878
|
* Restores files to their clean generated state from HEAD.
|
|
26776
26879
|
* Skips .fernignore-protected files to prevent overwriting user content.
|
|
26777
26880
|
*/
|
|
26881
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26778
26882
|
async cleanupStaleConflictMarkers() {
|
|
26779
26883
|
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
|
|
26780
26884
|
const files = markerFiles.trim().split("\n").filter(Boolean);
|
|
@@ -26789,10 +26893,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26789
26893
|
}
|
|
26790
26894
|
}
|
|
26791
26895
|
readFernignorePatterns() {
|
|
26792
|
-
const fernignorePath = (0,
|
|
26896
|
+
const fernignorePath = (0, import_path13.join)(this.outputDir, ".fernignore");
|
|
26793
26897
|
if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
|
|
26794
26898
|
return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
26795
26899
|
}
|
|
26900
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26796
26901
|
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
26797
26902
|
const conflictResults = results.filter((r2) => r2.status === "conflict");
|
|
26798
26903
|
const conflictDetails = conflictResults.map((r2) => {
|
|
@@ -26847,10 +26952,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26847
26952
|
this.outputDir = outputDir;
|
|
26848
26953
|
}
|
|
26849
26954
|
fernignoreExists() {
|
|
26850
|
-
return (0, import_fs8.existsSync)((0,
|
|
26955
|
+
return (0, import_fs8.existsSync)((0, import_path15.join)(this.outputDir, ".fernignore"));
|
|
26851
26956
|
}
|
|
26852
26957
|
readFernignorePatterns() {
|
|
26853
|
-
const fernignorePath = (0,
|
|
26958
|
+
const fernignorePath = (0, import_path15.join)(this.outputDir, ".fernignore");
|
|
26854
26959
|
if (!(0, import_fs8.existsSync)(fernignorePath)) {
|
|
26855
26960
|
return [];
|
|
26856
26961
|
}
|
|
@@ -26959,7 +27064,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26959
27064
|
return results;
|
|
26960
27065
|
}
|
|
26961
27066
|
readFileContent(filePath) {
|
|
26962
|
-
const fullPath = (0,
|
|
27067
|
+
const fullPath = (0, import_path15.join)(this.outputDir, filePath);
|
|
26963
27068
|
if (!(0, import_fs8.existsSync)(fullPath)) {
|
|
26964
27069
|
return null;
|
|
26965
27070
|
}
|
|
@@ -27024,7 +27129,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
27024
27129
|
};
|
|
27025
27130
|
}
|
|
27026
27131
|
movePatternsToReplayYml(patterns) {
|
|
27027
|
-
const replayYmlPath = (0,
|
|
27132
|
+
const replayYmlPath = (0, import_path15.join)(this.outputDir, ".fern", "replay.yml");
|
|
27028
27133
|
let config = {};
|
|
27029
27134
|
if ((0, import_fs8.existsSync)(replayYmlPath)) {
|
|
27030
27135
|
const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -27033,7 +27138,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
27033
27138
|
const existing = config.exclude ?? [];
|
|
27034
27139
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
27035
27140
|
config.exclude = merged;
|
|
27036
|
-
const dir = (0,
|
|
27141
|
+
const dir = (0, import_path15.dirname)(replayYmlPath);
|
|
27037
27142
|
if (!(0, import_fs8.existsSync)(dir)) {
|
|
27038
27143
|
(0, import_fs8.mkdirSync)(dir, { recursive: true });
|
|
27039
27144
|
}
|
|
@@ -29639,8 +29744,8 @@ function resolve(...paths) {
|
|
|
29639
29744
|
|
|
29640
29745
|
// src/cli.ts
|
|
29641
29746
|
var import_fs16 = __toESM(require("fs"));
|
|
29642
|
-
var
|
|
29643
|
-
var
|
|
29747
|
+
var import_promises18 = require("fs/promises");
|
|
29748
|
+
var import_path25 = __toESM(require("path"));
|
|
29644
29749
|
|
|
29645
29750
|
// ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
|
|
29646
29751
|
var YError = class _YError extends Error {
|
|
@@ -31607,15 +31712,18 @@ function parseRepository(githubRepository) {
|
|
|
31607
31712
|
githubRepository = githubRepository.slice(0, -4);
|
|
31608
31713
|
}
|
|
31609
31714
|
const parts = githubRepository.split("/");
|
|
31610
|
-
if (parts.length === 2 && parts[0]
|
|
31715
|
+
if (parts.length === 2 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1])) {
|
|
31611
31716
|
[owner, repo] = parts;
|
|
31612
|
-
} else if (parts.length === 3 && parts[0]
|
|
31717
|
+
} else if (parts.length === 3 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1]) && isNonEmptyPart(parts[2])) {
|
|
31613
31718
|
[remote, owner, repo] = parts;
|
|
31614
31719
|
} else {
|
|
31615
31720
|
throw new Error(`Failed to parse GitHub repository ${githubRepository}`);
|
|
31616
31721
|
}
|
|
31617
31722
|
return newRepositoryReference({ remote, owner, repo });
|
|
31618
31723
|
}
|
|
31724
|
+
function isNonEmptyPart(part) {
|
|
31725
|
+
return part != null && part.length > 0;
|
|
31726
|
+
}
|
|
31619
31727
|
function newRepositoryReference({ remote, owner, repo }) {
|
|
31620
31728
|
const repoUrl = getRepoUrl({ remote, owner, repo });
|
|
31621
31729
|
const cloneUrl = getCloneUrl({ remote, owner, repo });
|
|
@@ -39354,42 +39462,42 @@ function replayForget(params) {
|
|
|
39354
39462
|
// src/replay/replay-init.ts
|
|
39355
39463
|
init_dist3();
|
|
39356
39464
|
var import_fs11 = require("fs");
|
|
39357
|
-
var
|
|
39465
|
+
var import_path19 = require("path");
|
|
39358
39466
|
var import_tmp_promise2 = __toESM(require_tmp_promise());
|
|
39359
39467
|
|
|
39360
39468
|
// src/replay/fernignore.ts
|
|
39361
|
-
var
|
|
39362
|
-
var
|
|
39469
|
+
var import_promises10 = require("fs/promises");
|
|
39470
|
+
var import_path18 = require("path");
|
|
39363
39471
|
var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
39364
39472
|
var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
|
|
39365
39473
|
async function ensureReplayFernignoreEntries(outputDir) {
|
|
39366
|
-
const fernignorePath = (0,
|
|
39474
|
+
const fernignorePath = (0, import_path18.join)(outputDir, ".fernignore");
|
|
39367
39475
|
let content = "";
|
|
39368
39476
|
try {
|
|
39369
|
-
content = await (0,
|
|
39477
|
+
content = await (0, import_promises10.readFile)(fernignorePath, "utf-8");
|
|
39370
39478
|
} catch {
|
|
39371
39479
|
}
|
|
39372
39480
|
const lines = content.split("\n");
|
|
39373
39481
|
const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
39374
39482
|
if (toAdd.length > 0) {
|
|
39375
39483
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
39376
|
-
await (0,
|
|
39484
|
+
await (0, import_promises10.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
39377
39485
|
return true;
|
|
39378
39486
|
}
|
|
39379
39487
|
return false;
|
|
39380
39488
|
}
|
|
39381
39489
|
async function ensureGitattributesEntries2(outputDir) {
|
|
39382
|
-
const gitattributesPath = (0,
|
|
39490
|
+
const gitattributesPath = (0, import_path18.join)(outputDir, ".gitattributes");
|
|
39383
39491
|
let content = "";
|
|
39384
39492
|
try {
|
|
39385
|
-
content = await (0,
|
|
39493
|
+
content = await (0, import_promises10.readFile)(gitattributesPath, "utf-8");
|
|
39386
39494
|
} catch {
|
|
39387
39495
|
}
|
|
39388
39496
|
const lines = content.split("\n");
|
|
39389
39497
|
const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
39390
39498
|
if (toAdd.length > 0) {
|
|
39391
39499
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
39392
|
-
await (0,
|
|
39500
|
+
await (0, import_promises10.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
39393
39501
|
}
|
|
39394
39502
|
}
|
|
39395
39503
|
|
|
@@ -39416,7 +39524,7 @@ async function replayInit(params) {
|
|
|
39416
39524
|
if (dryRun) {
|
|
39417
39525
|
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
|
|
39418
39526
|
}
|
|
39419
|
-
const lockfilePath = (0,
|
|
39527
|
+
const lockfilePath = (0, import_path19.join)(repoPath, ".fern", "replay.lock");
|
|
39420
39528
|
const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
39421
39529
|
return {
|
|
39422
39530
|
bootstrap: bootstrapResult,
|
|
@@ -39539,7 +39647,7 @@ init_dist3();
|
|
|
39539
39647
|
init_dist3();
|
|
39540
39648
|
var import_child_process4 = require("child_process");
|
|
39541
39649
|
var import_fs12 = require("fs");
|
|
39542
|
-
var
|
|
39650
|
+
var import_path20 = require("path");
|
|
39543
39651
|
async function replayPrepare(params, state) {
|
|
39544
39652
|
const {
|
|
39545
39653
|
outputDir,
|
|
@@ -39550,7 +39658,7 @@ async function replayPrepare(params, state) {
|
|
|
39550
39658
|
skipApplication,
|
|
39551
39659
|
logger
|
|
39552
39660
|
} = params;
|
|
39553
|
-
const lockfilePath = (0,
|
|
39661
|
+
const lockfilePath = (0, import_path20.join)(outputDir, ".fern", "replay.lock");
|
|
39554
39662
|
let autoBootstrapped = false;
|
|
39555
39663
|
let bootstrapAttempted = false;
|
|
39556
39664
|
if (!(0, import_fs12.existsSync)(lockfilePath)) {
|
|
@@ -39804,29 +39912,29 @@ function replayStatus(params) {
|
|
|
39804
39912
|
}
|
|
39805
39913
|
|
|
39806
39914
|
// src/configuration/loadGitHubConfig.ts
|
|
39807
|
-
var
|
|
39915
|
+
var import_promises11 = require("fs/promises");
|
|
39808
39916
|
async function loadGitHubConfig({
|
|
39809
39917
|
absolutePathToConfig
|
|
39810
39918
|
}) {
|
|
39811
|
-
const rawContents = await (0,
|
|
39919
|
+
const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
|
|
39812
39920
|
return JSON.parse(rawContents);
|
|
39813
39921
|
}
|
|
39814
39922
|
|
|
39815
39923
|
// src/configuration/loadReadmeConfig.ts
|
|
39816
|
-
var
|
|
39924
|
+
var import_promises12 = require("fs/promises");
|
|
39817
39925
|
async function loadReadmeConfig({
|
|
39818
39926
|
absolutePathToConfig
|
|
39819
39927
|
}) {
|
|
39820
|
-
const rawContents = await (0,
|
|
39928
|
+
const rawContents = await (0, import_promises12.readFile)(absolutePathToConfig, "utf8");
|
|
39821
39929
|
return JSON.parse(rawContents);
|
|
39822
39930
|
}
|
|
39823
39931
|
|
|
39824
39932
|
// src/configuration/loadReferenceConfig.ts
|
|
39825
|
-
var
|
|
39933
|
+
var import_promises13 = require("fs/promises");
|
|
39826
39934
|
async function loadReferenceConfig({
|
|
39827
39935
|
absolutePathToConfig
|
|
39828
39936
|
}) {
|
|
39829
|
-
const rawContents = await (0,
|
|
39937
|
+
const rawContents = await (0, import_promises13.readFile)(absolutePathToConfig, "utf8");
|
|
39830
39938
|
return JSON.parse(rawContents);
|
|
39831
39939
|
}
|
|
39832
39940
|
|
|
@@ -39853,8 +39961,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
|
|
|
39853
39961
|
// src/pipeline/steps/AutoVersionStep.ts
|
|
39854
39962
|
var import_child_process5 = require("child_process");
|
|
39855
39963
|
var import_fs14 = require("fs");
|
|
39856
|
-
var
|
|
39857
|
-
var
|
|
39964
|
+
var import_promises15 = require("fs/promises");
|
|
39965
|
+
var import_path22 = require("path");
|
|
39858
39966
|
|
|
39859
39967
|
// ../commons/logging-execa/lib/loggingExeca.js
|
|
39860
39968
|
var import_execa = __toESM(require_execa(), 1);
|
|
@@ -39911,10 +40019,136 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
|
|
|
39911
40019
|
return result;
|
|
39912
40020
|
}
|
|
39913
40021
|
|
|
40022
|
+
// ../cli/task-context/lib/CliError.js
|
|
40023
|
+
var CliError = class _CliError extends Error {
|
|
40024
|
+
code;
|
|
40025
|
+
docsLink;
|
|
40026
|
+
constructor({ message, code, docsLink }) {
|
|
40027
|
+
super(message);
|
|
40028
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
40029
|
+
if (Error.captureStackTrace) {
|
|
40030
|
+
Error.captureStackTrace(this, this.constructor);
|
|
40031
|
+
}
|
|
40032
|
+
this.name = this.constructor.name;
|
|
40033
|
+
this.code = code;
|
|
40034
|
+
this.docsLink = docsLink;
|
|
40035
|
+
}
|
|
40036
|
+
static authRequired(message) {
|
|
40037
|
+
return new _CliError({
|
|
40038
|
+
message: message ?? "Authentication required. Please run 'fern login' or set the FERN_TOKEN environment variable.",
|
|
40039
|
+
code: _CliError.Code.AuthError
|
|
40040
|
+
});
|
|
40041
|
+
}
|
|
40042
|
+
static unauthorized(message) {
|
|
40043
|
+
return new _CliError({
|
|
40044
|
+
message: message ?? "Unauthorized. Please run 'fern auth login' or set the FERN_TOKEN environment variable.",
|
|
40045
|
+
code: _CliError.Code.AuthError
|
|
40046
|
+
});
|
|
40047
|
+
}
|
|
40048
|
+
static notFound(message) {
|
|
40049
|
+
return new _CliError({ message, code: _CliError.Code.ConfigError });
|
|
40050
|
+
}
|
|
40051
|
+
static badRequest(message) {
|
|
40052
|
+
return new _CliError({ message, code: _CliError.Code.NetworkError });
|
|
40053
|
+
}
|
|
40054
|
+
static validationError(message) {
|
|
40055
|
+
return new _CliError({ message, code: _CliError.Code.ValidationError });
|
|
40056
|
+
}
|
|
40057
|
+
static internalError(message) {
|
|
40058
|
+
return new _CliError({ message, code: _CliError.Code.InternalError });
|
|
40059
|
+
}
|
|
40060
|
+
};
|
|
40061
|
+
(function(CliError2) {
|
|
40062
|
+
CliError2.Code = {
|
|
40063
|
+
InternalError: "INTERNAL_ERROR",
|
|
40064
|
+
ResolutionError: "RESOLUTION_ERROR",
|
|
40065
|
+
IrConversionError: "IR_CONVERSION_ERROR",
|
|
40066
|
+
ContainerError: "CONTAINER_ERROR",
|
|
40067
|
+
VersionError: "VERSION_ERROR",
|
|
40068
|
+
ParseError: "PARSE_ERROR",
|
|
40069
|
+
EnvironmentError: "ENVIRONMENT_ERROR",
|
|
40070
|
+
ReferenceError: "REFERENCE_ERROR",
|
|
40071
|
+
ValidationError: "VALIDATION_ERROR",
|
|
40072
|
+
NetworkError: "NETWORK_ERROR",
|
|
40073
|
+
AuthError: "AUTH_ERROR",
|
|
40074
|
+
ConfigError: "CONFIG_ERROR",
|
|
40075
|
+
UserError: "USER_ERROR"
|
|
40076
|
+
};
|
|
40077
|
+
})(CliError || (CliError = {}));
|
|
40078
|
+
var SENTRY_REPORTABLE = {
|
|
40079
|
+
[CliError.Code.InternalError]: true,
|
|
40080
|
+
[CliError.Code.ResolutionError]: true,
|
|
40081
|
+
[CliError.Code.IrConversionError]: true,
|
|
40082
|
+
[CliError.Code.ContainerError]: false,
|
|
40083
|
+
[CliError.Code.VersionError]: false,
|
|
40084
|
+
[CliError.Code.ParseError]: false,
|
|
40085
|
+
[CliError.Code.EnvironmentError]: false,
|
|
40086
|
+
[CliError.Code.ReferenceError]: false,
|
|
40087
|
+
[CliError.Code.ValidationError]: false,
|
|
40088
|
+
[CliError.Code.NetworkError]: false,
|
|
40089
|
+
[CliError.Code.AuthError]: false,
|
|
40090
|
+
[CliError.Code.ConfigError]: false,
|
|
40091
|
+
[CliError.Code.UserError]: false
|
|
40092
|
+
};
|
|
40093
|
+
|
|
40094
|
+
// ../cli/logger/lib/LogLevel.js
|
|
40095
|
+
var LogLevel2 = {
|
|
40096
|
+
Trace: "trace",
|
|
40097
|
+
Debug: "debug",
|
|
40098
|
+
Info: "info",
|
|
40099
|
+
Warn: "warn",
|
|
40100
|
+
Error: "error"
|
|
40101
|
+
};
|
|
40102
|
+
var LOG_LEVELS = Object.values(LogLevel2);
|
|
40103
|
+
|
|
40104
|
+
// ../cli/logger/lib/createLogger.js
|
|
40105
|
+
var LoggerImpl = class {
|
|
40106
|
+
log;
|
|
40107
|
+
enabled;
|
|
40108
|
+
constructor(log, enabled = true) {
|
|
40109
|
+
this.log = log;
|
|
40110
|
+
this.enabled = enabled;
|
|
40111
|
+
}
|
|
40112
|
+
disable() {
|
|
40113
|
+
this.enabled = false;
|
|
40114
|
+
}
|
|
40115
|
+
enable() {
|
|
40116
|
+
this.enabled = true;
|
|
40117
|
+
}
|
|
40118
|
+
debug(...args) {
|
|
40119
|
+
if (this.enabled) {
|
|
40120
|
+
this.log(LogLevel2.Debug, ...args);
|
|
40121
|
+
}
|
|
40122
|
+
}
|
|
40123
|
+
info(...args) {
|
|
40124
|
+
if (this.enabled) {
|
|
40125
|
+
this.log(LogLevel2.Info, ...args);
|
|
40126
|
+
}
|
|
40127
|
+
}
|
|
40128
|
+
warn(...args) {
|
|
40129
|
+
if (this.enabled) {
|
|
40130
|
+
this.log(LogLevel2.Warn, ...args);
|
|
40131
|
+
}
|
|
40132
|
+
}
|
|
40133
|
+
error(...args) {
|
|
40134
|
+
if (this.enabled) {
|
|
40135
|
+
this.log(LogLevel2.Error, ...args);
|
|
40136
|
+
}
|
|
40137
|
+
}
|
|
40138
|
+
trace(...args) {
|
|
40139
|
+
if (this.enabled) {
|
|
40140
|
+
this.log(LogLevel2.Trace, ...args);
|
|
40141
|
+
}
|
|
40142
|
+
}
|
|
40143
|
+
};
|
|
40144
|
+
function createLogger4(log) {
|
|
40145
|
+
return new LoggerImpl(log);
|
|
40146
|
+
}
|
|
40147
|
+
|
|
39914
40148
|
// src/autoversion/AutoVersioningService.ts
|
|
39915
40149
|
var import_fs13 = require("fs");
|
|
39916
|
-
var
|
|
39917
|
-
var
|
|
40150
|
+
var import_promises14 = require("fs/promises");
|
|
40151
|
+
var import_path21 = require("path");
|
|
39918
40152
|
var import_semver = __toESM(require_semver2());
|
|
39919
40153
|
var AutoVersioningException = class extends Error {
|
|
39920
40154
|
/**
|
|
@@ -40577,10 +40811,17 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40577
40811
|
} else {
|
|
40578
40812
|
command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
|
|
40579
40813
|
}
|
|
40580
|
-
|
|
40581
|
-
|
|
40582
|
-
|
|
40583
|
-
|
|
40814
|
+
try {
|
|
40815
|
+
await loggingExeca(this.logger, "bash", ["-c", command], {
|
|
40816
|
+
cwd: workingDirectory,
|
|
40817
|
+
doNotPipeOutput: true
|
|
40818
|
+
});
|
|
40819
|
+
} catch (error) {
|
|
40820
|
+
throw new CliError({
|
|
40821
|
+
message: `Failed to replace placeholder version in generated files: ${extractErrorMessage(error)}`,
|
|
40822
|
+
code: CliError.Code.UserError
|
|
40823
|
+
});
|
|
40824
|
+
}
|
|
40584
40825
|
this.logger.debug("Placeholder version replaced successfully");
|
|
40585
40826
|
}
|
|
40586
40827
|
/**
|
|
@@ -40601,7 +40842,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40601
40842
|
);
|
|
40602
40843
|
return;
|
|
40603
40844
|
}
|
|
40604
|
-
const goModPath = (0,
|
|
40845
|
+
const goModPath = (0, import_path21.join)(workingDirectory, "go.mod");
|
|
40605
40846
|
if (!(0, import_fs13.existsSync)(goModPath)) {
|
|
40606
40847
|
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
40607
40848
|
return;
|
|
@@ -40635,7 +40876,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40635
40876
|
* Reads the module path from a go.mod file.
|
|
40636
40877
|
*/
|
|
40637
40878
|
async readGoModulePath(goModPath) {
|
|
40638
|
-
const content = await (0,
|
|
40879
|
+
const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
|
|
40639
40880
|
for (const line of content.split("\n")) {
|
|
40640
40881
|
const trimmed2 = line.trim();
|
|
40641
40882
|
if (trimmed2.startsWith("module ")) {
|
|
@@ -40648,7 +40889,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40648
40889
|
* Adds a version suffix to the module line in go.mod.
|
|
40649
40890
|
*/
|
|
40650
40891
|
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
40651
|
-
const content = await (0,
|
|
40892
|
+
const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
|
|
40652
40893
|
const lines = content.split("\n");
|
|
40653
40894
|
let changed = false;
|
|
40654
40895
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
@@ -40660,7 +40901,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40660
40901
|
}
|
|
40661
40902
|
}
|
|
40662
40903
|
if (changed) {
|
|
40663
|
-
await (0,
|
|
40904
|
+
await (0, import_promises14.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
40664
40905
|
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
40665
40906
|
}
|
|
40666
40907
|
}
|
|
@@ -40669,10 +40910,10 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40669
40910
|
*/
|
|
40670
40911
|
async walkDirectory(dir, filter2) {
|
|
40671
40912
|
const results = [];
|
|
40672
|
-
const entries2 = await (0,
|
|
40913
|
+
const entries2 = await (0, import_promises14.readdir)(dir);
|
|
40673
40914
|
for (const entry of entries2) {
|
|
40674
|
-
const fullPath = (0,
|
|
40675
|
-
const entryStat = await (0,
|
|
40915
|
+
const fullPath = (0, import_path21.join)(dir, entry);
|
|
40916
|
+
const entryStat = await (0, import_promises14.stat)(fullPath);
|
|
40676
40917
|
if (entryStat.isDirectory()) {
|
|
40677
40918
|
if (entry === ".git" || entry === "vendor") {
|
|
40678
40919
|
continue;
|
|
@@ -40689,7 +40930,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40689
40930
|
* Adds a version suffix to import paths in all .go files in the repository.
|
|
40690
40931
|
*/
|
|
40691
40932
|
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
40692
|
-
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0,
|
|
40933
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path21.extname)(filePath) === ".go");
|
|
40693
40934
|
for (const filePath of goFiles) {
|
|
40694
40935
|
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
40695
40936
|
}
|
|
@@ -40699,7 +40940,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40699
40940
|
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
40700
40941
|
*/
|
|
40701
40942
|
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
40702
|
-
const content = await (0,
|
|
40943
|
+
const content = await (0, import_promises14.readFile)(filePath, "utf-8");
|
|
40703
40944
|
const lines = content.split("\n");
|
|
40704
40945
|
let changed = false;
|
|
40705
40946
|
let inImportBlock = false;
|
|
@@ -40728,7 +40969,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40728
40969
|
}
|
|
40729
40970
|
}
|
|
40730
40971
|
if (changed) {
|
|
40731
|
-
await (0,
|
|
40972
|
+
await (0, import_promises14.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
40732
40973
|
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
40733
40974
|
}
|
|
40734
40975
|
}
|
|
@@ -41134,7 +41375,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
41134
41375
|
}
|
|
41135
41376
|
async prependChangelogEntry(params) {
|
|
41136
41377
|
const { version: version2, entry } = params;
|
|
41137
|
-
const changelogPath = (0,
|
|
41378
|
+
const changelogPath = (0, import_path22.join)(this.outputDir, "changelog.md");
|
|
41138
41379
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
41139
41380
|
const header = `## [${version2}] - ${now}
|
|
41140
41381
|
`;
|
|
@@ -41143,7 +41384,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
41143
41384
|
`;
|
|
41144
41385
|
let existing = "";
|
|
41145
41386
|
if ((0, import_fs14.existsSync)(changelogPath)) {
|
|
41146
|
-
existing = await (0,
|
|
41387
|
+
existing = await (0, import_promises15.readFile)(changelogPath, "utf-8");
|
|
41147
41388
|
}
|
|
41148
41389
|
let output;
|
|
41149
41390
|
if (existing.trim().length === 0) {
|
|
@@ -41160,7 +41401,7 @@ ${newBlock}${remainder}`;
|
|
|
41160
41401
|
} else {
|
|
41161
41402
|
output = `${newBlock}${existing}`;
|
|
41162
41403
|
}
|
|
41163
|
-
await (0,
|
|
41404
|
+
await (0, import_promises15.writeFile)(changelogPath, output, "utf-8");
|
|
41164
41405
|
}
|
|
41165
41406
|
brandMessage(message) {
|
|
41166
41407
|
if (this.config.isWhitelabel) {
|
|
@@ -41383,8 +41624,8 @@ function tryRevParse(cwd2, rev) {
|
|
|
41383
41624
|
}
|
|
41384
41625
|
|
|
41385
41626
|
// src/pipeline/steps/GithubStep.ts
|
|
41386
|
-
var
|
|
41387
|
-
var
|
|
41627
|
+
var import_promises16 = require("fs/promises");
|
|
41628
|
+
var import_path23 = require("path");
|
|
41388
41629
|
|
|
41389
41630
|
// src/pipeline/github/createReplayBranch.ts
|
|
41390
41631
|
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
@@ -42024,13 +42265,13 @@ var GithubStep = class extends BaseStep {
|
|
|
42024
42265
|
}
|
|
42025
42266
|
async ensureFernignore() {
|
|
42026
42267
|
this.logger.debug("Checking for .fernignore file...");
|
|
42027
|
-
const fernignorePath = (0,
|
|
42268
|
+
const fernignorePath = (0, import_path23.join)(this.outputDir, ".fernignore");
|
|
42028
42269
|
try {
|
|
42029
|
-
await (0,
|
|
42270
|
+
await (0, import_promises16.access)(fernignorePath);
|
|
42030
42271
|
this.logger.debug(".fernignore already exists");
|
|
42031
42272
|
} catch {
|
|
42032
42273
|
this.logger.debug("Creating .fernignore file...");
|
|
42033
|
-
await (0,
|
|
42274
|
+
await (0, import_promises16.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
42034
42275
|
}
|
|
42035
42276
|
}
|
|
42036
42277
|
deriveSkipCommit(replayResult) {
|
|
@@ -42241,7 +42482,7 @@ var ReplayStep = class extends BaseStep {
|
|
|
42241
42482
|
};
|
|
42242
42483
|
|
|
42243
42484
|
// ../cli/generation/local-generation/docker-utils/lib/runDocker.js
|
|
42244
|
-
var
|
|
42485
|
+
var import_promises17 = require("fs/promises");
|
|
42245
42486
|
var import_tmp_promise3 = __toESM(require_tmp_promise(), 1);
|
|
42246
42487
|
async function pullImage(imageName, runner, signal) {
|
|
42247
42488
|
await loggingExeca(void 0, runner ?? "docker", ["pull", imageName], {
|
|
@@ -42288,7 +42529,7 @@ async function execInContainer({ logger, containerId, command, runner, writeLogs
|
|
|
42288
42529
|
const logs = stdout + stderr;
|
|
42289
42530
|
if (writeLogsToFile) {
|
|
42290
42531
|
const tmpFile = await import_tmp_promise3.default.file();
|
|
42291
|
-
await (0,
|
|
42532
|
+
await (0, import_promises17.writeFile)(tmpFile.path, logs);
|
|
42292
42533
|
logger.info(`Generator logs here: ${tmpFile.path}`);
|
|
42293
42534
|
}
|
|
42294
42535
|
const resolvedExitCode = exitCode ?? 1;
|
|
@@ -42319,63 +42560,9 @@ async function stopContainer({ logger, containerId, runner }) {
|
|
|
42319
42560
|
});
|
|
42320
42561
|
}
|
|
42321
42562
|
|
|
42322
|
-
// ../cli/logger/lib/LogLevel.js
|
|
42323
|
-
var LogLevel2 = {
|
|
42324
|
-
Trace: "trace",
|
|
42325
|
-
Debug: "debug",
|
|
42326
|
-
Info: "info",
|
|
42327
|
-
Warn: "warn",
|
|
42328
|
-
Error: "error"
|
|
42329
|
-
};
|
|
42330
|
-
var LOG_LEVELS = Object.values(LogLevel2);
|
|
42331
|
-
|
|
42332
|
-
// ../cli/logger/lib/createLogger.js
|
|
42333
|
-
var LoggerImpl = class {
|
|
42334
|
-
log;
|
|
42335
|
-
enabled;
|
|
42336
|
-
constructor(log, enabled = true) {
|
|
42337
|
-
this.log = log;
|
|
42338
|
-
this.enabled = enabled;
|
|
42339
|
-
}
|
|
42340
|
-
disable() {
|
|
42341
|
-
this.enabled = false;
|
|
42342
|
-
}
|
|
42343
|
-
enable() {
|
|
42344
|
-
this.enabled = true;
|
|
42345
|
-
}
|
|
42346
|
-
debug(...args) {
|
|
42347
|
-
if (this.enabled) {
|
|
42348
|
-
this.log(LogLevel2.Debug, ...args);
|
|
42349
|
-
}
|
|
42350
|
-
}
|
|
42351
|
-
info(...args) {
|
|
42352
|
-
if (this.enabled) {
|
|
42353
|
-
this.log(LogLevel2.Info, ...args);
|
|
42354
|
-
}
|
|
42355
|
-
}
|
|
42356
|
-
warn(...args) {
|
|
42357
|
-
if (this.enabled) {
|
|
42358
|
-
this.log(LogLevel2.Warn, ...args);
|
|
42359
|
-
}
|
|
42360
|
-
}
|
|
42361
|
-
error(...args) {
|
|
42362
|
-
if (this.enabled) {
|
|
42363
|
-
this.log(LogLevel2.Error, ...args);
|
|
42364
|
-
}
|
|
42365
|
-
}
|
|
42366
|
-
trace(...args) {
|
|
42367
|
-
if (this.enabled) {
|
|
42368
|
-
this.log(LogLevel2.Trace, ...args);
|
|
42369
|
-
}
|
|
42370
|
-
}
|
|
42371
|
-
};
|
|
42372
|
-
function createLogger4(log) {
|
|
42373
|
-
return new LoggerImpl(log);
|
|
42374
|
-
}
|
|
42375
|
-
|
|
42376
42563
|
// src/pipeline/steps/VerificationStep.ts
|
|
42377
42564
|
var import_fs15 = require("fs");
|
|
42378
|
-
var
|
|
42565
|
+
var import_path24 = require("path");
|
|
42379
42566
|
var CONTAINER_WORKSPACE_PATH = "/workspace";
|
|
42380
42567
|
var VERIFY_SCRIPT_RELATIVE_PATH = ".fern/verify.sh";
|
|
42381
42568
|
var VERIFY_SCRIPT_CONTAINER_PATH = `${CONTAINER_WORKSPACE_PATH}/${VERIFY_SCRIPT_RELATIVE_PATH}`;
|
|
@@ -42391,7 +42578,7 @@ var VerificationStep = class extends BaseStep {
|
|
|
42391
42578
|
generatorVersions;
|
|
42392
42579
|
name = "verify";
|
|
42393
42580
|
async execute(_context) {
|
|
42394
|
-
const verifyScriptHostPath = (0,
|
|
42581
|
+
const verifyScriptHostPath = (0, import_path24.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
|
|
42395
42582
|
if (!(0, import_fs15.existsSync)(verifyScriptHostPath)) {
|
|
42396
42583
|
return { executed: true, success: true, skipped: true };
|
|
42397
42584
|
}
|
|
@@ -42664,7 +42851,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
42664
42851
|
const readmeConfig = await loadReadmeConfig({
|
|
42665
42852
|
absolutePathToConfig: resolve(wd, argv.config)
|
|
42666
42853
|
});
|
|
42667
|
-
const originalReadmeContent = argv.originalReadme != null ? await (0,
|
|
42854
|
+
const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises18.readFile)(argv.originalReadme, "utf8") : void 0;
|
|
42668
42855
|
await generateReadmeToStream({
|
|
42669
42856
|
originalReadmeContent,
|
|
42670
42857
|
readmeConfig,
|
|
@@ -42792,7 +42979,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
42792
42979
|
try {
|
|
42793
42980
|
const wd = cwd();
|
|
42794
42981
|
const configValue = argv.config;
|
|
42795
|
-
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0,
|
|
42982
|
+
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises18.readFile)(resolve(wd, configValue), "utf-8");
|
|
42796
42983
|
let config;
|
|
42797
42984
|
try {
|
|
42798
42985
|
config = JSON.parse(configContent);
|
|
@@ -43088,7 +43275,7 @@ async function createWriteStream(outputPath) {
|
|
|
43088
43275
|
}
|
|
43089
43276
|
async function createWriteStreamFromFile(filepath) {
|
|
43090
43277
|
if (!await doesPathExist(filepath)) {
|
|
43091
|
-
await (0,
|
|
43278
|
+
await (0, import_promises18.mkdir)(import_path25.default.dirname(filepath), { recursive: true });
|
|
43092
43279
|
}
|
|
43093
43280
|
return import_fs16.default.createWriteStream(filepath);
|
|
43094
43281
|
}
|