@fern-api/replay 0.6.2 → 0.8.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 +354 -48
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +329 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -7
- package/dist/index.d.ts +50 -7
- package/dist/index.js +332 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -5335,6 +5335,9 @@ var init_GitClient = __esm({
|
|
|
5335
5335
|
return false;
|
|
5336
5336
|
}
|
|
5337
5337
|
}
|
|
5338
|
+
async getCommitBody(commitSha) {
|
|
5339
|
+
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
5340
|
+
}
|
|
5338
5341
|
getRepoPath() {
|
|
5339
5342
|
return this.repoPath;
|
|
5340
5343
|
}
|
|
@@ -12935,6 +12938,26 @@ var LockfileManager = class {
|
|
|
12935
12938
|
this.ensureLoaded();
|
|
12936
12939
|
this.lock.patches = [];
|
|
12937
12940
|
}
|
|
12941
|
+
getUnresolvedPatches() {
|
|
12942
|
+
this.ensureLoaded();
|
|
12943
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
12944
|
+
}
|
|
12945
|
+
getResolvingPatches() {
|
|
12946
|
+
this.ensureLoaded();
|
|
12947
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
12948
|
+
}
|
|
12949
|
+
markPatchUnresolved(patchId) {
|
|
12950
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
12951
|
+
}
|
|
12952
|
+
markPatchResolved(patchId, updates) {
|
|
12953
|
+
this.ensureLoaded();
|
|
12954
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
12955
|
+
if (!patch) {
|
|
12956
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
12957
|
+
}
|
|
12958
|
+
delete patch.status;
|
|
12959
|
+
Object.assign(patch, updates);
|
|
12960
|
+
}
|
|
12938
12961
|
getPatches() {
|
|
12939
12962
|
this.ensureLoaded();
|
|
12940
12963
|
return this.lock.patches;
|
|
@@ -12989,6 +13012,17 @@ function isGenerationCommit(commit) {
|
|
|
12989
13012
|
function isReplayCommit(commit) {
|
|
12990
13013
|
return commit.message.startsWith("[fern-replay]");
|
|
12991
13014
|
}
|
|
13015
|
+
function isRevertCommit(message) {
|
|
13016
|
+
return message.startsWith('Revert "');
|
|
13017
|
+
}
|
|
13018
|
+
function parseRevertedSha(fullBody) {
|
|
13019
|
+
const match2 = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
|
|
13020
|
+
return match2?.[1];
|
|
13021
|
+
}
|
|
13022
|
+
function parseRevertedMessage(subject) {
|
|
13023
|
+
const match2 = subject.match(/^Revert "(.+)"$/);
|
|
13024
|
+
return match2?.[1];
|
|
13025
|
+
}
|
|
12992
13026
|
|
|
12993
13027
|
// src/ReplayDetector.ts
|
|
12994
13028
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
@@ -13006,14 +13040,14 @@ var ReplayDetector = class {
|
|
|
13006
13040
|
const lock = this.lockManager.read();
|
|
13007
13041
|
const lastGen = this.getLastGeneration(lock);
|
|
13008
13042
|
if (!lastGen) {
|
|
13009
|
-
return [];
|
|
13043
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13010
13044
|
}
|
|
13011
13045
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
13012
13046
|
if (!exists2) {
|
|
13013
13047
|
this.warnings.push(
|
|
13014
13048
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
|
|
13015
13049
|
);
|
|
13016
|
-
return [];
|
|
13050
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13017
13051
|
}
|
|
13018
13052
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
13019
13053
|
if (!isAncestor) {
|
|
@@ -13027,7 +13061,7 @@ var ReplayDetector = class {
|
|
|
13027
13061
|
this.sdkOutputDir
|
|
13028
13062
|
]);
|
|
13029
13063
|
if (!log.trim()) {
|
|
13030
|
-
return [];
|
|
13064
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13031
13065
|
}
|
|
13032
13066
|
const commits = this.parseGitLog(log);
|
|
13033
13067
|
const newPatches = [];
|
|
@@ -13063,7 +13097,60 @@ var ReplayDetector = class {
|
|
|
13063
13097
|
patch_content: patchContent
|
|
13064
13098
|
});
|
|
13065
13099
|
}
|
|
13066
|
-
|
|
13100
|
+
newPatches.reverse();
|
|
13101
|
+
const revertedPatchIdSet = /* @__PURE__ */ new Set();
|
|
13102
|
+
const revertIndicesToRemove = /* @__PURE__ */ new Set();
|
|
13103
|
+
for (let i = 0; i < newPatches.length; i++) {
|
|
13104
|
+
const patch = newPatches[i];
|
|
13105
|
+
if (!isRevertCommit(patch.original_message)) continue;
|
|
13106
|
+
let body = "";
|
|
13107
|
+
try {
|
|
13108
|
+
body = await this.git.getCommitBody(patch.original_commit);
|
|
13109
|
+
} catch {
|
|
13110
|
+
}
|
|
13111
|
+
const revertedSha = parseRevertedSha(body);
|
|
13112
|
+
const revertedMessage = parseRevertedMessage(patch.original_message);
|
|
13113
|
+
let matchedExisting = false;
|
|
13114
|
+
if (revertedSha) {
|
|
13115
|
+
const existing = lock.patches.find((p) => p.original_commit === revertedSha);
|
|
13116
|
+
if (existing) {
|
|
13117
|
+
revertedPatchIdSet.add(existing.id);
|
|
13118
|
+
revertIndicesToRemove.add(i);
|
|
13119
|
+
matchedExisting = true;
|
|
13120
|
+
}
|
|
13121
|
+
}
|
|
13122
|
+
if (!matchedExisting && revertedMessage) {
|
|
13123
|
+
const existing = lock.patches.find((p) => p.original_message === revertedMessage);
|
|
13124
|
+
if (existing) {
|
|
13125
|
+
revertedPatchIdSet.add(existing.id);
|
|
13126
|
+
revertIndicesToRemove.add(i);
|
|
13127
|
+
matchedExisting = true;
|
|
13128
|
+
}
|
|
13129
|
+
}
|
|
13130
|
+
if (matchedExisting) continue;
|
|
13131
|
+
let matchedNew = false;
|
|
13132
|
+
if (revertedSha) {
|
|
13133
|
+
const idx = newPatches.findIndex(
|
|
13134
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
|
|
13135
|
+
);
|
|
13136
|
+
if (idx !== -1) {
|
|
13137
|
+
revertIndicesToRemove.add(i);
|
|
13138
|
+
revertIndicesToRemove.add(idx);
|
|
13139
|
+
matchedNew = true;
|
|
13140
|
+
}
|
|
13141
|
+
}
|
|
13142
|
+
if (!matchedNew && revertedMessage) {
|
|
13143
|
+
const idx = newPatches.findIndex(
|
|
13144
|
+
(p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
|
|
13145
|
+
);
|
|
13146
|
+
if (idx !== -1) {
|
|
13147
|
+
revertIndicesToRemove.add(i);
|
|
13148
|
+
revertIndicesToRemove.add(idx);
|
|
13149
|
+
}
|
|
13150
|
+
}
|
|
13151
|
+
}
|
|
13152
|
+
const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
13153
|
+
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
13067
13154
|
}
|
|
13068
13155
|
/**
|
|
13069
13156
|
* Compute content hash for deduplication.
|
|
@@ -13074,31 +13161,34 @@ var ReplayDetector = class {
|
|
|
13074
13161
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
13075
13162
|
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
13076
13163
|
}
|
|
13077
|
-
/**
|
|
13164
|
+
/**
|
|
13165
|
+
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
13166
|
+
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
13167
|
+
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
13168
|
+
*/
|
|
13078
13169
|
async detectPatchesViaTreeDiff(lastGen) {
|
|
13079
13170
|
const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
|
|
13080
13171
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
13081
|
-
if (files.length === 0) return [];
|
|
13172
|
+
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
13082
13173
|
const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
|
|
13083
|
-
if (!diff.trim()) return [];
|
|
13174
|
+
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
13084
13175
|
const contentHash = this.computeContentHash(diff);
|
|
13085
13176
|
const lock = this.lockManager.read();
|
|
13086
13177
|
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
13087
|
-
return [];
|
|
13178
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13088
13179
|
}
|
|
13089
13180
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
13090
|
-
|
|
13091
|
-
{
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
];
|
|
13181
|
+
const compositePatch = {
|
|
13182
|
+
id: `patch-composite-${headSha.slice(0, 8)}`,
|
|
13183
|
+
content_hash: contentHash,
|
|
13184
|
+
original_commit: headSha,
|
|
13185
|
+
original_message: "Customer customizations (composite)",
|
|
13186
|
+
original_author: "composite",
|
|
13187
|
+
base_generation: lastGen.commit_sha,
|
|
13188
|
+
files,
|
|
13189
|
+
patch_content: diff
|
|
13190
|
+
};
|
|
13191
|
+
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
13102
13192
|
}
|
|
13103
13193
|
parseGitLog(log) {
|
|
13104
13194
|
return log.trim().split("\n").map((line) => {
|
|
@@ -15206,12 +15296,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
15206
15296
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
15207
15297
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
15208
15298
|
}
|
|
15209
|
-
async commitReplay(_patchCount, patches) {
|
|
15299
|
+
async commitReplay(_patchCount, patches, message) {
|
|
15210
15300
|
await this.stageAll();
|
|
15211
15301
|
if (!await this.hasStagedChanges()) {
|
|
15212
15302
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
15213
15303
|
}
|
|
15214
|
-
let fullMessage = `[fern-replay] Applied customizations`;
|
|
15304
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
15215
15305
|
if (patches && patches.length > 0) {
|
|
15216
15306
|
fullMessage += "\n\nPatches replayed:";
|
|
15217
15307
|
for (const patch of patches) {
|
|
@@ -15246,6 +15336,34 @@ CLI Version: ${options.cliVersion}`;
|
|
|
15246
15336
|
}
|
|
15247
15337
|
};
|
|
15248
15338
|
|
|
15339
|
+
// src/conflict-utils.ts
|
|
15340
|
+
function stripConflictMarkers(content) {
|
|
15341
|
+
const lines = content.split("\n");
|
|
15342
|
+
const result = [];
|
|
15343
|
+
let inConflict = false;
|
|
15344
|
+
let inOurs = false;
|
|
15345
|
+
for (const line of lines) {
|
|
15346
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
15347
|
+
inConflict = true;
|
|
15348
|
+
inOurs = true;
|
|
15349
|
+
continue;
|
|
15350
|
+
}
|
|
15351
|
+
if (inConflict && line === "=======") {
|
|
15352
|
+
inOurs = false;
|
|
15353
|
+
continue;
|
|
15354
|
+
}
|
|
15355
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
15356
|
+
inConflict = false;
|
|
15357
|
+
inOurs = false;
|
|
15358
|
+
continue;
|
|
15359
|
+
}
|
|
15360
|
+
if (!inConflict || inOurs) {
|
|
15361
|
+
result.push(line);
|
|
15362
|
+
}
|
|
15363
|
+
}
|
|
15364
|
+
return result.join("\n");
|
|
15365
|
+
}
|
|
15366
|
+
|
|
15249
15367
|
// src/ReplayService.ts
|
|
15250
15368
|
var ReplayService = class {
|
|
15251
15369
|
git;
|
|
@@ -15302,13 +15420,27 @@ var ReplayService = class {
|
|
|
15302
15420
|
this.lockManager.initializeInMemory(record);
|
|
15303
15421
|
} else {
|
|
15304
15422
|
this.lockManager.read();
|
|
15423
|
+
const unresolvedPatches = [
|
|
15424
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
15425
|
+
...this.lockManager.getResolvingPatches()
|
|
15426
|
+
];
|
|
15305
15427
|
this.lockManager.addGeneration(record);
|
|
15306
15428
|
this.lockManager.clearPatches();
|
|
15429
|
+
for (const patch of unresolvedPatches) {
|
|
15430
|
+
this.lockManager.addPatch(patch);
|
|
15431
|
+
}
|
|
15307
15432
|
}
|
|
15308
15433
|
this.lockManager.save();
|
|
15309
15434
|
try {
|
|
15310
|
-
const redetectedPatches = await this.detector.detectNewPatches();
|
|
15435
|
+
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
15311
15436
|
if (redetectedPatches.length > 0) {
|
|
15437
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
15438
|
+
const currentPatches = this.lockManager.getPatches();
|
|
15439
|
+
for (const patch of currentPatches) {
|
|
15440
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
15441
|
+
this.lockManager.removePatch(patch.id);
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
15312
15444
|
for (const patch of redetectedPatches) {
|
|
15313
15445
|
this.lockManager.addPatch(patch);
|
|
15314
15446
|
}
|
|
@@ -15391,7 +15523,7 @@ var ReplayService = class {
|
|
|
15391
15523
|
};
|
|
15392
15524
|
}
|
|
15393
15525
|
async handleNoPatchesRegeneration(options) {
|
|
15394
|
-
const newPatches = await this.detector.detectNewPatches();
|
|
15526
|
+
const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
15395
15527
|
const warnings = [...this.detector.warnings];
|
|
15396
15528
|
if (options?.dryRun) {
|
|
15397
15529
|
return {
|
|
@@ -15400,11 +15532,18 @@ var ReplayService = class {
|
|
|
15400
15532
|
patchesApplied: 0,
|
|
15401
15533
|
patchesWithConflicts: 0,
|
|
15402
15534
|
patchesSkipped: 0,
|
|
15535
|
+
patchesReverted: revertedPatchIds.length,
|
|
15403
15536
|
conflicts: [],
|
|
15404
15537
|
wouldApply: newPatches,
|
|
15405
15538
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
15406
15539
|
};
|
|
15407
15540
|
}
|
|
15541
|
+
for (const id of revertedPatchIds) {
|
|
15542
|
+
try {
|
|
15543
|
+
this.lockManager.removePatch(id);
|
|
15544
|
+
} catch {
|
|
15545
|
+
}
|
|
15546
|
+
}
|
|
15408
15547
|
const commitOpts = options ? {
|
|
15409
15548
|
cliVersion: options.cliVersion ?? "unknown",
|
|
15410
15549
|
generatorVersions: options.generatorVersions ?? {},
|
|
@@ -15416,6 +15555,12 @@ var ReplayService = class {
|
|
|
15416
15555
|
let results = [];
|
|
15417
15556
|
if (newPatches.length > 0) {
|
|
15418
15557
|
results = await this.applicator.applyPatches(newPatches);
|
|
15558
|
+
this.revertConflictingFiles(results);
|
|
15559
|
+
for (const result of results) {
|
|
15560
|
+
if (result.status === "conflict") {
|
|
15561
|
+
result.patch.status = "unresolved";
|
|
15562
|
+
}
|
|
15563
|
+
}
|
|
15419
15564
|
}
|
|
15420
15565
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
15421
15566
|
for (const patch of newPatches) {
|
|
@@ -15426,20 +15571,18 @@ var ReplayService = class {
|
|
|
15426
15571
|
this.lockManager.save();
|
|
15427
15572
|
if (newPatches.length > 0) {
|
|
15428
15573
|
if (!options?.stageOnly) {
|
|
15429
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
15430
|
-
|
|
15431
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
15432
|
-
}
|
|
15574
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
15575
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
15433
15576
|
} else {
|
|
15434
15577
|
await this.committer.stageAll();
|
|
15435
15578
|
}
|
|
15436
15579
|
}
|
|
15437
|
-
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
|
|
15580
|
+
return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
|
|
15438
15581
|
}
|
|
15439
15582
|
async handleNormalRegeneration(options) {
|
|
15440
15583
|
if (options?.dryRun) {
|
|
15441
15584
|
const existingPatches2 = this.lockManager.getPatches();
|
|
15442
|
-
const newPatches2 = await this.detector.detectNewPatches();
|
|
15585
|
+
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
|
|
15443
15586
|
const warnings2 = [...this.detector.warnings];
|
|
15444
15587
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
15445
15588
|
return {
|
|
@@ -15448,13 +15591,19 @@ var ReplayService = class {
|
|
|
15448
15591
|
patchesApplied: 0,
|
|
15449
15592
|
patchesWithConflicts: 0,
|
|
15450
15593
|
patchesSkipped: 0,
|
|
15594
|
+
patchesReverted: dryRunReverted.length,
|
|
15451
15595
|
conflicts: [],
|
|
15452
15596
|
wouldApply: allPatches2,
|
|
15453
15597
|
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
15454
15598
|
};
|
|
15455
15599
|
}
|
|
15456
15600
|
let existingPatches = this.lockManager.getPatches();
|
|
15601
|
+
const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
|
|
15457
15602
|
const preRebaseCounts = await this.preGenerationRebase(existingPatches);
|
|
15603
|
+
const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
|
|
15604
|
+
const removedByPreRebase = existingPatches.filter(
|
|
15605
|
+
(p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
|
|
15606
|
+
);
|
|
15458
15607
|
existingPatches = this.lockManager.getPatches();
|
|
15459
15608
|
const seenHashes = /* @__PURE__ */ new Set();
|
|
15460
15609
|
for (const p of existingPatches) {
|
|
@@ -15465,8 +15614,28 @@ var ReplayService = class {
|
|
|
15465
15614
|
}
|
|
15466
15615
|
}
|
|
15467
15616
|
existingPatches = this.lockManager.getPatches();
|
|
15468
|
-
|
|
15617
|
+
let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
|
|
15469
15618
|
const warnings = [...this.detector.warnings];
|
|
15619
|
+
if (removedByPreRebase.length > 0) {
|
|
15620
|
+
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
15621
|
+
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
15622
|
+
newPatches = newPatches.filter((p) => {
|
|
15623
|
+
if (removedOriginalCommits.has(p.original_commit)) return false;
|
|
15624
|
+
if (isRevertCommit(p.original_message)) {
|
|
15625
|
+
const revertedMsg = parseRevertedMessage(p.original_message);
|
|
15626
|
+
if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
|
|
15627
|
+
}
|
|
15628
|
+
return true;
|
|
15629
|
+
});
|
|
15630
|
+
}
|
|
15631
|
+
for (const id of revertedPatchIds) {
|
|
15632
|
+
try {
|
|
15633
|
+
this.lockManager.removePatch(id);
|
|
15634
|
+
} catch {
|
|
15635
|
+
}
|
|
15636
|
+
}
|
|
15637
|
+
const revertedSet = new Set(revertedPatchIds);
|
|
15638
|
+
existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
|
|
15470
15639
|
const allPatches = [...existingPatches, ...newPatches];
|
|
15471
15640
|
const commitOpts = options ? {
|
|
15472
15641
|
cliVersion: options.cliVersion ?? "unknown",
|
|
@@ -15477,20 +15646,32 @@ var ReplayService = class {
|
|
|
15477
15646
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15478
15647
|
this.lockManager.addGeneration(genRecord);
|
|
15479
15648
|
const results = await this.applicator.applyPatches(allPatches);
|
|
15649
|
+
this.revertConflictingFiles(results);
|
|
15650
|
+
for (const result of results) {
|
|
15651
|
+
if (result.status === "conflict") {
|
|
15652
|
+
result.patch.status = "unresolved";
|
|
15653
|
+
}
|
|
15654
|
+
}
|
|
15480
15655
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
15481
15656
|
for (const patch of newPatches) {
|
|
15482
15657
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
15483
15658
|
this.lockManager.addPatch(patch);
|
|
15484
15659
|
}
|
|
15485
15660
|
}
|
|
15661
|
+
for (const result of results) {
|
|
15662
|
+
if (result.status === "conflict") {
|
|
15663
|
+
try {
|
|
15664
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
15665
|
+
} catch {
|
|
15666
|
+
}
|
|
15667
|
+
}
|
|
15668
|
+
}
|
|
15486
15669
|
this.lockManager.save();
|
|
15487
15670
|
if (options?.stageOnly) {
|
|
15488
15671
|
await this.committer.stageAll();
|
|
15489
15672
|
} else {
|
|
15490
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
15491
|
-
|
|
15492
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
15493
|
-
}
|
|
15673
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
15674
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
15494
15675
|
}
|
|
15495
15676
|
return this.buildReport(
|
|
15496
15677
|
"normal-regeneration",
|
|
@@ -15499,7 +15680,8 @@ var ReplayService = class {
|
|
|
15499
15680
|
options,
|
|
15500
15681
|
warnings,
|
|
15501
15682
|
rebaseCounts,
|
|
15502
|
-
preRebaseCounts
|
|
15683
|
+
preRebaseCounts,
|
|
15684
|
+
revertedPatchIds.length
|
|
15503
15685
|
);
|
|
15504
15686
|
}
|
|
15505
15687
|
/**
|
|
@@ -15638,6 +15820,10 @@ var ReplayService = class {
|
|
|
15638
15820
|
let conflictAbsorbed = 0;
|
|
15639
15821
|
let contentRefreshed = 0;
|
|
15640
15822
|
for (const patch of patches) {
|
|
15823
|
+
if (patch.status != null) {
|
|
15824
|
+
delete patch.status;
|
|
15825
|
+
continue;
|
|
15826
|
+
}
|
|
15641
15827
|
if (patch.base_generation === currentGen) {
|
|
15642
15828
|
try {
|
|
15643
15829
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -15688,12 +15874,31 @@ var ReplayService = class {
|
|
|
15688
15874
|
}
|
|
15689
15875
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
15690
15876
|
}
|
|
15877
|
+
/**
|
|
15878
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
15879
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
15880
|
+
*/
|
|
15881
|
+
revertConflictingFiles(results) {
|
|
15882
|
+
for (const result of results) {
|
|
15883
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
15884
|
+
for (const fileResult of result.fileResults) {
|
|
15885
|
+
if (fileResult.status !== "conflict") continue;
|
|
15886
|
+
const filePath = (0, import_node_path4.join)(this.outputDir, fileResult.file);
|
|
15887
|
+
try {
|
|
15888
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
15889
|
+
const stripped = stripConflictMarkers(content);
|
|
15890
|
+
(0, import_node_fs2.writeFileSync)(filePath, stripped);
|
|
15891
|
+
} catch {
|
|
15892
|
+
}
|
|
15893
|
+
}
|
|
15894
|
+
}
|
|
15895
|
+
}
|
|
15691
15896
|
readFernignorePatterns() {
|
|
15692
15897
|
const fernignorePath = (0, import_node_path4.join)(this.outputDir, ".fernignore");
|
|
15693
15898
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
15694
15899
|
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
15695
15900
|
}
|
|
15696
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
|
|
15901
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
15697
15902
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
15698
15903
|
const conflictDetails = conflictResults.map((r) => {
|
|
15699
15904
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -15718,10 +15923,17 @@ var ReplayService = class {
|
|
|
15718
15923
|
patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
|
|
15719
15924
|
patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
|
|
15720
15925
|
patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
|
|
15926
|
+
patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
|
|
15721
15927
|
patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
|
|
15722
15928
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
15723
15929
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
15724
15930
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
15931
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
15932
|
+
patchId: r.patch.id,
|
|
15933
|
+
patchMessage: r.patch.original_message,
|
|
15934
|
+
files: r.patch.files,
|
|
15935
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
15936
|
+
})) : void 0,
|
|
15725
15937
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
15726
15938
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
15727
15939
|
};
|
|
@@ -15757,7 +15969,7 @@ var FernignoreMigrator = class {
|
|
|
15757
15969
|
async analyzeMigration() {
|
|
15758
15970
|
const patterns = this.readFernignorePatterns();
|
|
15759
15971
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
15760
|
-
const patches = await detector.detectNewPatches();
|
|
15972
|
+
const { patches } = await detector.detectNewPatches();
|
|
15761
15973
|
const trackedByBoth = [];
|
|
15762
15974
|
const fernignoreOnly = [];
|
|
15763
15975
|
const commitsOnly = [];
|
|
@@ -15882,7 +16094,7 @@ var FernignoreMigrator = class {
|
|
|
15882
16094
|
async migrate() {
|
|
15883
16095
|
const analysis = await this.analyzeMigration();
|
|
15884
16096
|
const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
|
|
15885
|
-
const patches = await detector.detectNewPatches();
|
|
16097
|
+
const { patches } = await detector.detectNewPatches();
|
|
15886
16098
|
const warnings = [];
|
|
15887
16099
|
let patchesCreated = 0;
|
|
15888
16100
|
for (const patch of patches) {
|
|
@@ -16181,17 +16393,82 @@ async function resolve(outputDir, options) {
|
|
|
16181
16393
|
return { success: false, reason: "no-patches" };
|
|
16182
16394
|
}
|
|
16183
16395
|
const git = new GitClient(outputDir);
|
|
16396
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
16397
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
16398
|
+
if (unresolvedPatches.length > 0) {
|
|
16399
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
16400
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
16401
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
16402
|
+
if (markerFiles.length > 0) {
|
|
16403
|
+
for (const patch of unresolvedPatches) {
|
|
16404
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
16405
|
+
}
|
|
16406
|
+
lockManager.save();
|
|
16407
|
+
return {
|
|
16408
|
+
success: false,
|
|
16409
|
+
reason: "conflicts-applied",
|
|
16410
|
+
unresolvedFiles: markerFiles,
|
|
16411
|
+
phase: "applied",
|
|
16412
|
+
patchesApplied: unresolvedPatches.length
|
|
16413
|
+
};
|
|
16414
|
+
}
|
|
16415
|
+
}
|
|
16184
16416
|
if (options?.checkMarkers !== false) {
|
|
16185
|
-
const
|
|
16186
|
-
if (
|
|
16187
|
-
|
|
16188
|
-
|
|
16417
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
16418
|
+
if (currentMarkerFiles.length > 0) {
|
|
16419
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
16420
|
+
}
|
|
16421
|
+
}
|
|
16422
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
16423
|
+
if (patchesToCommit.length > 0) {
|
|
16424
|
+
const currentGen = lock.current_generation;
|
|
16425
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
16426
|
+
let patchesResolved = 0;
|
|
16427
|
+
for (const patch of patchesToCommit) {
|
|
16428
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
16429
|
+
if (!diff || !diff.trim()) {
|
|
16430
|
+
lockManager.removePatch(patch.id);
|
|
16431
|
+
continue;
|
|
16432
|
+
}
|
|
16433
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
16434
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
16435
|
+
lockManager.markPatchResolved(patch.id, {
|
|
16436
|
+
patch_content: diff,
|
|
16437
|
+
content_hash: newContentHash,
|
|
16438
|
+
base_generation: currentGen,
|
|
16439
|
+
files: changedFiles
|
|
16440
|
+
});
|
|
16441
|
+
patchesResolved++;
|
|
16189
16442
|
}
|
|
16443
|
+
lockManager.save();
|
|
16444
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
16445
|
+
await committer2.stageAll();
|
|
16446
|
+
const commitSha2 = await committer2.commitReplay(
|
|
16447
|
+
lock.patches.length,
|
|
16448
|
+
lock.patches,
|
|
16449
|
+
"[fern-replay] Resolved conflicts"
|
|
16450
|
+
);
|
|
16451
|
+
return {
|
|
16452
|
+
success: true,
|
|
16453
|
+
commitSha: commitSha2,
|
|
16454
|
+
phase: "committed",
|
|
16455
|
+
patchesResolved
|
|
16456
|
+
};
|
|
16190
16457
|
}
|
|
16191
16458
|
const committer = new ReplayCommitter(git, outputDir);
|
|
16192
16459
|
await committer.stageAll();
|
|
16193
16460
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
16194
|
-
return { success: true, commitSha };
|
|
16461
|
+
return { success: true, commitSha, phase: "committed" };
|
|
16462
|
+
}
|
|
16463
|
+
async function findConflictMarkerFiles(git) {
|
|
16464
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
16465
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
16466
|
+
}
|
|
16467
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
16468
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
16469
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
16470
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
16471
|
+
return changed.length > 0 ? changed : files;
|
|
16195
16472
|
}
|
|
16196
16473
|
|
|
16197
16474
|
// src/cli.ts
|
|
@@ -16370,11 +16647,14 @@ async function runDetect(dir) {
|
|
|
16370
16647
|
lockManager.read();
|
|
16371
16648
|
const git = new GitClient(dir);
|
|
16372
16649
|
const detector = new ReplayDetector(git, lockManager, dir);
|
|
16373
|
-
const patches = await detector.detectNewPatches();
|
|
16650
|
+
const { patches, revertedPatchIds } = await detector.detectNewPatches();
|
|
16374
16651
|
if (detector.warnings.length > 0) {
|
|
16375
16652
|
for (const w of detector.warnings) console.log(`Warning: ${w}`);
|
|
16376
16653
|
console.log();
|
|
16377
16654
|
}
|
|
16655
|
+
if (revertedPatchIds.length > 0) {
|
|
16656
|
+
console.log(`Reverted patches: ${revertedPatchIds.length}`);
|
|
16657
|
+
}
|
|
16378
16658
|
console.log(`Detected ${patches.length} new patch(es) since last generation:
|
|
16379
16659
|
`);
|
|
16380
16660
|
if (patches.length === 0) {
|
|
@@ -16416,6 +16696,9 @@ function printReport(report) {
|
|
|
16416
16696
|
if (report.patchesKeptAsUserOwned) {
|
|
16417
16697
|
console.log(`Patches kept (user-owned files): ${report.patchesKeptAsUserOwned}`);
|
|
16418
16698
|
}
|
|
16699
|
+
if (report.patchesReverted) {
|
|
16700
|
+
console.log(`Patches reverted by user: ${report.patchesReverted}`);
|
|
16701
|
+
}
|
|
16419
16702
|
if (report.patchesPartiallyApplied) {
|
|
16420
16703
|
console.log(`Patches partially applied: ${report.patchesPartiallyApplied}`);
|
|
16421
16704
|
}
|
|
@@ -16449,14 +16732,23 @@ function printReport(report) {
|
|
|
16449
16732
|
}
|
|
16450
16733
|
}
|
|
16451
16734
|
}
|
|
16452
|
-
console.log("\
|
|
16453
|
-
console.log("Or run: fern-replay forget <file-pattern> to remove problematic patches");
|
|
16735
|
+
console.log("\nOr run: fern-replay forget <file-pattern> to remove problematic patches");
|
|
16454
16736
|
} else if (report.conflicts.length > 0) {
|
|
16455
16737
|
console.log("\nConflicts:");
|
|
16456
16738
|
for (const conflict of report.conflicts) {
|
|
16457
16739
|
console.log(` ${conflict.file}: ${conflict.reason ?? "merge conflict"}`);
|
|
16458
16740
|
}
|
|
16459
16741
|
}
|
|
16742
|
+
if (report.unresolvedPatches && report.unresolvedPatches.length > 0) {
|
|
16743
|
+
console.log("\nUnresolved Patches (need local resolution):");
|
|
16744
|
+
for (const patch of report.unresolvedPatches) {
|
|
16745
|
+
console.log(` Patch: ${patch.patchId} - "${patch.patchMessage}"`);
|
|
16746
|
+
for (const file of patch.files) {
|
|
16747
|
+
console.log(` ${file}`);
|
|
16748
|
+
}
|
|
16749
|
+
}
|
|
16750
|
+
console.log("\nRun 'fern-replay resolve <dir>' on this branch to resolve conflicts.");
|
|
16751
|
+
}
|
|
16460
16752
|
if (report.warnings && report.warnings.length > 0) {
|
|
16461
16753
|
console.log("\nWarnings:");
|
|
16462
16754
|
for (const w of report.warnings) console.log(` ${w}`);
|
|
@@ -16572,6 +16864,17 @@ async function runResolve(dir, flags) {
|
|
|
16572
16864
|
case "no-patches":
|
|
16573
16865
|
console.error("No patches in lockfile. Nothing to resolve.");
|
|
16574
16866
|
break;
|
|
16867
|
+
case "conflicts-applied":
|
|
16868
|
+
console.log(`Applied ${result.patchesApplied} unresolved patch(es) to working tree.`);
|
|
16869
|
+
console.log(`
|
|
16870
|
+
${result.unresolvedFiles.length} file(s) have conflicts:`);
|
|
16871
|
+
for (const f of result.unresolvedFiles) {
|
|
16872
|
+
console.log(` ${f}`);
|
|
16873
|
+
}
|
|
16874
|
+
console.log(`
|
|
16875
|
+
Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' again.`);
|
|
16876
|
+
return;
|
|
16877
|
+
// Normal flow, don't exit(1)
|
|
16575
16878
|
case "unresolved-conflicts":
|
|
16576
16879
|
console.error("Conflict markers still present in:");
|
|
16577
16880
|
for (const f of result.unresolvedFiles ?? []) {
|
|
@@ -16585,8 +16888,11 @@ async function runResolve(dir, flags) {
|
|
|
16585
16888
|
}
|
|
16586
16889
|
process.exit(1);
|
|
16587
16890
|
}
|
|
16891
|
+
if (result.patchesResolved) {
|
|
16892
|
+
console.log(`Resolution complete. Updated ${result.patchesResolved} patch(es) in lockfile.`);
|
|
16893
|
+
}
|
|
16588
16894
|
console.log(`Created [fern-replay] commit: ${result.commitSha?.slice(0, 7)}`);
|
|
16589
|
-
console.log("
|
|
16895
|
+
console.log("Push to update the PR.");
|
|
16590
16896
|
}
|
|
16591
16897
|
async function main() {
|
|
16592
16898
|
const { command, dir, flags, pattern } = parseArgs(process.argv);
|