@fern-api/replay 0.7.0 → 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/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 indicates a git 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 indicates a git 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 message.startsWith('Revert "');
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,60 @@ 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
+ }
426
+ const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
427
+ return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
361
428
  }
362
429
  /**
363
430
  * Compute content hash for deduplication.
@@ -368,31 +435,34 @@ var ReplayDetector = class {
368
435
  const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
369
436
  return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
370
437
  }
371
- /** Detect patches via tree diff for non-linear history. Returns a composite patch. */
438
+ /**
439
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
440
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
441
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
442
+ */
372
443
  async detectPatchesViaTreeDiff(lastGen) {
373
444
  const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
374
445
  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 [];
446
+ if (files.length === 0) return { patches: [], revertedPatchIds: [] };
376
447
  const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
377
- if (!diff.trim()) return [];
448
+ if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
378
449
  const contentHash = this.computeContentHash(diff);
379
450
  const lock = this.lockManager.read();
380
451
  if (lock.patches.some((p) => p.content_hash === contentHash)) {
381
- return [];
452
+ return { patches: [], revertedPatchIds: [] };
382
453
  }
383
454
  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
- ];
455
+ const compositePatch = {
456
+ id: `patch-composite-${headSha.slice(0, 8)}`,
457
+ content_hash: contentHash,
458
+ original_commit: headSha,
459
+ original_message: "Customer customizations (composite)",
460
+ original_author: "composite",
461
+ base_generation: lastGen.commit_sha,
462
+ files,
463
+ patch_content: diff
464
+ };
465
+ return { patches: [compositePatch], revertedPatchIds: [] };
396
466
  }
397
467
  parseGitLog(log) {
398
468
  return log.trim().split("\n").map((line) => {
@@ -990,10 +1060,40 @@ CLI Version: ${options.cliVersion}`;
990
1060
  };
991
1061
 
992
1062
  // src/ReplayService.ts
993
- init_GitClient();
994
1063
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
995
1064
  import { join as join3 } from "path";
996
1065
  import { minimatch as minimatch2 } from "minimatch";
1066
+ init_GitClient();
1067
+
1068
+ // src/conflict-utils.ts
1069
+ function stripConflictMarkers(content) {
1070
+ const lines = content.split("\n");
1071
+ const result = [];
1072
+ let inConflict = false;
1073
+ let inOurs = false;
1074
+ for (const line of lines) {
1075
+ if (line.startsWith("<<<<<<< ")) {
1076
+ inConflict = true;
1077
+ inOurs = true;
1078
+ continue;
1079
+ }
1080
+ if (inConflict && line === "=======") {
1081
+ inOurs = false;
1082
+ continue;
1083
+ }
1084
+ if (inConflict && line.startsWith(">>>>>>> ")) {
1085
+ inConflict = false;
1086
+ inOurs = false;
1087
+ continue;
1088
+ }
1089
+ if (!inConflict || inOurs) {
1090
+ result.push(line);
1091
+ }
1092
+ }
1093
+ return result.join("\n");
1094
+ }
1095
+
1096
+ // src/ReplayService.ts
997
1097
  var ReplayService = class {
998
1098
  git;
999
1099
  detector;
@@ -1061,7 +1161,7 @@ var ReplayService = class {
1061
1161
  }
1062
1162
  this.lockManager.save();
1063
1163
  try {
1064
- const redetectedPatches = await this.detector.detectNewPatches();
1164
+ const { patches: redetectedPatches } = await this.detector.detectNewPatches();
1065
1165
  if (redetectedPatches.length > 0) {
1066
1166
  const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
1067
1167
  const currentPatches = this.lockManager.getPatches();
@@ -1152,7 +1252,7 @@ var ReplayService = class {
1152
1252
  };
1153
1253
  }
1154
1254
  async handleNoPatchesRegeneration(options) {
1155
- const newPatches = await this.detector.detectNewPatches();
1255
+ const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1156
1256
  const warnings = [...this.detector.warnings];
1157
1257
  if (options?.dryRun) {
1158
1258
  return {
@@ -1161,11 +1261,18 @@ var ReplayService = class {
1161
1261
  patchesApplied: 0,
1162
1262
  patchesWithConflicts: 0,
1163
1263
  patchesSkipped: 0,
1264
+ patchesReverted: revertedPatchIds.length,
1164
1265
  conflicts: [],
1165
1266
  wouldApply: newPatches,
1166
1267
  warnings: warnings.length > 0 ? warnings : void 0
1167
1268
  };
1168
1269
  }
1270
+ for (const id of revertedPatchIds) {
1271
+ try {
1272
+ this.lockManager.removePatch(id);
1273
+ } catch {
1274
+ }
1275
+ }
1169
1276
  const commitOpts = options ? {
1170
1277
  cliVersion: options.cliVersion ?? "unknown",
1171
1278
  generatorVersions: options.generatorVersions ?? {},
@@ -1199,12 +1306,12 @@ var ReplayService = class {
1199
1306
  await this.committer.stageAll();
1200
1307
  }
1201
1308
  }
1202
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
1309
+ return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
1203
1310
  }
1204
1311
  async handleNormalRegeneration(options) {
1205
1312
  if (options?.dryRun) {
1206
1313
  const existingPatches2 = this.lockManager.getPatches();
1207
- const newPatches2 = await this.detector.detectNewPatches();
1314
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
1208
1315
  const warnings2 = [...this.detector.warnings];
1209
1316
  const allPatches2 = [...existingPatches2, ...newPatches2];
1210
1317
  return {
@@ -1213,13 +1320,19 @@ var ReplayService = class {
1213
1320
  patchesApplied: 0,
1214
1321
  patchesWithConflicts: 0,
1215
1322
  patchesSkipped: 0,
1323
+ patchesReverted: dryRunReverted.length,
1216
1324
  conflicts: [],
1217
1325
  wouldApply: allPatches2,
1218
1326
  warnings: warnings2.length > 0 ? warnings2 : void 0
1219
1327
  };
1220
1328
  }
1221
1329
  let existingPatches = this.lockManager.getPatches();
1330
+ const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
1222
1331
  const preRebaseCounts = await this.preGenerationRebase(existingPatches);
1332
+ const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
1333
+ const removedByPreRebase = existingPatches.filter(
1334
+ (p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
1335
+ );
1223
1336
  existingPatches = this.lockManager.getPatches();
1224
1337
  const seenHashes = /* @__PURE__ */ new Set();
1225
1338
  for (const p of existingPatches) {
@@ -1230,8 +1343,28 @@ var ReplayService = class {
1230
1343
  }
1231
1344
  }
1232
1345
  existingPatches = this.lockManager.getPatches();
1233
- const newPatches = await this.detector.detectNewPatches();
1346
+ let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1234
1347
  const warnings = [...this.detector.warnings];
1348
+ if (removedByPreRebase.length > 0) {
1349
+ const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
1350
+ const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
1351
+ newPatches = newPatches.filter((p) => {
1352
+ if (removedOriginalCommits.has(p.original_commit)) return false;
1353
+ if (isRevertCommit(p.original_message)) {
1354
+ const revertedMsg = parseRevertedMessage(p.original_message);
1355
+ if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
1356
+ }
1357
+ return true;
1358
+ });
1359
+ }
1360
+ for (const id of revertedPatchIds) {
1361
+ try {
1362
+ this.lockManager.removePatch(id);
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ const revertedSet = new Set(revertedPatchIds);
1367
+ existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
1235
1368
  const allPatches = [...existingPatches, ...newPatches];
1236
1369
  const commitOpts = options ? {
1237
1370
  cliVersion: options.cliVersion ?? "unknown",
@@ -1276,7 +1409,8 @@ var ReplayService = class {
1276
1409
  options,
1277
1410
  warnings,
1278
1411
  rebaseCounts,
1279
- preRebaseCounts
1412
+ preRebaseCounts,
1413
+ revertedPatchIds.length
1280
1414
  );
1281
1415
  }
1282
1416
  /**
@@ -1469,36 +1603,6 @@ var ReplayService = class {
1469
1603
  }
1470
1604
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
1471
1605
  }
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
1606
  /**
1503
1607
  * After applyPatches(), strip conflict markers from conflicting files
1504
1608
  * so only clean content is committed. Keeps the Generated (OURS) side.
@@ -1511,7 +1615,7 @@ var ReplayService = class {
1511
1615
  const filePath = join3(this.outputDir, fileResult.file);
1512
1616
  try {
1513
1617
  const content = readFileSync2(filePath, "utf-8");
1514
- const stripped = this.stripConflictMarkers(content);
1618
+ const stripped = stripConflictMarkers(content);
1515
1619
  writeFileSync2(filePath, stripped);
1516
1620
  } catch {
1517
1621
  }
@@ -1523,7 +1627,7 @@ var ReplayService = class {
1523
1627
  if (!existsSync2(fernignorePath)) return [];
1524
1628
  return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
1525
1629
  }
1526
- buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
1630
+ buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
1527
1631
  const conflictResults = results.filter((r) => r.status === "conflict");
1528
1632
  const conflictDetails = conflictResults.map((r) => {
1529
1633
  const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
@@ -1548,6 +1652,7 @@ var ReplayService = class {
1548
1652
  patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
1549
1653
  patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
1550
1654
  patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
1655
+ patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
1551
1656
  patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
1552
1657
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
1553
1658
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
@@ -1594,7 +1699,7 @@ var FernignoreMigrator = class {
1594
1699
  async analyzeMigration() {
1595
1700
  const patterns = this.readFernignorePatterns();
1596
1701
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1597
- const patches = await detector.detectNewPatches();
1702
+ const { patches } = await detector.detectNewPatches();
1598
1703
  const trackedByBoth = [];
1599
1704
  const fernignoreOnly = [];
1600
1705
  const commitsOnly = [];
@@ -1719,7 +1824,7 @@ var FernignoreMigrator = class {
1719
1824
  async migrate() {
1720
1825
  const analysis = await this.analyzeMigration();
1721
1826
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1722
- const patches = await detector.detectNewPatches();
1827
+ const { patches } = await detector.detectNewPatches();
1723
1828
  const warnings = [];
1724
1829
  let patchesCreated = 0;
1725
1830
  for (const patch of patches) {
@@ -2135,6 +2240,9 @@ export {
2135
2240
  forget,
2136
2241
  isGenerationCommit,
2137
2242
  isReplayCommit,
2243
+ isRevertCommit,
2244
+ parseRevertedMessage,
2245
+ parseRevertedSha,
2138
2246
  reset,
2139
2247
  resolve,
2140
2248
  status,