@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/cli.cjs
CHANGED
|
@@ -15081,6 +15081,22 @@ var ReplayDetector = class {
|
|
|
15081
15081
|
return commit.sha;
|
|
15082
15082
|
}
|
|
15083
15083
|
}
|
|
15084
|
+
let fullLog;
|
|
15085
|
+
try {
|
|
15086
|
+
fullLog = await this.git.exec([
|
|
15087
|
+
"log",
|
|
15088
|
+
"--no-merges",
|
|
15089
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
15090
|
+
"HEAD"
|
|
15091
|
+
]);
|
|
15092
|
+
} catch {
|
|
15093
|
+
return null;
|
|
15094
|
+
}
|
|
15095
|
+
for (const commit of this.parseGitLog(fullLog)) {
|
|
15096
|
+
if (isGenerationBoundary(commit)) {
|
|
15097
|
+
return commit.sha;
|
|
15098
|
+
}
|
|
15099
|
+
}
|
|
15084
15100
|
return null;
|
|
15085
15101
|
}
|
|
15086
15102
|
async detectNewPatches() {
|
|
@@ -15092,6 +15108,11 @@ var ReplayDetector = class {
|
|
|
15092
15108
|
}
|
|
15093
15109
|
const lastGen = this.getLastGeneration(lock);
|
|
15094
15110
|
if (!lastGen) {
|
|
15111
|
+
if (lock.current_generation) {
|
|
15112
|
+
this.warnings.push(
|
|
15113
|
+
`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.`
|
|
15114
|
+
);
|
|
15115
|
+
}
|
|
15095
15116
|
return { patches: [], revertedPatchIds: [] };
|
|
15096
15117
|
}
|
|
15097
15118
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
@@ -15712,16 +15733,20 @@ async function resolveInputs(args) {
|
|
|
15712
15733
|
const oursPath = (0, import_node_path4.join)(outputDir, resolvedPath);
|
|
15713
15734
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
15714
15735
|
let ghostTheirs = null;
|
|
15715
|
-
|
|
15736
|
+
let baseTreeUnreachable = false;
|
|
15737
|
+
if (!base && !renameSourcePath) {
|
|
15716
15738
|
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
15717
15739
|
if (!treeReachable) {
|
|
15718
|
-
|
|
15719
|
-
if (
|
|
15720
|
-
const
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15740
|
+
baseTreeUnreachable = true;
|
|
15741
|
+
if (ours) {
|
|
15742
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
15743
|
+
if (fileDiff) {
|
|
15744
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
15745
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
15746
|
+
if (result) {
|
|
15747
|
+
base = result.base;
|
|
15748
|
+
ghostTheirs = result.theirs;
|
|
15749
|
+
}
|
|
15725
15750
|
}
|
|
15726
15751
|
}
|
|
15727
15752
|
}
|
|
@@ -15733,7 +15758,11 @@ async function resolveInputs(args) {
|
|
|
15733
15758
|
ours,
|
|
15734
15759
|
oursPath,
|
|
15735
15760
|
renameSourcePath,
|
|
15736
|
-
ghostTheirs
|
|
15761
|
+
ghostTheirs,
|
|
15762
|
+
// Only meaningful when ghost reconstruction did NOT recover a base; if it
|
|
15763
|
+
// did, `base` is non-null and the merge proceeds normally. We still set
|
|
15764
|
+
// the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
|
|
15765
|
+
baseTreeUnreachable
|
|
15737
15766
|
};
|
|
15738
15767
|
}
|
|
15739
15768
|
|
|
@@ -16299,7 +16328,11 @@ async function buildMergePlan(args) {
|
|
|
16299
16328
|
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
16300
16329
|
}
|
|
16301
16330
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
16302
|
-
return {
|
|
16331
|
+
return {
|
|
16332
|
+
kind: "new-file-both",
|
|
16333
|
+
theirs: effective_theirs,
|
|
16334
|
+
...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
|
|
16335
|
+
};
|
|
16303
16336
|
}
|
|
16304
16337
|
if (effective_theirs == null) {
|
|
16305
16338
|
return { kind: "skipped", reason: "missing-content" };
|
|
@@ -16347,17 +16380,19 @@ async function runMergeAndRecord(args) {
|
|
|
16347
16380
|
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
16348
16381
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
16349
16382
|
await (0, import_promises2.writeFile)(oursPath, merged2.content);
|
|
16383
|
+
const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
|
|
16350
16384
|
if (merged2.hasConflicts) {
|
|
16351
16385
|
return {
|
|
16352
16386
|
file: resolvedPath,
|
|
16353
16387
|
status: "conflict",
|
|
16354
16388
|
conflicts: merged2.conflicts,
|
|
16355
16389
|
conflictReason: "new-file-both",
|
|
16356
|
-
conflictMetadata: metadata
|
|
16390
|
+
conflictMetadata: metadata,
|
|
16391
|
+
...unreachableFlag
|
|
16357
16392
|
};
|
|
16358
16393
|
}
|
|
16359
16394
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
16360
|
-
return { file: resolvedPath, status: "merged" };
|
|
16395
|
+
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
16361
16396
|
}
|
|
16362
16397
|
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
16363
16398
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
@@ -17565,6 +17600,7 @@ var NoPatchesFlow = class {
|
|
|
17565
17600
|
results = await this.service.applicator.applyPatches(newPatches);
|
|
17566
17601
|
this.service._lastApplyResults = results;
|
|
17567
17602
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17603
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17568
17604
|
this.service.revertConflictingFiles(results);
|
|
17569
17605
|
for (const result of results) {
|
|
17570
17606
|
if (result.status === "conflict") {
|
|
@@ -17720,6 +17756,7 @@ var NormalRegenerationFlow = class {
|
|
|
17720
17756
|
const results = await this.service.applicator.applyPatches(allPatches);
|
|
17721
17757
|
this.service._lastApplyResults = results;
|
|
17722
17758
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17759
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17723
17760
|
this.service.revertConflictingFiles(results);
|
|
17724
17761
|
for (const result of results) {
|
|
17725
17762
|
if (result.status === "conflict") {
|
|
@@ -18639,6 +18676,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18639
18676
|
}
|
|
18640
18677
|
}
|
|
18641
18678
|
}
|
|
18679
|
+
/**
|
|
18680
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
18681
|
+
* customization's BASE could not be read because the patch's
|
|
18682
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
18683
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
18684
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
18685
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
18686
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
18687
|
+
* likely cause (insufficient clone window).
|
|
18688
|
+
*
|
|
18689
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
18690
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
18691
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
18692
|
+
*/
|
|
18693
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18694
|
+
recordUnreachableBaseWarnings(results) {
|
|
18695
|
+
const warned = /* @__PURE__ */ new Set();
|
|
18696
|
+
for (const result of results) {
|
|
18697
|
+
if (!result.fileResults) continue;
|
|
18698
|
+
for (const fr of result.fileResults) {
|
|
18699
|
+
if (!fr.baseTreeUnreachable) continue;
|
|
18700
|
+
if (warned.has(fr.file)) continue;
|
|
18701
|
+
warned.add(fr.file);
|
|
18702
|
+
const base = fr.conflictMetadata?.baseGeneration;
|
|
18703
|
+
const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
|
|
18704
|
+
this.warnings.push(
|
|
18705
|
+
`${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.`
|
|
18706
|
+
);
|
|
18707
|
+
}
|
|
18708
|
+
}
|
|
18709
|
+
}
|
|
18642
18710
|
/**
|
|
18643
18711
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
18644
18712
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|