@fern-api/replay 0.7.0 → 0.8.1

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/index.d.cts CHANGED
@@ -100,6 +100,7 @@ declare class GitClient {
100
100
  }>>;
101
101
  isAncestor(commit: string, descendant: string): Promise<boolean>;
102
102
  commitExists(sha: string): Promise<boolean>;
103
+ getCommitBody(commitSha: string): Promise<string>;
103
104
  getRepoPath(): string;
104
105
  }
105
106
 
@@ -108,6 +109,12 @@ declare const FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com
108
109
  declare const FERN_BOT_LOGIN = "fern-api[bot]";
109
110
  declare function isGenerationCommit(commit: CommitInfo): boolean;
110
111
  declare function isReplayCommit(commit: CommitInfo): boolean;
112
+ /** Check if a commit message matches git's standard revert format: Revert "..." */
113
+ declare function isRevertCommit(message: string): boolean;
114
+ /** Extract the reverted commit SHA from a full commit body containing "This reverts commit SHA." */
115
+ declare function parseRevertedSha(fullBody: string): string | undefined;
116
+ /** Extract the original commit message from a revert subject like 'Revert "original message"' */
117
+ declare function parseRevertedMessage(subject: string): string | undefined;
111
118
 
112
119
  declare class LockfileManager {
113
120
  private outputDir;
@@ -143,20 +150,28 @@ declare class LockfileManager {
143
150
  private ensureLoaded;
144
151
  }
145
152
 
153
+ interface DetectionResult {
154
+ patches: StoredPatch[];
155
+ revertedPatchIds: string[];
156
+ }
146
157
  declare class ReplayDetector {
147
158
  private git;
148
159
  private lockManager;
149
160
  private sdkOutputDir;
150
161
  readonly warnings: string[];
151
162
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
152
- detectNewPatches(): Promise<StoredPatch[]>;
163
+ detectNewPatches(): Promise<DetectionResult>;
153
164
  /**
154
165
  * Compute content hash for deduplication.
155
166
  * Removes commit SHA line and index lines before hashing,
156
167
  * so rebased commits with same content produce the same hash.
157
168
  */
158
169
  computeContentHash(patchContent: string): string;
159
- /** Detect patches via tree diff for non-linear history. Returns a composite patch. */
170
+ /**
171
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
172
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
173
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
174
+ */
160
175
  private detectPatchesViaTreeDiff;
161
176
  private parseGitLog;
162
177
  private getLastGeneration;
@@ -242,6 +257,7 @@ interface ReplayReport {
242
257
  patchesKeptAsUserOwned?: number;
243
258
  patchesPartiallyApplied?: number;
244
259
  patchesConflictResolved?: number;
260
+ patchesReverted?: number;
245
261
  patchesRefreshed?: number;
246
262
  conflicts: FileResult[];
247
263
  conflictDetails?: ConflictDetail[];
@@ -319,11 +335,6 @@ declare class ReplayService {
319
335
  * Called BEFORE commitGeneration() while HEAD has customer code.
320
336
  */
321
337
  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
338
  /**
328
339
  * After applyPatches(), strip conflict markers from conflicting files
329
340
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -492,4 +503,4 @@ interface StatusGeneration {
492
503
  }
493
504
  declare function status(outputDir: string): StatusResult;
494
505
 
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 };
506
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
package/dist/index.d.ts CHANGED
@@ -100,6 +100,7 @@ declare class GitClient {
100
100
  }>>;
101
101
  isAncestor(commit: string, descendant: string): Promise<boolean>;
102
102
  commitExists(sha: string): Promise<boolean>;
103
+ getCommitBody(commitSha: string): Promise<string>;
103
104
  getRepoPath(): string;
104
105
  }
105
106
 
@@ -108,6 +109,12 @@ declare const FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com
108
109
  declare const FERN_BOT_LOGIN = "fern-api[bot]";
109
110
  declare function isGenerationCommit(commit: CommitInfo): boolean;
110
111
  declare function isReplayCommit(commit: CommitInfo): boolean;
112
+ /** Check if a commit message matches git's standard revert format: Revert "..." */
113
+ declare function isRevertCommit(message: string): boolean;
114
+ /** Extract the reverted commit SHA from a full commit body containing "This reverts commit SHA." */
115
+ declare function parseRevertedSha(fullBody: string): string | undefined;
116
+ /** Extract the original commit message from a revert subject like 'Revert "original message"' */
117
+ declare function parseRevertedMessage(subject: string): string | undefined;
111
118
 
112
119
  declare class LockfileManager {
113
120
  private outputDir;
@@ -143,20 +150,28 @@ declare class LockfileManager {
143
150
  private ensureLoaded;
144
151
  }
145
152
 
153
+ interface DetectionResult {
154
+ patches: StoredPatch[];
155
+ revertedPatchIds: string[];
156
+ }
146
157
  declare class ReplayDetector {
147
158
  private git;
148
159
  private lockManager;
149
160
  private sdkOutputDir;
150
161
  readonly warnings: string[];
151
162
  constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
152
- detectNewPatches(): Promise<StoredPatch[]>;
163
+ detectNewPatches(): Promise<DetectionResult>;
153
164
  /**
154
165
  * Compute content hash for deduplication.
155
166
  * Removes commit SHA line and index lines before hashing,
156
167
  * so rebased commits with same content produce the same hash.
157
168
  */
158
169
  computeContentHash(patchContent: string): string;
159
- /** Detect patches via tree diff for non-linear history. Returns a composite patch. */
170
+ /**
171
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
172
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
173
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
174
+ */
160
175
  private detectPatchesViaTreeDiff;
161
176
  private parseGitLog;
162
177
  private getLastGeneration;
@@ -242,6 +257,7 @@ interface ReplayReport {
242
257
  patchesKeptAsUserOwned?: number;
243
258
  patchesPartiallyApplied?: number;
244
259
  patchesConflictResolved?: number;
260
+ patchesReverted?: number;
245
261
  patchesRefreshed?: number;
246
262
  conflicts: FileResult[];
247
263
  conflictDetails?: ConflictDetail[];
@@ -319,11 +335,6 @@ declare class ReplayService {
319
335
  * Called BEFORE commitGeneration() while HEAD has customer code.
320
336
  */
321
337
  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
338
  /**
328
339
  * After applyPatches(), strip conflict markers from conflicting files
329
340
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -492,4 +503,4 @@ interface StatusGeneration {
492
503
  }
493
504
  declare function status(outputDir: string): StatusResult;
494
505
 
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 };
506
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
package/dist/index.js CHANGED
@@ -111,6 +111,9 @@ var init_GitClient = __esm({
111
111
  return false;
112
112
  }
113
113
  }
114
+ async getCommitBody(commitSha) {
115
+ return this.exec(["log", "-1", "--format=%B", commitSha]);
116
+ }
114
117
  getRepoPath() {
115
118
  return this.repoPath;
116
119
  }
@@ -138,6 +141,17 @@ function isGenerationCommit(commit) {
138
141
  function isReplayCommit(commit) {
139
142
  return commit.message.startsWith("[fern-replay]");
140
143
  }
144
+ function isRevertCommit(message) {
145
+ return /^Revert ".+"$/.test(message);
146
+ }
147
+ function parseRevertedSha(fullBody) {
148
+ const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
149
+ return match?.[1];
150
+ }
151
+ function parseRevertedMessage(subject) {
152
+ const match = subject.match(/^Revert "(.+)"$/);
153
+ return match?.[1];
154
+ }
141
155
 
142
156
  // src/LockfileManager.ts
143
157
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
@@ -300,14 +314,14 @@ var ReplayDetector = class {
300
314
  const lock = this.lockManager.read();
301
315
  const lastGen = this.getLastGeneration(lock);
302
316
  if (!lastGen) {
303
- return [];
317
+ return { patches: [], revertedPatchIds: [] };
304
318
  }
305
319
  const exists = await this.git.commitExists(lastGen.commit_sha);
306
320
  if (!exists) {
307
321
  this.warnings.push(
308
322
  `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
309
323
  );
310
- return [];
324
+ return { patches: [], revertedPatchIds: [] };
311
325
  }
312
326
  const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
313
327
  if (!isAncestor) {
@@ -321,7 +335,7 @@ var ReplayDetector = class {
321
335
  this.sdkOutputDir
322
336
  ]);
323
337
  if (!log.trim()) {
324
- return [];
338
+ return { patches: [], revertedPatchIds: [] };
325
339
  }
326
340
  const commits = this.parseGitLog(log);
327
341
  const newPatches = [];
@@ -357,7 +371,63 @@ var ReplayDetector = class {
357
371
  patch_content: patchContent
358
372
  });
359
373
  }
360
- return newPatches.reverse();
374
+ newPatches.reverse();
375
+ const revertedPatchIdSet = /* @__PURE__ */ new Set();
376
+ const revertIndicesToRemove = /* @__PURE__ */ new Set();
377
+ for (let i = 0; i < newPatches.length; i++) {
378
+ const patch = newPatches[i];
379
+ if (!isRevertCommit(patch.original_message)) continue;
380
+ let body = "";
381
+ try {
382
+ body = await this.git.getCommitBody(patch.original_commit);
383
+ } catch {
384
+ }
385
+ const revertedSha = parseRevertedSha(body);
386
+ const revertedMessage = parseRevertedMessage(patch.original_message);
387
+ let matchedExisting = false;
388
+ if (revertedSha) {
389
+ const existing = lock.patches.find((p) => p.original_commit === revertedSha);
390
+ if (existing) {
391
+ revertedPatchIdSet.add(existing.id);
392
+ revertIndicesToRemove.add(i);
393
+ matchedExisting = true;
394
+ }
395
+ }
396
+ if (!matchedExisting && revertedMessage) {
397
+ const existing = lock.patches.find((p) => p.original_message === revertedMessage);
398
+ if (existing) {
399
+ revertedPatchIdSet.add(existing.id);
400
+ revertIndicesToRemove.add(i);
401
+ matchedExisting = true;
402
+ }
403
+ }
404
+ if (matchedExisting) continue;
405
+ let matchedNew = false;
406
+ if (revertedSha) {
407
+ const idx = newPatches.findIndex(
408
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
409
+ );
410
+ if (idx !== -1) {
411
+ revertIndicesToRemove.add(i);
412
+ revertIndicesToRemove.add(idx);
413
+ matchedNew = true;
414
+ }
415
+ }
416
+ if (!matchedNew && revertedMessage) {
417
+ const idx = newPatches.findIndex(
418
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
419
+ );
420
+ if (idx !== -1) {
421
+ revertIndicesToRemove.add(i);
422
+ revertIndicesToRemove.add(idx);
423
+ }
424
+ }
425
+ if (!matchedExisting && !matchedNew) {
426
+ revertIndicesToRemove.add(i);
427
+ }
428
+ }
429
+ const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
430
+ return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
361
431
  }
362
432
  /**
363
433
  * Compute content hash for deduplication.
@@ -368,31 +438,34 @@ var ReplayDetector = class {
368
438
  const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
369
439
  return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
370
440
  }
371
- /** Detect patches via tree diff for non-linear history. Returns a composite patch. */
441
+ /**
442
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
443
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
444
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
445
+ */
372
446
  async detectPatchesViaTreeDiff(lastGen) {
373
447
  const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
374
448
  const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
375
- if (files.length === 0) return [];
449
+ if (files.length === 0) return { patches: [], revertedPatchIds: [] };
376
450
  const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
377
- if (!diff.trim()) return [];
451
+ if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
378
452
  const contentHash = this.computeContentHash(diff);
379
453
  const lock = this.lockManager.read();
380
454
  if (lock.patches.some((p) => p.content_hash === contentHash)) {
381
- return [];
455
+ return { patches: [], revertedPatchIds: [] };
382
456
  }
383
457
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
384
- return [
385
- {
386
- id: `patch-composite-${headSha.slice(0, 8)}`,
387
- content_hash: contentHash,
388
- original_commit: headSha,
389
- original_message: "Customer customizations (composite)",
390
- original_author: "composite",
391
- base_generation: lastGen.commit_sha,
392
- files,
393
- patch_content: diff
394
- }
395
- ];
458
+ const compositePatch = {
459
+ id: `patch-composite-${headSha.slice(0, 8)}`,
460
+ content_hash: contentHash,
461
+ original_commit: headSha,
462
+ original_message: "Customer customizations (composite)",
463
+ original_author: "composite",
464
+ base_generation: lastGen.commit_sha,
465
+ files,
466
+ patch_content: diff
467
+ };
468
+ return { patches: [compositePatch], revertedPatchIds: [] };
396
469
  }
397
470
  parseGitLog(log) {
398
471
  return log.trim().split("\n").map((line) => {
@@ -990,10 +1063,40 @@ CLI Version: ${options.cliVersion}`;
990
1063
  };
991
1064
 
992
1065
  // src/ReplayService.ts
993
- init_GitClient();
994
1066
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
995
1067
  import { join as join3 } from "path";
996
1068
  import { minimatch as minimatch2 } from "minimatch";
1069
+ init_GitClient();
1070
+
1071
+ // src/conflict-utils.ts
1072
+ function stripConflictMarkers(content) {
1073
+ const lines = content.split("\n");
1074
+ const result = [];
1075
+ let inConflict = false;
1076
+ let inOurs = false;
1077
+ for (const line of lines) {
1078
+ if (line.startsWith("<<<<<<< ")) {
1079
+ inConflict = true;
1080
+ inOurs = true;
1081
+ continue;
1082
+ }
1083
+ if (inConflict && line === "=======") {
1084
+ inOurs = false;
1085
+ continue;
1086
+ }
1087
+ if (inConflict && line.startsWith(">>>>>>> ")) {
1088
+ inConflict = false;
1089
+ inOurs = false;
1090
+ continue;
1091
+ }
1092
+ if (!inConflict || inOurs) {
1093
+ result.push(line);
1094
+ }
1095
+ }
1096
+ return result.join("\n");
1097
+ }
1098
+
1099
+ // src/ReplayService.ts
997
1100
  var ReplayService = class {
998
1101
  git;
999
1102
  detector;
@@ -1061,7 +1164,7 @@ var ReplayService = class {
1061
1164
  }
1062
1165
  this.lockManager.save();
1063
1166
  try {
1064
- const redetectedPatches = await this.detector.detectNewPatches();
1167
+ const { patches: redetectedPatches } = await this.detector.detectNewPatches();
1065
1168
  if (redetectedPatches.length > 0) {
1066
1169
  const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
1067
1170
  const currentPatches = this.lockManager.getPatches();
@@ -1152,7 +1255,7 @@ var ReplayService = class {
1152
1255
  };
1153
1256
  }
1154
1257
  async handleNoPatchesRegeneration(options) {
1155
- const newPatches = await this.detector.detectNewPatches();
1258
+ const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1156
1259
  const warnings = [...this.detector.warnings];
1157
1260
  if (options?.dryRun) {
1158
1261
  return {
@@ -1161,11 +1264,18 @@ var ReplayService = class {
1161
1264
  patchesApplied: 0,
1162
1265
  patchesWithConflicts: 0,
1163
1266
  patchesSkipped: 0,
1267
+ patchesReverted: revertedPatchIds.length,
1164
1268
  conflicts: [],
1165
1269
  wouldApply: newPatches,
1166
1270
  warnings: warnings.length > 0 ? warnings : void 0
1167
1271
  };
1168
1272
  }
1273
+ for (const id of revertedPatchIds) {
1274
+ try {
1275
+ this.lockManager.removePatch(id);
1276
+ } catch {
1277
+ }
1278
+ }
1169
1279
  const commitOpts = options ? {
1170
1280
  cliVersion: options.cliVersion ?? "unknown",
1171
1281
  generatorVersions: options.generatorVersions ?? {},
@@ -1199,12 +1309,12 @@ var ReplayService = class {
1199
1309
  await this.committer.stageAll();
1200
1310
  }
1201
1311
  }
1202
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
1312
+ return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
1203
1313
  }
1204
1314
  async handleNormalRegeneration(options) {
1205
1315
  if (options?.dryRun) {
1206
1316
  const existingPatches2 = this.lockManager.getPatches();
1207
- const newPatches2 = await this.detector.detectNewPatches();
1317
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
1208
1318
  const warnings2 = [...this.detector.warnings];
1209
1319
  const allPatches2 = [...existingPatches2, ...newPatches2];
1210
1320
  return {
@@ -1213,13 +1323,19 @@ var ReplayService = class {
1213
1323
  patchesApplied: 0,
1214
1324
  patchesWithConflicts: 0,
1215
1325
  patchesSkipped: 0,
1326
+ patchesReverted: dryRunReverted.length,
1216
1327
  conflicts: [],
1217
1328
  wouldApply: allPatches2,
1218
1329
  warnings: warnings2.length > 0 ? warnings2 : void 0
1219
1330
  };
1220
1331
  }
1221
1332
  let existingPatches = this.lockManager.getPatches();
1333
+ const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
1222
1334
  const preRebaseCounts = await this.preGenerationRebase(existingPatches);
1335
+ const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
1336
+ const removedByPreRebase = existingPatches.filter(
1337
+ (p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
1338
+ );
1223
1339
  existingPatches = this.lockManager.getPatches();
1224
1340
  const seenHashes = /* @__PURE__ */ new Set();
1225
1341
  for (const p of existingPatches) {
@@ -1230,8 +1346,28 @@ var ReplayService = class {
1230
1346
  }
1231
1347
  }
1232
1348
  existingPatches = this.lockManager.getPatches();
1233
- const newPatches = await this.detector.detectNewPatches();
1349
+ let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1234
1350
  const warnings = [...this.detector.warnings];
1351
+ if (removedByPreRebase.length > 0) {
1352
+ const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
1353
+ const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
1354
+ newPatches = newPatches.filter((p) => {
1355
+ if (removedOriginalCommits.has(p.original_commit)) return false;
1356
+ if (isRevertCommit(p.original_message)) {
1357
+ const revertedMsg = parseRevertedMessage(p.original_message);
1358
+ if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
1359
+ }
1360
+ return true;
1361
+ });
1362
+ }
1363
+ for (const id of revertedPatchIds) {
1364
+ try {
1365
+ this.lockManager.removePatch(id);
1366
+ } catch {
1367
+ }
1368
+ }
1369
+ const revertedSet = new Set(revertedPatchIds);
1370
+ existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
1235
1371
  const allPatches = [...existingPatches, ...newPatches];
1236
1372
  const commitOpts = options ? {
1237
1373
  cliVersion: options.cliVersion ?? "unknown",
@@ -1276,7 +1412,8 @@ var ReplayService = class {
1276
1412
  options,
1277
1413
  warnings,
1278
1414
  rebaseCounts,
1279
- preRebaseCounts
1415
+ preRebaseCounts,
1416
+ revertedPatchIds.length
1280
1417
  );
1281
1418
  }
1282
1419
  /**
@@ -1469,36 +1606,6 @@ var ReplayService = class {
1469
1606
  }
1470
1607
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
1471
1608
  }
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
1609
  /**
1503
1610
  * After applyPatches(), strip conflict markers from conflicting files
1504
1611
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -1511,7 +1618,7 @@ var ReplayService = class {
1511
1618
  const filePath = join3(this.outputDir, fileResult.file);
1512
1619
  try {
1513
1620
  const content = readFileSync2(filePath, "utf-8");
1514
- const stripped = this.stripConflictMarkers(content);
1621
+ const stripped = stripConflictMarkers(content);
1515
1622
  writeFileSync2(filePath, stripped);
1516
1623
  } catch {
1517
1624
  }
@@ -1523,7 +1630,7 @@ var ReplayService = class {
1523
1630
  if (!existsSync2(fernignorePath)) return [];
1524
1631
  return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
1525
1632
  }
1526
- buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
1633
+ buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
1527
1634
  const conflictResults = results.filter((r) => r.status === "conflict");
1528
1635
  const conflictDetails = conflictResults.map((r) => {
1529
1636
  const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
@@ -1548,6 +1655,7 @@ var ReplayService = class {
1548
1655
  patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
1549
1656
  patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
1550
1657
  patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
1658
+ patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
1551
1659
  patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
1552
1660
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
1553
1661
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
@@ -1594,7 +1702,7 @@ var FernignoreMigrator = class {
1594
1702
  async analyzeMigration() {
1595
1703
  const patterns = this.readFernignorePatterns();
1596
1704
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1597
- const patches = await detector.detectNewPatches();
1705
+ const { patches } = await detector.detectNewPatches();
1598
1706
  const trackedByBoth = [];
1599
1707
  const fernignoreOnly = [];
1600
1708
  const commitsOnly = [];
@@ -1719,7 +1827,7 @@ var FernignoreMigrator = class {
1719
1827
  async migrate() {
1720
1828
  const analysis = await this.analyzeMigration();
1721
1829
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1722
- const patches = await detector.detectNewPatches();
1830
+ const { patches } = await detector.detectNewPatches();
1723
1831
  const warnings = [];
1724
1832
  let patchesCreated = 0;
1725
1833
  for (const patch of patches) {
@@ -2135,6 +2243,9 @@ export {
2135
2243
  forget,
2136
2244
  isGenerationCommit,
2137
2245
  isReplayCommit,
2246
+ isRevertCommit,
2247
+ parseRevertedMessage,
2248
+ parseRevertedSha,
2138
2249
  reset,
2139
2250
  resolve,
2140
2251
  status,