@fern-api/replay 0.12.0 → 0.14.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 +707 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +705 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -3
- package/dist/index.d.ts +97 -3
- package/dist/index.js +704 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -5368,9 +5368,9 @@ var init_GitClient = __esm({
|
|
|
5368
5368
|
return this.git.raw(args);
|
|
5369
5369
|
}
|
|
5370
5370
|
async execWithInput(args, input) {
|
|
5371
|
-
const { spawn:
|
|
5371
|
+
const { spawn: spawn3 } = await import("child_process");
|
|
5372
5372
|
return new Promise((resolve2, reject) => {
|
|
5373
|
-
const proc =
|
|
5373
|
+
const proc = spawn3("git", args, { cwd: this.repoPath });
|
|
5374
5374
|
let stdout = "";
|
|
5375
5375
|
let stderr = "";
|
|
5376
5376
|
proc.stdout.on("data", (data) => {
|
|
@@ -9886,7 +9886,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
9886
9886
|
if (!header)
|
|
9887
9887
|
return { value: "", type: null, comment: "", range: [start, start, start] };
|
|
9888
9888
|
const type = header.mode === ">" ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
|
|
9889
|
-
const lines = scalar.source ?
|
|
9889
|
+
const lines = scalar.source ? splitLines2(scalar.source) : [];
|
|
9890
9890
|
let chompStart = lines.length;
|
|
9891
9891
|
for (let i = lines.length - 1; i >= 0; --i) {
|
|
9892
9892
|
const content = lines[i][1];
|
|
@@ -10044,7 +10044,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
10044
10044
|
}
|
|
10045
10045
|
return { mode, indent, chomp, comment, length };
|
|
10046
10046
|
}
|
|
10047
|
-
function
|
|
10047
|
+
function splitLines2(source) {
|
|
10048
10048
|
const split = source.split(/\n( *)/);
|
|
10049
10049
|
const first2 = split[0];
|
|
10050
10050
|
const m = first2.match(/^( *)/);
|
|
@@ -13233,7 +13233,7 @@ var init_HybridReconstruction = __esm({
|
|
|
13233
13233
|
});
|
|
13234
13234
|
|
|
13235
13235
|
// src/cli.ts
|
|
13236
|
-
var
|
|
13236
|
+
var import_node_path9 = require("path");
|
|
13237
13237
|
var import_node_fs6 = require("fs");
|
|
13238
13238
|
var import_node_readline = require("readline");
|
|
13239
13239
|
init_GitClient();
|
|
@@ -13552,17 +13552,57 @@ var ReplayDetector = class {
|
|
|
13552
13552
|
}
|
|
13553
13553
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
13554
13554
|
if (!isAncestor) {
|
|
13555
|
+
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
13556
|
+
}
|
|
13557
|
+
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
13558
|
+
}
|
|
13559
|
+
/**
|
|
13560
|
+
* FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
13561
|
+
*
|
|
13562
|
+
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
13563
|
+
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
13564
|
+
* merge-base is the commit on main from which the bot's branch was created —
|
|
13565
|
+
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
13566
|
+
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
13567
|
+
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
13568
|
+
* generation) get bundled as customer customizations.
|
|
13569
|
+
*
|
|
13570
|
+
* Falls through to the legacy composite path when no common ancestor exists
|
|
13571
|
+
* (truly disjoint history — orphan branches with no shared root).
|
|
13572
|
+
*/
|
|
13573
|
+
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
13574
|
+
let mergeBase = "";
|
|
13575
|
+
try {
|
|
13576
|
+
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
13577
|
+
} catch {
|
|
13578
|
+
}
|
|
13579
|
+
if (!mergeBase) {
|
|
13580
|
+
this.warnings.push(
|
|
13581
|
+
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
13582
|
+
);
|
|
13555
13583
|
return this.detectPatchesViaTreeDiff(
|
|
13556
13584
|
lastGen,
|
|
13557
13585
|
/* commitKnownMissing */
|
|
13558
13586
|
false
|
|
13559
13587
|
);
|
|
13560
13588
|
}
|
|
13589
|
+
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
13590
|
+
}
|
|
13591
|
+
/**
|
|
13592
|
+
* FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
13593
|
+
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
13594
|
+
* (rangeStart = lastGen.commit_sha) and the merge-base path.
|
|
13595
|
+
*
|
|
13596
|
+
* Patch `base_generation` is always `lastGen.commit_sha` — the
|
|
13597
|
+
* lockfile-tracked reference the applicator uses to fetch base file
|
|
13598
|
+
* content, regardless of where the walk actually started.
|
|
13599
|
+
*/
|
|
13600
|
+
async detectPatchesInRange(rangeStart, lastGen, lock) {
|
|
13561
13601
|
const log = await this.git.exec([
|
|
13562
13602
|
"log",
|
|
13563
13603
|
"--first-parent",
|
|
13564
13604
|
"--format=%H%x00%an%x00%ae%x00%s",
|
|
13565
|
-
`${
|
|
13605
|
+
`${rangeStart}..HEAD`,
|
|
13566
13606
|
"--",
|
|
13567
13607
|
this.sdkOutputDir
|
|
13568
13608
|
]);
|
|
@@ -14020,7 +14060,7 @@ var ReplayDetector = class {
|
|
|
14020
14060
|
|
|
14021
14061
|
// src/ReplayService.ts
|
|
14022
14062
|
var import_node_fs2 = require("fs");
|
|
14023
|
-
var
|
|
14063
|
+
var import_node_path5 = require("path");
|
|
14024
14064
|
|
|
14025
14065
|
// node_modules/minimatch/dist/esm/index.js
|
|
14026
14066
|
var import_brace_expansion = __toESM(require_brace_expansion(), 1);
|
|
@@ -15721,11 +15761,43 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
15721
15761
|
}
|
|
15722
15762
|
}
|
|
15723
15763
|
}
|
|
15724
|
-
|
|
15764
|
+
const result = {
|
|
15725
15765
|
content: outputLines.join("\n"),
|
|
15726
15766
|
hasConflicts: conflicts2.length > 0,
|
|
15727
15767
|
conflicts: conflicts2
|
|
15728
15768
|
};
|
|
15769
|
+
if (conflicts2.length === 0) {
|
|
15770
|
+
const dropped = computeDroppedContextLines(baseLines, theirsLines, outputLines);
|
|
15771
|
+
if (dropped.length > 0) {
|
|
15772
|
+
result.droppedContextLines = dropped;
|
|
15773
|
+
}
|
|
15774
|
+
}
|
|
15775
|
+
return result;
|
|
15776
|
+
}
|
|
15777
|
+
function computeDroppedContextLines(baseLines, theirsLines, mergedLines) {
|
|
15778
|
+
const baseCounts = countLines(baseLines);
|
|
15779
|
+
const theirsCounts = countLines(theirsLines);
|
|
15780
|
+
const mergedCounts = countLines(mergedLines);
|
|
15781
|
+
const dropped = [];
|
|
15782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15783
|
+
for (const line of baseLines) {
|
|
15784
|
+
if (seen.has(line)) continue;
|
|
15785
|
+
const inBase = baseCounts.get(line) ?? 0;
|
|
15786
|
+
const inTheirs = theirsCounts.get(line) ?? 0;
|
|
15787
|
+
const inMerged = mergedCounts.get(line) ?? 0;
|
|
15788
|
+
if (inBase > 0 && inTheirs > 0 && inMerged < Math.min(inBase, inTheirs)) {
|
|
15789
|
+
dropped.push(line);
|
|
15790
|
+
seen.add(line);
|
|
15791
|
+
}
|
|
15792
|
+
}
|
|
15793
|
+
return dropped;
|
|
15794
|
+
}
|
|
15795
|
+
function countLines(lines) {
|
|
15796
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15797
|
+
for (const line of lines) {
|
|
15798
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
15799
|
+
}
|
|
15800
|
+
return counts;
|
|
15729
15801
|
}
|
|
15730
15802
|
function tryResolveConflict(oursLines, baseLines, theirsLines) {
|
|
15731
15803
|
if (baseLines.length === 0) {
|
|
@@ -15841,6 +15913,19 @@ var ReplayApplicator = class {
|
|
|
15841
15913
|
renameCache = /* @__PURE__ */ new Map();
|
|
15842
15914
|
treeExistsCache = /* @__PURE__ */ new Map();
|
|
15843
15915
|
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
15916
|
+
/**
|
|
15917
|
+
* Apply mode for the current `applyPatches` invocation. Set at the start
|
|
15918
|
+
* of `applyPatches`, read by `mergeFile` to decide:
|
|
15919
|
+
* - whether to skip the intra-loop marker strip (kept in `applyPatches`)
|
|
15920
|
+
* - whether to populate the accumulator after a conflicted merge
|
|
15921
|
+
* (resolve mode populates so subsequent patches on the same file
|
|
15922
|
+
* can use patch A's THEIRS as a structurally-correct merge base
|
|
15923
|
+
* when their diff was authored against post-A structure)
|
|
15924
|
+
* - whether to retry THEIRS reconstruction against the accumulator
|
|
15925
|
+
* when the BASE-relative reconstruction produced markers from a
|
|
15926
|
+
* cross-patch context mismatch
|
|
15927
|
+
*/
|
|
15928
|
+
currentApplyMode = "replay";
|
|
15844
15929
|
constructor(git, lockManager, outputDir) {
|
|
15845
15930
|
this.git = git;
|
|
15846
15931
|
this.lockManager = lockManager;
|
|
@@ -15888,8 +15973,23 @@ var ReplayApplicator = class {
|
|
|
15888
15973
|
/**
|
|
15889
15974
|
* Apply all patches, returning results for each.
|
|
15890
15975
|
* Skips patches that match exclude patterns in replay.yml
|
|
15976
|
+
*
|
|
15977
|
+
* `applyMode` controls the post-conflict marker strategy:
|
|
15978
|
+
* - `"replay"` (default): strip conflict markers from disk between
|
|
15979
|
+
* iterations whenever a later patch touches the same file. Lets the
|
|
15980
|
+
* follow-up patch's `git apply --3way` see clean OURS content,
|
|
15981
|
+
* which is what the silent-loss fix (PR #73) relies on. The replay
|
|
15982
|
+
* pipeline calls `revertConflictingFiles` after the loop to clean
|
|
15983
|
+
* any markers that survive.
|
|
15984
|
+
* - `"resolve"`: keep markers on disk. The resolve command needs the
|
|
15985
|
+
* customer to see them. Slow-path 3-way merge naturally preserves
|
|
15986
|
+
* A's markers and B's clean writes when their regions don't overlap;
|
|
15987
|
+
* when they do overlap, the customer gets nested markers and
|
|
15988
|
+
* resolves manually. Either way they aren't silently dropped.
|
|
15891
15989
|
*/
|
|
15892
|
-
async applyPatches(patches) {
|
|
15990
|
+
async applyPatches(patches, opts) {
|
|
15991
|
+
const applyMode = opts?.applyMode ?? "replay";
|
|
15992
|
+
this.currentApplyMode = applyMode;
|
|
15893
15993
|
this.resetAccumulator();
|
|
15894
15994
|
const results = [];
|
|
15895
15995
|
for (let i = 0; i < patches.length; i++) {
|
|
@@ -15904,7 +16004,7 @@ var ReplayApplicator = class {
|
|
|
15904
16004
|
}
|
|
15905
16005
|
const result = await this.applyPatchWithFallback(patch);
|
|
15906
16006
|
results.push(result);
|
|
15907
|
-
if (result.status === "conflict" && result.fileResults) {
|
|
16007
|
+
if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
|
|
15908
16008
|
const laterFiles = /* @__PURE__ */ new Set();
|
|
15909
16009
|
for (let j = i + 1; j < patches.length; j++) {
|
|
15910
16010
|
for (const f of patches[j].files) {
|
|
@@ -15934,9 +16034,17 @@ var ReplayApplicator = class {
|
|
|
15934
16034
|
}
|
|
15935
16035
|
return results;
|
|
15936
16036
|
}
|
|
15937
|
-
/**
|
|
16037
|
+
/**
|
|
16038
|
+
* Populate accumulator after git apply succeeds, AND collect per-file
|
|
16039
|
+
* results including any "dropped context lines" — lines that were
|
|
16040
|
+
* present in BASE and THEIRS (unchanged context) but absent from the
|
|
16041
|
+
* final on-disk state because OURS deleted them. This is the fast-path
|
|
16042
|
+
* counterpart to ThreeWayMerge.computeDroppedContextLines used by the
|
|
16043
|
+
* 3-way slow path; both must surface the same warning.
|
|
16044
|
+
*/
|
|
15938
16045
|
async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
|
|
15939
|
-
|
|
16046
|
+
const fileResults = [];
|
|
16047
|
+
if (!baseGen) return fileResults;
|
|
15940
16048
|
const tempDir = await (0, import_promises.mkdtemp)((0, import_node_path3.join)((0, import_node_os.tmpdir)(), "replay-acc-"));
|
|
15941
16049
|
const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
|
|
15942
16050
|
const tempGit = new GitClient2(tempDir);
|
|
@@ -15949,18 +16057,30 @@ var ReplayApplicator = class {
|
|
|
15949
16057
|
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
15950
16058
|
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
15951
16059
|
const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
15952
|
-
const
|
|
16060
|
+
const finalOnDisk = await (0, import_promises.readFile)((0, import_node_path3.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
|
|
16061
|
+
const effectiveTheirs = theirs ?? finalOnDisk;
|
|
15953
16062
|
if (effectiveTheirs != null) {
|
|
15954
16063
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
15955
16064
|
content: effectiveTheirs,
|
|
15956
16065
|
baseGeneration: patch.base_generation
|
|
15957
16066
|
});
|
|
15958
16067
|
}
|
|
16068
|
+
if (base != null && effectiveTheirs != null && finalOnDisk != null) {
|
|
16069
|
+
const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
|
|
16070
|
+
if (dropped.length > 0) {
|
|
16071
|
+
fileResults.push({
|
|
16072
|
+
file: resolvedPath,
|
|
16073
|
+
status: "merged",
|
|
16074
|
+
droppedContextLines: dropped
|
|
16075
|
+
});
|
|
16076
|
+
}
|
|
16077
|
+
}
|
|
15959
16078
|
}
|
|
15960
16079
|
} finally {
|
|
15961
16080
|
await (0, import_promises.rm)(tempDir, { recursive: true }).catch(() => {
|
|
15962
16081
|
});
|
|
15963
16082
|
}
|
|
16083
|
+
return fileResults;
|
|
15964
16084
|
}
|
|
15965
16085
|
async applyPatchWithFallback(patch) {
|
|
15966
16086
|
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
@@ -15991,11 +16111,12 @@ var ReplayApplicator = class {
|
|
|
15991
16111
|
}
|
|
15992
16112
|
try {
|
|
15993
16113
|
await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
|
|
15994
|
-
await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
16114
|
+
const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
|
|
15995
16115
|
return {
|
|
15996
16116
|
patch,
|
|
15997
16117
|
status: "applied",
|
|
15998
16118
|
method: "git-am",
|
|
16119
|
+
...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
|
|
15999
16120
|
...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
|
|
16000
16121
|
};
|
|
16001
16122
|
} catch {
|
|
@@ -16107,19 +16228,23 @@ var ReplayApplicator = class {
|
|
|
16107
16228
|
renameSourcePath
|
|
16108
16229
|
);
|
|
16109
16230
|
}
|
|
16231
|
+
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
16110
16232
|
if (theirs) {
|
|
16111
16233
|
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
16112
16234
|
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
16113
16235
|
if (theirsHasMarkers && !baseHasMarkers) {
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
|
|
16117
|
-
|
|
16118
|
-
|
|
16236
|
+
if (accumulatorEntry) {
|
|
16237
|
+
theirs = null;
|
|
16238
|
+
} else {
|
|
16239
|
+
return {
|
|
16240
|
+
file: resolvedPath,
|
|
16241
|
+
status: "skipped",
|
|
16242
|
+
reason: "stale-conflict-markers"
|
|
16243
|
+
};
|
|
16244
|
+
}
|
|
16119
16245
|
}
|
|
16120
16246
|
}
|
|
16121
16247
|
let useAccumulatorAsMergeBase = false;
|
|
16122
|
-
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
16123
16248
|
if (accumulatorEntry && (theirs == null || base == null)) {
|
|
16124
16249
|
theirs = await this.applyPatchToContent(
|
|
16125
16250
|
accumulatorEntry.content,
|
|
@@ -16226,7 +16351,8 @@ var ReplayApplicator = class {
|
|
|
16226
16351
|
const outDir = (0, import_node_path3.dirname)(oursPath);
|
|
16227
16352
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
16228
16353
|
await (0, import_promises.writeFile)(oursPath, merged.content);
|
|
16229
|
-
|
|
16354
|
+
const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
|
|
16355
|
+
if (populateAccumulator) {
|
|
16230
16356
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
16231
16357
|
content: effective_theirs,
|
|
16232
16358
|
baseGeneration: patch.base_generation
|
|
@@ -16241,7 +16367,11 @@ var ReplayApplicator = class {
|
|
|
16241
16367
|
conflictMetadata: metadata
|
|
16242
16368
|
};
|
|
16243
16369
|
}
|
|
16244
|
-
return {
|
|
16370
|
+
return {
|
|
16371
|
+
file: resolvedPath,
|
|
16372
|
+
status: "merged",
|
|
16373
|
+
...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
|
|
16374
|
+
};
|
|
16245
16375
|
} catch (error) {
|
|
16246
16376
|
return {
|
|
16247
16377
|
file: filePath,
|
|
@@ -16427,6 +16557,30 @@ function isBinaryFile(filePath) {
|
|
|
16427
16557
|
const ext2 = (0, import_node_path3.extname)(filePath).toLowerCase();
|
|
16428
16558
|
return BINARY_EXTENSIONS.has(ext2);
|
|
16429
16559
|
}
|
|
16560
|
+
function computeDroppedContextLinesForFile(base, theirs, finalContent) {
|
|
16561
|
+
const baseLines = base.split("\n");
|
|
16562
|
+
const theirsLines = theirs.split("\n");
|
|
16563
|
+
const finalLines = finalContent.split("\n");
|
|
16564
|
+
const baseCounts = /* @__PURE__ */ new Map();
|
|
16565
|
+
const theirsCounts = /* @__PURE__ */ new Map();
|
|
16566
|
+
const finalCounts = /* @__PURE__ */ new Map();
|
|
16567
|
+
for (const l of baseLines) baseCounts.set(l, (baseCounts.get(l) ?? 0) + 1);
|
|
16568
|
+
for (const l of theirsLines) theirsCounts.set(l, (theirsCounts.get(l) ?? 0) + 1);
|
|
16569
|
+
for (const l of finalLines) finalCounts.set(l, (finalCounts.get(l) ?? 0) + 1);
|
|
16570
|
+
const dropped = [];
|
|
16571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16572
|
+
for (const line of baseLines) {
|
|
16573
|
+
if (seen.has(line)) continue;
|
|
16574
|
+
const inBase = baseCounts.get(line) ?? 0;
|
|
16575
|
+
const inTheirs = theirsCounts.get(line) ?? 0;
|
|
16576
|
+
const inFinal = finalCounts.get(line) ?? 0;
|
|
16577
|
+
if (inBase > 0 && inTheirs > 0 && inFinal < Math.min(inBase, inTheirs)) {
|
|
16578
|
+
dropped.push(line);
|
|
16579
|
+
seen.add(line);
|
|
16580
|
+
}
|
|
16581
|
+
}
|
|
16582
|
+
return dropped;
|
|
16583
|
+
}
|
|
16430
16584
|
function isDiffLineForFile(diffLine, filePath) {
|
|
16431
16585
|
const match2 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
|
|
16432
16586
|
return match2 !== null && match2[1] === filePath;
|
|
@@ -16462,13 +16616,46 @@ CLI Version: ${options.cliVersion}`;
|
|
|
16462
16616
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
16463
16617
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
16464
16618
|
}
|
|
16465
|
-
async commitReplay(_patchCount, patches, message) {
|
|
16619
|
+
async commitReplay(_patchCount, patches, message, options) {
|
|
16466
16620
|
await this.stageAll();
|
|
16467
16621
|
if (!await this.hasStagedChanges()) {
|
|
16468
16622
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
16469
16623
|
}
|
|
16470
16624
|
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
16471
|
-
|
|
16625
|
+
const buckets = options?.buckets;
|
|
16626
|
+
if (buckets) {
|
|
16627
|
+
if (buckets.applied.length > 0) {
|
|
16628
|
+
fullMessage += `
|
|
16629
|
+
|
|
16630
|
+
Patches applied (${buckets.applied.length}):`;
|
|
16631
|
+
for (const p of buckets.applied) {
|
|
16632
|
+
fullMessage += `
|
|
16633
|
+
- ${p.id}: ${p.original_message}`;
|
|
16634
|
+
}
|
|
16635
|
+
}
|
|
16636
|
+
if (buckets.unresolved.length > 0) {
|
|
16637
|
+
fullMessage += `
|
|
16638
|
+
|
|
16639
|
+
Patches with unresolved conflicts (${buckets.unresolved.length}):`;
|
|
16640
|
+
for (const p of buckets.unresolved) {
|
|
16641
|
+
fullMessage += `
|
|
16642
|
+
- ${p.id}: ${p.original_message}`;
|
|
16643
|
+
}
|
|
16644
|
+
fullMessage += `
|
|
16645
|
+
Run \`fern-replay resolve\` to apply these customizations.`;
|
|
16646
|
+
}
|
|
16647
|
+
if (buckets.absorbed.length > 0) {
|
|
16648
|
+
fullMessage += `
|
|
16649
|
+
|
|
16650
|
+
Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
16651
|
+
for (const p of buckets.absorbed) {
|
|
16652
|
+
fullMessage += `
|
|
16653
|
+
- ${p.id}: ${p.original_message}`;
|
|
16654
|
+
}
|
|
16655
|
+
fullMessage += `
|
|
16656
|
+
The generator now produces these customizations natively.`;
|
|
16657
|
+
}
|
|
16658
|
+
} else if (patches && patches.length > 0) {
|
|
16472
16659
|
fullMessage += "\n\nPatches replayed:";
|
|
16473
16660
|
for (const patch of patches) {
|
|
16474
16661
|
fullMessage += `
|
|
@@ -16502,6 +16689,298 @@ CLI Version: ${options.cliVersion}`;
|
|
|
16502
16689
|
}
|
|
16503
16690
|
};
|
|
16504
16691
|
|
|
16692
|
+
// src/PatchRegionDiff.ts
|
|
16693
|
+
var import_promises2 = require("fs/promises");
|
|
16694
|
+
var import_node_os2 = require("os");
|
|
16695
|
+
var import_node_path4 = require("path");
|
|
16696
|
+
var import_node_child_process = require("child_process");
|
|
16697
|
+
init_HybridReconstruction();
|
|
16698
|
+
function normalizeHunkBody(hunk) {
|
|
16699
|
+
let contextC = 0;
|
|
16700
|
+
let removeC = 0;
|
|
16701
|
+
let addC = 0;
|
|
16702
|
+
const trimmed2 = [];
|
|
16703
|
+
for (const line of hunk.lines) {
|
|
16704
|
+
const oldUsed = contextC + removeC;
|
|
16705
|
+
const newUsed = contextC + addC;
|
|
16706
|
+
if (oldUsed >= hunk.oldCount && newUsed >= hunk.newCount) break;
|
|
16707
|
+
trimmed2.push(line);
|
|
16708
|
+
if (line.type === "context") contextC++;
|
|
16709
|
+
else if (line.type === "remove") removeC++;
|
|
16710
|
+
else if (line.type === "add") addC++;
|
|
16711
|
+
}
|
|
16712
|
+
return { ...hunk, lines: trimmed2 };
|
|
16713
|
+
}
|
|
16714
|
+
function extractFileDiffForFile(patchContent, filePath) {
|
|
16715
|
+
const lines = patchContent.split("\n");
|
|
16716
|
+
const out = [];
|
|
16717
|
+
let inTarget = false;
|
|
16718
|
+
for (const line of lines) {
|
|
16719
|
+
if (line.startsWith("diff --git")) {
|
|
16720
|
+
if (inTarget) break;
|
|
16721
|
+
if (isDiffLineForFile2(line, filePath)) {
|
|
16722
|
+
inTarget = true;
|
|
16723
|
+
out.push(line);
|
|
16724
|
+
}
|
|
16725
|
+
continue;
|
|
16726
|
+
}
|
|
16727
|
+
if (inTarget) out.push(line);
|
|
16728
|
+
}
|
|
16729
|
+
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
16730
|
+
}
|
|
16731
|
+
function isDiffLineForFile2(line, filePath) {
|
|
16732
|
+
const match2 = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
16733
|
+
return match2 !== null && (match2[2] === filePath || match2[1] === filePath);
|
|
16734
|
+
}
|
|
16735
|
+
async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeContent) {
|
|
16736
|
+
const fragments = [];
|
|
16737
|
+
const unlocatableFiles = [];
|
|
16738
|
+
for (const file of patch.files) {
|
|
16739
|
+
const fileDiff = extractFileDiffForFile(patch.patch_content, file);
|
|
16740
|
+
if (fileDiff == null) {
|
|
16741
|
+
unlocatableFiles.push(file);
|
|
16742
|
+
continue;
|
|
16743
|
+
}
|
|
16744
|
+
const hunksRaw = parseHunks(fileDiff).map(normalizeHunkBody);
|
|
16745
|
+
const hunks = hunksRaw.filter((h) => {
|
|
16746
|
+
if (h.lines.length === 0) return false;
|
|
16747
|
+
let ctx = 0, rm3 = 0, add = 0;
|
|
16748
|
+
for (const ln of h.lines) {
|
|
16749
|
+
if (ln.type === "context") ctx++;
|
|
16750
|
+
else if (ln.type === "remove") rm3++;
|
|
16751
|
+
else if (ln.type === "add") add++;
|
|
16752
|
+
}
|
|
16753
|
+
return ctx + rm3 === h.oldCount && ctx + add === h.newCount;
|
|
16754
|
+
});
|
|
16755
|
+
if (hunks.length === 0) {
|
|
16756
|
+
if (hunksRaw.length > 0) unlocatableFiles.push(file);
|
|
16757
|
+
continue;
|
|
16758
|
+
}
|
|
16759
|
+
const currentGen = await getCurrentGenContent(file);
|
|
16760
|
+
const workingTree = await getWorkingTreeContent(file);
|
|
16761
|
+
const isPureNewFile = hunks.every((h) => h.oldCount === 0);
|
|
16762
|
+
const isPureDelete = hunks.every((h) => h.newCount === 0);
|
|
16763
|
+
if (currentGen == null) {
|
|
16764
|
+
if (!isPureNewFile) {
|
|
16765
|
+
unlocatableFiles.push(file);
|
|
16766
|
+
continue;
|
|
16767
|
+
}
|
|
16768
|
+
if (workingTree == null) continue;
|
|
16769
|
+
fragments.push(synthesizeNewFileDiff(file, workingTree));
|
|
16770
|
+
continue;
|
|
16771
|
+
}
|
|
16772
|
+
if (isPureNewFile) {
|
|
16773
|
+
if (workingTree == null) {
|
|
16774
|
+
fragments.push(synthesizeDeleteFileDiff(file, currentGen));
|
|
16775
|
+
continue;
|
|
16776
|
+
}
|
|
16777
|
+
if (workingTree === currentGen) {
|
|
16778
|
+
continue;
|
|
16779
|
+
}
|
|
16780
|
+
const fullDiff = await unifiedDiff(currentGen, workingTree, file);
|
|
16781
|
+
if (fullDiff && fullDiff.trim()) fragments.push(fullDiff);
|
|
16782
|
+
continue;
|
|
16783
|
+
}
|
|
16784
|
+
if (workingTree == null) {
|
|
16785
|
+
if (!isPureDelete) {
|
|
16786
|
+
unlocatableFiles.push(file);
|
|
16787
|
+
continue;
|
|
16788
|
+
}
|
|
16789
|
+
fragments.push(synthesizeDeleteFileDiff(file, currentGen));
|
|
16790
|
+
continue;
|
|
16791
|
+
}
|
|
16792
|
+
const currentGenLines = splitLines(currentGen);
|
|
16793
|
+
const workingTreeLines = splitLines(workingTree);
|
|
16794
|
+
const locInCurrentGenRaw = locateHunksInOurs(hunks, currentGenLines);
|
|
16795
|
+
const locInWorkingTreeRaw = locateHunksInOurs(hunks, workingTreeLines);
|
|
16796
|
+
if (locInCurrentGenRaw == null || locInWorkingTreeRaw == null) {
|
|
16797
|
+
unlocatableFiles.push(file);
|
|
16798
|
+
continue;
|
|
16799
|
+
}
|
|
16800
|
+
const locInCurrentGen = locInCurrentGenRaw.map(
|
|
16801
|
+
(l) => resolveSpan(l, currentGenLines, "old")
|
|
16802
|
+
);
|
|
16803
|
+
const locInWorkingTree = locInWorkingTreeRaw.map(
|
|
16804
|
+
(l) => resolveSpan(l, workingTreeLines, "new")
|
|
16805
|
+
);
|
|
16806
|
+
const overlay = overlayRegions(
|
|
16807
|
+
currentGenLines,
|
|
16808
|
+
locInCurrentGen,
|
|
16809
|
+
workingTreeLines,
|
|
16810
|
+
locInWorkingTree
|
|
16811
|
+
);
|
|
16812
|
+
if (overlay === null) {
|
|
16813
|
+
unlocatableFiles.push(file);
|
|
16814
|
+
continue;
|
|
16815
|
+
}
|
|
16816
|
+
if (overlay === currentGen) {
|
|
16817
|
+
continue;
|
|
16818
|
+
}
|
|
16819
|
+
const fileUnifiedDiff = await unifiedDiff(currentGen, overlay, file);
|
|
16820
|
+
if (fileUnifiedDiff && fileUnifiedDiff.trim()) {
|
|
16821
|
+
fragments.push(fileUnifiedDiff);
|
|
16822
|
+
}
|
|
16823
|
+
}
|
|
16824
|
+
return {
|
|
16825
|
+
diff: fragments.join(""),
|
|
16826
|
+
unlocatableFiles
|
|
16827
|
+
};
|
|
16828
|
+
}
|
|
16829
|
+
async function computePerPatchDiffWithFallback(patch, getCurrentGenContent, getWorkingTreeContent, runCumulativeDiff) {
|
|
16830
|
+
const ppDiff = await computePerPatchDiff(
|
|
16831
|
+
patch,
|
|
16832
|
+
getCurrentGenContent,
|
|
16833
|
+
getWorkingTreeContent
|
|
16834
|
+
);
|
|
16835
|
+
if (ppDiff.unlocatableFiles.length === 0) {
|
|
16836
|
+
return { diff: ppDiff.diff, hadFallback: false, fallbackFiles: [] };
|
|
16837
|
+
}
|
|
16838
|
+
const cumulative = await runCumulativeDiff(ppDiff.unlocatableFiles);
|
|
16839
|
+
if (cumulative === null) {
|
|
16840
|
+
return null;
|
|
16841
|
+
}
|
|
16842
|
+
const merged = ppDiff.diff + (cumulative.trim() ? cumulative : "");
|
|
16843
|
+
return { diff: merged, hadFallback: true, fallbackFiles: ppDiff.unlocatableFiles };
|
|
16844
|
+
}
|
|
16845
|
+
function splitLines(content) {
|
|
16846
|
+
return content.split("\n");
|
|
16847
|
+
}
|
|
16848
|
+
function resolveSpan(loc, oursLines, prefer) {
|
|
16849
|
+
const { hunk, oursOffset } = loc;
|
|
16850
|
+
const oldSide = [];
|
|
16851
|
+
const newSide = [];
|
|
16852
|
+
for (const line of hunk.lines) {
|
|
16853
|
+
if (line.type === "context") {
|
|
16854
|
+
oldSide.push(line.content);
|
|
16855
|
+
newSide.push(line.content);
|
|
16856
|
+
} else if (line.type === "remove") {
|
|
16857
|
+
oldSide.push(line.content);
|
|
16858
|
+
} else if (line.type === "add") {
|
|
16859
|
+
newSide.push(line.content);
|
|
16860
|
+
}
|
|
16861
|
+
}
|
|
16862
|
+
const oldFits = sequenceMatches(oldSide, oursLines, oursOffset);
|
|
16863
|
+
const newFits = sequenceMatches(newSide, oursLines, oursOffset);
|
|
16864
|
+
if (prefer === "old") {
|
|
16865
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
16866
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
16867
|
+
} else {
|
|
16868
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
16869
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
16870
|
+
}
|
|
16871
|
+
return loc;
|
|
16872
|
+
}
|
|
16873
|
+
function sequenceMatches(needle, haystack, offset) {
|
|
16874
|
+
if (offset + needle.length > haystack.length) return false;
|
|
16875
|
+
for (let i = 0; i < needle.length; i++) {
|
|
16876
|
+
if (haystack[offset + i] !== needle[i]) return false;
|
|
16877
|
+
}
|
|
16878
|
+
return true;
|
|
16879
|
+
}
|
|
16880
|
+
function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locInWorkingTree) {
|
|
16881
|
+
if (locInCurrentGen.length !== locInWorkingTree.length) {
|
|
16882
|
+
return null;
|
|
16883
|
+
}
|
|
16884
|
+
const out = [];
|
|
16885
|
+
let cursor = 0;
|
|
16886
|
+
for (let i = 0; i < locInCurrentGen.length; i++) {
|
|
16887
|
+
const cg = locInCurrentGen[i];
|
|
16888
|
+
const wt = locInWorkingTree[i];
|
|
16889
|
+
if (cg.oursOffset > cursor) {
|
|
16890
|
+
out.push(...currentGenLines.slice(cursor, cg.oursOffset));
|
|
16891
|
+
}
|
|
16892
|
+
out.push(...workingTreeLines.slice(wt.oursOffset, wt.oursOffset + wt.oursSpan));
|
|
16893
|
+
cursor = cg.oursOffset + cg.oursSpan;
|
|
16894
|
+
}
|
|
16895
|
+
if (cursor < currentGenLines.length) {
|
|
16896
|
+
out.push(...currentGenLines.slice(cursor));
|
|
16897
|
+
}
|
|
16898
|
+
return out.join("\n");
|
|
16899
|
+
}
|
|
16900
|
+
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
16901
|
+
if (beforeContent === afterContent) return "";
|
|
16902
|
+
const tmp = await (0, import_promises2.mkdtemp)((0, import_node_path4.join)((0, import_node_os2.tmpdir)(), "fern-replay-pp-"));
|
|
16903
|
+
try {
|
|
16904
|
+
const beforePath = (0, import_node_path4.join)(tmp, "before");
|
|
16905
|
+
const afterPath = (0, import_node_path4.join)(tmp, "after");
|
|
16906
|
+
await (0, import_promises2.writeFile)(beforePath, beforeContent, "utf-8");
|
|
16907
|
+
await (0, import_promises2.writeFile)(afterPath, afterContent, "utf-8");
|
|
16908
|
+
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
16909
|
+
if (!raw.trim()) return "";
|
|
16910
|
+
return rewriteDiffHeaders(raw, filePath);
|
|
16911
|
+
} finally {
|
|
16912
|
+
await (0, import_promises2.rm)(tmp, { recursive: true, force: true });
|
|
16913
|
+
}
|
|
16914
|
+
}
|
|
16915
|
+
function runGitDiffNoIndex(beforePath, afterPath) {
|
|
16916
|
+
return new Promise((resolve2, reject) => {
|
|
16917
|
+
const proc = (0, import_node_child_process.spawn)("git", ["diff", "--no-index", "--", beforePath, afterPath]);
|
|
16918
|
+
let stdout = "";
|
|
16919
|
+
let stderr = "";
|
|
16920
|
+
proc.stdout.on("data", (d) => stdout += d.toString());
|
|
16921
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
16922
|
+
proc.on("close", (code) => {
|
|
16923
|
+
if (code === 0 || code === 1) resolve2(stdout);
|
|
16924
|
+
else reject(new Error(`git diff --no-index failed (code ${code}): ${stderr}`));
|
|
16925
|
+
});
|
|
16926
|
+
});
|
|
16927
|
+
}
|
|
16928
|
+
function rewriteDiffHeaders(raw, filePath) {
|
|
16929
|
+
const lines = raw.split("\n");
|
|
16930
|
+
const out = [];
|
|
16931
|
+
let seenFirstHeader = false;
|
|
16932
|
+
for (const line of lines) {
|
|
16933
|
+
if (line.startsWith("diff --git ")) {
|
|
16934
|
+
out.push(`diff --git a/${filePath} b/${filePath}`);
|
|
16935
|
+
seenFirstHeader = true;
|
|
16936
|
+
continue;
|
|
16937
|
+
}
|
|
16938
|
+
if (!seenFirstHeader) {
|
|
16939
|
+
continue;
|
|
16940
|
+
}
|
|
16941
|
+
if (line.startsWith("--- ")) {
|
|
16942
|
+
out.push(`--- a/${filePath}`);
|
|
16943
|
+
continue;
|
|
16944
|
+
}
|
|
16945
|
+
if (line.startsWith("+++ ")) {
|
|
16946
|
+
out.push(`+++ b/${filePath}`);
|
|
16947
|
+
continue;
|
|
16948
|
+
}
|
|
16949
|
+
out.push(line);
|
|
16950
|
+
}
|
|
16951
|
+
return out.join("\n");
|
|
16952
|
+
}
|
|
16953
|
+
function synthesizeNewFileDiff(file, content) {
|
|
16954
|
+
const lines = content.split("\n");
|
|
16955
|
+
const hasTrailingNewline = content.endsWith("\n");
|
|
16956
|
+
const bodyLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
16957
|
+
const header = [
|
|
16958
|
+
`diff --git a/${file} b/${file}`,
|
|
16959
|
+
"new file mode 100644",
|
|
16960
|
+
"--- /dev/null",
|
|
16961
|
+
`+++ b/${file}`,
|
|
16962
|
+
`@@ -0,0 +1,${bodyLines.length} @@`
|
|
16963
|
+
].join("\n");
|
|
16964
|
+
const body = bodyLines.map((l) => `+${l}`).join("\n");
|
|
16965
|
+
const trailer = hasTrailingNewline ? "" : "\n\";
|
|
16966
|
+
return header + "\n" + body + trailer + "\n";
|
|
16967
|
+
}
|
|
16968
|
+
function synthesizeDeleteFileDiff(file, content) {
|
|
16969
|
+
const lines = content.split("\n");
|
|
16970
|
+
const hasTrailingNewline = content.endsWith("\n");
|
|
16971
|
+
const bodyLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
16972
|
+
const header = [
|
|
16973
|
+
`diff --git a/${file} b/${file}`,
|
|
16974
|
+
"deleted file mode 100644",
|
|
16975
|
+
`--- a/${file}`,
|
|
16976
|
+
"+++ /dev/null",
|
|
16977
|
+
`@@ -1,${bodyLines.length} +0,0 @@`
|
|
16978
|
+
].join("\n");
|
|
16979
|
+
const body = bodyLines.map((l) => `-${l}`).join("\n");
|
|
16980
|
+
const trailer = hasTrailingNewline ? "" : "\n\";
|
|
16981
|
+
return header + "\n" + body + trailer + "\n";
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16505
16984
|
// src/ReplayService.ts
|
|
16506
16985
|
var ReplayService = class {
|
|
16507
16986
|
git;
|
|
@@ -16896,6 +17375,7 @@ var ReplayService = class {
|
|
|
16896
17375
|
if (newPatches.length > 0) {
|
|
16897
17376
|
results = await this.applicator.applyPatches(newPatches);
|
|
16898
17377
|
this._lastApplyResults = results;
|
|
17378
|
+
this.recordDroppedContextLineWarnings(results);
|
|
16899
17379
|
this.revertConflictingFiles(results);
|
|
16900
17380
|
for (const result of results) {
|
|
16901
17381
|
if (result.status === "conflict") {
|
|
@@ -16914,7 +17394,8 @@ var ReplayService = class {
|
|
|
16914
17394
|
if (newPatches.length > 0) {
|
|
16915
17395
|
if (!options?.stageOnly) {
|
|
16916
17396
|
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
16917
|
-
|
|
17397
|
+
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
|
|
17398
|
+
await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
|
|
16918
17399
|
} else {
|
|
16919
17400
|
await this.committer.stageAll();
|
|
16920
17401
|
}
|
|
@@ -16977,12 +17458,21 @@ var ReplayService = class {
|
|
|
16977
17458
|
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
16978
17459
|
);
|
|
16979
17460
|
existingPatches = this.lockManager.getPatches();
|
|
16980
|
-
const seenHashes = /* @__PURE__ */ new
|
|
17461
|
+
const seenHashes = /* @__PURE__ */ new Map();
|
|
16981
17462
|
for (const p of existingPatches) {
|
|
16982
|
-
|
|
16983
|
-
|
|
17463
|
+
const priorOrigin = seenHashes.get(p.content_hash);
|
|
17464
|
+
if (priorOrigin !== void 0) {
|
|
17465
|
+
const provenanceMissing = !priorOrigin || !p.original_commit;
|
|
17466
|
+
const sameOrigin = priorOrigin === p.original_commit;
|
|
17467
|
+
if (sameOrigin && !provenanceMissing) {
|
|
17468
|
+
this.lockManager.removePatch(p.id);
|
|
17469
|
+
} else {
|
|
17470
|
+
this.warnings.push(
|
|
17471
|
+
`Preserving patch ${p.id} (commit ${(p.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
17472
|
+
);
|
|
17473
|
+
}
|
|
16984
17474
|
} else {
|
|
16985
|
-
seenHashes.
|
|
17475
|
+
seenHashes.set(p.content_hash, p.original_commit);
|
|
16986
17476
|
}
|
|
16987
17477
|
}
|
|
16988
17478
|
existingPatches = this.lockManager.getPatches();
|
|
@@ -17043,6 +17533,7 @@ var ReplayService = class {
|
|
|
17043
17533
|
const genSha = prep._prepared.genSha;
|
|
17044
17534
|
const results = await this.applicator.applyPatches(allPatches);
|
|
17045
17535
|
this._lastApplyResults = results;
|
|
17536
|
+
this.recordDroppedContextLineWarnings(results);
|
|
17046
17537
|
this.revertConflictingFiles(results);
|
|
17047
17538
|
for (const result of results) {
|
|
17048
17539
|
if (result.status === "conflict") {
|
|
@@ -17070,7 +17561,8 @@ var ReplayService = class {
|
|
|
17070
17561
|
await this.committer.stageAll();
|
|
17071
17562
|
} else {
|
|
17072
17563
|
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
17073
|
-
|
|
17564
|
+
const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
|
|
17565
|
+
await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
17074
17566
|
}
|
|
17075
17567
|
const warnings = [...detectorWarnings, ...this.warnings];
|
|
17076
17568
|
return this.buildReport(
|
|
@@ -17094,7 +17586,7 @@ var ReplayService = class {
|
|
|
17094
17586
|
let repointed = 0;
|
|
17095
17587
|
let contentRebased = 0;
|
|
17096
17588
|
let keptAsUserOwned = 0;
|
|
17097
|
-
const seenContentHashes = /* @__PURE__ */ new
|
|
17589
|
+
const seenContentHashes = /* @__PURE__ */ new Map();
|
|
17098
17590
|
const absorbedPatchIds = /* @__PURE__ */ new Set();
|
|
17099
17591
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
17100
17592
|
for (const result of results) {
|
|
@@ -17108,6 +17600,23 @@ var ReplayService = class {
|
|
|
17108
17600
|
}
|
|
17109
17601
|
}
|
|
17110
17602
|
}
|
|
17603
|
+
const conflictedFilePaths = /* @__PURE__ */ new Set();
|
|
17604
|
+
for (const r of results) {
|
|
17605
|
+
if (r.status !== "conflict") continue;
|
|
17606
|
+
if (r.fileResults) {
|
|
17607
|
+
for (const fr of r.fileResults) {
|
|
17608
|
+
if (fr.status !== "conflict") continue;
|
|
17609
|
+
conflictedFilePaths.add(fr.file);
|
|
17610
|
+
if (r.resolvedFiles) {
|
|
17611
|
+
for (const [orig, resolved] of Object.entries(r.resolvedFiles)) {
|
|
17612
|
+
if (resolved === fr.file) conflictedFilePaths.add(orig);
|
|
17613
|
+
}
|
|
17614
|
+
}
|
|
17615
|
+
}
|
|
17616
|
+
} else {
|
|
17617
|
+
for (const f of r.patch.files) conflictedFilePaths.add(f);
|
|
17618
|
+
}
|
|
17619
|
+
}
|
|
17111
17620
|
for (const result of results) {
|
|
17112
17621
|
if (result.status === "conflict" && result.fileResults) {
|
|
17113
17622
|
await this.trimAbsorbedFiles(result, currentGenSha);
|
|
@@ -17175,6 +17684,18 @@ var ReplayService = class {
|
|
|
17175
17684
|
keptAsUserOwned++;
|
|
17176
17685
|
continue;
|
|
17177
17686
|
}
|
|
17687
|
+
const overlapsConflicted = patch.files.some((f) => conflictedFilePaths.has(f));
|
|
17688
|
+
if (overlapsConflicted) {
|
|
17689
|
+
patch.status = "unresolved";
|
|
17690
|
+
try {
|
|
17691
|
+
this.lockManager.updatePatch(patch.id, { status: "unresolved" });
|
|
17692
|
+
} catch {
|
|
17693
|
+
}
|
|
17694
|
+
this.warnings.push(
|
|
17695
|
+
`Patch ${patch.id} (${patch.original_message}) was preserved as unresolved because an earlier patch conflicted on the same file(s) (${patch.files.join(", ")}). Run \`fern replay resolve\` to apply this customization manually.`
|
|
17696
|
+
);
|
|
17697
|
+
continue;
|
|
17698
|
+
}
|
|
17178
17699
|
this.lockManager.removePatch(patch.id);
|
|
17179
17700
|
absorbedPatchIds.add(patch.id);
|
|
17180
17701
|
absorbed++;
|
|
@@ -17190,13 +17711,22 @@ var ReplayService = class {
|
|
|
17190
17711
|
continue;
|
|
17191
17712
|
}
|
|
17192
17713
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17714
|
+
const priorOrigin = seenContentHashes.get(newContentHash);
|
|
17715
|
+
if (priorOrigin !== void 0) {
|
|
17716
|
+
const provenanceMissing = !priorOrigin || !patch.original_commit;
|
|
17717
|
+
const sameOrigin = priorOrigin === patch.original_commit;
|
|
17718
|
+
if (sameOrigin && !provenanceMissing) {
|
|
17719
|
+
this.lockManager.removePatch(patch.id);
|
|
17720
|
+
absorbedPatchIds.add(patch.id);
|
|
17721
|
+
absorbed++;
|
|
17722
|
+
continue;
|
|
17723
|
+
}
|
|
17724
|
+
this.warnings.push(
|
|
17725
|
+
`Preserving patch ${patch.id} (commit ${(patch.original_commit || "<missing>").slice(0, 7)}): shares content_hash with a prior patch ${provenanceMissing ? "(provenance missing on at least one side)" : "from a different source commit"}. Both retained to avoid silent customization loss (FER-9983).`
|
|
17726
|
+
);
|
|
17727
|
+
} else {
|
|
17728
|
+
seenContentHashes.set(newContentHash, patch.original_commit);
|
|
17198
17729
|
}
|
|
17199
|
-
seenContentHashes.add(newContentHash);
|
|
17200
17730
|
patch.base_generation = currentGenSha;
|
|
17201
17731
|
patch.patch_content = diff;
|
|
17202
17732
|
patch.content_hash = newContentHash;
|
|
@@ -17372,8 +17902,14 @@ var ReplayService = class {
|
|
|
17372
17902
|
}
|
|
17373
17903
|
if (patch.base_generation === currentGen) {
|
|
17374
17904
|
try {
|
|
17375
|
-
const
|
|
17376
|
-
|
|
17905
|
+
const ppResult = await computePerPatchDiffWithFallback(
|
|
17906
|
+
patch,
|
|
17907
|
+
(f) => this.git.showFile(currentGen, f),
|
|
17908
|
+
(f) => this.git.showFile("HEAD", f),
|
|
17909
|
+
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
17910
|
+
);
|
|
17911
|
+
if (ppResult === null) continue;
|
|
17912
|
+
const diff = ppResult.diff;
|
|
17377
17913
|
if (!diff.trim()) {
|
|
17378
17914
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
17379
17915
|
this.lockManager.removePatch(patch.id);
|
|
@@ -17415,8 +17951,14 @@ var ReplayService = class {
|
|
|
17415
17951
|
try {
|
|
17416
17952
|
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
17417
17953
|
if (markerFiles.trim()) continue;
|
|
17418
|
-
const
|
|
17419
|
-
|
|
17954
|
+
const ppResult = await computePerPatchDiffWithFallback(
|
|
17955
|
+
patch,
|
|
17956
|
+
(f) => this.git.showFile(currentGen, f),
|
|
17957
|
+
(f) => this.git.showFile("HEAD", f),
|
|
17958
|
+
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
17959
|
+
);
|
|
17960
|
+
if (ppResult === null) continue;
|
|
17961
|
+
const diff = ppResult.diff;
|
|
17420
17962
|
if (!diff.trim()) {
|
|
17421
17963
|
if (await allPatchFilesUserOwned(patch)) {
|
|
17422
17964
|
try {
|
|
@@ -17441,6 +17983,32 @@ var ReplayService = class {
|
|
|
17441
17983
|
}
|
|
17442
17984
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
17443
17985
|
}
|
|
17986
|
+
/**
|
|
17987
|
+
* After applyPatches(), surface a warning for each (patch × file) where
|
|
17988
|
+
* diff3 silently dropped lines that were unchanged context in the
|
|
17989
|
+
* customer's THEIRS. The deletion stays on disk (correct diff3 outcome),
|
|
17990
|
+
* but the customer must learn so they can re-add the lines if they
|
|
17991
|
+
* relied on them. This is the "surface or preserve, never silently drop"
|
|
17992
|
+
* contract for legitimate-deletion cases.
|
|
17993
|
+
*/
|
|
17994
|
+
recordDroppedContextLineWarnings(results) {
|
|
17995
|
+
const PREVIEW_COUNT = 5;
|
|
17996
|
+
for (const result of results) {
|
|
17997
|
+
if (!result.fileResults) continue;
|
|
17998
|
+
for (const fr of result.fileResults) {
|
|
17999
|
+
const dropped = fr.droppedContextLines;
|
|
18000
|
+
if (!dropped || dropped.length === 0) continue;
|
|
18001
|
+
const preview = dropped.slice(0, PREVIEW_COUNT).map((line) => ` ${line}`).join("\n");
|
|
18002
|
+
const more = dropped.length > PREVIEW_COUNT ? `
|
|
18003
|
+
... and ${dropped.length - PREVIEW_COUNT} more` : "";
|
|
18004
|
+
this.warnings.push(
|
|
18005
|
+
`${fr.file}: ${dropped.length} line(s) that appeared as unchanged context in your customization were deleted by the new generator output. The merge followed the deletion. First lines deleted:
|
|
18006
|
+
${preview}${more}
|
|
18007
|
+
If you want to keep these lines, restore them in a follow-up commit and re-run replay; otherwise this warning can be safely ignored.`
|
|
18008
|
+
);
|
|
18009
|
+
}
|
|
18010
|
+
}
|
|
18011
|
+
}
|
|
17444
18012
|
/**
|
|
17445
18013
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
17446
18014
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -17450,7 +18018,7 @@ var ReplayService = class {
|
|
|
17450
18018
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
17451
18019
|
for (const fileResult of result.fileResults) {
|
|
17452
18020
|
if (fileResult.status !== "conflict") continue;
|
|
17453
|
-
const filePath = (0,
|
|
18021
|
+
const filePath = (0, import_node_path5.join)(this.outputDir, fileResult.file);
|
|
17454
18022
|
try {
|
|
17455
18023
|
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
17456
18024
|
const stripped = stripConflictMarkers(content);
|
|
@@ -17480,7 +18048,7 @@ var ReplayService = class {
|
|
|
17480
18048
|
}
|
|
17481
18049
|
}
|
|
17482
18050
|
readFernignorePatterns() {
|
|
17483
|
-
const fernignorePath = (0,
|
|
18051
|
+
const fernignorePath = (0, import_node_path5.join)(this.outputDir, ".fernignore");
|
|
17484
18052
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
17485
18053
|
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
17486
18054
|
}
|
|
@@ -17498,6 +18066,9 @@ var ReplayService = class {
|
|
|
17498
18066
|
};
|
|
17499
18067
|
}).filter((d) => d.files.length > 0);
|
|
17500
18068
|
const partialCount = conflictDetails.filter((d) => d.cleanFiles && d.cleanFiles.length > 0).length;
|
|
18069
|
+
const unresolvedResults = results.filter(
|
|
18070
|
+
(r) => r.status === "conflict" || r.patch.status === "unresolved"
|
|
18071
|
+
);
|
|
17501
18072
|
return {
|
|
17502
18073
|
flow,
|
|
17503
18074
|
patchesDetected: patches.length,
|
|
@@ -17514,7 +18085,7 @@ var ReplayService = class {
|
|
|
17514
18085
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
17515
18086
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
17516
18087
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
17517
|
-
unresolvedPatches:
|
|
18088
|
+
unresolvedPatches: unresolvedResults.length > 0 ? unresolvedResults.map((r) => ({
|
|
17518
18089
|
patchId: r.patch.id,
|
|
17519
18090
|
patchMessage: r.patch.original_message,
|
|
17520
18091
|
files: r.patch.files,
|
|
@@ -17525,11 +18096,38 @@ var ReplayService = class {
|
|
|
17525
18096
|
};
|
|
17526
18097
|
}
|
|
17527
18098
|
};
|
|
18099
|
+
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
18100
|
+
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
18101
|
+
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
18102
|
+
const applied = [];
|
|
18103
|
+
const unresolved = [];
|
|
18104
|
+
const absorbed = [];
|
|
18105
|
+
for (const patch of patchesPassedToCommit) {
|
|
18106
|
+
if (absorbedPatchIds.has(patch.id)) {
|
|
18107
|
+
absorbed.push(patch);
|
|
18108
|
+
continue;
|
|
18109
|
+
}
|
|
18110
|
+
const r = resultByPatchId.get(patch.id);
|
|
18111
|
+
if (!r) {
|
|
18112
|
+
applied.push(patch);
|
|
18113
|
+
continue;
|
|
18114
|
+
}
|
|
18115
|
+
if (r.status === "conflict" || patch.status === "unresolved") {
|
|
18116
|
+
unresolved.push(patch);
|
|
18117
|
+
continue;
|
|
18118
|
+
}
|
|
18119
|
+
if (r.status === "applied") {
|
|
18120
|
+
applied.push(patch);
|
|
18121
|
+
continue;
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
return { applied, unresolved, absorbed };
|
|
18125
|
+
}
|
|
17528
18126
|
|
|
17529
18127
|
// src/FernignoreMigrator.ts
|
|
17530
18128
|
var import_node_crypto2 = require("crypto");
|
|
17531
18129
|
var import_node_fs3 = require("fs");
|
|
17532
|
-
var
|
|
18130
|
+
var import_node_path6 = require("path");
|
|
17533
18131
|
var import_yaml2 = __toESM(require_dist3(), 1);
|
|
17534
18132
|
var FernignoreMigrator = class {
|
|
17535
18133
|
git;
|
|
@@ -17541,10 +18139,10 @@ var FernignoreMigrator = class {
|
|
|
17541
18139
|
this.outputDir = outputDir;
|
|
17542
18140
|
}
|
|
17543
18141
|
fernignoreExists() {
|
|
17544
|
-
return (0, import_node_fs3.existsSync)((0,
|
|
18142
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path6.join)(this.outputDir, ".fernignore"));
|
|
17545
18143
|
}
|
|
17546
18144
|
readFernignorePatterns() {
|
|
17547
|
-
const fernignorePath = (0,
|
|
18145
|
+
const fernignorePath = (0, import_node_path6.join)(this.outputDir, ".fernignore");
|
|
17548
18146
|
if (!(0, import_node_fs3.existsSync)(fernignorePath)) {
|
|
17549
18147
|
return [];
|
|
17550
18148
|
}
|
|
@@ -17604,7 +18202,16 @@ var FernignoreMigrator = class {
|
|
|
17604
18202
|
original_author: "Fern Replay <replay@buildwithfern.com>",
|
|
17605
18203
|
base_generation: currentGen.commit_sha,
|
|
17606
18204
|
files: patchFiles,
|
|
17607
|
-
patch_content: patchContent
|
|
18205
|
+
patch_content: patchContent,
|
|
18206
|
+
// Synthetic patches captured from .fernignore-protected files
|
|
18207
|
+
// are user-owned by definition — the customer's content
|
|
18208
|
+
// diverges from the generator's pristine output, and the
|
|
18209
|
+
// generator never produced this divergent content. Without
|
|
18210
|
+
// this flag, removing the file from .fernignore later (so
|
|
18211
|
+
// it leaves the protection check in `isFileUserOwned`) would
|
|
18212
|
+
// expose the patch to absorption on the next regen,
|
|
18213
|
+
// silently losing the customization.
|
|
18214
|
+
user_owned: true
|
|
17608
18215
|
});
|
|
17609
18216
|
trackedByBoth.push({
|
|
17610
18217
|
file: pattern,
|
|
@@ -17637,7 +18244,7 @@ var FernignoreMigrator = class {
|
|
|
17637
18244
|
return results;
|
|
17638
18245
|
}
|
|
17639
18246
|
readFileContent(filePath) {
|
|
17640
|
-
const fullPath = (0,
|
|
18247
|
+
const fullPath = (0, import_node_path6.join)(this.outputDir, filePath);
|
|
17641
18248
|
if (!(0, import_node_fs3.existsSync)(fullPath)) {
|
|
17642
18249
|
return null;
|
|
17643
18250
|
}
|
|
@@ -17702,7 +18309,7 @@ var FernignoreMigrator = class {
|
|
|
17702
18309
|
};
|
|
17703
18310
|
}
|
|
17704
18311
|
movePatternsToReplayYml(patterns) {
|
|
17705
|
-
const replayYmlPath = (0,
|
|
18312
|
+
const replayYmlPath = (0, import_node_path6.join)(this.outputDir, ".fern", "replay.yml");
|
|
17706
18313
|
let config = {};
|
|
17707
18314
|
if ((0, import_node_fs3.existsSync)(replayYmlPath)) {
|
|
17708
18315
|
const content = (0, import_node_fs3.readFileSync)(replayYmlPath, "utf-8");
|
|
@@ -17711,7 +18318,7 @@ var FernignoreMigrator = class {
|
|
|
17711
18318
|
const existing = config.exclude ?? [];
|
|
17712
18319
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
17713
18320
|
config.exclude = merged;
|
|
17714
|
-
const dir = (0,
|
|
18321
|
+
const dir = (0, import_node_path6.dirname)(replayYmlPath);
|
|
17715
18322
|
if (!(0, import_node_fs3.existsSync)(dir)) {
|
|
17716
18323
|
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
17717
18324
|
}
|
|
@@ -17722,7 +18329,7 @@ var FernignoreMigrator = class {
|
|
|
17722
18329
|
// src/commands/bootstrap.ts
|
|
17723
18330
|
var import_node_crypto3 = require("crypto");
|
|
17724
18331
|
var import_node_fs4 = require("fs");
|
|
17725
|
-
var
|
|
18332
|
+
var import_node_path7 = require("path");
|
|
17726
18333
|
init_GitClient();
|
|
17727
18334
|
async function bootstrap(outputDir, options) {
|
|
17728
18335
|
const git = new GitClient(outputDir);
|
|
@@ -17889,7 +18496,7 @@ function parseGitLog(log) {
|
|
|
17889
18496
|
}
|
|
17890
18497
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
17891
18498
|
function ensureFernignoreEntries(outputDir) {
|
|
17892
|
-
const fernignorePath = (0,
|
|
18499
|
+
const fernignorePath = (0, import_node_path7.join)(outputDir, ".fernignore");
|
|
17893
18500
|
let content = "";
|
|
17894
18501
|
if ((0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
17895
18502
|
content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
@@ -17913,7 +18520,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
17913
18520
|
}
|
|
17914
18521
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
17915
18522
|
function ensureGitattributesEntries(outputDir) {
|
|
17916
|
-
const gitattributesPath = (0,
|
|
18523
|
+
const gitattributesPath = (0, import_node_path7.join)(outputDir, ".gitattributes");
|
|
17917
18524
|
let content = "";
|
|
17918
18525
|
if ((0, import_node_fs4.existsSync)(gitattributesPath)) {
|
|
17919
18526
|
content = (0, import_node_fs4.readFileSync)(gitattributesPath, "utf-8");
|
|
@@ -18110,6 +18717,8 @@ function reset(outputDir, options) {
|
|
|
18110
18717
|
}
|
|
18111
18718
|
|
|
18112
18719
|
// src/commands/resolve.ts
|
|
18720
|
+
var import_promises3 = require("fs/promises");
|
|
18721
|
+
var import_node_path8 = require("path");
|
|
18113
18722
|
init_GitClient();
|
|
18114
18723
|
async function resolve(outputDir, options) {
|
|
18115
18724
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -18125,7 +18734,7 @@ async function resolve(outputDir, options) {
|
|
|
18125
18734
|
const resolvingPatches = lockManager.getResolvingPatches();
|
|
18126
18735
|
if (unresolvedPatches.length > 0) {
|
|
18127
18736
|
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
18128
|
-
await applicator.applyPatches(unresolvedPatches);
|
|
18737
|
+
await applicator.applyPatches(unresolvedPatches, { applyMode: "resolve" });
|
|
18129
18738
|
const markerFiles = await findConflictMarkerFiles(git);
|
|
18130
18739
|
if (markerFiles.length > 0) {
|
|
18131
18740
|
for (const patch of unresolvedPatches) {
|
|
@@ -18151,20 +18760,38 @@ async function resolve(outputDir, options) {
|
|
|
18151
18760
|
if (patchesToCommit.length > 0) {
|
|
18152
18761
|
const currentGen = lock.current_generation;
|
|
18153
18762
|
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
18763
|
+
const warnings = [];
|
|
18154
18764
|
let patchesResolved = 0;
|
|
18155
18765
|
for (const patch of patchesToCommit) {
|
|
18156
|
-
const
|
|
18157
|
-
|
|
18766
|
+
const result = await computePerPatchDiffWithFallback(
|
|
18767
|
+
patch,
|
|
18768
|
+
(f) => git.showFile(currentGen, f),
|
|
18769
|
+
(f) => (0, import_promises3.readFile)((0, import_node_path8.join)(outputDir, f), "utf-8").catch(() => null),
|
|
18770
|
+
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
18771
|
+
);
|
|
18772
|
+
if (result === null) {
|
|
18773
|
+
warnings.push(
|
|
18774
|
+
`Patch ${patch.id}: cumulative-diff fallback failed for unlocatable files; preserving patch unchanged`
|
|
18775
|
+
);
|
|
18776
|
+
continue;
|
|
18777
|
+
}
|
|
18778
|
+
const diff = result.diff;
|
|
18779
|
+
if (!diff.trim()) {
|
|
18158
18780
|
lockManager.removePatch(patch.id);
|
|
18159
18781
|
continue;
|
|
18160
18782
|
}
|
|
18783
|
+
if (result.hadFallback) {
|
|
18784
|
+
warnings.push(
|
|
18785
|
+
`Patch ${patch.id}: ${result.fallbackFiles.join(", ")} could not be anchored; cumulative-diff fallback applied for those files`
|
|
18786
|
+
);
|
|
18787
|
+
}
|
|
18161
18788
|
const newContentHash = detector.computeContentHash(diff);
|
|
18162
|
-
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
18789
|
+
const changedFiles = result.hadFallback ? await getChangedFiles(git, currentGen, patch.files) : extractFilesFromDiff(diff);
|
|
18163
18790
|
lockManager.markPatchResolved(patch.id, {
|
|
18164
18791
|
patch_content: diff,
|
|
18165
18792
|
content_hash: newContentHash,
|
|
18166
18793
|
base_generation: currentGen,
|
|
18167
|
-
files: changedFiles
|
|
18794
|
+
files: changedFiles.length > 0 ? changedFiles : patch.files
|
|
18168
18795
|
});
|
|
18169
18796
|
patchesResolved++;
|
|
18170
18797
|
}
|
|
@@ -18180,7 +18807,8 @@ async function resolve(outputDir, options) {
|
|
|
18180
18807
|
success: true,
|
|
18181
18808
|
commitSha: commitSha2,
|
|
18182
18809
|
phase: "committed",
|
|
18183
|
-
patchesResolved
|
|
18810
|
+
patchesResolved,
|
|
18811
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
18184
18812
|
};
|
|
18185
18813
|
}
|
|
18186
18814
|
const committer = new ReplayCommitter(git, outputDir);
|
|
@@ -18198,6 +18826,24 @@ async function getChangedFiles(git, currentGen, files) {
|
|
|
18198
18826
|
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
18199
18827
|
return changed.length > 0 ? changed : files;
|
|
18200
18828
|
}
|
|
18829
|
+
function extractFilesFromDiff(unifiedDiff2) {
|
|
18830
|
+
const out = /* @__PURE__ */ new Set();
|
|
18831
|
+
const renameSources = /* @__PURE__ */ new Set();
|
|
18832
|
+
let currentTarget = null;
|
|
18833
|
+
for (const line of unifiedDiff2.split("\n")) {
|
|
18834
|
+
const headerMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
18835
|
+
if (headerMatch) {
|
|
18836
|
+
currentTarget = headerMatch[2];
|
|
18837
|
+
if (!currentTarget.startsWith(".fern/")) out.add(currentTarget);
|
|
18838
|
+
continue;
|
|
18839
|
+
}
|
|
18840
|
+
if (currentTarget && line.startsWith("rename from ")) {
|
|
18841
|
+
renameSources.add(line.slice("rename from ".length));
|
|
18842
|
+
}
|
|
18843
|
+
}
|
|
18844
|
+
for (const src of renameSources) out.delete(src);
|
|
18845
|
+
return Array.from(out);
|
|
18846
|
+
}
|
|
18201
18847
|
|
|
18202
18848
|
// src/commands/status.ts
|
|
18203
18849
|
function status(outputDir) {
|
|
@@ -18332,7 +18978,7 @@ function parseArgs(argv) {
|
|
|
18332
18978
|
positionals.push(arg);
|
|
18333
18979
|
}
|
|
18334
18980
|
}
|
|
18335
|
-
return { command, dir: (0,
|
|
18981
|
+
return { command, dir: (0, import_node_path9.resolve)(dir), flags, positionals };
|
|
18336
18982
|
}
|
|
18337
18983
|
async function runBootstrap(dir, flags) {
|
|
18338
18984
|
const dryRun = !!flags.dryRun;
|