@fern-api/replay 0.16.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +80 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +80 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +80 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -963,6 +963,22 @@ var ReplayDetector = class {
|
|
|
963
963
|
return commit.sha;
|
|
964
964
|
}
|
|
965
965
|
}
|
|
966
|
+
let fullLog;
|
|
967
|
+
try {
|
|
968
|
+
fullLog = await this.git.exec([
|
|
969
|
+
"log",
|
|
970
|
+
"--no-merges",
|
|
971
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
972
|
+
"HEAD"
|
|
973
|
+
]);
|
|
974
|
+
} catch {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
for (const commit of this.parseGitLog(fullLog)) {
|
|
978
|
+
if (isGenerationBoundary(commit)) {
|
|
979
|
+
return commit.sha;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
966
982
|
return null;
|
|
967
983
|
}
|
|
968
984
|
async detectNewPatches() {
|
|
@@ -974,6 +990,11 @@ var ReplayDetector = class {
|
|
|
974
990
|
}
|
|
975
991
|
const lastGen = this.getLastGeneration(lock);
|
|
976
992
|
if (!lastGen) {
|
|
993
|
+
if (lock.current_generation) {
|
|
994
|
+
this.warnings.push(
|
|
995
|
+
`No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
977
998
|
return { patches: [], revertedPatchIds: [] };
|
|
978
999
|
}
|
|
979
1000
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
@@ -1728,16 +1749,20 @@ async function resolveInputs(args) {
|
|
|
1728
1749
|
const oursPath = (0, import_node_path3.join)(outputDir, resolvedPath);
|
|
1729
1750
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
1730
1751
|
let ghostTheirs = null;
|
|
1731
|
-
|
|
1752
|
+
let baseTreeUnreachable = false;
|
|
1753
|
+
if (!base && !renameSourcePath) {
|
|
1732
1754
|
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
1733
1755
|
if (!treeReachable) {
|
|
1734
|
-
|
|
1735
|
-
if (
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1756
|
+
baseTreeUnreachable = true;
|
|
1757
|
+
if (ours) {
|
|
1758
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
1759
|
+
if (fileDiff) {
|
|
1760
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
1761
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
1762
|
+
if (result) {
|
|
1763
|
+
base = result.base;
|
|
1764
|
+
ghostTheirs = result.theirs;
|
|
1765
|
+
}
|
|
1741
1766
|
}
|
|
1742
1767
|
}
|
|
1743
1768
|
}
|
|
@@ -1749,7 +1774,11 @@ async function resolveInputs(args) {
|
|
|
1749
1774
|
ours,
|
|
1750
1775
|
oursPath,
|
|
1751
1776
|
renameSourcePath,
|
|
1752
|
-
ghostTheirs
|
|
1777
|
+
ghostTheirs,
|
|
1778
|
+
// Only meaningful when ghost reconstruction did NOT recover a base; if it
|
|
1779
|
+
// did, `base` is non-null and the merge proceeds normally. We still set
|
|
1780
|
+
// the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
|
|
1781
|
+
baseTreeUnreachable
|
|
1753
1782
|
};
|
|
1754
1783
|
}
|
|
1755
1784
|
|
|
@@ -1932,7 +1961,11 @@ async function buildMergePlan(args) {
|
|
|
1932
1961
|
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
1933
1962
|
}
|
|
1934
1963
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
1935
|
-
return {
|
|
1964
|
+
return {
|
|
1965
|
+
kind: "new-file-both",
|
|
1966
|
+
theirs: effective_theirs,
|
|
1967
|
+
...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
|
|
1968
|
+
};
|
|
1936
1969
|
}
|
|
1937
1970
|
if (effective_theirs == null) {
|
|
1938
1971
|
return { kind: "skipped", reason: "missing-content" };
|
|
@@ -1980,17 +2013,19 @@ async function runMergeAndRecord(args) {
|
|
|
1980
2013
|
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
1981
2014
|
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
|
|
1982
2015
|
await (0, import_promises2.writeFile)(oursPath, merged2.content);
|
|
2016
|
+
const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
|
|
1983
2017
|
if (merged2.hasConflicts) {
|
|
1984
2018
|
return {
|
|
1985
2019
|
file: resolvedPath,
|
|
1986
2020
|
status: "conflict",
|
|
1987
2021
|
conflicts: merged2.conflicts,
|
|
1988
2022
|
conflictReason: "new-file-both",
|
|
1989
|
-
conflictMetadata: metadata
|
|
2023
|
+
conflictMetadata: metadata,
|
|
2024
|
+
...unreachableFlag
|
|
1990
2025
|
};
|
|
1991
2026
|
}
|
|
1992
2027
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
1993
|
-
return { file: resolvedPath, status: "merged" };
|
|
2028
|
+
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
1994
2029
|
}
|
|
1995
2030
|
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
1996
2031
|
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
|
|
@@ -3206,6 +3241,7 @@ var NoPatchesFlow = class {
|
|
|
3206
3241
|
results = await this.service.applicator.applyPatches(newPatches);
|
|
3207
3242
|
this.service._lastApplyResults = results;
|
|
3208
3243
|
this.service.recordDroppedContextLineWarnings(results);
|
|
3244
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
3209
3245
|
this.service.revertConflictingFiles(results);
|
|
3210
3246
|
for (const result of results) {
|
|
3211
3247
|
if (result.status === "conflict") {
|
|
@@ -3361,6 +3397,7 @@ var NormalRegenerationFlow = class {
|
|
|
3361
3397
|
const results = await this.service.applicator.applyPatches(allPatches);
|
|
3362
3398
|
this.service._lastApplyResults = results;
|
|
3363
3399
|
this.service.recordDroppedContextLineWarnings(results);
|
|
3400
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
3364
3401
|
this.service.revertConflictingFiles(results);
|
|
3365
3402
|
for (const result of results) {
|
|
3366
3403
|
if (result.status === "conflict") {
|
|
@@ -4280,6 +4317,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4280
4317
|
}
|
|
4281
4318
|
}
|
|
4282
4319
|
}
|
|
4320
|
+
/**
|
|
4321
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
4322
|
+
* customization's BASE could not be read because the patch's
|
|
4323
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
4324
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
4325
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
4326
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
4327
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
4328
|
+
* likely cause (insufficient clone window).
|
|
4329
|
+
*
|
|
4330
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
4331
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
4332
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
4333
|
+
*/
|
|
4334
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4335
|
+
recordUnreachableBaseWarnings(results) {
|
|
4336
|
+
const warned = /* @__PURE__ */ new Set();
|
|
4337
|
+
for (const result of results) {
|
|
4338
|
+
if (!result.fileResults) continue;
|
|
4339
|
+
for (const fr of result.fileResults) {
|
|
4340
|
+
if (!fr.baseTreeUnreachable) continue;
|
|
4341
|
+
if (warned.has(fr.file)) continue;
|
|
4342
|
+
warned.add(fr.file);
|
|
4343
|
+
const base = fr.conflictMetadata?.baseGeneration;
|
|
4344
|
+
const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
|
|
4345
|
+
this.warnings.push(
|
|
4346
|
+
`${fr.file}: could not read the prior generation's version of this file${baseRef} because that generation's tree is outside the clone window (insufficient shallow-clone depth or pruned history). The customization was treated as a new file, which can drop your edits silently. Re-run with the prior generation in the clone window \u2014 e.g. \`git fetch --shallow-since=<date just before the prior [fern-generated] commit>\` or \`git fetch --unshallow\` \u2014 then re-run replay.`
|
|
4347
|
+
);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4283
4351
|
/**
|
|
4284
4352
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
4285
4353
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|