@fern-api/generator-cli 0.9.27 → 0.9.29
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 +1624 -1366
- 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.1/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.1/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
|
*
|
|
@@ -24910,9 +24527,7 @@ var init_dist3 = __esm({
|
|
|
24910
24527
|
continue;
|
|
24911
24528
|
}
|
|
24912
24529
|
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));
|
|
24530
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24916
24531
|
if (sensitiveFiles.length > 0) {
|
|
24917
24532
|
this.warnings.push(
|
|
24918
24533
|
`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 +24548,7 @@ var init_dist3 = __esm({
|
|
|
24933
24548
|
if (files.length === 0) {
|
|
24934
24549
|
continue;
|
|
24935
24550
|
}
|
|
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
|
-
}
|
|
24551
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
24942
24552
|
newPatches.push({
|
|
24943
24553
|
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
24944
24554
|
content_hash: contentHash,
|
|
@@ -25026,7 +24636,7 @@ var init_dist3 = __esm({
|
|
|
25026
24636
|
*
|
|
25027
24637
|
* This ensures content hashes match across the no-patches and normal-
|
|
25028
24638
|
* regeneration flows, which store formatPatch and git-diff formats
|
|
25029
|
-
* respectively
|
|
24639
|
+
* respectively.
|
|
25030
24640
|
*/
|
|
25031
24641
|
computeContentHash(patchContent) {
|
|
25032
24642
|
const lines = patchContent.split("\n");
|
|
@@ -25036,8 +24646,8 @@ var init_dist3 = __esm({
|
|
|
25036
24646
|
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
25037
24647
|
}
|
|
25038
24648
|
/**
|
|
25039
|
-
*
|
|
25040
|
-
*
|
|
24649
|
+
* Drop patches whose files were transiently created and then deleted
|
|
24650
|
+
* within the current linear detection window, and are absent from
|
|
25041
24651
|
* HEAD at the end of the window.
|
|
25042
24652
|
*
|
|
25043
24653
|
* Rationale: on a merge commit, createMergeCompositePatch already diffs
|
|
@@ -25062,8 +24672,8 @@ var init_dist3 = __esm({
|
|
|
25062
24672
|
* single files from a multi-file patch would require a follow-up that
|
|
25063
24673
|
* regenerates the patch content via `git format-patch -- <files>`. No
|
|
25064
24674
|
* current failing test exercises this, and leaving the patch intact
|
|
25065
|
-
* preserves today's behaviour.
|
|
25066
|
-
*
|
|
24675
|
+
* preserves today's behaviour. Revisit if a future adversarial test
|
|
24676
|
+
* demands per-file stripping.
|
|
25067
24677
|
*/
|
|
25068
24678
|
async collapseNetZeroFiles(patches) {
|
|
25069
24679
|
if (patches.length === 0) return patches;
|
|
@@ -25124,12 +24734,10 @@ var init_dist3 = __esm({
|
|
|
25124
24734
|
firstParent,
|
|
25125
24735
|
mergeCommit.sha
|
|
25126
24736
|
]);
|
|
25127
|
-
const
|
|
25128
|
-
|
|
25129
|
-
const files = allMergeFiles.filter((f) => !isSensitiveFile(f));
|
|
25130
|
-
if (sensitiveMergeFiles.length > 0) {
|
|
24737
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24738
|
+
if (sensitiveFiles.length > 0) {
|
|
25131
24739
|
this.warnings.push(
|
|
25132
|
-
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${
|
|
24740
|
+
`Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25133
24741
|
);
|
|
25134
24742
|
}
|
|
25135
24743
|
if (files.length === 0) return null;
|
|
@@ -25145,12 +24753,7 @@ var init_dist3 = __esm({
|
|
|
25145
24753
|
if (lock.patches.some((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
25146
24754
|
return null;
|
|
25147
24755
|
}
|
|
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
|
-
}
|
|
24756
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, mergeCommit.sha);
|
|
25154
24757
|
return {
|
|
25155
24758
|
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
25156
24759
|
content_hash: contentHash,
|
|
@@ -25174,13 +24777,24 @@ var init_dist3 = __esm({
|
|
|
25174
24777
|
if (!diffBase) {
|
|
25175
24778
|
return this.detectPatchesViaCommitScan();
|
|
25176
24779
|
}
|
|
24780
|
+
const lockForGuard = this.lockManager.read();
|
|
24781
|
+
const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
|
|
24782
|
+
let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
|
|
24783
|
+
if (commitKnownMissing && !knownGenerations.has(diffBase)) {
|
|
24784
|
+
const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
|
|
24785
|
+
if (!fallback) {
|
|
24786
|
+
this.warnings.push(
|
|
24787
|
+
`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.`
|
|
24788
|
+
);
|
|
24789
|
+
return { patches: [], revertedPatchIds: [] };
|
|
24790
|
+
}
|
|
24791
|
+
resolvedBaseGeneration = fallback;
|
|
24792
|
+
}
|
|
25177
24793
|
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) {
|
|
24794
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24795
|
+
if (sensitiveFiles.length > 0) {
|
|
25182
24796
|
this.warnings.push(
|
|
25183
|
-
`Sensitive file(s) excluded from composite patch: ${
|
|
24797
|
+
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25184
24798
|
);
|
|
25185
24799
|
}
|
|
25186
24800
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
@@ -25192,21 +24806,19 @@ var init_dist3 = __esm({
|
|
|
25192
24806
|
return { patches: [], revertedPatchIds: [] };
|
|
25193
24807
|
}
|
|
25194
24808
|
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
|
-
}
|
|
24809
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
|
|
25201
24810
|
const compositePatch = {
|
|
25202
24811
|
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
25203
24812
|
content_hash: contentHash,
|
|
25204
24813
|
original_commit: headSha,
|
|
25205
24814
|
original_message: "Customer customizations (composite)",
|
|
25206
24815
|
original_author: "composite",
|
|
25207
|
-
//
|
|
25208
|
-
//
|
|
25209
|
-
|
|
24816
|
+
// Anchor `base_generation` on a SHA the applicator's
|
|
24817
|
+
// `resolveBaseGeneration` can find — always a member of
|
|
24818
|
+
// `lock.generations`. When `diffBase` is a recorded
|
|
24819
|
+
// generation, use it directly; otherwise the guard above
|
|
24820
|
+
// has already mapped it onto `current_generation`.
|
|
24821
|
+
base_generation: resolvedBaseGeneration,
|
|
25210
24822
|
files,
|
|
25211
24823
|
patch_content: diff,
|
|
25212
24824
|
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
|
|
@@ -25224,6 +24836,8 @@ var init_dist3 = __esm({
|
|
|
25224
24836
|
*/
|
|
25225
24837
|
async detectPatchesViaCommitScan() {
|
|
25226
24838
|
const lock = this.lockManager.read();
|
|
24839
|
+
const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
|
|
24840
|
+
const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
|
|
25227
24841
|
const log = await this.git.exec([
|
|
25228
24842
|
"log",
|
|
25229
24843
|
"--max-count=200",
|
|
@@ -25265,12 +24879,10 @@ var init_dist3 = __esm({
|
|
|
25265
24879
|
continue;
|
|
25266
24880
|
}
|
|
25267
24881
|
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) {
|
|
24882
|
+
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
|
|
24883
|
+
if (sensitiveFiles.length > 0) {
|
|
25272
24884
|
this.warnings.push(
|
|
25273
|
-
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${
|
|
24885
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
25274
24886
|
);
|
|
25275
24887
|
if (files.length > 0) {
|
|
25276
24888
|
if (parents.length === 0) {
|
|
@@ -25292,19 +24904,25 @@ var init_dist3 = __esm({
|
|
|
25292
24904
|
continue;
|
|
25293
24905
|
}
|
|
25294
24906
|
const parentSha = parents[0];
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
25299
|
-
|
|
24907
|
+
let baseGeneration;
|
|
24908
|
+
if (knownGenerations.has(parentSha)) {
|
|
24909
|
+
baseGeneration = parentSha;
|
|
24910
|
+
} else if (fallbackAnchor) {
|
|
24911
|
+
baseGeneration = fallbackAnchor;
|
|
24912
|
+
} else {
|
|
24913
|
+
this.warnings.push(
|
|
24914
|
+
`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.`
|
|
24915
|
+
);
|
|
24916
|
+
continue;
|
|
25300
24917
|
}
|
|
24918
|
+
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
25301
24919
|
newPatches.push({
|
|
25302
24920
|
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
25303
24921
|
content_hash: contentHash,
|
|
25304
24922
|
original_commit: commit.sha,
|
|
25305
24923
|
original_message: commit.message,
|
|
25306
24924
|
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
25307
|
-
base_generation:
|
|
24925
|
+
base_generation: baseGeneration,
|
|
25308
24926
|
files,
|
|
25309
24927
|
patch_content: patchContent,
|
|
25310
24928
|
...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
|
|
@@ -25349,6 +24967,541 @@ var init_dist3 = __esm({
|
|
|
25349
24967
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
25350
24968
|
}
|
|
25351
24969
|
};
|
|
24970
|
+
MergeAccumulator = class {
|
|
24971
|
+
constructor(mode, sharedFiles) {
|
|
24972
|
+
this.mode = mode;
|
|
24973
|
+
this.sharedFiles = sharedFiles;
|
|
24974
|
+
}
|
|
24975
|
+
entries = /* @__PURE__ */ new Map();
|
|
24976
|
+
/** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
|
|
24977
|
+
isShared(path5) {
|
|
24978
|
+
return this.sharedFiles.has(path5);
|
|
24979
|
+
}
|
|
24980
|
+
/** Has any prior patch already recorded a result for this path? */
|
|
24981
|
+
has(path5) {
|
|
24982
|
+
return this.entries.has(path5);
|
|
24983
|
+
}
|
|
24984
|
+
/** Look up the accumulated content + base generation for this path. */
|
|
24985
|
+
lookup(path5) {
|
|
24986
|
+
return this.entries.get(path5);
|
|
24987
|
+
}
|
|
24988
|
+
/**
|
|
24989
|
+
* True if any of the patch's files have a prior accumulator entry — in
|
|
24990
|
+
* which case the fast path (`git apply --3way`) cannot honor prior
|
|
24991
|
+
* patches' contributions and we must route through the slow path.
|
|
24992
|
+
*/
|
|
24993
|
+
shouldSkipFastPath(patch) {
|
|
24994
|
+
return patch.files.some((f) => this.entries.has(f));
|
|
24995
|
+
}
|
|
24996
|
+
/** Record a successful (clean) merge result. */
|
|
24997
|
+
recordMerge(path5, content, baseGeneration) {
|
|
24998
|
+
this.entries.set(path5, { content, baseGeneration });
|
|
24999
|
+
}
|
|
25000
|
+
/**
|
|
25001
|
+
* Record a conflicted merge result. Populates only in `"resolve"` mode
|
|
25002
|
+
* — replay mode keeps the accumulator clean of marker-laden content
|
|
25003
|
+
* since markers will be stripped from disk after the apply loop.
|
|
25004
|
+
*/
|
|
25005
|
+
recordConflict(path5, content, baseGeneration) {
|
|
25006
|
+
if (this.mode === "resolve") {
|
|
25007
|
+
this.entries.set(path5, { content, baseGeneration });
|
|
25008
|
+
}
|
|
25009
|
+
}
|
|
25010
|
+
/**
|
|
25011
|
+
* Defensive copy for test introspection. Used by invariant I2 to assert
|
|
25012
|
+
* that after every applied patch, the accumulator has an entry for each
|
|
25013
|
+
* file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
|
|
25014
|
+
*
|
|
25015
|
+
* @internal
|
|
25016
|
+
*/
|
|
25017
|
+
snapshot() {
|
|
25018
|
+
const copy = /* @__PURE__ */ new Map();
|
|
25019
|
+
for (const [path5, entry] of this.entries) {
|
|
25020
|
+
copy.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
|
|
25021
|
+
}
|
|
25022
|
+
return copy;
|
|
25023
|
+
}
|
|
25024
|
+
};
|
|
25025
|
+
CONFLICT_OPENER = "<<<<<<< Generated";
|
|
25026
|
+
CONFLICT_SEPARATOR = "=======";
|
|
25027
|
+
CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
25028
|
+
ReplayApplicator = class {
|
|
25029
|
+
git;
|
|
25030
|
+
lockManager;
|
|
25031
|
+
outputDir;
|
|
25032
|
+
renameCache = /* @__PURE__ */ new Map();
|
|
25033
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
25034
|
+
/**
|
|
25035
|
+
* Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
|
|
25036
|
+
* invocation with the run's apply mode + precomputed shared-file set.
|
|
25037
|
+
* See `src/accumulator/MergeAccumulator.ts` for the full contract.
|
|
25038
|
+
*
|
|
25039
|
+
* Initialized to an empty default in the constructor so type narrowing
|
|
25040
|
+
* works for callers that read it before any `applyPatches` invocation
|
|
25041
|
+
* (e.g., the `getAccumulatorSnapshot` test accessor).
|
|
25042
|
+
*/
|
|
25043
|
+
accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
|
|
25044
|
+
constructor(git, lockManager, outputDir) {
|
|
25045
|
+
this.git = git;
|
|
25046
|
+
this.lockManager = lockManager;
|
|
25047
|
+
this.outputDir = outputDir;
|
|
25048
|
+
}
|
|
25049
|
+
/**
|
|
25050
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
25051
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
25052
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
25053
|
+
*/
|
|
25054
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
25055
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
25056
|
+
if (gen) return gen;
|
|
25057
|
+
try {
|
|
25058
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
25059
|
+
return {
|
|
25060
|
+
commit_sha: baseGeneration,
|
|
25061
|
+
tree_hash: treeHash,
|
|
25062
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25063
|
+
cli_version: "unknown",
|
|
25064
|
+
generator_versions: {}
|
|
25065
|
+
};
|
|
25066
|
+
} catch {
|
|
25067
|
+
return void 0;
|
|
25068
|
+
}
|
|
25069
|
+
}
|
|
25070
|
+
/**
|
|
25071
|
+
* @internal Test-only.
|
|
25072
|
+
* Returns a defensive copy of the inter-patch accumulator. Used by
|
|
25073
|
+
* invariant I2 to assert that after every applied patch, the
|
|
25074
|
+
* accumulator has an entry for each file the patch touched, and the
|
|
25075
|
+
* entry's content matches on-disk.
|
|
25076
|
+
*/
|
|
25077
|
+
getAccumulatorSnapshot() {
|
|
25078
|
+
return this.accumulator.snapshot();
|
|
25079
|
+
}
|
|
25080
|
+
/**
|
|
25081
|
+
* Apply all patches, returning results for each.
|
|
25082
|
+
* Skips patches that match exclude patterns in replay.yml
|
|
25083
|
+
*
|
|
25084
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
25085
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
25086
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
25087
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
25088
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
25089
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
25090
|
+
* any markers that survive.
|
|
25091
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
25092
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
25093
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
25094
|
+
* when they do overlap, the customer gets nested markers and
|
|
25095
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
25096
|
+
*/
|
|
25097
|
+
async applyPatches(patches, opts) {
|
|
25098
|
+
const applyMode = opts?.applyMode ?? "replay";
|
|
25099
|
+
const fileFreq = /* @__PURE__ */ new Map();
|
|
25100
|
+
for (const p2 of patches) {
|
|
25101
|
+
for (const f of p2.files) {
|
|
25102
|
+
fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
|
|
25103
|
+
}
|
|
25104
|
+
}
|
|
25105
|
+
const sharedFiles = new Set(
|
|
25106
|
+
Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
|
|
25107
|
+
);
|
|
25108
|
+
this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
|
|
25109
|
+
const results = [];
|
|
25110
|
+
for (let i2 = 0; i2 < patches.length; i2++) {
|
|
25111
|
+
const patch = patches[i2];
|
|
25112
|
+
if (this.isExcluded(patch)) {
|
|
25113
|
+
results.push({
|
|
25114
|
+
patch,
|
|
25115
|
+
status: "skipped",
|
|
25116
|
+
method: "git-am"
|
|
25117
|
+
});
|
|
25118
|
+
continue;
|
|
25119
|
+
}
|
|
25120
|
+
const result = await this.applyPatchWithFallback(patch);
|
|
25121
|
+
results.push(result);
|
|
25122
|
+
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
25123
|
+
const laterFiles = /* @__PURE__ */ new Set();
|
|
25124
|
+
for (let j2 = i2 + 1; j2 < patches.length; j2++) {
|
|
25125
|
+
for (const f of patches[j2].files) {
|
|
25126
|
+
laterFiles.add(f);
|
|
25127
|
+
}
|
|
25128
|
+
}
|
|
25129
|
+
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
25130
|
+
if (result.resolvedFiles) {
|
|
25131
|
+
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
25132
|
+
resolvedToOriginal.set(resolved, orig);
|
|
25133
|
+
}
|
|
25134
|
+
}
|
|
25135
|
+
for (const fileResult of result.fileResults) {
|
|
25136
|
+
if (fileResult.status !== "conflict") continue;
|
|
25137
|
+
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
25138
|
+
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
25139
|
+
const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
|
|
25140
|
+
try {
|
|
25141
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf-8");
|
|
25142
|
+
const stripped = stripConflictMarkers(content);
|
|
25143
|
+
await (0, import_promises4.writeFile)(filePath, stripped);
|
|
25144
|
+
} catch {
|
|
25145
|
+
}
|
|
25146
|
+
}
|
|
25147
|
+
}
|
|
25148
|
+
}
|
|
25149
|
+
}
|
|
25150
|
+
return results;
|
|
25151
|
+
}
|
|
25152
|
+
/**
|
|
25153
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
25154
|
+
* results including any "dropped context lines" — lines that were
|
|
25155
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
25156
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
25157
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
25158
|
+
* 3-way slow path; both must surface the same warning.
|
|
25159
|
+
*/
|
|
25160
|
+
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
25161
|
+
const fileResults = [];
|
|
25162
|
+
if (!baseGen) return fileResults;
|
|
25163
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-acc-"));
|
|
25164
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
25165
|
+
const tempGit = new GitClient2(tempDir);
|
|
25166
|
+
await tempGit.exec(["init"]);
|
|
25167
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
25168
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
25169
|
+
try {
|
|
25170
|
+
for (const filePath of patch.files) {
|
|
25171
|
+
if (isBinaryFile(filePath)) continue;
|
|
25172
|
+
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
25173
|
+
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
25174
|
+
const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
|
|
25175
|
+
const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
|
|
25176
|
+
const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
25177
|
+
const finalOnDisk = await (0, import_promises4.readFile)((0, import_path10.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
25178
|
+
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
25179
|
+
if (effectiveTheirs != null) {
|
|
25180
|
+
this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch.base_generation);
|
|
25181
|
+
}
|
|
25182
|
+
if (base != null && effectiveTheirs != null && finalOnDisk != null) {
|
|
25183
|
+
const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
|
|
25184
|
+
if (dropped.length > 0) {
|
|
25185
|
+
fileResults.push({
|
|
25186
|
+
file: resolvedPath,
|
|
25187
|
+
status: "merged",
|
|
25188
|
+
droppedContextLines: dropped
|
|
25189
|
+
});
|
|
25190
|
+
}
|
|
25191
|
+
}
|
|
25192
|
+
}
|
|
25193
|
+
} finally {
|
|
25194
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
25195
|
+
});
|
|
25196
|
+
}
|
|
25197
|
+
return fileResults;
|
|
25198
|
+
}
|
|
25199
|
+
async applyPatchWithFallback(patch) {
|
|
25200
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
25201
|
+
const lock = this.lockManager.read();
|
|
25202
|
+
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
25203
|
+
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
25204
|
+
const needsAccumulation = await Promise.all(
|
|
25205
|
+
patch.files.map(async (f) => {
|
|
25206
|
+
if (!baseGen) return false;
|
|
25207
|
+
const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
|
|
25208
|
+
return this.accumulator.has(resolved);
|
|
25209
|
+
})
|
|
25210
|
+
).then((results) => results.some(Boolean));
|
|
25211
|
+
const resolvedFiles = {};
|
|
25212
|
+
for (const filePath of patch.files) {
|
|
25213
|
+
const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
|
|
25214
|
+
if (resolvedPath !== filePath) {
|
|
25215
|
+
resolvedFiles[filePath] = resolvedPath;
|
|
25216
|
+
}
|
|
25217
|
+
}
|
|
25218
|
+
const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
|
|
25219
|
+
const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
|
|
25220
|
+
if (!needsAccumulation && !hasGeneratorRename) {
|
|
25221
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
25222
|
+
for (const filePath of patch.files) {
|
|
25223
|
+
const fullPath = (0, import_path10.join)(this.outputDir, filePath);
|
|
25224
|
+
snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
|
|
25225
|
+
}
|
|
25226
|
+
try {
|
|
25227
|
+
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
25228
|
+
const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
25229
|
+
return {
|
|
25230
|
+
patch,
|
|
25231
|
+
status: "applied",
|
|
25232
|
+
method: "git-am",
|
|
25233
|
+
...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
|
|
25234
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
25235
|
+
};
|
|
25236
|
+
} catch {
|
|
25237
|
+
for (const [filePath, content] of snapshots) {
|
|
25238
|
+
const fullPath = (0, import_path10.join)(this.outputDir, filePath);
|
|
25239
|
+
if (content != null) {
|
|
25240
|
+
await (0, import_promises4.writeFile)(fullPath, content);
|
|
25241
|
+
} else {
|
|
25242
|
+
await (0, import_promises4.unlink)(fullPath).catch(() => {
|
|
25243
|
+
});
|
|
25244
|
+
}
|
|
25245
|
+
}
|
|
25246
|
+
}
|
|
25247
|
+
}
|
|
25248
|
+
return this.applyWithThreeWayMerge(patch);
|
|
25249
|
+
}
|
|
25250
|
+
async applyWithThreeWayMerge(patch) {
|
|
25251
|
+
const fileResults = [];
|
|
25252
|
+
const resolvedFiles = {};
|
|
25253
|
+
const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-"));
|
|
25254
|
+
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
25255
|
+
const tempGit = new GitClient2(tempDir);
|
|
25256
|
+
await tempGit.exec(["init"]);
|
|
25257
|
+
await tempGit.exec(["config", "user.email", "replay@fern.com"]);
|
|
25258
|
+
await tempGit.exec(["config", "user.name", "Fern Replay"]);
|
|
25259
|
+
try {
|
|
25260
|
+
for (const filePath of patch.files) {
|
|
25261
|
+
if (isBinaryFile(filePath)) {
|
|
25262
|
+
fileResults.push({
|
|
25263
|
+
file: filePath,
|
|
25264
|
+
status: "skipped",
|
|
25265
|
+
reason: "binary-file"
|
|
25266
|
+
});
|
|
25267
|
+
continue;
|
|
25268
|
+
}
|
|
25269
|
+
const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
|
|
25270
|
+
if (result.file !== filePath) {
|
|
25271
|
+
resolvedFiles[filePath] = result.file;
|
|
25272
|
+
}
|
|
25273
|
+
fileResults.push(result);
|
|
25274
|
+
}
|
|
25275
|
+
} finally {
|
|
25276
|
+
await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
|
|
25277
|
+
});
|
|
25278
|
+
}
|
|
25279
|
+
const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
|
|
25280
|
+
const hasConflicts = conflictFiles.length > 0;
|
|
25281
|
+
const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
|
|
25282
|
+
return {
|
|
25283
|
+
patch,
|
|
25284
|
+
status: hasConflicts ? "conflict" : "applied",
|
|
25285
|
+
method: "3way-merge",
|
|
25286
|
+
fileResults,
|
|
25287
|
+
conflictReason,
|
|
25288
|
+
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
25289
|
+
};
|
|
25290
|
+
}
|
|
25291
|
+
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
25292
|
+
try {
|
|
25293
|
+
const inputs = await resolveInputs({
|
|
25294
|
+
patch,
|
|
25295
|
+
filePath,
|
|
25296
|
+
git: this.git,
|
|
25297
|
+
lockManager: this.lockManager,
|
|
25298
|
+
outputDir: this.outputDir,
|
|
25299
|
+
isTreeReachable: (h2) => this.isTreeReachable(h2),
|
|
25300
|
+
resolveBaseGeneration: (s) => this.resolveBaseGeneration(s),
|
|
25301
|
+
resolveFilePath: (f, b3, c3) => this.resolveFilePath(f, b3, c3),
|
|
25302
|
+
extractRenameSource: (p2, f) => this.extractRenameSource(p2, f),
|
|
25303
|
+
extractFileDiff: (p2, f) => this.extractFileDiff(p2, f)
|
|
25304
|
+
});
|
|
25305
|
+
if (inputs.earlyExit) {
|
|
25306
|
+
return { file: filePath, ...inputs.earlyExit };
|
|
25307
|
+
}
|
|
25308
|
+
const plan = await buildMergePlan({
|
|
25309
|
+
inputs,
|
|
25310
|
+
patch,
|
|
25311
|
+
filePath,
|
|
25312
|
+
accumulator: this.accumulator,
|
|
25313
|
+
tempGit,
|
|
25314
|
+
tempDir,
|
|
25315
|
+
applyPatchToContent: (b3, p2, f, g, d, s) => this.applyPatchToContent(b3, p2, f, g, d, s),
|
|
25316
|
+
isPatchAlreadyApplied: (p2, f, c3, g, d) => this.isPatchAlreadyApplied(p2, f, c3, g, d)
|
|
25317
|
+
});
|
|
25318
|
+
return await runMergeAndRecord({
|
|
25319
|
+
plan,
|
|
25320
|
+
inputs,
|
|
25321
|
+
patch,
|
|
25322
|
+
accumulator: this.accumulator
|
|
25323
|
+
});
|
|
25324
|
+
} catch (error) {
|
|
25325
|
+
return {
|
|
25326
|
+
file: filePath,
|
|
25327
|
+
status: "skipped",
|
|
25328
|
+
reason: `error: ${error instanceof Error ? error.message : String(error)}`
|
|
25329
|
+
};
|
|
25330
|
+
}
|
|
25331
|
+
}
|
|
25332
|
+
async isTreeReachable(treeHash) {
|
|
25333
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
25334
|
+
if (result === void 0) {
|
|
25335
|
+
result = await this.git.treeExists(treeHash);
|
|
25336
|
+
this.treeExistsCache.set(treeHash, result);
|
|
25337
|
+
}
|
|
25338
|
+
return result;
|
|
25339
|
+
}
|
|
25340
|
+
isExcluded(patch) {
|
|
25341
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
25342
|
+
if (!config.exclude) return false;
|
|
25343
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
|
|
25344
|
+
}
|
|
25345
|
+
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
25346
|
+
const config = this.lockManager.getCustomizationsConfig();
|
|
25347
|
+
if (config.moves) {
|
|
25348
|
+
for (const move of config.moves) {
|
|
25349
|
+
if (minimatch(filePath, move.from) || filePath === move.from) {
|
|
25350
|
+
if (filePath === move.from) {
|
|
25351
|
+
return move.to;
|
|
25352
|
+
}
|
|
25353
|
+
const fromBase = move.from.replace(/\*\*.*$/, "");
|
|
25354
|
+
const toBase = move.to.replace(/\*\*.*$/, "");
|
|
25355
|
+
if (filePath.startsWith(fromBase)) {
|
|
25356
|
+
return toBase + filePath.slice(fromBase.length);
|
|
25357
|
+
}
|
|
25358
|
+
}
|
|
25359
|
+
}
|
|
25360
|
+
}
|
|
25361
|
+
const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
|
|
25362
|
+
let renames = this.renameCache.get(cacheKey);
|
|
25363
|
+
if (!renames) {
|
|
25364
|
+
renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
|
|
25365
|
+
this.renameCache.set(cacheKey, renames);
|
|
25366
|
+
}
|
|
25367
|
+
const gitRename = renames.find((r2) => r2.from === filePath);
|
|
25368
|
+
if (gitRename) {
|
|
25369
|
+
return gitRename.to;
|
|
25370
|
+
}
|
|
25371
|
+
return filePath;
|
|
25372
|
+
}
|
|
25373
|
+
async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
|
|
25374
|
+
if (!base) {
|
|
25375
|
+
return this.extractNewFileFromPatch(patchContent, filePath);
|
|
25376
|
+
}
|
|
25377
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
25378
|
+
if (!fileDiff) return null;
|
|
25379
|
+
try {
|
|
25380
|
+
if (sourceFilePath) {
|
|
25381
|
+
const tempSourcePath = (0, import_path10.join)(tempDir, sourceFilePath);
|
|
25382
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempSourcePath), { recursive: true });
|
|
25383
|
+
await (0, import_promises4.writeFile)(tempSourcePath, base);
|
|
25384
|
+
await tempGit.exec(["add", sourceFilePath]);
|
|
25385
|
+
await tempGit.exec([
|
|
25386
|
+
"commit",
|
|
25387
|
+
"-m",
|
|
25388
|
+
`base for rename ${sourceFilePath} -> ${filePath}`,
|
|
25389
|
+
"--allow-empty"
|
|
25390
|
+
]);
|
|
25391
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
25392
|
+
const tempTargetPath = (0, import_path10.join)(tempDir, filePath);
|
|
25393
|
+
return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
|
|
25394
|
+
}
|
|
25395
|
+
const tempFilePath = (0, import_path10.join)(tempDir, filePath);
|
|
25396
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
|
|
25397
|
+
await (0, import_promises4.writeFile)(tempFilePath, base);
|
|
25398
|
+
await tempGit.exec(["add", filePath]);
|
|
25399
|
+
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
25400
|
+
await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
|
|
25401
|
+
return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
|
|
25402
|
+
} catch {
|
|
25403
|
+
return null;
|
|
25404
|
+
}
|
|
25405
|
+
}
|
|
25406
|
+
/**
|
|
25407
|
+
* Detects whether a patch's additions are already present in `content`.
|
|
25408
|
+
* Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
|
|
25409
|
+
* the forward patch is effectively a no-op (its +lines are already there and
|
|
25410
|
+
* its -lines are already gone). Used to distinguish "patch already applied"
|
|
25411
|
+
* from "patch base mismatch" after a forward-apply fails.
|
|
25412
|
+
*/
|
|
25413
|
+
async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
|
|
25414
|
+
const fileDiff = this.extractFileDiff(patchContent, filePath);
|
|
25415
|
+
if (!fileDiff) return false;
|
|
25416
|
+
try {
|
|
25417
|
+
const tempFilePath = (0, import_path10.join)(tempDir, filePath);
|
|
25418
|
+
await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
|
|
25419
|
+
await (0, import_promises4.writeFile)(tempFilePath, content);
|
|
25420
|
+
await tempGit.exec(["add", filePath]);
|
|
25421
|
+
await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
|
|
25422
|
+
await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
|
|
25423
|
+
return true;
|
|
25424
|
+
} catch {
|
|
25425
|
+
return false;
|
|
25426
|
+
}
|
|
25427
|
+
}
|
|
25428
|
+
extractFileDiff(patchContent, filePath) {
|
|
25429
|
+
const lines = patchContent.split("\n");
|
|
25430
|
+
const diffLines = [];
|
|
25431
|
+
let inTargetFile = false;
|
|
25432
|
+
for (const line of lines) {
|
|
25433
|
+
if (line.startsWith("diff --git")) {
|
|
25434
|
+
if (inTargetFile) {
|
|
25435
|
+
break;
|
|
25436
|
+
}
|
|
25437
|
+
if (isDiffLineForFile(line, filePath)) {
|
|
25438
|
+
inTargetFile = true;
|
|
25439
|
+
diffLines.push(line);
|
|
25440
|
+
}
|
|
25441
|
+
continue;
|
|
25442
|
+
}
|
|
25443
|
+
if (inTargetFile) {
|
|
25444
|
+
diffLines.push(line);
|
|
25445
|
+
}
|
|
25446
|
+
}
|
|
25447
|
+
return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
|
|
25448
|
+
}
|
|
25449
|
+
extractRenameSource(patchContent, targetFilePath) {
|
|
25450
|
+
const lines = patchContent.split("\n");
|
|
25451
|
+
let inTargetFile = false;
|
|
25452
|
+
for (const line of lines) {
|
|
25453
|
+
if (line.startsWith("diff --git")) {
|
|
25454
|
+
if (inTargetFile) break;
|
|
25455
|
+
inTargetFile = isDiffLineForFile(line, targetFilePath);
|
|
25456
|
+
continue;
|
|
25457
|
+
}
|
|
25458
|
+
if (!inTargetFile) continue;
|
|
25459
|
+
if (line.startsWith("@@")) break;
|
|
25460
|
+
if (line.startsWith("rename from ")) {
|
|
25461
|
+
return line.slice("rename from ".length);
|
|
25462
|
+
}
|
|
25463
|
+
}
|
|
25464
|
+
return null;
|
|
25465
|
+
}
|
|
25466
|
+
extractNewFileFromPatch(patchContent, filePath) {
|
|
25467
|
+
const lines = patchContent.split("\n");
|
|
25468
|
+
const addedLines = [];
|
|
25469
|
+
let inTargetFile = false;
|
|
25470
|
+
let inHunk = false;
|
|
25471
|
+
let isNewFile = false;
|
|
25472
|
+
let noTrailingNewline = false;
|
|
25473
|
+
for (const line of lines) {
|
|
25474
|
+
if (line.startsWith("diff --git")) {
|
|
25475
|
+
if (inTargetFile) break;
|
|
25476
|
+
inTargetFile = isDiffLineForFile(line, filePath);
|
|
25477
|
+
inHunk = false;
|
|
25478
|
+
isNewFile = false;
|
|
25479
|
+
continue;
|
|
25480
|
+
}
|
|
25481
|
+
if (!inTargetFile) continue;
|
|
25482
|
+
if (!inHunk) {
|
|
25483
|
+
if (line === "--- /dev/null" || line.startsWith("new file mode")) {
|
|
25484
|
+
isNewFile = true;
|
|
25485
|
+
}
|
|
25486
|
+
}
|
|
25487
|
+
if (line.startsWith("@@")) {
|
|
25488
|
+
if (!isNewFile) return null;
|
|
25489
|
+
inHunk = true;
|
|
25490
|
+
continue;
|
|
25491
|
+
}
|
|
25492
|
+
if (!inHunk) continue;
|
|
25493
|
+
if (line === "\") {
|
|
25494
|
+
noTrailingNewline = true;
|
|
25495
|
+
continue;
|
|
25496
|
+
}
|
|
25497
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
25498
|
+
addedLines.push(line.slice(1));
|
|
25499
|
+
}
|
|
25500
|
+
}
|
|
25501
|
+
if (addedLines.length === 0) return null;
|
|
25502
|
+
return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
|
|
25503
|
+
}
|
|
25504
|
+
};
|
|
25352
25505
|
ReplayCommitter = class {
|
|
25353
25506
|
git;
|
|
25354
25507
|
outputDir;
|
|
@@ -25452,210 +25605,66 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25452
25605
|
};
|
|
25453
25606
|
init_GitClient();
|
|
25454
25607
|
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);
|
|
25608
|
+
FileOwnership = class {
|
|
25609
|
+
constructor(git) {
|
|
25491
25610
|
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
|
-
}
|
|
25498
|
-
/**
|
|
25499
|
-
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
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.
|
|
25506
|
-
*
|
|
25507
|
-
* Callers may land additional commits on HEAD between `prepareReplay` and
|
|
25508
|
-
* `applyPreparedReplay` (e.g., `[fern-autoversion]`).
|
|
25509
|
-
*/
|
|
25510
|
-
async prepareReplay(options) {
|
|
25511
|
-
this.warnings = [];
|
|
25512
|
-
this.detector.warnings.length = 0;
|
|
25513
|
-
if (options?.skipApplication) {
|
|
25514
|
-
return this._prepareSkipApplication(options);
|
|
25515
|
-
}
|
|
25516
|
-
const flow = this.determineFlow();
|
|
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
25611
|
}
|
|
25526
25612
|
/**
|
|
25527
|
-
*
|
|
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.
|
|
25613
|
+
* Canonical resolution. Returns true if `file` is user-owned per:
|
|
25531
25614
|
*
|
|
25532
|
-
*
|
|
25533
|
-
*
|
|
25615
|
+
* 1. `patch.user_owned === true` — authoritative, persisted flag.
|
|
25616
|
+
* 2. `file === ".fernignore"` — always user-owned.
|
|
25617
|
+
* 3. File matches a `.fernignore` pattern (via minimatch).
|
|
25618
|
+
* 4. Patch content shows `--- /dev/null` for this file (legacy-safe
|
|
25619
|
+
* signal for patches predating `user_owned` or whose flag was lost).
|
|
25620
|
+
* 5. Tree-based check against `patch.base_generation` when reachable
|
|
25621
|
+
* AND distinct from `currentGenSha`. Unreachable base → conservative
|
|
25622
|
+
* user-owned (silent-absorption safety net), with a one-time warning
|
|
25623
|
+
* per base SHA.
|
|
25624
|
+
* 6. Final fallback: check `currentGenSha` when no usable base exists
|
|
25625
|
+
* (legacy one-gen lockfile).
|
|
25534
25626
|
*/
|
|
25535
|
-
async
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
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
|
-
}
|
|
25627
|
+
async resolveCanonical(ctx) {
|
|
25628
|
+
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
25629
|
+
if (patch.user_owned) return true;
|
|
25630
|
+
if (file === ".fernignore") return true;
|
|
25631
|
+
if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
|
|
25632
|
+
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
25633
|
+
return true;
|
|
25594
25634
|
}
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
}
|
|
25605
|
-
for (const patch of redetectedPatches) {
|
|
25606
|
-
this.lockManager.addPatch(patch);
|
|
25635
|
+
const base = patch.base_generation;
|
|
25636
|
+
if (base && base !== currentGenSha) {
|
|
25637
|
+
const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
|
|
25638
|
+
if (!reachable) {
|
|
25639
|
+
if (!warnedGens.has(base)) {
|
|
25640
|
+
warnedGens.add(base);
|
|
25641
|
+
warnings.push(
|
|
25642
|
+
`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.`
|
|
25643
|
+
);
|
|
25607
25644
|
}
|
|
25645
|
+
return true;
|
|
25608
25646
|
}
|
|
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;
|
|
25647
|
+
return await this.git.showFile(base, originalFileName ?? file) === null;
|
|
25629
25648
|
}
|
|
25649
|
+
return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
|
|
25630
25650
|
}
|
|
25631
|
-
|
|
25632
|
-
|
|
25633
|
-
|
|
25634
|
-
|
|
25635
|
-
patchesApplied: 0,
|
|
25636
|
-
patchesWithConflicts: 0,
|
|
25637
|
-
patchesSkipped: 0,
|
|
25638
|
-
conflicts: []
|
|
25639
|
-
};
|
|
25651
|
+
};
|
|
25652
|
+
FirstGenerationFlow = class {
|
|
25653
|
+
constructor(service) {
|
|
25654
|
+
this.service = service;
|
|
25640
25655
|
}
|
|
25641
|
-
async
|
|
25656
|
+
async prepare(options) {
|
|
25642
25657
|
if (options?.dryRun) {
|
|
25643
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25658
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25644
25659
|
return {
|
|
25645
25660
|
flow: "first-generation",
|
|
25646
25661
|
previousGenerationSha: null,
|
|
25647
25662
|
currentGenerationSha: headSha,
|
|
25648
25663
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25649
25664
|
_prepared: {
|
|
25665
|
+
kind: "terminal",
|
|
25650
25666
|
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
|
|
25667
|
+
preparedReport: firstGenerationReport()
|
|
25659
25668
|
}
|
|
25660
25669
|
};
|
|
25661
25670
|
}
|
|
@@ -25664,17 +25673,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25664
25673
|
generatorVersions: options.generatorVersions ?? {},
|
|
25665
25674
|
baseBranchHead: options.baseBranchHead
|
|
25666
25675
|
} : void 0;
|
|
25667
|
-
await this.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
25668
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25669
|
-
this.lockManager.initializeInMemory(genRecord);
|
|
25676
|
+
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
25677
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25678
|
+
this.service.lockManager.initializeInMemory(genRecord);
|
|
25670
25679
|
return {
|
|
25671
25680
|
flow: "first-generation",
|
|
25672
25681
|
previousGenerationSha: null,
|
|
25673
25682
|
currentGenerationSha: genRecord.commit_sha,
|
|
25674
25683
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25675
25684
|
_prepared: {
|
|
25685
|
+
kind: "active",
|
|
25676
25686
|
flow: "first-generation",
|
|
25677
|
-
terminal: false,
|
|
25678
25687
|
patchesToApply: [],
|
|
25679
25688
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25680
25689
|
detectorWarnings: [],
|
|
@@ -25684,47 +25693,45 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25684
25693
|
}
|
|
25685
25694
|
};
|
|
25686
25695
|
}
|
|
25687
|
-
async
|
|
25688
|
-
this.lockManager.save();
|
|
25689
|
-
return
|
|
25696
|
+
async apply(_prep) {
|
|
25697
|
+
this.service.lockManager.save();
|
|
25698
|
+
return firstGenerationReport();
|
|
25690
25699
|
}
|
|
25691
|
-
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
*/
|
|
25698
|
-
async _prepareSkipApplication(options) {
|
|
25700
|
+
};
|
|
25701
|
+
SkipApplicationFlow = class {
|
|
25702
|
+
constructor(service) {
|
|
25703
|
+
this.service = service;
|
|
25704
|
+
}
|
|
25705
|
+
async prepare(options) {
|
|
25699
25706
|
const commitOpts = options ? {
|
|
25700
25707
|
cliVersion: options.cliVersion ?? "unknown",
|
|
25701
25708
|
generatorVersions: options.generatorVersions ?? {},
|
|
25702
25709
|
baseBranchHead: options.baseBranchHead
|
|
25703
25710
|
} : void 0;
|
|
25704
|
-
await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
25705
|
-
await this.cleanupStaleConflictMarkers();
|
|
25706
|
-
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
25711
|
+
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
25712
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25713
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25707
25714
|
let previousGenerationSha = null;
|
|
25708
25715
|
try {
|
|
25709
|
-
const lock = this.lockManager.read();
|
|
25716
|
+
const lock = this.service.lockManager.read();
|
|
25710
25717
|
previousGenerationSha = lock.current_generation ?? null;
|
|
25711
|
-
this.lockManager.addGeneration(genRecord);
|
|
25718
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25712
25719
|
} catch (error) {
|
|
25713
25720
|
if (error instanceof LockfileNotFoundError) {
|
|
25714
|
-
this.lockManager.initializeInMemory(genRecord);
|
|
25721
|
+
this.service.lockManager.initializeInMemory(genRecord);
|
|
25715
25722
|
} else {
|
|
25716
25723
|
throw error;
|
|
25717
25724
|
}
|
|
25718
25725
|
}
|
|
25719
|
-
this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
25726
|
+
this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
25720
25727
|
return {
|
|
25721
25728
|
flow: "skip-application",
|
|
25722
25729
|
previousGenerationSha,
|
|
25723
25730
|
currentGenerationSha: genRecord.commit_sha,
|
|
25724
25731
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25725
25732
|
_prepared: {
|
|
25733
|
+
kind: "active",
|
|
25726
25734
|
flow: "skip-application",
|
|
25727
|
-
terminal: false,
|
|
25728
25735
|
patchesToApply: [],
|
|
25729
25736
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
25730
25737
|
detectorWarnings: [],
|
|
@@ -25734,13 +25741,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25734
25741
|
}
|
|
25735
25742
|
};
|
|
25736
25743
|
}
|
|
25737
|
-
async
|
|
25738
|
-
this.lockManager.save();
|
|
25739
|
-
const credentialWarnings = [...this.lockManager.warnings];
|
|
25744
|
+
async apply(_prep, options) {
|
|
25745
|
+
this.service.lockManager.save();
|
|
25746
|
+
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
25740
25747
|
if (!options?.stageOnly) {
|
|
25741
|
-
await this.committer.commitReplay(0);
|
|
25748
|
+
await this.service.committer.commitReplay(0);
|
|
25742
25749
|
} else {
|
|
25743
|
-
await this.committer.stageAll();
|
|
25750
|
+
await this.service.committer.stageAll();
|
|
25744
25751
|
}
|
|
25745
25752
|
return {
|
|
25746
25753
|
flow: "skip-application",
|
|
@@ -25752,13 +25759,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25752
25759
|
warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
|
|
25753
25760
|
};
|
|
25754
25761
|
}
|
|
25755
|
-
|
|
25756
|
-
|
|
25762
|
+
};
|
|
25763
|
+
NoPatchesFlow = class {
|
|
25764
|
+
constructor(service) {
|
|
25765
|
+
this.service = service;
|
|
25766
|
+
}
|
|
25767
|
+
async prepare(options) {
|
|
25768
|
+
const preLock = this.service.lockManager.read();
|
|
25757
25769
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
25758
|
-
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
25759
|
-
const detectorWarnings = [...this.detector.warnings];
|
|
25770
|
+
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
25771
|
+
const detectorWarnings = [...this.service.detector.warnings];
|
|
25760
25772
|
if (options?.dryRun) {
|
|
25761
|
-
const dryRunWarnings = [...detectorWarnings, ...this.warnings];
|
|
25773
|
+
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
25762
25774
|
const preparedReport = {
|
|
25763
25775
|
flow: "no-patches",
|
|
25764
25776
|
patchesDetected: newPatches.length,
|
|
@@ -25770,22 +25782,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25770
25782
|
wouldApply: newPatches,
|
|
25771
25783
|
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
25772
25784
|
};
|
|
25773
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25785
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25774
25786
|
return {
|
|
25775
25787
|
flow: "no-patches",
|
|
25776
25788
|
previousGenerationSha,
|
|
25777
25789
|
currentGenerationSha: headSha,
|
|
25778
25790
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25779
25791
|
_prepared: {
|
|
25792
|
+
kind: "terminal",
|
|
25780
25793
|
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
|
|
25794
|
+
preparedReport
|
|
25789
25795
|
}
|
|
25790
25796
|
};
|
|
25791
25797
|
}
|
|
@@ -25794,7 +25800,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25794
25800
|
const uniqueFiles = [...new Set(newPatches.flatMap((p2) => p2.files))];
|
|
25795
25801
|
const absorbedFiles = /* @__PURE__ */ new Set();
|
|
25796
25802
|
for (const file of uniqueFiles) {
|
|
25797
|
-
const diff = await this.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
|
|
25803
|
+
const diff = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
|
|
25798
25804
|
if (diff !== null && !diff.trim()) {
|
|
25799
25805
|
absorbedFiles.add(file);
|
|
25800
25806
|
}
|
|
@@ -25807,7 +25813,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25807
25813
|
}
|
|
25808
25814
|
for (const id of revertedPatchIds) {
|
|
25809
25815
|
try {
|
|
25810
|
-
this.lockManager.removePatch(id);
|
|
25816
|
+
this.service.lockManager.removePatch(id);
|
|
25811
25817
|
} catch {
|
|
25812
25818
|
}
|
|
25813
25819
|
}
|
|
@@ -25816,18 +25822,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25816
25822
|
generatorVersions: options.generatorVersions ?? {},
|
|
25817
25823
|
baseBranchHead: options.baseBranchHead
|
|
25818
25824
|
} : 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);
|
|
25825
|
+
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
25826
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25827
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25828
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25823
25829
|
return {
|
|
25824
25830
|
flow: "no-patches",
|
|
25825
25831
|
previousGenerationSha,
|
|
25826
25832
|
currentGenerationSha: genRecord.commit_sha,
|
|
25827
25833
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25828
25834
|
_prepared: {
|
|
25835
|
+
kind: "active",
|
|
25829
25836
|
flow: "no-patches",
|
|
25830
|
-
terminal: false,
|
|
25831
25837
|
patchesToApply: newPatches,
|
|
25832
25838
|
newPatchIds: new Set(newPatches.map((p2) => p2.id)),
|
|
25833
25839
|
detectorWarnings,
|
|
@@ -25837,41 +25843,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25837
25843
|
}
|
|
25838
25844
|
};
|
|
25839
25845
|
}
|
|
25840
|
-
async
|
|
25846
|
+
async apply(prep, options) {
|
|
25847
|
+
assertActive(prep);
|
|
25841
25848
|
const newPatches = prep._prepared.patchesToApply;
|
|
25842
25849
|
const detectorWarnings = prep._prepared.detectorWarnings;
|
|
25843
25850
|
const genSha = prep._prepared.genSha;
|
|
25844
25851
|
let results = [];
|
|
25845
25852
|
if (newPatches.length > 0) {
|
|
25846
|
-
results = await this.applicator.applyPatches(newPatches);
|
|
25847
|
-
this._lastApplyResults = results;
|
|
25848
|
-
this.recordDroppedContextLineWarnings(results);
|
|
25849
|
-
this.revertConflictingFiles(results);
|
|
25853
|
+
results = await this.service.applicator.applyPatches(newPatches);
|
|
25854
|
+
this.service._lastApplyResults = results;
|
|
25855
|
+
this.service.recordDroppedContextLineWarnings(results);
|
|
25856
|
+
this.service.revertConflictingFiles(results);
|
|
25850
25857
|
for (const result of results) {
|
|
25851
25858
|
if (result.status === "conflict") {
|
|
25852
25859
|
result.patch.status = "unresolved";
|
|
25853
25860
|
}
|
|
25854
25861
|
}
|
|
25855
25862
|
}
|
|
25856
|
-
const rebaseCounts = await this.rebasePatches(results, genSha);
|
|
25863
|
+
const rebaseCounts = await this.service.rebasePatches(results, genSha);
|
|
25857
25864
|
for (const patch of newPatches) {
|
|
25858
25865
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
25859
|
-
this.lockManager.addPatch(patch);
|
|
25866
|
+
this.service.lockManager.addPatch(patch);
|
|
25860
25867
|
}
|
|
25861
25868
|
}
|
|
25862
|
-
this.lockManager.save();
|
|
25863
|
-
this.warnings.push(...this.lockManager.warnings);
|
|
25869
|
+
this.service.lockManager.save();
|
|
25870
|
+
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
25864
25871
|
if (newPatches.length > 0) {
|
|
25865
25872
|
if (!options?.stageOnly) {
|
|
25866
25873
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
25867
25874
|
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
|
|
25868
|
-
await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
25875
|
+
await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
25869
25876
|
} else {
|
|
25870
|
-
await this.committer.stageAll();
|
|
25877
|
+
await this.service.committer.stageAll();
|
|
25871
25878
|
}
|
|
25872
25879
|
}
|
|
25873
|
-
const warnings = [...detectorWarnings, ...this.warnings];
|
|
25874
|
-
return this.buildReport(
|
|
25880
|
+
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
25881
|
+
return this.service.buildReport(
|
|
25875
25882
|
"no-patches",
|
|
25876
25883
|
newPatches,
|
|
25877
25884
|
results,
|
|
@@ -25882,13 +25889,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25882
25889
|
prep._prepared.revertedCount
|
|
25883
25890
|
);
|
|
25884
25891
|
}
|
|
25885
|
-
|
|
25886
|
-
|
|
25892
|
+
};
|
|
25893
|
+
NormalRegenerationFlow = class {
|
|
25894
|
+
constructor(service) {
|
|
25895
|
+
this.service = service;
|
|
25896
|
+
}
|
|
25897
|
+
async prepare(options) {
|
|
25898
|
+
const preLock = this.service.lockManager.read();
|
|
25887
25899
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
25888
25900
|
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];
|
|
25901
|
+
const existingPatches2 = this.service.lockManager.getPatches();
|
|
25902
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
25903
|
+
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
25892
25904
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
25893
25905
|
const preparedReport = {
|
|
25894
25906
|
flow: "normal-regeneration",
|
|
@@ -25901,48 +25913,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25901
25913
|
wouldApply: allPatches2,
|
|
25902
25914
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
25903
25915
|
};
|
|
25904
|
-
const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25916
|
+
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
25905
25917
|
return {
|
|
25906
25918
|
flow: "normal-regeneration",
|
|
25907
25919
|
previousGenerationSha,
|
|
25908
25920
|
currentGenerationSha: headSha,
|
|
25909
25921
|
baseBranchHead: options.baseBranchHead ?? null,
|
|
25910
25922
|
_prepared: {
|
|
25923
|
+
kind: "terminal",
|
|
25911
25924
|
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
|
|
25925
|
+
preparedReport
|
|
25920
25926
|
}
|
|
25921
25927
|
};
|
|
25922
25928
|
}
|
|
25923
|
-
let existingPatches = this.lockManager.getPatches();
|
|
25929
|
+
let existingPatches = this.service.lockManager.getPatches();
|
|
25924
25930
|
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));
|
|
25931
|
+
const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
|
|
25932
|
+
const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p2) => p2.id));
|
|
25927
25933
|
const removedByPreRebase = existingPatches.filter(
|
|
25928
25934
|
(p2) => preRebasePatchIds.has(p2.id) && !postRebasePatchIds.has(p2.id)
|
|
25929
25935
|
);
|
|
25930
|
-
existingPatches = this.lockManager.getPatches();
|
|
25936
|
+
existingPatches = this.service.lockManager.getPatches();
|
|
25931
25937
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
25932
25938
|
for (const p2 of existingPatches) {
|
|
25933
25939
|
const priorPatchId = seenHashes.get(p2.content_hash);
|
|
25934
25940
|
if (priorPatchId !== void 0) {
|
|
25935
|
-
this.lockManager.removePatch(p2.id);
|
|
25936
|
-
this.warnings.push(
|
|
25941
|
+
this.service.lockManager.removePatch(p2.id);
|
|
25942
|
+
this.service.warnings.push(
|
|
25937
25943
|
`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
25944
|
);
|
|
25939
25945
|
} else {
|
|
25940
25946
|
seenHashes.set(p2.content_hash, p2.id);
|
|
25941
25947
|
}
|
|
25942
25948
|
}
|
|
25943
|
-
existingPatches = this.lockManager.getPatches();
|
|
25944
|
-
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
25945
|
-
const detectorWarnings = [...this.detector.warnings];
|
|
25949
|
+
existingPatches = this.service.lockManager.getPatches();
|
|
25950
|
+
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
25951
|
+
const detectorWarnings = [...this.service.detector.warnings];
|
|
25946
25952
|
if (removedByPreRebase.length > 0) {
|
|
25947
25953
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p2) => p2.original_commit));
|
|
25948
25954
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p2) => p2.original_message));
|
|
@@ -25957,7 +25963,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25957
25963
|
}
|
|
25958
25964
|
for (const id of revertedPatchIds) {
|
|
25959
25965
|
try {
|
|
25960
|
-
this.lockManager.removePatch(id);
|
|
25966
|
+
this.service.lockManager.removePatch(id);
|
|
25961
25967
|
} catch {
|
|
25962
25968
|
}
|
|
25963
25969
|
}
|
|
@@ -25969,18 +25975,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25969
25975
|
generatorVersions: options.generatorVersions ?? {},
|
|
25970
25976
|
baseBranchHead: options.baseBranchHead
|
|
25971
25977
|
} : 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);
|
|
25978
|
+
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
25979
|
+
await this.service.cleanupStaleConflictMarkers();
|
|
25980
|
+
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
25981
|
+
this.service.lockManager.addGeneration(genRecord);
|
|
25976
25982
|
return {
|
|
25977
25983
|
flow: "normal-regeneration",
|
|
25978
25984
|
previousGenerationSha,
|
|
25979
25985
|
currentGenerationSha: genRecord.commit_sha,
|
|
25980
25986
|
baseBranchHead: options?.baseBranchHead ?? null,
|
|
25981
25987
|
_prepared: {
|
|
25988
|
+
kind: "active",
|
|
25982
25989
|
flow: "normal-regeneration",
|
|
25983
|
-
terminal: false,
|
|
25984
25990
|
patchesToApply: allPatches,
|
|
25985
25991
|
newPatchIds: new Set(newPatches.map((p2) => p2.id)),
|
|
25986
25992
|
preRebaseCounts,
|
|
@@ -25991,46 +25997,47 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
25991
25997
|
}
|
|
25992
25998
|
};
|
|
25993
25999
|
}
|
|
25994
|
-
async
|
|
26000
|
+
async apply(prep, options) {
|
|
26001
|
+
assertActive(prep);
|
|
25995
26002
|
const allPatches = prep._prepared.patchesToApply;
|
|
25996
26003
|
const newPatchIds = prep._prepared.newPatchIds;
|
|
25997
26004
|
const detectorWarnings = prep._prepared.detectorWarnings;
|
|
25998
26005
|
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);
|
|
26006
|
+
const results = await this.service.applicator.applyPatches(allPatches);
|
|
26007
|
+
this.service._lastApplyResults = results;
|
|
26008
|
+
this.service.recordDroppedContextLineWarnings(results);
|
|
26009
|
+
this.service.revertConflictingFiles(results);
|
|
26003
26010
|
for (const result of results) {
|
|
26004
26011
|
if (result.status === "conflict") {
|
|
26005
26012
|
result.patch.status = "unresolved";
|
|
26006
26013
|
}
|
|
26007
26014
|
}
|
|
26008
|
-
const rebaseCounts = await this.rebasePatches(results, genSha);
|
|
26015
|
+
const rebaseCounts = await this.service.rebasePatches(results, genSha);
|
|
26009
26016
|
const newPatches = allPatches.filter((p2) => newPatchIds.has(p2.id));
|
|
26010
26017
|
for (const patch of newPatches) {
|
|
26011
26018
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
26012
|
-
this.lockManager.addPatch(patch);
|
|
26019
|
+
this.service.lockManager.addPatch(patch);
|
|
26013
26020
|
}
|
|
26014
26021
|
}
|
|
26015
26022
|
for (const result of results) {
|
|
26016
26023
|
if (result.status === "conflict") {
|
|
26017
26024
|
try {
|
|
26018
|
-
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
26025
|
+
this.service.lockManager.markPatchUnresolved(result.patch.id);
|
|
26019
26026
|
} catch {
|
|
26020
26027
|
}
|
|
26021
26028
|
}
|
|
26022
26029
|
}
|
|
26023
|
-
this.lockManager.save();
|
|
26024
|
-
this.warnings.push(...this.lockManager.warnings);
|
|
26030
|
+
this.service.lockManager.save();
|
|
26031
|
+
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
26025
26032
|
if (options?.stageOnly) {
|
|
26026
|
-
await this.committer.stageAll();
|
|
26033
|
+
await this.service.committer.stageAll();
|
|
26027
26034
|
} else {
|
|
26028
26035
|
const appliedCount = results.filter((r2) => r2.status === "applied").length;
|
|
26029
26036
|
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
|
|
26030
|
-
await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
26037
|
+
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
26031
26038
|
}
|
|
26032
|
-
const warnings = [...detectorWarnings, ...this.warnings];
|
|
26033
|
-
return this.buildReport(
|
|
26039
|
+
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
26040
|
+
return this.service.buildReport(
|
|
26034
26041
|
"normal-regeneration",
|
|
26035
26042
|
allPatches,
|
|
26036
26043
|
results,
|
|
@@ -26041,10 +26048,204 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26041
26048
|
prep._prepared.revertedCount
|
|
26042
26049
|
);
|
|
26043
26050
|
}
|
|
26051
|
+
};
|
|
26052
|
+
ReplayService = class {
|
|
26053
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26054
|
+
git;
|
|
26055
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26056
|
+
detector;
|
|
26057
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26058
|
+
applicator;
|
|
26059
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26060
|
+
committer;
|
|
26061
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26062
|
+
lockManager;
|
|
26063
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26064
|
+
fileOwnership;
|
|
26065
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26066
|
+
outputDir;
|
|
26067
|
+
/** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
|
|
26068
|
+
_lastApplyResults = [];
|
|
26069
|
+
/**
|
|
26070
|
+
* Service-level warnings accumulated during a single runReplay() call.
|
|
26071
|
+
* Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
|
|
26072
|
+
* Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
|
|
26073
|
+
* is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
|
|
26074
|
+
* conservative heuristic. Cleared at the top of `runReplay()`.
|
|
26075
|
+
*
|
|
26076
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
26077
|
+
*/
|
|
26078
|
+
warnings = [];
|
|
26079
|
+
/**
|
|
26080
|
+
* @internal Test-only accessor.
|
|
26081
|
+
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
26082
|
+
* to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
|
|
26083
|
+
*/
|
|
26084
|
+
get applicatorRef() {
|
|
26085
|
+
return this.applicator;
|
|
26086
|
+
}
|
|
26087
|
+
/**
|
|
26088
|
+
* @internal Test-only accessor.
|
|
26089
|
+
* Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
|
|
26090
|
+
* this to filter applied-vs-conflicted-vs-absorbed patches for invariant
|
|
26091
|
+
* assertions. Returns an empty array if no patches have been applied yet.
|
|
26092
|
+
*/
|
|
26093
|
+
getLastResults() {
|
|
26094
|
+
return this._lastApplyResults;
|
|
26095
|
+
}
|
|
26096
|
+
constructor(outputDir, _config) {
|
|
26097
|
+
const git = new GitClient(outputDir);
|
|
26098
|
+
this.git = git;
|
|
26099
|
+
this.outputDir = outputDir;
|
|
26100
|
+
this.lockManager = new LockfileManager(outputDir);
|
|
26101
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir);
|
|
26102
|
+
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
26103
|
+
this.committer = new ReplayCommitter(git, outputDir);
|
|
26104
|
+
this.fileOwnership = new FileOwnership(git);
|
|
26105
|
+
}
|
|
26106
|
+
/**
|
|
26107
|
+
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
26108
|
+
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
26109
|
+
* the generation commit (or unchanged for dry-run).
|
|
26110
|
+
*
|
|
26111
|
+
* Does NOT apply patches — the returned handle must be passed to
|
|
26112
|
+
* `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
|
|
26113
|
+
* happen here; lockfile persistence is deferred to phase 2.
|
|
26114
|
+
*
|
|
26115
|
+
* Callers may land additional commits on HEAD between `prepareReplay` and
|
|
26116
|
+
* `applyPreparedReplay` (e.g., `[fern-autoversion]`).
|
|
26117
|
+
*/
|
|
26118
|
+
async prepareReplay(options) {
|
|
26119
|
+
this.warnings = [];
|
|
26120
|
+
this.detector.warnings.length = 0;
|
|
26121
|
+
return this.selectFlow(options).prepare(options);
|
|
26122
|
+
}
|
|
26123
|
+
/**
|
|
26124
|
+
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
26125
|
+
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
26126
|
+
* under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
|
|
26127
|
+
* respected.
|
|
26128
|
+
*
|
|
26129
|
+
* For terminal handles (dry-run, first-gen has no patch work, skip-app does
|
|
26130
|
+
* its own post-commit logic), this returns the precomputed report.
|
|
26131
|
+
*/
|
|
26132
|
+
async applyPreparedReplay(prep, options) {
|
|
26133
|
+
if (prep._prepared.kind === "terminal") {
|
|
26134
|
+
return prep._prepared.preparedReport;
|
|
26135
|
+
}
|
|
26136
|
+
return this.flowForKind(prep._prepared.flow).apply(prep, options);
|
|
26137
|
+
}
|
|
26138
|
+
/**
|
|
26139
|
+
* Construct the flow for the current run. `selectFlow` is called from
|
|
26140
|
+
* `prepareReplay` and decides based on options + lockfile state.
|
|
26141
|
+
*/
|
|
26142
|
+
selectFlow(options) {
|
|
26143
|
+
if (options?.skipApplication) return new SkipApplicationFlow(this);
|
|
26144
|
+
return this.flowForKind(this.determineFlow());
|
|
26145
|
+
}
|
|
26146
|
+
/**
|
|
26147
|
+
* Construct a flow given its kind. Used by `applyPreparedReplay` to
|
|
26148
|
+
* route to the same kind that was selected in phase 1.
|
|
26149
|
+
*/
|
|
26150
|
+
flowForKind(kind) {
|
|
26151
|
+
switch (kind) {
|
|
26152
|
+
case "first-generation":
|
|
26153
|
+
return new FirstGenerationFlow(this);
|
|
26154
|
+
case "no-patches":
|
|
26155
|
+
return new NoPatchesFlow(this);
|
|
26156
|
+
case "normal-regeneration":
|
|
26157
|
+
return new NormalRegenerationFlow(this);
|
|
26158
|
+
case "skip-application":
|
|
26159
|
+
return new SkipApplicationFlow(this);
|
|
26160
|
+
}
|
|
26161
|
+
}
|
|
26162
|
+
/**
|
|
26163
|
+
* Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
|
|
26164
|
+
* Behavior is byte-identical to the pre-split implementation for all existing callers.
|
|
26165
|
+
*/
|
|
26166
|
+
async runReplay(options) {
|
|
26167
|
+
const prep = await this.prepareReplay(options);
|
|
26168
|
+
return this.applyPreparedReplay(prep, options);
|
|
26169
|
+
}
|
|
26170
|
+
/**
|
|
26171
|
+
* Sync the lockfile after a divergent PR was squash-merged.
|
|
26172
|
+
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
26173
|
+
*
|
|
26174
|
+
* After updating the generation record, re-detects customer patches via
|
|
26175
|
+
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
26176
|
+
* (which includes customer customizations after squash merge). This ensures
|
|
26177
|
+
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
26178
|
+
* was restored from the base branch during conflict PR creation.
|
|
26179
|
+
*/
|
|
26180
|
+
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
26181
|
+
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
26182
|
+
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
26183
|
+
const record2 = {
|
|
26184
|
+
commit_sha: generationCommitSha,
|
|
26185
|
+
tree_hash: treeHash,
|
|
26186
|
+
timestamp,
|
|
26187
|
+
cli_version: options?.cliVersion ?? "unknown",
|
|
26188
|
+
generator_versions: options?.generatorVersions ?? {},
|
|
26189
|
+
base_branch_head: options?.baseBranchHead
|
|
26190
|
+
};
|
|
26191
|
+
let resolvedPatches;
|
|
26192
|
+
if (!this.lockManager.exists()) {
|
|
26193
|
+
this.lockManager.initializeInMemory(record2);
|
|
26194
|
+
} else {
|
|
26195
|
+
this.lockManager.read();
|
|
26196
|
+
const unresolvedPatches = [
|
|
26197
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
26198
|
+
...this.lockManager.getResolvingPatches()
|
|
26199
|
+
];
|
|
26200
|
+
resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
|
|
26201
|
+
this.lockManager.addGeneration(record2);
|
|
26202
|
+
this.lockManager.clearPatches();
|
|
26203
|
+
for (const patch of unresolvedPatches) {
|
|
26204
|
+
this.lockManager.addPatch(patch);
|
|
26205
|
+
}
|
|
26206
|
+
}
|
|
26207
|
+
try {
|
|
26208
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
26209
|
+
if (redetectedPatches.length > 0) {
|
|
26210
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p2) => p2.files));
|
|
26211
|
+
const currentPatches = this.lockManager.getPatches();
|
|
26212
|
+
for (const patch of currentPatches) {
|
|
26213
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
26214
|
+
this.lockManager.removePatch(patch.id);
|
|
26215
|
+
}
|
|
26216
|
+
}
|
|
26217
|
+
for (const patch of redetectedPatches) {
|
|
26218
|
+
this.lockManager.addPatch(patch);
|
|
26219
|
+
}
|
|
26220
|
+
}
|
|
26221
|
+
} catch (error) {
|
|
26222
|
+
for (const patch of resolvedPatches ?? []) {
|
|
26223
|
+
this.lockManager.addPatch(patch);
|
|
26224
|
+
}
|
|
26225
|
+
this.detector.warnings.push(
|
|
26226
|
+
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
26227
|
+
);
|
|
26228
|
+
}
|
|
26229
|
+
this.lockManager.save();
|
|
26230
|
+
this.detector.warnings.push(...this.lockManager.warnings);
|
|
26231
|
+
}
|
|
26232
|
+
determineFlow() {
|
|
26233
|
+
try {
|
|
26234
|
+
const lock = this.lockManager.read();
|
|
26235
|
+
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
26236
|
+
} catch (error) {
|
|
26237
|
+
if (error instanceof LockfileNotFoundError) {
|
|
26238
|
+
return "first-generation";
|
|
26239
|
+
}
|
|
26240
|
+
throw error;
|
|
26241
|
+
}
|
|
26242
|
+
}
|
|
26044
26243
|
/**
|
|
26045
26244
|
* Rebase cleanly applied patches so they are relative to the current generation.
|
|
26046
26245
|
* This prevents recurring conflicts on subsequent regenerations.
|
|
26047
26246
|
* Returns the number of patches rebased.
|
|
26247
|
+
*
|
|
26248
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
26048
26249
|
*/
|
|
26049
26250
|
async rebasePatches(results, currentGenSha) {
|
|
26050
26251
|
let absorbed = 0;
|
|
@@ -26054,6 +26255,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26054
26255
|
const seenContentHashes = /* @__PURE__ */ new Map();
|
|
26055
26256
|
const absorbedPatchIds = /* @__PURE__ */ new Set();
|
|
26056
26257
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
26258
|
+
const warnedGens = /* @__PURE__ */ new Set();
|
|
26057
26259
|
for (const result of results) {
|
|
26058
26260
|
if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
|
|
26059
26261
|
const patch = result.patch;
|
|
@@ -26098,18 +26300,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26098
26300
|
}
|
|
26099
26301
|
}
|
|
26100
26302
|
const isUserOwnedPerFile = await Promise.all(
|
|
26101
|
-
patch.files.map(
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26106
|
-
|
|
26107
|
-
|
|
26108
|
-
|
|
26109
|
-
|
|
26110
|
-
|
|
26111
|
-
|
|
26112
|
-
})
|
|
26303
|
+
patch.files.map(
|
|
26304
|
+
(file) => this.fileOwnership.resolveCanonical({
|
|
26305
|
+
file,
|
|
26306
|
+
originalFileName: originalByResolved[file],
|
|
26307
|
+
patch,
|
|
26308
|
+
currentGenSha,
|
|
26309
|
+
fernignorePatterns,
|
|
26310
|
+
warnedGens,
|
|
26311
|
+
warnings: this.warnings
|
|
26312
|
+
})
|
|
26313
|
+
)
|
|
26113
26314
|
);
|
|
26114
26315
|
const hasProtectedFiles = patch.files.some(
|
|
26115
26316
|
(file) => file === ".fernignore" || fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))
|
|
@@ -26236,7 +26437,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26236
26437
|
for (const file of files) {
|
|
26237
26438
|
if (isBinaryFile(file)) continue;
|
|
26238
26439
|
try {
|
|
26239
|
-
const content = await (0,
|
|
26440
|
+
const content = await (0, import_promises7.readFile)((0, import_path13.join)(this.outputDir, file), "utf-8");
|
|
26240
26441
|
snapshot[file] = content;
|
|
26241
26442
|
} catch {
|
|
26242
26443
|
}
|
|
@@ -26244,10 +26445,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26244
26445
|
return snapshot;
|
|
26245
26446
|
}
|
|
26246
26447
|
/**
|
|
26247
|
-
*
|
|
26248
|
-
* `fromSha` and `toSha` touched any
|
|
26249
|
-
*
|
|
26250
|
-
*
|
|
26448
|
+
* Detects autoversion contamination — returns true when a
|
|
26449
|
+
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
|
26450
|
+
* of the patch's `files`. Re-classifies each grep match via
|
|
26451
|
+
* `isGenerationCommit` so a customer commit that merely quotes the
|
|
26452
|
+
* marker in its body doesn't false-positive.
|
|
26453
|
+
*
|
|
26454
|
+
* Used by preGenerationRebase to skip the customer-content rebuild for
|
|
26455
|
+
* patches whose files were last touched by an autoversion step — a
|
|
26456
|
+
* naive `git diff currentGen HEAD` there would capture the
|
|
26457
|
+
* placeholder→semver swap as customer customization.
|
|
26251
26458
|
*/
|
|
26252
26459
|
async hasAutoversionContamination(fromSha, toSha, files) {
|
|
26253
26460
|
if (files.length === 0) return false;
|
|
@@ -26276,10 +26483,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26276
26483
|
return false;
|
|
26277
26484
|
}
|
|
26278
26485
|
/**
|
|
26279
|
-
*
|
|
26280
|
-
*
|
|
26281
|
-
*
|
|
26282
|
-
*
|
|
26486
|
+
* Rebuild patch_content from `theirs_snapshot` (captured at detection
|
|
26487
|
+
* time, predates pipeline content) rather than a HEAD-relative diff.
|
|
26488
|
+
* Returns `null` when snapshot is missing or doesn't cover every file
|
|
26489
|
+
* in `patch.files` — caller leaves the patch unchanged.
|
|
26490
|
+
*
|
|
26491
|
+
* Used as the autoversion-contamination escape hatch: when the
|
|
26492
|
+
* HEAD-relative diff would capture pipeline content (autoversion's
|
|
26493
|
+
* placeholder swap) as customer customization, the snapshot path
|
|
26494
|
+
* preserves the customer's actual intent.
|
|
26283
26495
|
*/
|
|
26284
26496
|
async computeSnapshotBasedPatchDiff(patch, currentGen) {
|
|
26285
26497
|
if (!patch.theirs_snapshot) return null;
|
|
@@ -26327,7 +26539,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26327
26539
|
* **Skips patches sharing files with other patches.** HEAD's content
|
|
26328
26540
|
* for a file shared by N patches is cumulative across all of them, so
|
|
26329
26541
|
* a HEAD-fallback would falsely encode other patches' contributions
|
|
26330
|
-
* into one patch's snapshot — breaking
|
|
26542
|
+
* into one patch's snapshot — breaking cross-patch isolation
|
|
26331
26543
|
* if the snapshot fallback later fires. Per-patch snapshots only
|
|
26332
26544
|
* make sense when the patch owns its file. Multi-patch-same-file
|
|
26333
26545
|
* legacy lockfiles keep using line-anchored reconstruction (the
|
|
@@ -26389,72 +26601,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26389
26601
|
);
|
|
26390
26602
|
}
|
|
26391
26603
|
}
|
|
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
26604
|
/**
|
|
26459
26605
|
* For conflict patches with mixed results (some files merged, some conflicted),
|
|
26460
26606
|
* check if the cleanly merged files were absorbed by the generator (empty diff).
|
|
@@ -26473,7 +26619,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26473
26619
|
const warnedGens = /* @__PURE__ */ new Set();
|
|
26474
26620
|
const generatorCleanFiles = [];
|
|
26475
26621
|
for (const file of cleanFiles) {
|
|
26476
|
-
|
|
26622
|
+
const userOwned = await this.fileOwnership.resolveCanonical({
|
|
26623
|
+
file,
|
|
26624
|
+
patch,
|
|
26625
|
+
currentGenSha,
|
|
26626
|
+
fernignorePatterns,
|
|
26627
|
+
warnedGens,
|
|
26628
|
+
warnings: this.warnings
|
|
26629
|
+
});
|
|
26630
|
+
if (userOwned) {
|
|
26477
26631
|
continue;
|
|
26478
26632
|
}
|
|
26479
26633
|
generatorCleanFiles.push(file);
|
|
@@ -26502,6 +26656,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26502
26656
|
* Pre-generation rebase: update patches using the customer's current state.
|
|
26503
26657
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
26504
26658
|
*/
|
|
26659
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26505
26660
|
async preGenerationRebase(patches) {
|
|
26506
26661
|
const lock = this.lockManager.read();
|
|
26507
26662
|
const currentGen = lock.current_generation;
|
|
@@ -26540,7 +26695,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26540
26695
|
if (patch.user_owned) return true;
|
|
26541
26696
|
if (patch.files.length === 0) return false;
|
|
26542
26697
|
for (const file of patch.files) {
|
|
26543
|
-
|
|
26698
|
+
const userOwned = await this.fileOwnership.resolveCanonical({
|
|
26699
|
+
file,
|
|
26700
|
+
patch,
|
|
26701
|
+
currentGenSha: currentGen,
|
|
26702
|
+
fernignorePatterns,
|
|
26703
|
+
warnedGens,
|
|
26704
|
+
warnings: this.warnings
|
|
26705
|
+
});
|
|
26706
|
+
if (!userOwned) {
|
|
26544
26707
|
return false;
|
|
26545
26708
|
}
|
|
26546
26709
|
}
|
|
@@ -26732,6 +26895,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
26732
26895
|
* relied on them. This is the "surface or preserve, never silently drop"
|
|
26733
26896
|
* contract for legitimate-deletion cases.
|
|
26734
26897
|
*/
|
|
26898
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26735
26899
|
recordDroppedContextLineWarnings(results) {
|
|
26736
26900
|
const PREVIEW_COUNT = 5;
|
|
26737
26901
|
for (const result of results) {
|
|
@@ -26754,12 +26918,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26754
26918
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
26755
26919
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
26756
26920
|
*/
|
|
26921
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26757
26922
|
revertConflictingFiles(results) {
|
|
26758
26923
|
for (const result of results) {
|
|
26759
26924
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
26760
26925
|
for (const fileResult of result.fileResults) {
|
|
26761
26926
|
if (fileResult.status !== "conflict") continue;
|
|
26762
|
-
const filePath = (0,
|
|
26927
|
+
const filePath = (0, import_path13.join)(this.outputDir, fileResult.file);
|
|
26763
26928
|
try {
|
|
26764
26929
|
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
26765
26930
|
const stripped = stripConflictMarkers(content);
|
|
@@ -26775,6 +26940,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26775
26940
|
* Restores files to their clean generated state from HEAD.
|
|
26776
26941
|
* Skips .fernignore-protected files to prevent overwriting user content.
|
|
26777
26942
|
*/
|
|
26943
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26778
26944
|
async cleanupStaleConflictMarkers() {
|
|
26779
26945
|
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
|
|
26780
26946
|
const files = markerFiles.trim().split("\n").filter(Boolean);
|
|
@@ -26789,10 +26955,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26789
26955
|
}
|
|
26790
26956
|
}
|
|
26791
26957
|
readFernignorePatterns() {
|
|
26792
|
-
const fernignorePath = (0,
|
|
26958
|
+
const fernignorePath = (0, import_path13.join)(this.outputDir, ".fernignore");
|
|
26793
26959
|
if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
|
|
26794
26960
|
return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
26795
26961
|
}
|
|
26962
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
26796
26963
|
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
26797
26964
|
const conflictResults = results.filter((r2) => r2.status === "conflict");
|
|
26798
26965
|
const conflictDetails = conflictResults.map((r2) => {
|
|
@@ -26847,10 +27014,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26847
27014
|
this.outputDir = outputDir;
|
|
26848
27015
|
}
|
|
26849
27016
|
fernignoreExists() {
|
|
26850
|
-
return (0, import_fs8.existsSync)((0,
|
|
27017
|
+
return (0, import_fs8.existsSync)((0, import_path15.join)(this.outputDir, ".fernignore"));
|
|
26851
27018
|
}
|
|
26852
27019
|
readFernignorePatterns() {
|
|
26853
|
-
const fernignorePath = (0,
|
|
27020
|
+
const fernignorePath = (0, import_path15.join)(this.outputDir, ".fernignore");
|
|
26854
27021
|
if (!(0, import_fs8.existsSync)(fernignorePath)) {
|
|
26855
27022
|
return [];
|
|
26856
27023
|
}
|
|
@@ -26959,7 +27126,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
26959
27126
|
return results;
|
|
26960
27127
|
}
|
|
26961
27128
|
readFileContent(filePath) {
|
|
26962
|
-
const fullPath = (0,
|
|
27129
|
+
const fullPath = (0, import_path15.join)(this.outputDir, filePath);
|
|
26963
27130
|
if (!(0, import_fs8.existsSync)(fullPath)) {
|
|
26964
27131
|
return null;
|
|
26965
27132
|
}
|
|
@@ -27024,7 +27191,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
27024
27191
|
};
|
|
27025
27192
|
}
|
|
27026
27193
|
movePatternsToReplayYml(patterns) {
|
|
27027
|
-
const replayYmlPath = (0,
|
|
27194
|
+
const replayYmlPath = (0, import_path15.join)(this.outputDir, ".fern", "replay.yml");
|
|
27028
27195
|
let config = {};
|
|
27029
27196
|
if ((0, import_fs8.existsSync)(replayYmlPath)) {
|
|
27030
27197
|
const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -27033,7 +27200,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
27033
27200
|
const existing = config.exclude ?? [];
|
|
27034
27201
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
27035
27202
|
config.exclude = merged;
|
|
27036
|
-
const dir = (0,
|
|
27203
|
+
const dir = (0, import_path15.dirname)(replayYmlPath);
|
|
27037
27204
|
if (!(0, import_fs8.existsSync)(dir)) {
|
|
27038
27205
|
(0, import_fs8.mkdirSync)(dir, { recursive: true });
|
|
27039
27206
|
}
|
|
@@ -29639,8 +29806,8 @@ function resolve(...paths) {
|
|
|
29639
29806
|
|
|
29640
29807
|
// src/cli.ts
|
|
29641
29808
|
var import_fs16 = __toESM(require("fs"));
|
|
29642
|
-
var
|
|
29643
|
-
var
|
|
29809
|
+
var import_promises18 = require("fs/promises");
|
|
29810
|
+
var import_path25 = __toESM(require("path"));
|
|
29644
29811
|
|
|
29645
29812
|
// ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
|
|
29646
29813
|
var YError = class _YError extends Error {
|
|
@@ -31607,15 +31774,18 @@ function parseRepository(githubRepository) {
|
|
|
31607
31774
|
githubRepository = githubRepository.slice(0, -4);
|
|
31608
31775
|
}
|
|
31609
31776
|
const parts = githubRepository.split("/");
|
|
31610
|
-
if (parts.length === 2 && parts[0]
|
|
31777
|
+
if (parts.length === 2 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1])) {
|
|
31611
31778
|
[owner, repo] = parts;
|
|
31612
|
-
} else if (parts.length === 3 && parts[0]
|
|
31779
|
+
} else if (parts.length === 3 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1]) && isNonEmptyPart(parts[2])) {
|
|
31613
31780
|
[remote, owner, repo] = parts;
|
|
31614
31781
|
} else {
|
|
31615
31782
|
throw new Error(`Failed to parse GitHub repository ${githubRepository}`);
|
|
31616
31783
|
}
|
|
31617
31784
|
return newRepositoryReference({ remote, owner, repo });
|
|
31618
31785
|
}
|
|
31786
|
+
function isNonEmptyPart(part) {
|
|
31787
|
+
return part != null && part.length > 0;
|
|
31788
|
+
}
|
|
31619
31789
|
function newRepositoryReference({ remote, owner, repo }) {
|
|
31620
31790
|
const repoUrl = getRepoUrl({ remote, owner, repo });
|
|
31621
31791
|
const cloneUrl = getCloneUrl({ remote, owner, repo });
|
|
@@ -39354,42 +39524,42 @@ function replayForget(params) {
|
|
|
39354
39524
|
// src/replay/replay-init.ts
|
|
39355
39525
|
init_dist3();
|
|
39356
39526
|
var import_fs11 = require("fs");
|
|
39357
|
-
var
|
|
39527
|
+
var import_path19 = require("path");
|
|
39358
39528
|
var import_tmp_promise2 = __toESM(require_tmp_promise());
|
|
39359
39529
|
|
|
39360
39530
|
// src/replay/fernignore.ts
|
|
39361
|
-
var
|
|
39362
|
-
var
|
|
39531
|
+
var import_promises10 = require("fs/promises");
|
|
39532
|
+
var import_path18 = require("path");
|
|
39363
39533
|
var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
39364
39534
|
var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
|
|
39365
39535
|
async function ensureReplayFernignoreEntries(outputDir) {
|
|
39366
|
-
const fernignorePath = (0,
|
|
39536
|
+
const fernignorePath = (0, import_path18.join)(outputDir, ".fernignore");
|
|
39367
39537
|
let content = "";
|
|
39368
39538
|
try {
|
|
39369
|
-
content = await (0,
|
|
39539
|
+
content = await (0, import_promises10.readFile)(fernignorePath, "utf-8");
|
|
39370
39540
|
} catch {
|
|
39371
39541
|
}
|
|
39372
39542
|
const lines = content.split("\n");
|
|
39373
39543
|
const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
39374
39544
|
if (toAdd.length > 0) {
|
|
39375
39545
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
39376
|
-
await (0,
|
|
39546
|
+
await (0, import_promises10.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
39377
39547
|
return true;
|
|
39378
39548
|
}
|
|
39379
39549
|
return false;
|
|
39380
39550
|
}
|
|
39381
39551
|
async function ensureGitattributesEntries2(outputDir) {
|
|
39382
|
-
const gitattributesPath = (0,
|
|
39552
|
+
const gitattributesPath = (0, import_path18.join)(outputDir, ".gitattributes");
|
|
39383
39553
|
let content = "";
|
|
39384
39554
|
try {
|
|
39385
|
-
content = await (0,
|
|
39555
|
+
content = await (0, import_promises10.readFile)(gitattributesPath, "utf-8");
|
|
39386
39556
|
} catch {
|
|
39387
39557
|
}
|
|
39388
39558
|
const lines = content.split("\n");
|
|
39389
39559
|
const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
39390
39560
|
if (toAdd.length > 0) {
|
|
39391
39561
|
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
39392
|
-
await (0,
|
|
39562
|
+
await (0, import_promises10.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
39393
39563
|
}
|
|
39394
39564
|
}
|
|
39395
39565
|
|
|
@@ -39416,7 +39586,7 @@ async function replayInit(params) {
|
|
|
39416
39586
|
if (dryRun) {
|
|
39417
39587
|
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
|
|
39418
39588
|
}
|
|
39419
|
-
const lockfilePath = (0,
|
|
39589
|
+
const lockfilePath = (0, import_path19.join)(repoPath, ".fern", "replay.lock");
|
|
39420
39590
|
const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
39421
39591
|
return {
|
|
39422
39592
|
bootstrap: bootstrapResult,
|
|
@@ -39539,7 +39709,7 @@ init_dist3();
|
|
|
39539
39709
|
init_dist3();
|
|
39540
39710
|
var import_child_process4 = require("child_process");
|
|
39541
39711
|
var import_fs12 = require("fs");
|
|
39542
|
-
var
|
|
39712
|
+
var import_path20 = require("path");
|
|
39543
39713
|
async function replayPrepare(params, state) {
|
|
39544
39714
|
const {
|
|
39545
39715
|
outputDir,
|
|
@@ -39550,7 +39720,7 @@ async function replayPrepare(params, state) {
|
|
|
39550
39720
|
skipApplication,
|
|
39551
39721
|
logger
|
|
39552
39722
|
} = params;
|
|
39553
|
-
const lockfilePath = (0,
|
|
39723
|
+
const lockfilePath = (0, import_path20.join)(outputDir, ".fern", "replay.lock");
|
|
39554
39724
|
let autoBootstrapped = false;
|
|
39555
39725
|
let bootstrapAttempted = false;
|
|
39556
39726
|
if (!(0, import_fs12.existsSync)(lockfilePath)) {
|
|
@@ -39804,29 +39974,29 @@ function replayStatus(params) {
|
|
|
39804
39974
|
}
|
|
39805
39975
|
|
|
39806
39976
|
// src/configuration/loadGitHubConfig.ts
|
|
39807
|
-
var
|
|
39977
|
+
var import_promises11 = require("fs/promises");
|
|
39808
39978
|
async function loadGitHubConfig({
|
|
39809
39979
|
absolutePathToConfig
|
|
39810
39980
|
}) {
|
|
39811
|
-
const rawContents = await (0,
|
|
39981
|
+
const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
|
|
39812
39982
|
return JSON.parse(rawContents);
|
|
39813
39983
|
}
|
|
39814
39984
|
|
|
39815
39985
|
// src/configuration/loadReadmeConfig.ts
|
|
39816
|
-
var
|
|
39986
|
+
var import_promises12 = require("fs/promises");
|
|
39817
39987
|
async function loadReadmeConfig({
|
|
39818
39988
|
absolutePathToConfig
|
|
39819
39989
|
}) {
|
|
39820
|
-
const rawContents = await (0,
|
|
39990
|
+
const rawContents = await (0, import_promises12.readFile)(absolutePathToConfig, "utf8");
|
|
39821
39991
|
return JSON.parse(rawContents);
|
|
39822
39992
|
}
|
|
39823
39993
|
|
|
39824
39994
|
// src/configuration/loadReferenceConfig.ts
|
|
39825
|
-
var
|
|
39995
|
+
var import_promises13 = require("fs/promises");
|
|
39826
39996
|
async function loadReferenceConfig({
|
|
39827
39997
|
absolutePathToConfig
|
|
39828
39998
|
}) {
|
|
39829
|
-
const rawContents = await (0,
|
|
39999
|
+
const rawContents = await (0, import_promises13.readFile)(absolutePathToConfig, "utf8");
|
|
39830
40000
|
return JSON.parse(rawContents);
|
|
39831
40001
|
}
|
|
39832
40002
|
|
|
@@ -39853,8 +40023,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
|
|
|
39853
40023
|
// src/pipeline/steps/AutoVersionStep.ts
|
|
39854
40024
|
var import_child_process5 = require("child_process");
|
|
39855
40025
|
var import_fs14 = require("fs");
|
|
39856
|
-
var
|
|
39857
|
-
var
|
|
40026
|
+
var import_promises15 = require("fs/promises");
|
|
40027
|
+
var import_path22 = require("path");
|
|
39858
40028
|
|
|
39859
40029
|
// ../commons/logging-execa/lib/loggingExeca.js
|
|
39860
40030
|
var import_execa = __toESM(require_execa(), 1);
|
|
@@ -39911,10 +40081,136 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
|
|
|
39911
40081
|
return result;
|
|
39912
40082
|
}
|
|
39913
40083
|
|
|
40084
|
+
// ../cli/task-context/lib/CliError.js
|
|
40085
|
+
var CliError = class _CliError extends Error {
|
|
40086
|
+
code;
|
|
40087
|
+
docsLink;
|
|
40088
|
+
constructor({ message, code, docsLink }) {
|
|
40089
|
+
super(message);
|
|
40090
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
40091
|
+
if (Error.captureStackTrace) {
|
|
40092
|
+
Error.captureStackTrace(this, this.constructor);
|
|
40093
|
+
}
|
|
40094
|
+
this.name = this.constructor.name;
|
|
40095
|
+
this.code = code;
|
|
40096
|
+
this.docsLink = docsLink;
|
|
40097
|
+
}
|
|
40098
|
+
static authRequired(message) {
|
|
40099
|
+
return new _CliError({
|
|
40100
|
+
message: message ?? "Authentication required. Please run 'fern login' or set the FERN_TOKEN environment variable.",
|
|
40101
|
+
code: _CliError.Code.AuthError
|
|
40102
|
+
});
|
|
40103
|
+
}
|
|
40104
|
+
static unauthorized(message) {
|
|
40105
|
+
return new _CliError({
|
|
40106
|
+
message: message ?? "Unauthorized. Please run 'fern auth login' or set the FERN_TOKEN environment variable.",
|
|
40107
|
+
code: _CliError.Code.AuthError
|
|
40108
|
+
});
|
|
40109
|
+
}
|
|
40110
|
+
static notFound(message) {
|
|
40111
|
+
return new _CliError({ message, code: _CliError.Code.ConfigError });
|
|
40112
|
+
}
|
|
40113
|
+
static badRequest(message) {
|
|
40114
|
+
return new _CliError({ message, code: _CliError.Code.NetworkError });
|
|
40115
|
+
}
|
|
40116
|
+
static validationError(message) {
|
|
40117
|
+
return new _CliError({ message, code: _CliError.Code.ValidationError });
|
|
40118
|
+
}
|
|
40119
|
+
static internalError(message) {
|
|
40120
|
+
return new _CliError({ message, code: _CliError.Code.InternalError });
|
|
40121
|
+
}
|
|
40122
|
+
};
|
|
40123
|
+
(function(CliError2) {
|
|
40124
|
+
CliError2.Code = {
|
|
40125
|
+
InternalError: "INTERNAL_ERROR",
|
|
40126
|
+
ResolutionError: "RESOLUTION_ERROR",
|
|
40127
|
+
IrConversionError: "IR_CONVERSION_ERROR",
|
|
40128
|
+
ContainerError: "CONTAINER_ERROR",
|
|
40129
|
+
VersionError: "VERSION_ERROR",
|
|
40130
|
+
ParseError: "PARSE_ERROR",
|
|
40131
|
+
EnvironmentError: "ENVIRONMENT_ERROR",
|
|
40132
|
+
ReferenceError: "REFERENCE_ERROR",
|
|
40133
|
+
ValidationError: "VALIDATION_ERROR",
|
|
40134
|
+
NetworkError: "NETWORK_ERROR",
|
|
40135
|
+
AuthError: "AUTH_ERROR",
|
|
40136
|
+
ConfigError: "CONFIG_ERROR",
|
|
40137
|
+
UserError: "USER_ERROR"
|
|
40138
|
+
};
|
|
40139
|
+
})(CliError || (CliError = {}));
|
|
40140
|
+
var SENTRY_REPORTABLE = {
|
|
40141
|
+
[CliError.Code.InternalError]: true,
|
|
40142
|
+
[CliError.Code.ResolutionError]: true,
|
|
40143
|
+
[CliError.Code.IrConversionError]: true,
|
|
40144
|
+
[CliError.Code.ContainerError]: false,
|
|
40145
|
+
[CliError.Code.VersionError]: false,
|
|
40146
|
+
[CliError.Code.ParseError]: false,
|
|
40147
|
+
[CliError.Code.EnvironmentError]: false,
|
|
40148
|
+
[CliError.Code.ReferenceError]: false,
|
|
40149
|
+
[CliError.Code.ValidationError]: false,
|
|
40150
|
+
[CliError.Code.NetworkError]: false,
|
|
40151
|
+
[CliError.Code.AuthError]: false,
|
|
40152
|
+
[CliError.Code.ConfigError]: false,
|
|
40153
|
+
[CliError.Code.UserError]: false
|
|
40154
|
+
};
|
|
40155
|
+
|
|
40156
|
+
// ../cli/logger/lib/LogLevel.js
|
|
40157
|
+
var LogLevel2 = {
|
|
40158
|
+
Trace: "trace",
|
|
40159
|
+
Debug: "debug",
|
|
40160
|
+
Info: "info",
|
|
40161
|
+
Warn: "warn",
|
|
40162
|
+
Error: "error"
|
|
40163
|
+
};
|
|
40164
|
+
var LOG_LEVELS = Object.values(LogLevel2);
|
|
40165
|
+
|
|
40166
|
+
// ../cli/logger/lib/createLogger.js
|
|
40167
|
+
var LoggerImpl = class {
|
|
40168
|
+
log;
|
|
40169
|
+
enabled;
|
|
40170
|
+
constructor(log, enabled = true) {
|
|
40171
|
+
this.log = log;
|
|
40172
|
+
this.enabled = enabled;
|
|
40173
|
+
}
|
|
40174
|
+
disable() {
|
|
40175
|
+
this.enabled = false;
|
|
40176
|
+
}
|
|
40177
|
+
enable() {
|
|
40178
|
+
this.enabled = true;
|
|
40179
|
+
}
|
|
40180
|
+
debug(...args) {
|
|
40181
|
+
if (this.enabled) {
|
|
40182
|
+
this.log(LogLevel2.Debug, ...args);
|
|
40183
|
+
}
|
|
40184
|
+
}
|
|
40185
|
+
info(...args) {
|
|
40186
|
+
if (this.enabled) {
|
|
40187
|
+
this.log(LogLevel2.Info, ...args);
|
|
40188
|
+
}
|
|
40189
|
+
}
|
|
40190
|
+
warn(...args) {
|
|
40191
|
+
if (this.enabled) {
|
|
40192
|
+
this.log(LogLevel2.Warn, ...args);
|
|
40193
|
+
}
|
|
40194
|
+
}
|
|
40195
|
+
error(...args) {
|
|
40196
|
+
if (this.enabled) {
|
|
40197
|
+
this.log(LogLevel2.Error, ...args);
|
|
40198
|
+
}
|
|
40199
|
+
}
|
|
40200
|
+
trace(...args) {
|
|
40201
|
+
if (this.enabled) {
|
|
40202
|
+
this.log(LogLevel2.Trace, ...args);
|
|
40203
|
+
}
|
|
40204
|
+
}
|
|
40205
|
+
};
|
|
40206
|
+
function createLogger4(log) {
|
|
40207
|
+
return new LoggerImpl(log);
|
|
40208
|
+
}
|
|
40209
|
+
|
|
39914
40210
|
// src/autoversion/AutoVersioningService.ts
|
|
39915
40211
|
var import_fs13 = require("fs");
|
|
39916
|
-
var
|
|
39917
|
-
var
|
|
40212
|
+
var import_promises14 = require("fs/promises");
|
|
40213
|
+
var import_path21 = require("path");
|
|
39918
40214
|
var import_semver = __toESM(require_semver2());
|
|
39919
40215
|
var AutoVersioningException = class extends Error {
|
|
39920
40216
|
/**
|
|
@@ -40577,10 +40873,17 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40577
40873
|
} else {
|
|
40578
40874
|
command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
|
|
40579
40875
|
}
|
|
40580
|
-
|
|
40581
|
-
|
|
40582
|
-
|
|
40583
|
-
|
|
40876
|
+
try {
|
|
40877
|
+
await loggingExeca(this.logger, "bash", ["-c", command], {
|
|
40878
|
+
cwd: workingDirectory,
|
|
40879
|
+
doNotPipeOutput: true
|
|
40880
|
+
});
|
|
40881
|
+
} catch (error) {
|
|
40882
|
+
throw new CliError({
|
|
40883
|
+
message: `Failed to replace placeholder version in generated files: ${extractErrorMessage(error)}`,
|
|
40884
|
+
code: CliError.Code.UserError
|
|
40885
|
+
});
|
|
40886
|
+
}
|
|
40584
40887
|
this.logger.debug("Placeholder version replaced successfully");
|
|
40585
40888
|
}
|
|
40586
40889
|
/**
|
|
@@ -40601,7 +40904,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40601
40904
|
);
|
|
40602
40905
|
return;
|
|
40603
40906
|
}
|
|
40604
|
-
const goModPath = (0,
|
|
40907
|
+
const goModPath = (0, import_path21.join)(workingDirectory, "go.mod");
|
|
40605
40908
|
if (!(0, import_fs13.existsSync)(goModPath)) {
|
|
40606
40909
|
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
40607
40910
|
return;
|
|
@@ -40635,7 +40938,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40635
40938
|
* Reads the module path from a go.mod file.
|
|
40636
40939
|
*/
|
|
40637
40940
|
async readGoModulePath(goModPath) {
|
|
40638
|
-
const content = await (0,
|
|
40941
|
+
const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
|
|
40639
40942
|
for (const line of content.split("\n")) {
|
|
40640
40943
|
const trimmed2 = line.trim();
|
|
40641
40944
|
if (trimmed2.startsWith("module ")) {
|
|
@@ -40648,7 +40951,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40648
40951
|
* Adds a version suffix to the module line in go.mod.
|
|
40649
40952
|
*/
|
|
40650
40953
|
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
40651
|
-
const content = await (0,
|
|
40954
|
+
const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
|
|
40652
40955
|
const lines = content.split("\n");
|
|
40653
40956
|
let changed = false;
|
|
40654
40957
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
@@ -40660,7 +40963,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40660
40963
|
}
|
|
40661
40964
|
}
|
|
40662
40965
|
if (changed) {
|
|
40663
|
-
await (0,
|
|
40966
|
+
await (0, import_promises14.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
40664
40967
|
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
40665
40968
|
}
|
|
40666
40969
|
}
|
|
@@ -40669,10 +40972,10 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40669
40972
|
*/
|
|
40670
40973
|
async walkDirectory(dir, filter2) {
|
|
40671
40974
|
const results = [];
|
|
40672
|
-
const entries2 = await (0,
|
|
40975
|
+
const entries2 = await (0, import_promises14.readdir)(dir);
|
|
40673
40976
|
for (const entry of entries2) {
|
|
40674
|
-
const fullPath = (0,
|
|
40675
|
-
const entryStat = await (0,
|
|
40977
|
+
const fullPath = (0, import_path21.join)(dir, entry);
|
|
40978
|
+
const entryStat = await (0, import_promises14.stat)(fullPath);
|
|
40676
40979
|
if (entryStat.isDirectory()) {
|
|
40677
40980
|
if (entry === ".git" || entry === "vendor") {
|
|
40678
40981
|
continue;
|
|
@@ -40689,7 +40992,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40689
40992
|
* Adds a version suffix to import paths in all .go files in the repository.
|
|
40690
40993
|
*/
|
|
40691
40994
|
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
40692
|
-
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0,
|
|
40995
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path21.extname)(filePath) === ".go");
|
|
40693
40996
|
for (const filePath of goFiles) {
|
|
40694
40997
|
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
40695
40998
|
}
|
|
@@ -40699,7 +41002,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40699
41002
|
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
40700
41003
|
*/
|
|
40701
41004
|
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
40702
|
-
const content = await (0,
|
|
41005
|
+
const content = await (0, import_promises14.readFile)(filePath, "utf-8");
|
|
40703
41006
|
const lines = content.split("\n");
|
|
40704
41007
|
let changed = false;
|
|
40705
41008
|
let inImportBlock = false;
|
|
@@ -40728,7 +41031,7 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
40728
41031
|
}
|
|
40729
41032
|
}
|
|
40730
41033
|
if (changed) {
|
|
40731
|
-
await (0,
|
|
41034
|
+
await (0, import_promises14.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
40732
41035
|
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
40733
41036
|
}
|
|
40734
41037
|
}
|
|
@@ -41134,7 +41437,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
41134
41437
|
}
|
|
41135
41438
|
async prependChangelogEntry(params) {
|
|
41136
41439
|
const { version: version2, entry } = params;
|
|
41137
|
-
const changelogPath = (0,
|
|
41440
|
+
const changelogPath = (0, import_path22.join)(this.outputDir, "changelog.md");
|
|
41138
41441
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
41139
41442
|
const header = `## [${version2}] - ${now}
|
|
41140
41443
|
`;
|
|
@@ -41143,7 +41446,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
|
41143
41446
|
`;
|
|
41144
41447
|
let existing = "";
|
|
41145
41448
|
if ((0, import_fs14.existsSync)(changelogPath)) {
|
|
41146
|
-
existing = await (0,
|
|
41449
|
+
existing = await (0, import_promises15.readFile)(changelogPath, "utf-8");
|
|
41147
41450
|
}
|
|
41148
41451
|
let output;
|
|
41149
41452
|
if (existing.trim().length === 0) {
|
|
@@ -41160,7 +41463,7 @@ ${newBlock}${remainder}`;
|
|
|
41160
41463
|
} else {
|
|
41161
41464
|
output = `${newBlock}${existing}`;
|
|
41162
41465
|
}
|
|
41163
|
-
await (0,
|
|
41466
|
+
await (0, import_promises15.writeFile)(changelogPath, output, "utf-8");
|
|
41164
41467
|
}
|
|
41165
41468
|
brandMessage(message) {
|
|
41166
41469
|
if (this.config.isWhitelabel) {
|
|
@@ -41383,8 +41686,8 @@ function tryRevParse(cwd2, rev) {
|
|
|
41383
41686
|
}
|
|
41384
41687
|
|
|
41385
41688
|
// src/pipeline/steps/GithubStep.ts
|
|
41386
|
-
var
|
|
41387
|
-
var
|
|
41689
|
+
var import_promises16 = require("fs/promises");
|
|
41690
|
+
var import_path23 = require("path");
|
|
41388
41691
|
|
|
41389
41692
|
// src/pipeline/github/createReplayBranch.ts
|
|
41390
41693
|
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
@@ -41531,6 +41834,7 @@ async function pushSignedCommit({
|
|
|
41531
41834
|
branch,
|
|
41532
41835
|
force,
|
|
41533
41836
|
rebaseOnConflict = false,
|
|
41837
|
+
author,
|
|
41534
41838
|
logger
|
|
41535
41839
|
}) {
|
|
41536
41840
|
const tempRef = `refs/temp/fern-${Date.now()}`;
|
|
@@ -41545,12 +41849,18 @@ async function pushSignedCommit({
|
|
|
41545
41849
|
await repository.pushObjectToRef(localHeadSha, tempRef);
|
|
41546
41850
|
tempRefPushed = true;
|
|
41547
41851
|
for (let attempt2 = 0; attempt2 < MAX_CONCURRENT_PUSH_RETRIES; attempt2++) {
|
|
41852
|
+
const commitAuthor = {
|
|
41853
|
+
name: author?.name ?? FERN_BOT_NAME2,
|
|
41854
|
+
email: author?.email ?? FERN_BOT_EMAIL2
|
|
41855
|
+
};
|
|
41548
41856
|
const { data: signedCommit } = await octokit.git.createCommit({
|
|
41549
41857
|
owner,
|
|
41550
41858
|
repo,
|
|
41551
41859
|
message,
|
|
41552
41860
|
tree: treeSha,
|
|
41553
|
-
parents
|
|
41861
|
+
parents,
|
|
41862
|
+
author: commitAuthor,
|
|
41863
|
+
committer: commitAuthor
|
|
41554
41864
|
});
|
|
41555
41865
|
const signedSha = signedCommit.sha;
|
|
41556
41866
|
try {
|
|
@@ -41876,6 +42186,7 @@ var GithubStep = class extends BaseStep {
|
|
|
41876
42186
|
repo,
|
|
41877
42187
|
branch: prBranch,
|
|
41878
42188
|
force: isUpdatingExistingPR,
|
|
42189
|
+
author: this.config.author,
|
|
41879
42190
|
logger: this.logger
|
|
41880
42191
|
});
|
|
41881
42192
|
const pushedBranch = await repository.getCurrentBranch();
|
|
@@ -41998,6 +42309,7 @@ var GithubStep = class extends BaseStep {
|
|
|
41998
42309
|
branch: baseBranch,
|
|
41999
42310
|
force: false,
|
|
42000
42311
|
rebaseOnConflict: true,
|
|
42312
|
+
author: this.config.author,
|
|
42001
42313
|
logger: this.logger
|
|
42002
42314
|
});
|
|
42003
42315
|
const pushedBranch = await repository.getCurrentBranch();
|
|
@@ -42015,13 +42327,13 @@ var GithubStep = class extends BaseStep {
|
|
|
42015
42327
|
}
|
|
42016
42328
|
async ensureFernignore() {
|
|
42017
42329
|
this.logger.debug("Checking for .fernignore file...");
|
|
42018
|
-
const fernignorePath = (0,
|
|
42330
|
+
const fernignorePath = (0, import_path23.join)(this.outputDir, ".fernignore");
|
|
42019
42331
|
try {
|
|
42020
|
-
await (0,
|
|
42332
|
+
await (0, import_promises16.access)(fernignorePath);
|
|
42021
42333
|
this.logger.debug(".fernignore already exists");
|
|
42022
42334
|
} catch {
|
|
42023
42335
|
this.logger.debug("Creating .fernignore file...");
|
|
42024
|
-
await (0,
|
|
42336
|
+
await (0, import_promises16.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
42025
42337
|
}
|
|
42026
42338
|
}
|
|
42027
42339
|
deriveSkipCommit(replayResult) {
|
|
@@ -42232,7 +42544,7 @@ var ReplayStep = class extends BaseStep {
|
|
|
42232
42544
|
};
|
|
42233
42545
|
|
|
42234
42546
|
// ../cli/generation/local-generation/docker-utils/lib/runDocker.js
|
|
42235
|
-
var
|
|
42547
|
+
var import_promises17 = require("fs/promises");
|
|
42236
42548
|
var import_tmp_promise3 = __toESM(require_tmp_promise(), 1);
|
|
42237
42549
|
async function pullImage(imageName, runner, signal) {
|
|
42238
42550
|
await loggingExeca(void 0, runner ?? "docker", ["pull", imageName], {
|
|
@@ -42279,7 +42591,7 @@ async function execInContainer({ logger, containerId, command, runner, writeLogs
|
|
|
42279
42591
|
const logs = stdout + stderr;
|
|
42280
42592
|
if (writeLogsToFile) {
|
|
42281
42593
|
const tmpFile = await import_tmp_promise3.default.file();
|
|
42282
|
-
await (0,
|
|
42594
|
+
await (0, import_promises17.writeFile)(tmpFile.path, logs);
|
|
42283
42595
|
logger.info(`Generator logs here: ${tmpFile.path}`);
|
|
42284
42596
|
}
|
|
42285
42597
|
const resolvedExitCode = exitCode ?? 1;
|
|
@@ -42310,63 +42622,9 @@ async function stopContainer({ logger, containerId, runner }) {
|
|
|
42310
42622
|
});
|
|
42311
42623
|
}
|
|
42312
42624
|
|
|
42313
|
-
// ../cli/logger/lib/LogLevel.js
|
|
42314
|
-
var LogLevel2 = {
|
|
42315
|
-
Trace: "trace",
|
|
42316
|
-
Debug: "debug",
|
|
42317
|
-
Info: "info",
|
|
42318
|
-
Warn: "warn",
|
|
42319
|
-
Error: "error"
|
|
42320
|
-
};
|
|
42321
|
-
var LOG_LEVELS = Object.values(LogLevel2);
|
|
42322
|
-
|
|
42323
|
-
// ../cli/logger/lib/createLogger.js
|
|
42324
|
-
var LoggerImpl = class {
|
|
42325
|
-
log;
|
|
42326
|
-
enabled;
|
|
42327
|
-
constructor(log, enabled = true) {
|
|
42328
|
-
this.log = log;
|
|
42329
|
-
this.enabled = enabled;
|
|
42330
|
-
}
|
|
42331
|
-
disable() {
|
|
42332
|
-
this.enabled = false;
|
|
42333
|
-
}
|
|
42334
|
-
enable() {
|
|
42335
|
-
this.enabled = true;
|
|
42336
|
-
}
|
|
42337
|
-
debug(...args) {
|
|
42338
|
-
if (this.enabled) {
|
|
42339
|
-
this.log(LogLevel2.Debug, ...args);
|
|
42340
|
-
}
|
|
42341
|
-
}
|
|
42342
|
-
info(...args) {
|
|
42343
|
-
if (this.enabled) {
|
|
42344
|
-
this.log(LogLevel2.Info, ...args);
|
|
42345
|
-
}
|
|
42346
|
-
}
|
|
42347
|
-
warn(...args) {
|
|
42348
|
-
if (this.enabled) {
|
|
42349
|
-
this.log(LogLevel2.Warn, ...args);
|
|
42350
|
-
}
|
|
42351
|
-
}
|
|
42352
|
-
error(...args) {
|
|
42353
|
-
if (this.enabled) {
|
|
42354
|
-
this.log(LogLevel2.Error, ...args);
|
|
42355
|
-
}
|
|
42356
|
-
}
|
|
42357
|
-
trace(...args) {
|
|
42358
|
-
if (this.enabled) {
|
|
42359
|
-
this.log(LogLevel2.Trace, ...args);
|
|
42360
|
-
}
|
|
42361
|
-
}
|
|
42362
|
-
};
|
|
42363
|
-
function createLogger4(log) {
|
|
42364
|
-
return new LoggerImpl(log);
|
|
42365
|
-
}
|
|
42366
|
-
|
|
42367
42625
|
// src/pipeline/steps/VerificationStep.ts
|
|
42368
42626
|
var import_fs15 = require("fs");
|
|
42369
|
-
var
|
|
42627
|
+
var import_path24 = require("path");
|
|
42370
42628
|
var CONTAINER_WORKSPACE_PATH = "/workspace";
|
|
42371
42629
|
var VERIFY_SCRIPT_RELATIVE_PATH = ".fern/verify.sh";
|
|
42372
42630
|
var VERIFY_SCRIPT_CONTAINER_PATH = `${CONTAINER_WORKSPACE_PATH}/${VERIFY_SCRIPT_RELATIVE_PATH}`;
|
|
@@ -42382,7 +42640,7 @@ var VerificationStep = class extends BaseStep {
|
|
|
42382
42640
|
generatorVersions;
|
|
42383
42641
|
name = "verify";
|
|
42384
42642
|
async execute(_context) {
|
|
42385
|
-
const verifyScriptHostPath = (0,
|
|
42643
|
+
const verifyScriptHostPath = (0, import_path24.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
|
|
42386
42644
|
if (!(0, import_fs15.existsSync)(verifyScriptHostPath)) {
|
|
42387
42645
|
return { executed: true, success: true, skipped: true };
|
|
42388
42646
|
}
|
|
@@ -42655,7 +42913,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
42655
42913
|
const readmeConfig = await loadReadmeConfig({
|
|
42656
42914
|
absolutePathToConfig: resolve(wd, argv.config)
|
|
42657
42915
|
});
|
|
42658
|
-
const originalReadmeContent = argv.originalReadme != null ? await (0,
|
|
42916
|
+
const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises18.readFile)(argv.originalReadme, "utf8") : void 0;
|
|
42659
42917
|
await generateReadmeToStream({
|
|
42660
42918
|
originalReadmeContent,
|
|
42661
42919
|
readmeConfig,
|
|
@@ -42783,7 +43041,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
|
|
|
42783
43041
|
try {
|
|
42784
43042
|
const wd = cwd();
|
|
42785
43043
|
const configValue = argv.config;
|
|
42786
|
-
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0,
|
|
43044
|
+
const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises18.readFile)(resolve(wd, configValue), "utf-8");
|
|
42787
43045
|
let config;
|
|
42788
43046
|
try {
|
|
42789
43047
|
config = JSON.parse(configContent);
|
|
@@ -43079,7 +43337,7 @@ async function createWriteStream(outputPath) {
|
|
|
43079
43337
|
}
|
|
43080
43338
|
async function createWriteStreamFromFile(filepath) {
|
|
43081
43339
|
if (!await doesPathExist(filepath)) {
|
|
43082
|
-
await (0,
|
|
43340
|
+
await (0, import_promises18.mkdir)(import_path25.default.dirname(filepath), { recursive: true });
|
|
43083
43341
|
}
|
|
43084
43342
|
return import_fs16.default.createWriteStream(filepath);
|
|
43085
43343
|
}
|