@fern-api/replay 0.16.2 → 0.18.0
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 +112 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +112 -31
- 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 +112 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -5379,6 +5379,11 @@ var init_GitClient = __esm({
|
|
|
5379
5379
|
proc.stderr.on("data", (data) => {
|
|
5380
5380
|
stderr += data.toString();
|
|
5381
5381
|
});
|
|
5382
|
+
proc.on("error", (err) => {
|
|
5383
|
+
reject(new Error(`git ${args.join(" ")} failed to spawn: ${err.message}`));
|
|
5384
|
+
});
|
|
5385
|
+
proc.stdin.on("error", () => {
|
|
5386
|
+
});
|
|
5382
5387
|
proc.on("close", (code) => {
|
|
5383
5388
|
if (code === 0) {
|
|
5384
5389
|
resolve2(stdout);
|
|
@@ -14981,7 +14986,8 @@ function parseRevertedMessage(subject) {
|
|
|
14981
14986
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
14982
14987
|
function matchesFernignorePattern(filePath, patterns) {
|
|
14983
14988
|
if (patterns.length === 0) return false;
|
|
14984
|
-
return patterns.some((
|
|
14989
|
+
return patterns.some((rawPattern) => {
|
|
14990
|
+
const p = rawPattern.endsWith("/") ? rawPattern.slice(0, -1) : rawPattern;
|
|
14985
14991
|
if (filePath === p) return true;
|
|
14986
14992
|
if (minimatch(filePath, p)) return true;
|
|
14987
14993
|
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
@@ -15081,6 +15087,22 @@ var ReplayDetector = class {
|
|
|
15081
15087
|
return commit.sha;
|
|
15082
15088
|
}
|
|
15083
15089
|
}
|
|
15090
|
+
let fullLog;
|
|
15091
|
+
try {
|
|
15092
|
+
fullLog = await this.git.exec([
|
|
15093
|
+
"log",
|
|
15094
|
+
"--no-merges",
|
|
15095
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
15096
|
+
"HEAD"
|
|
15097
|
+
]);
|
|
15098
|
+
} catch {
|
|
15099
|
+
return null;
|
|
15100
|
+
}
|
|
15101
|
+
for (const commit of this.parseGitLog(fullLog)) {
|
|
15102
|
+
if (isGenerationBoundary(commit)) {
|
|
15103
|
+
return commit.sha;
|
|
15104
|
+
}
|
|
15105
|
+
}
|
|
15084
15106
|
return null;
|
|
15085
15107
|
}
|
|
15086
15108
|
async detectNewPatches() {
|
|
@@ -15092,6 +15114,11 @@ var ReplayDetector = class {
|
|
|
15092
15114
|
}
|
|
15093
15115
|
const lastGen = this.getLastGeneration(lock);
|
|
15094
15116
|
if (!lastGen) {
|
|
15117
|
+
if (lock.current_generation) {
|
|
15118
|
+
this.warnings.push(
|
|
15119
|
+
`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.`
|
|
15120
|
+
);
|
|
15121
|
+
}
|
|
15095
15122
|
return { patches: [], revertedPatchIds: [] };
|
|
15096
15123
|
}
|
|
15097
15124
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
@@ -15139,20 +15166,15 @@ var ReplayDetector = class {
|
|
|
15139
15166
|
continue;
|
|
15140
15167
|
}
|
|
15141
15168
|
const parents = await this.git.getCommitParents(commit.sha);
|
|
15142
|
-
let
|
|
15169
|
+
let filesOutput;
|
|
15143
15170
|
try {
|
|
15144
|
-
|
|
15171
|
+
filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15145
15172
|
} catch {
|
|
15146
15173
|
this.warnings.push(
|
|
15147
15174
|
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
15148
15175
|
);
|
|
15149
15176
|
continue;
|
|
15150
15177
|
}
|
|
15151
|
-
let contentHash = this.computeContentHash(patchContent);
|
|
15152
|
-
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
15153
|
-
continue;
|
|
15154
|
-
}
|
|
15155
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15156
15178
|
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15157
15179
|
filesOutput,
|
|
15158
15180
|
this.fernignorePatterns
|
|
@@ -15167,19 +15189,31 @@ var ReplayDetector = class {
|
|
|
15167
15189
|
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15168
15190
|
);
|
|
15169
15191
|
}
|
|
15170
|
-
if (
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15192
|
+
if (files.length === 0) {
|
|
15193
|
+
continue;
|
|
15194
|
+
}
|
|
15195
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0;
|
|
15196
|
+
const parentSha = parents[0];
|
|
15197
|
+
let patchContent;
|
|
15198
|
+
if (wasFiltered && parentSha) {
|
|
15199
|
+
try {
|
|
15200
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
15201
|
+
} catch {
|
|
15202
|
+
continue;
|
|
15203
|
+
}
|
|
15204
|
+
if (!patchContent.trim()) continue;
|
|
15205
|
+
} else {
|
|
15206
|
+
try {
|
|
15207
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
15208
|
+
} catch {
|
|
15209
|
+
this.warnings.push(
|
|
15210
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
15211
|
+
);
|
|
15212
|
+
continue;
|
|
15180
15213
|
}
|
|
15181
15214
|
}
|
|
15182
|
-
|
|
15215
|
+
const contentHash = this.computeContentHash(patchContent);
|
|
15216
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
15183
15217
|
continue;
|
|
15184
15218
|
}
|
|
15185
15219
|
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
@@ -15712,16 +15746,20 @@ async function resolveInputs(args) {
|
|
|
15712
15746
|
const oursPath = (0, import_node_path4.join)(outputDir, resolvedPath);
|
|
15713
15747
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
15714
15748
|
let ghostTheirs = null;
|
|
15715
|
-
|
|
15749
|
+
let baseTreeUnreachable = false;
|
|
15750
|
+
if (!base && !renameSourcePath) {
|
|
15716
15751
|
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
15717
15752
|
if (!treeReachable) {
|
|
15718
|
-
|
|
15719
|
-
if (
|
|
15720
|
-
const
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15753
|
+
baseTreeUnreachable = true;
|
|
15754
|
+
if (ours) {
|
|
15755
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
15756
|
+
if (fileDiff) {
|
|
15757
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
15758
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
15759
|
+
if (result) {
|
|
15760
|
+
base = result.base;
|
|
15761
|
+
ghostTheirs = result.theirs;
|
|
15762
|
+
}
|
|
15725
15763
|
}
|
|
15726
15764
|
}
|
|
15727
15765
|
}
|
|
@@ -15733,7 +15771,11 @@ async function resolveInputs(args) {
|
|
|
15733
15771
|
ours,
|
|
15734
15772
|
oursPath,
|
|
15735
15773
|
renameSourcePath,
|
|
15736
|
-
ghostTheirs
|
|
15774
|
+
ghostTheirs,
|
|
15775
|
+
// Only meaningful when ghost reconstruction did NOT recover a base; if it
|
|
15776
|
+
// did, `base` is non-null and the merge proceeds normally. We still set
|
|
15777
|
+
// the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
|
|
15778
|
+
baseTreeUnreachable
|
|
15737
15779
|
};
|
|
15738
15780
|
}
|
|
15739
15781
|
|
|
@@ -16299,7 +16341,11 @@ async function buildMergePlan(args) {
|
|
|
16299
16341
|
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
16300
16342
|
}
|
|
16301
16343
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
16302
|
-
return {
|
|
16344
|
+
return {
|
|
16345
|
+
kind: "new-file-both",
|
|
16346
|
+
theirs: effective_theirs,
|
|
16347
|
+
...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
|
|
16348
|
+
};
|
|
16303
16349
|
}
|
|
16304
16350
|
if (effective_theirs == null) {
|
|
16305
16351
|
return { kind: "skipped", reason: "missing-content" };
|
|
@@ -16347,17 +16393,19 @@ async function runMergeAndRecord(args) {
|
|
|
16347
16393
|
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
16348
16394
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
16349
16395
|
await (0, import_promises2.writeFile)(oursPath, merged2.content);
|
|
16396
|
+
const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
|
|
16350
16397
|
if (merged2.hasConflicts) {
|
|
16351
16398
|
return {
|
|
16352
16399
|
file: resolvedPath,
|
|
16353
16400
|
status: "conflict",
|
|
16354
16401
|
conflicts: merged2.conflicts,
|
|
16355
16402
|
conflictReason: "new-file-both",
|
|
16356
|
-
conflictMetadata: metadata
|
|
16403
|
+
conflictMetadata: metadata,
|
|
16404
|
+
...unreachableFlag
|
|
16357
16405
|
};
|
|
16358
16406
|
}
|
|
16359
16407
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
16360
|
-
return { file: resolvedPath, status: "merged" };
|
|
16408
|
+
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
16361
16409
|
}
|
|
16362
16410
|
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
16363
16411
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
@@ -17565,6 +17613,7 @@ var NoPatchesFlow = class {
|
|
|
17565
17613
|
results = await this.service.applicator.applyPatches(newPatches);
|
|
17566
17614
|
this.service._lastApplyResults = results;
|
|
17567
17615
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17616
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17568
17617
|
this.service.revertConflictingFiles(results);
|
|
17569
17618
|
for (const result of results) {
|
|
17570
17619
|
if (result.status === "conflict") {
|
|
@@ -17720,6 +17769,7 @@ var NormalRegenerationFlow = class {
|
|
|
17720
17769
|
const results = await this.service.applicator.applyPatches(allPatches);
|
|
17721
17770
|
this.service._lastApplyResults = results;
|
|
17722
17771
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17772
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17723
17773
|
this.service.revertConflictingFiles(results);
|
|
17724
17774
|
for (const result of results) {
|
|
17725
17775
|
if (result.status === "conflict") {
|
|
@@ -18639,6 +18689,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18639
18689
|
}
|
|
18640
18690
|
}
|
|
18641
18691
|
}
|
|
18692
|
+
/**
|
|
18693
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
18694
|
+
* customization's BASE could not be read because the patch's
|
|
18695
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
18696
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
18697
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
18698
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
18699
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
18700
|
+
* likely cause (insufficient clone window).
|
|
18701
|
+
*
|
|
18702
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
18703
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
18704
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
18705
|
+
*/
|
|
18706
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18707
|
+
recordUnreachableBaseWarnings(results) {
|
|
18708
|
+
const warned = /* @__PURE__ */ new Set();
|
|
18709
|
+
for (const result of results) {
|
|
18710
|
+
if (!result.fileResults) continue;
|
|
18711
|
+
for (const fr of result.fileResults) {
|
|
18712
|
+
if (!fr.baseTreeUnreachable) continue;
|
|
18713
|
+
if (warned.has(fr.file)) continue;
|
|
18714
|
+
warned.add(fr.file);
|
|
18715
|
+
const base = fr.conflictMetadata?.baseGeneration;
|
|
18716
|
+
const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
|
|
18717
|
+
this.warnings.push(
|
|
18718
|
+
`${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.`
|
|
18719
|
+
);
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
}
|
|
18642
18723
|
/**
|
|
18643
18724
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
18644
18725
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|