@fern-api/replay 0.6.1 → 0.7.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 +215 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +189 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -5
- package/dist/index.d.ts +37 -5
- package/dist/index.js +194 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,10 @@ interface StoredPatch {
|
|
|
23
23
|
base_generation: string;
|
|
24
24
|
files: string[];
|
|
25
25
|
patch_content: string;
|
|
26
|
+
/** When "unresolved", the patch conflicted during CI and needs local resolution.
|
|
27
|
+
* When "resolving", the resolve command has applied patches to the working tree
|
|
28
|
+
* and is waiting for the customer to resolve conflicts and run resolve again. */
|
|
29
|
+
status?: "unresolved" | "resolving";
|
|
26
30
|
}
|
|
27
31
|
interface CustomizationsConfig {
|
|
28
32
|
exclude?: string[];
|
|
@@ -123,9 +127,13 @@ declare class LockfileManager {
|
|
|
123
127
|
save(): void;
|
|
124
128
|
addGeneration(record: GenerationRecord): void;
|
|
125
129
|
addPatch(patch: StoredPatch): void;
|
|
126
|
-
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files">>): void;
|
|
130
|
+
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
|
|
127
131
|
removePatch(patchId: string): void;
|
|
128
132
|
clearPatches(): void;
|
|
133
|
+
getUnresolvedPatches(): StoredPatch[];
|
|
134
|
+
getResolvingPatches(): StoredPatch[];
|
|
135
|
+
markPatchUnresolved(patchId: string): void;
|
|
136
|
+
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
|
|
129
137
|
getPatches(): StoredPatch[];
|
|
130
138
|
setReplaySkippedAt(timestamp: string): void;
|
|
131
139
|
clearReplaySkippedAt(): void;
|
|
@@ -201,7 +209,7 @@ declare class ReplayCommitter {
|
|
|
201
209
|
private outputDir;
|
|
202
210
|
constructor(git: GitClient, outputDir: string);
|
|
203
211
|
commitGeneration(message: string, options?: CommitOptions): Promise<string>;
|
|
204
|
-
commitReplay(
|
|
212
|
+
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
|
|
205
213
|
createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
|
|
206
214
|
stageAll(): Promise<void>;
|
|
207
215
|
hasStagedChanges(): Promise<boolean>;
|
|
@@ -216,6 +224,12 @@ interface ConflictDetail {
|
|
|
216
224
|
/** Files that applied cleanly in a patch that also had conflicts. */
|
|
217
225
|
cleanFiles?: string[];
|
|
218
226
|
}
|
|
227
|
+
interface UnresolvedPatchInfo {
|
|
228
|
+
patchId: string;
|
|
229
|
+
patchMessage: string;
|
|
230
|
+
files: string[];
|
|
231
|
+
conflictDetails: FileResult[];
|
|
232
|
+
}
|
|
219
233
|
interface ReplayReport {
|
|
220
234
|
flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application";
|
|
221
235
|
patchesDetected: number;
|
|
@@ -231,6 +245,8 @@ interface ReplayReport {
|
|
|
231
245
|
patchesRefreshed?: number;
|
|
232
246
|
conflicts: FileResult[];
|
|
233
247
|
conflictDetails?: ConflictDetail[];
|
|
248
|
+
/** Patches that conflicted and need local resolution via `fern-replay resolve` */
|
|
249
|
+
unresolvedPatches?: UnresolvedPatchInfo[];
|
|
234
250
|
wouldApply?: StoredPatch[];
|
|
235
251
|
warnings?: string[];
|
|
236
252
|
}
|
|
@@ -303,6 +319,16 @@ declare class ReplayService {
|
|
|
303
319
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
304
320
|
*/
|
|
305
321
|
private preGenerationRebase;
|
|
322
|
+
/**
|
|
323
|
+
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
324
|
+
* Preserves clean patches' non-conflicting changes on shared files.
|
|
325
|
+
*/
|
|
326
|
+
private stripConflictMarkers;
|
|
327
|
+
/**
|
|
328
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
329
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
330
|
+
*/
|
|
331
|
+
private revertConflictingFiles;
|
|
306
332
|
private readFernignorePatterns;
|
|
307
333
|
private buildReport;
|
|
308
334
|
}
|
|
@@ -427,10 +453,16 @@ interface ResolveResult {
|
|
|
427
453
|
success: boolean;
|
|
428
454
|
/** Commit SHA of the [fern-replay] commit, if created */
|
|
429
455
|
commitSha?: string;
|
|
430
|
-
/** Reason for failure */
|
|
456
|
+
/** Reason for failure or current state */
|
|
431
457
|
reason?: string;
|
|
432
|
-
/** Files that
|
|
458
|
+
/** Files that have conflict markers */
|
|
433
459
|
unresolvedFiles?: string[];
|
|
460
|
+
/** Phase the command executed */
|
|
461
|
+
phase?: "applied" | "committed" | "nothing-to-resolve";
|
|
462
|
+
/** Number of patches applied to working tree (phase 1) */
|
|
463
|
+
patchesApplied?: number;
|
|
464
|
+
/** Number of patches resolved and committed (phase 2) */
|
|
465
|
+
patchesResolved?: number;
|
|
434
466
|
}
|
|
435
467
|
declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
|
|
436
468
|
|
|
@@ -460,4 +492,4 @@ interface StatusGeneration {
|
|
|
460
492
|
}
|
|
461
493
|
declare function status(outputDir: string): StatusResult;
|
|
462
494
|
|
|
463
|
-
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
|
495
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ interface StoredPatch {
|
|
|
23
23
|
base_generation: string;
|
|
24
24
|
files: string[];
|
|
25
25
|
patch_content: string;
|
|
26
|
+
/** When "unresolved", the patch conflicted during CI and needs local resolution.
|
|
27
|
+
* When "resolving", the resolve command has applied patches to the working tree
|
|
28
|
+
* and is waiting for the customer to resolve conflicts and run resolve again. */
|
|
29
|
+
status?: "unresolved" | "resolving";
|
|
26
30
|
}
|
|
27
31
|
interface CustomizationsConfig {
|
|
28
32
|
exclude?: string[];
|
|
@@ -123,9 +127,13 @@ declare class LockfileManager {
|
|
|
123
127
|
save(): void;
|
|
124
128
|
addGeneration(record: GenerationRecord): void;
|
|
125
129
|
addPatch(patch: StoredPatch): void;
|
|
126
|
-
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files">>): void;
|
|
130
|
+
updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
|
|
127
131
|
removePatch(patchId: string): void;
|
|
128
132
|
clearPatches(): void;
|
|
133
|
+
getUnresolvedPatches(): StoredPatch[];
|
|
134
|
+
getResolvingPatches(): StoredPatch[];
|
|
135
|
+
markPatchUnresolved(patchId: string): void;
|
|
136
|
+
markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
|
|
129
137
|
getPatches(): StoredPatch[];
|
|
130
138
|
setReplaySkippedAt(timestamp: string): void;
|
|
131
139
|
clearReplaySkippedAt(): void;
|
|
@@ -201,7 +209,7 @@ declare class ReplayCommitter {
|
|
|
201
209
|
private outputDir;
|
|
202
210
|
constructor(git: GitClient, outputDir: string);
|
|
203
211
|
commitGeneration(message: string, options?: CommitOptions): Promise<string>;
|
|
204
|
-
commitReplay(
|
|
212
|
+
commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
|
|
205
213
|
createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
|
|
206
214
|
stageAll(): Promise<void>;
|
|
207
215
|
hasStagedChanges(): Promise<boolean>;
|
|
@@ -216,6 +224,12 @@ interface ConflictDetail {
|
|
|
216
224
|
/** Files that applied cleanly in a patch that also had conflicts. */
|
|
217
225
|
cleanFiles?: string[];
|
|
218
226
|
}
|
|
227
|
+
interface UnresolvedPatchInfo {
|
|
228
|
+
patchId: string;
|
|
229
|
+
patchMessage: string;
|
|
230
|
+
files: string[];
|
|
231
|
+
conflictDetails: FileResult[];
|
|
232
|
+
}
|
|
219
233
|
interface ReplayReport {
|
|
220
234
|
flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application";
|
|
221
235
|
patchesDetected: number;
|
|
@@ -231,6 +245,8 @@ interface ReplayReport {
|
|
|
231
245
|
patchesRefreshed?: number;
|
|
232
246
|
conflicts: FileResult[];
|
|
233
247
|
conflictDetails?: ConflictDetail[];
|
|
248
|
+
/** Patches that conflicted and need local resolution via `fern-replay resolve` */
|
|
249
|
+
unresolvedPatches?: UnresolvedPatchInfo[];
|
|
234
250
|
wouldApply?: StoredPatch[];
|
|
235
251
|
warnings?: string[];
|
|
236
252
|
}
|
|
@@ -303,6 +319,16 @@ declare class ReplayService {
|
|
|
303
319
|
* Called BEFORE commitGeneration() while HEAD has customer code.
|
|
304
320
|
*/
|
|
305
321
|
private preGenerationRebase;
|
|
322
|
+
/**
|
|
323
|
+
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
324
|
+
* Preserves clean patches' non-conflicting changes on shared files.
|
|
325
|
+
*/
|
|
326
|
+
private stripConflictMarkers;
|
|
327
|
+
/**
|
|
328
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
329
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
330
|
+
*/
|
|
331
|
+
private revertConflictingFiles;
|
|
306
332
|
private readFernignorePatterns;
|
|
307
333
|
private buildReport;
|
|
308
334
|
}
|
|
@@ -427,10 +453,16 @@ interface ResolveResult {
|
|
|
427
453
|
success: boolean;
|
|
428
454
|
/** Commit SHA of the [fern-replay] commit, if created */
|
|
429
455
|
commitSha?: string;
|
|
430
|
-
/** Reason for failure */
|
|
456
|
+
/** Reason for failure or current state */
|
|
431
457
|
reason?: string;
|
|
432
|
-
/** Files that
|
|
458
|
+
/** Files that have conflict markers */
|
|
433
459
|
unresolvedFiles?: string[];
|
|
460
|
+
/** Phase the command executed */
|
|
461
|
+
phase?: "applied" | "committed" | "nothing-to-resolve";
|
|
462
|
+
/** Number of patches applied to working tree (phase 1) */
|
|
463
|
+
patchesApplied?: number;
|
|
464
|
+
/** Number of patches resolved and committed (phase 2) */
|
|
465
|
+
patchesResolved?: number;
|
|
434
466
|
}
|
|
435
467
|
declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
|
|
436
468
|
|
|
@@ -460,4 +492,4 @@ interface StatusGeneration {
|
|
|
460
492
|
}
|
|
461
493
|
declare function status(outputDir: string): StatusResult;
|
|
462
494
|
|
|
463
|
-
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
|
495
|
+
export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
|
package/dist/index.js
CHANGED
|
@@ -229,6 +229,26 @@ var LockfileManager = class {
|
|
|
229
229
|
this.ensureLoaded();
|
|
230
230
|
this.lock.patches = [];
|
|
231
231
|
}
|
|
232
|
+
getUnresolvedPatches() {
|
|
233
|
+
this.ensureLoaded();
|
|
234
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
235
|
+
}
|
|
236
|
+
getResolvingPatches() {
|
|
237
|
+
this.ensureLoaded();
|
|
238
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
239
|
+
}
|
|
240
|
+
markPatchUnresolved(patchId) {
|
|
241
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
242
|
+
}
|
|
243
|
+
markPatchResolved(patchId, updates) {
|
|
244
|
+
this.ensureLoaded();
|
|
245
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
246
|
+
if (!patch) {
|
|
247
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
248
|
+
}
|
|
249
|
+
delete patch.status;
|
|
250
|
+
Object.assign(patch, updates);
|
|
251
|
+
}
|
|
232
252
|
getPatches() {
|
|
233
253
|
this.ensureLoaded();
|
|
234
254
|
return this.lock.patches;
|
|
@@ -929,12 +949,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
929
949
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
930
950
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
931
951
|
}
|
|
932
|
-
async commitReplay(
|
|
952
|
+
async commitReplay(_patchCount, patches, message) {
|
|
933
953
|
await this.stageAll();
|
|
934
954
|
if (!await this.hasStagedChanges()) {
|
|
935
955
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
936
956
|
}
|
|
937
|
-
let fullMessage = `[fern-replay] Applied
|
|
957
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
938
958
|
if (patches && patches.length > 0) {
|
|
939
959
|
fullMessage += "\n\nPatches replayed:";
|
|
940
960
|
for (const patch of patches) {
|
|
@@ -971,7 +991,7 @@ CLI Version: ${options.cliVersion}`;
|
|
|
971
991
|
|
|
972
992
|
// src/ReplayService.ts
|
|
973
993
|
init_GitClient();
|
|
974
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
994
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
975
995
|
import { join as join3 } from "path";
|
|
976
996
|
import { minimatch as minimatch2 } from "minimatch";
|
|
977
997
|
var ReplayService = class {
|
|
@@ -1029,13 +1049,27 @@ var ReplayService = class {
|
|
|
1029
1049
|
this.lockManager.initializeInMemory(record);
|
|
1030
1050
|
} else {
|
|
1031
1051
|
this.lockManager.read();
|
|
1052
|
+
const unresolvedPatches = [
|
|
1053
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
1054
|
+
...this.lockManager.getResolvingPatches()
|
|
1055
|
+
];
|
|
1032
1056
|
this.lockManager.addGeneration(record);
|
|
1033
1057
|
this.lockManager.clearPatches();
|
|
1058
|
+
for (const patch of unresolvedPatches) {
|
|
1059
|
+
this.lockManager.addPatch(patch);
|
|
1060
|
+
}
|
|
1034
1061
|
}
|
|
1035
1062
|
this.lockManager.save();
|
|
1036
1063
|
try {
|
|
1037
1064
|
const redetectedPatches = await this.detector.detectNewPatches();
|
|
1038
1065
|
if (redetectedPatches.length > 0) {
|
|
1066
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
1067
|
+
const currentPatches = this.lockManager.getPatches();
|
|
1068
|
+
for (const patch of currentPatches) {
|
|
1069
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
1070
|
+
this.lockManager.removePatch(patch.id);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1039
1073
|
for (const patch of redetectedPatches) {
|
|
1040
1074
|
this.lockManager.addPatch(patch);
|
|
1041
1075
|
}
|
|
@@ -1143,6 +1177,12 @@ var ReplayService = class {
|
|
|
1143
1177
|
let results = [];
|
|
1144
1178
|
if (newPatches.length > 0) {
|
|
1145
1179
|
results = await this.applicator.applyPatches(newPatches);
|
|
1180
|
+
this.revertConflictingFiles(results);
|
|
1181
|
+
for (const result of results) {
|
|
1182
|
+
if (result.status === "conflict") {
|
|
1183
|
+
result.patch.status = "unresolved";
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1146
1186
|
}
|
|
1147
1187
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1148
1188
|
for (const patch of newPatches) {
|
|
@@ -1153,10 +1193,8 @@ var ReplayService = class {
|
|
|
1153
1193
|
this.lockManager.save();
|
|
1154
1194
|
if (newPatches.length > 0) {
|
|
1155
1195
|
if (!options?.stageOnly) {
|
|
1156
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1157
|
-
|
|
1158
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1159
|
-
}
|
|
1196
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1197
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
1160
1198
|
} else {
|
|
1161
1199
|
await this.committer.stageAll();
|
|
1162
1200
|
}
|
|
@@ -1204,20 +1242,32 @@ var ReplayService = class {
|
|
|
1204
1242
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
1205
1243
|
this.lockManager.addGeneration(genRecord);
|
|
1206
1244
|
const results = await this.applicator.applyPatches(allPatches);
|
|
1245
|
+
this.revertConflictingFiles(results);
|
|
1246
|
+
for (const result of results) {
|
|
1247
|
+
if (result.status === "conflict") {
|
|
1248
|
+
result.patch.status = "unresolved";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1207
1251
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
1208
1252
|
for (const patch of newPatches) {
|
|
1209
1253
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
1210
1254
|
this.lockManager.addPatch(patch);
|
|
1211
1255
|
}
|
|
1212
1256
|
}
|
|
1257
|
+
for (const result of results) {
|
|
1258
|
+
if (result.status === "conflict") {
|
|
1259
|
+
try {
|
|
1260
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1213
1265
|
this.lockManager.save();
|
|
1214
1266
|
if (options?.stageOnly) {
|
|
1215
1267
|
await this.committer.stageAll();
|
|
1216
1268
|
} else {
|
|
1217
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
1218
|
-
|
|
1219
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1220
|
-
}
|
|
1269
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
1270
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
1221
1271
|
}
|
|
1222
1272
|
return this.buildReport(
|
|
1223
1273
|
"normal-regeneration",
|
|
@@ -1365,6 +1415,10 @@ var ReplayService = class {
|
|
|
1365
1415
|
let conflictAbsorbed = 0;
|
|
1366
1416
|
let contentRefreshed = 0;
|
|
1367
1417
|
for (const patch of patches) {
|
|
1418
|
+
if (patch.status != null) {
|
|
1419
|
+
delete patch.status;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1368
1422
|
if (patch.base_generation === currentGen) {
|
|
1369
1423
|
try {
|
|
1370
1424
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -1415,6 +1469,55 @@ var ReplayService = class {
|
|
|
1415
1469
|
}
|
|
1416
1470
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
1417
1471
|
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
1474
|
+
* Preserves clean patches' non-conflicting changes on shared files.
|
|
1475
|
+
*/
|
|
1476
|
+
stripConflictMarkers(content) {
|
|
1477
|
+
const lines = content.split("\n");
|
|
1478
|
+
const result = [];
|
|
1479
|
+
let inConflict = false;
|
|
1480
|
+
let inOurs = false;
|
|
1481
|
+
for (const line of lines) {
|
|
1482
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1483
|
+
inConflict = true;
|
|
1484
|
+
inOurs = true;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (inConflict && line === "=======") {
|
|
1488
|
+
inOurs = false;
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1492
|
+
inConflict = false;
|
|
1493
|
+
inOurs = false;
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (!inConflict || inOurs) {
|
|
1497
|
+
result.push(line);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return result.join("\n");
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
1504
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
1505
|
+
*/
|
|
1506
|
+
revertConflictingFiles(results) {
|
|
1507
|
+
for (const result of results) {
|
|
1508
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
1509
|
+
for (const fileResult of result.fileResults) {
|
|
1510
|
+
if (fileResult.status !== "conflict") continue;
|
|
1511
|
+
const filePath = join3(this.outputDir, fileResult.file);
|
|
1512
|
+
try {
|
|
1513
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1514
|
+
const stripped = this.stripConflictMarkers(content);
|
|
1515
|
+
writeFileSync2(filePath, stripped);
|
|
1516
|
+
} catch {
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1418
1521
|
readFernignorePatterns() {
|
|
1419
1522
|
const fernignorePath = join3(this.outputDir, ".fernignore");
|
|
1420
1523
|
if (!existsSync2(fernignorePath)) return [];
|
|
@@ -1449,6 +1552,12 @@ var ReplayService = class {
|
|
|
1449
1552
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
1450
1553
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
1451
1554
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
1555
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
1556
|
+
patchId: r.patch.id,
|
|
1557
|
+
patchMessage: r.patch.original_message,
|
|
1558
|
+
files: r.patch.files,
|
|
1559
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
1560
|
+
})) : void 0,
|
|
1452
1561
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
1453
1562
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
1454
1563
|
};
|
|
@@ -1457,7 +1566,7 @@ var ReplayService = class {
|
|
|
1457
1566
|
|
|
1458
1567
|
// src/FernignoreMigrator.ts
|
|
1459
1568
|
import { createHash as createHash2 } from "crypto";
|
|
1460
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as
|
|
1569
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1461
1570
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1462
1571
|
import { minimatch as minimatch3 } from "minimatch";
|
|
1463
1572
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
@@ -1645,13 +1754,13 @@ var FernignoreMigrator = class {
|
|
|
1645
1754
|
if (!existsSync3(dir)) {
|
|
1646
1755
|
mkdirSync2(dir, { recursive: true });
|
|
1647
1756
|
}
|
|
1648
|
-
|
|
1757
|
+
writeFileSync3(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
|
|
1649
1758
|
}
|
|
1650
1759
|
};
|
|
1651
1760
|
|
|
1652
1761
|
// src/commands/bootstrap.ts
|
|
1653
1762
|
import { createHash as createHash3 } from "crypto";
|
|
1654
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
1763
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1655
1764
|
import { join as join5 } from "path";
|
|
1656
1765
|
init_GitClient();
|
|
1657
1766
|
async function bootstrap(outputDir, options) {
|
|
@@ -1837,7 +1946,7 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
1837
1946
|
content += "\n";
|
|
1838
1947
|
}
|
|
1839
1948
|
content += toAdd.join("\n") + "\n";
|
|
1840
|
-
|
|
1949
|
+
writeFileSync4(fernignorePath, content, "utf-8");
|
|
1841
1950
|
return true;
|
|
1842
1951
|
}
|
|
1843
1952
|
function computeContentHash(patchContent) {
|
|
@@ -1910,17 +2019,82 @@ async function resolve(outputDir, options) {
|
|
|
1910
2019
|
return { success: false, reason: "no-patches" };
|
|
1911
2020
|
}
|
|
1912
2021
|
const git = new GitClient(outputDir);
|
|
2022
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
2023
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
2024
|
+
if (unresolvedPatches.length > 0) {
|
|
2025
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
2026
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
2027
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
2028
|
+
if (markerFiles.length > 0) {
|
|
2029
|
+
for (const patch of unresolvedPatches) {
|
|
2030
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
2031
|
+
}
|
|
2032
|
+
lockManager.save();
|
|
2033
|
+
return {
|
|
2034
|
+
success: false,
|
|
2035
|
+
reason: "conflicts-applied",
|
|
2036
|
+
unresolvedFiles: markerFiles,
|
|
2037
|
+
phase: "applied",
|
|
2038
|
+
patchesApplied: unresolvedPatches.length
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1913
2042
|
if (options?.checkMarkers !== false) {
|
|
1914
|
-
const
|
|
1915
|
-
if (
|
|
1916
|
-
|
|
1917
|
-
|
|
2043
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
2044
|
+
if (currentMarkerFiles.length > 0) {
|
|
2045
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
2049
|
+
if (patchesToCommit.length > 0) {
|
|
2050
|
+
const currentGen = lock.current_generation;
|
|
2051
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
2052
|
+
let patchesResolved = 0;
|
|
2053
|
+
for (const patch of patchesToCommit) {
|
|
2054
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
2055
|
+
if (!diff || !diff.trim()) {
|
|
2056
|
+
lockManager.removePatch(patch.id);
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
2060
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
2061
|
+
lockManager.markPatchResolved(patch.id, {
|
|
2062
|
+
patch_content: diff,
|
|
2063
|
+
content_hash: newContentHash,
|
|
2064
|
+
base_generation: currentGen,
|
|
2065
|
+
files: changedFiles
|
|
2066
|
+
});
|
|
2067
|
+
patchesResolved++;
|
|
1918
2068
|
}
|
|
2069
|
+
lockManager.save();
|
|
2070
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
2071
|
+
await committer2.stageAll();
|
|
2072
|
+
const commitSha2 = await committer2.commitReplay(
|
|
2073
|
+
lock.patches.length,
|
|
2074
|
+
lock.patches,
|
|
2075
|
+
"[fern-replay] Resolved conflicts"
|
|
2076
|
+
);
|
|
2077
|
+
return {
|
|
2078
|
+
success: true,
|
|
2079
|
+
commitSha: commitSha2,
|
|
2080
|
+
phase: "committed",
|
|
2081
|
+
patchesResolved
|
|
2082
|
+
};
|
|
1919
2083
|
}
|
|
1920
2084
|
const committer = new ReplayCommitter(git, outputDir);
|
|
1921
2085
|
await committer.stageAll();
|
|
1922
2086
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
1923
|
-
return { success: true, commitSha };
|
|
2087
|
+
return { success: true, commitSha, phase: "committed" };
|
|
2088
|
+
}
|
|
2089
|
+
async function findConflictMarkerFiles(git) {
|
|
2090
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
2091
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
2092
|
+
}
|
|
2093
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
2094
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
2095
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
2096
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
2097
|
+
return changed.length > 0 ? changed : files;
|
|
1924
2098
|
}
|
|
1925
2099
|
|
|
1926
2100
|
// src/commands/status.ts
|