@fern-api/replay 0.8.0 → 0.9.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.cjs CHANGED
@@ -189,7 +189,7 @@ function isReplayCommit(commit) {
189
189
  return commit.message.startsWith("[fern-replay]");
190
190
  }
191
191
  function isRevertCommit(message) {
192
- return message.startsWith('Revert "');
192
+ return /^Revert ".+"$/.test(message);
193
193
  }
194
194
  function parseRevertedSha(fullBody) {
195
195
  const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
@@ -290,6 +290,15 @@ var LockfileManager = class {
290
290
  this.ensureLoaded();
291
291
  this.lock.patches = [];
292
292
  }
293
+ addForgottenHash(hash) {
294
+ this.ensureLoaded();
295
+ if (!this.lock.forgotten_hashes) {
296
+ this.lock.forgotten_hashes = [];
297
+ }
298
+ if (!this.lock.forgotten_hashes.includes(hash)) {
299
+ this.lock.forgotten_hashes.push(hash);
300
+ }
301
+ }
293
302
  getUnresolvedPatches() {
294
303
  this.ensureLoaded();
295
304
  return this.lock.patches.filter((p) => p.status === "unresolved");
@@ -386,6 +395,7 @@ var ReplayDetector = class {
386
395
  }
387
396
  const commits = this.parseGitLog(log);
388
397
  const newPatches = [];
398
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
389
399
  for (const commit of commits) {
390
400
  if (isGenerationCommit(commit)) {
391
401
  continue;
@@ -399,7 +409,7 @@ var ReplayDetector = class {
399
409
  }
400
410
  const patchContent = await this.git.formatPatch(commit.sha);
401
411
  const contentHash = this.computeContentHash(patchContent);
402
- if (lock.patches.find((p) => p.content_hash === contentHash)) {
412
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
403
413
  continue;
404
414
  }
405
415
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
@@ -469,6 +479,9 @@ var ReplayDetector = class {
469
479
  revertIndicesToRemove.add(idx);
470
480
  }
471
481
  }
482
+ if (!matchedExisting && !matchedNew) {
483
+ revertIndicesToRemove.add(i);
484
+ }
472
485
  }
473
486
  const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
474
487
  return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
@@ -495,7 +508,7 @@ var ReplayDetector = class {
495
508
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
496
509
  const contentHash = this.computeContentHash(diff);
497
510
  const lock = this.lockManager.read();
498
- if (lock.patches.some((p) => p.content_hash === contentHash)) {
511
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
499
512
  return { patches: [], revertedPatchIds: [] };
500
513
  }
501
514
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
@@ -565,6 +578,36 @@ var import_promises = require("fs/promises");
565
578
  var import_node_os = require("os");
566
579
  var import_node_path2 = require("path");
567
580
  var import_minimatch = require("minimatch");
581
+
582
+ // src/conflict-utils.ts
583
+ function stripConflictMarkers(content) {
584
+ const lines = content.split("\n");
585
+ const result = [];
586
+ let inConflict = false;
587
+ let inOurs = false;
588
+ for (const line of lines) {
589
+ if (line.startsWith("<<<<<<< ")) {
590
+ inConflict = true;
591
+ inOurs = true;
592
+ continue;
593
+ }
594
+ if (inConflict && line === "=======") {
595
+ inOurs = false;
596
+ continue;
597
+ }
598
+ if (inConflict && line.startsWith(">>>>>>> ")) {
599
+ inConflict = false;
600
+ inOurs = false;
601
+ continue;
602
+ }
603
+ if (!inConflict || inOurs) {
604
+ result.push(line);
605
+ }
606
+ }
607
+ return result.join("\n");
608
+ }
609
+
610
+ // src/ReplayApplicator.ts
568
611
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
569
612
  ".png",
570
613
  ".jpg",
@@ -636,7 +679,8 @@ var ReplayApplicator = class {
636
679
  async applyPatches(patches) {
637
680
  this.resetAccumulator();
638
681
  const results = [];
639
- for (const patch of patches) {
682
+ for (let i = 0; i < patches.length; i++) {
683
+ const patch = patches[i];
640
684
  if (this.isExcluded(patch)) {
641
685
  results.push({
642
686
  patch,
@@ -647,6 +691,33 @@ var ReplayApplicator = class {
647
691
  }
648
692
  const result = await this.applyPatchWithFallback(patch);
649
693
  results.push(result);
694
+ if (result.status === "conflict" && result.fileResults) {
695
+ const laterFiles = /* @__PURE__ */ new Set();
696
+ for (let j = i + 1; j < patches.length; j++) {
697
+ for (const f of patches[j].files) {
698
+ laterFiles.add(f);
699
+ }
700
+ }
701
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
702
+ if (result.resolvedFiles) {
703
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
704
+ resolvedToOriginal.set(resolved, orig);
705
+ }
706
+ }
707
+ for (const fileResult of result.fileResults) {
708
+ if (fileResult.status !== "conflict") continue;
709
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
710
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
711
+ const filePath = (0, import_node_path2.join)(this.outputDir, fileResult.file);
712
+ try {
713
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
714
+ const stripped = stripConflictMarkers(content);
715
+ await (0, import_promises.writeFile)(filePath, stripped);
716
+ } catch {
717
+ }
718
+ }
719
+ }
720
+ }
650
721
  }
651
722
  return results;
652
723
  }
@@ -665,7 +736,7 @@ var ReplayApplicator = class {
665
736
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
666
737
  const base = await this.git.showFile(baseGen.tree_hash, filePath);
667
738
  const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
668
- if (theirs && base) {
739
+ if (theirs) {
669
740
  this.fileTheirsAccumulator.set(resolvedPath, {
670
741
  content: theirs,
671
742
  baseGeneration: patch.base_generation
@@ -797,7 +868,7 @@ var ReplayApplicator = class {
797
868
  );
798
869
  let useAccumulatorAsMergeBase = false;
799
870
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
800
- if (!theirs && base && accumulatorEntry) {
871
+ if (!theirs && accumulatorEntry) {
801
872
  theirs = await this.applyPatchToContent(
802
873
  accumulatorEntry.content,
803
874
  patch.patch_content,
@@ -856,7 +927,7 @@ var ReplayApplicator = class {
856
927
  reason: "missing-content"
857
928
  };
858
929
  }
859
- if (!base || !ours) {
930
+ if (!base && !useAccumulatorAsMergeBase || !ours) {
860
931
  return {
861
932
  file: resolvedPath,
862
933
  status: "skipped",
@@ -864,11 +935,18 @@ var ReplayApplicator = class {
864
935
  };
865
936
  }
866
937
  const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
938
+ if (mergeBase == null) {
939
+ return {
940
+ file: resolvedPath,
941
+ status: "skipped",
942
+ reason: "missing-content"
943
+ };
944
+ }
867
945
  const merged = threeWayMerge(mergeBase, ours, effective_theirs);
868
946
  const outDir = (0, import_node_path2.dirname)(oursPath);
869
947
  await (0, import_promises.mkdir)(outDir, { recursive: true });
870
948
  await (0, import_promises.writeFile)(oursPath, merged.content);
871
- if (effective_theirs && base) {
949
+ if (effective_theirs) {
872
950
  this.fileTheirsAccumulator.set(resolvedPath, {
873
951
  content: effective_theirs,
874
952
  baseGeneration: patch.base_generation
@@ -1111,36 +1189,6 @@ var import_node_fs2 = require("fs");
1111
1189
  var import_node_path3 = require("path");
1112
1190
  var import_minimatch2 = require("minimatch");
1113
1191
  init_GitClient();
1114
-
1115
- // src/conflict-utils.ts
1116
- function stripConflictMarkers(content) {
1117
- const lines = content.split("\n");
1118
- const result = [];
1119
- let inConflict = false;
1120
- let inOurs = false;
1121
- for (const line of lines) {
1122
- if (line.startsWith("<<<<<<< ")) {
1123
- inConflict = true;
1124
- inOurs = true;
1125
- continue;
1126
- }
1127
- if (inConflict && line === "=======") {
1128
- inOurs = false;
1129
- continue;
1130
- }
1131
- if (inConflict && line.startsWith(">>>>>>> ")) {
1132
- inConflict = false;
1133
- inOurs = false;
1134
- continue;
1135
- }
1136
- if (!inConflict || inOurs) {
1137
- result.push(line);
1138
- }
1139
- }
1140
- return result.join("\n");
1141
- }
1142
-
1143
- // src/ReplayService.ts
1144
1192
  var ReplayService = class {
1145
1193
  git;
1146
1194
  detector;
@@ -2108,30 +2156,145 @@ function computeContentHash(patchContent) {
2108
2156
 
2109
2157
  // src/commands/forget.ts
2110
2158
  var import_minimatch4 = require("minimatch");
2111
- function forget(outputDir, filePattern, options) {
2159
+ function parseDiffStat(patchContent) {
2160
+ let additions = 0;
2161
+ let deletions = 0;
2162
+ let inDiffHunk = false;
2163
+ for (const line of patchContent.split("\n")) {
2164
+ if (line.startsWith("diff --git ")) {
2165
+ inDiffHunk = true;
2166
+ continue;
2167
+ }
2168
+ if (!inDiffHunk) continue;
2169
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2170
+ additions++;
2171
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2172
+ deletions++;
2173
+ }
2174
+ }
2175
+ return { additions, deletions };
2176
+ }
2177
+ function toMatchedPatch(patch) {
2178
+ return {
2179
+ id: patch.id,
2180
+ message: patch.original_message,
2181
+ files: patch.files,
2182
+ diffstat: parseDiffStat(patch.patch_content),
2183
+ ...patch.status ? { status: patch.status } : {}
2184
+ };
2185
+ }
2186
+ function matchesPatch(patch, pattern) {
2187
+ const fileMatch = patch.files.some(
2188
+ (file) => file === pattern || (0, import_minimatch4.minimatch)(file, pattern)
2189
+ );
2190
+ if (fileMatch) return true;
2191
+ return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
2192
+ }
2193
+ function buildWarnings(patches) {
2194
+ const warnings = [];
2195
+ for (const patch of patches) {
2196
+ if (patch.status === "resolving") {
2197
+ warnings.push(
2198
+ `patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
2199
+ );
2200
+ } else if (patch.status === "unresolved") {
2201
+ warnings.push(
2202
+ `patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
2203
+ );
2204
+ }
2205
+ }
2206
+ return warnings;
2207
+ }
2208
+ var EMPTY_RESULT = {
2209
+ initialized: false,
2210
+ removed: [],
2211
+ remaining: 0,
2212
+ notFound: false,
2213
+ alreadyForgotten: [],
2214
+ totalPatches: 0,
2215
+ warnings: []
2216
+ };
2217
+ function forget(outputDir, options) {
2112
2218
  const lockManager = new LockfileManager(outputDir);
2113
2219
  if (!lockManager.exists()) {
2114
- return { removed: [], notFound: true };
2220
+ return { ...EMPTY_RESULT };
2115
2221
  }
2116
2222
  const lock = lockManager.read();
2117
- const matchingPatches = lock.patches.filter(
2118
- (patch) => patch.files.some((file) => file === filePattern || (0, import_minimatch4.minimatch)(file, filePattern))
2119
- );
2120
- if (matchingPatches.length === 0) {
2121
- return { removed: [], notFound: true };
2223
+ const totalPatches = lock.patches.length;
2224
+ if (options?.all) {
2225
+ const removed = lock.patches.map(toMatchedPatch);
2226
+ const warnings = buildWarnings(lock.patches);
2227
+ if (!options.dryRun) {
2228
+ for (const patch of lock.patches) {
2229
+ lockManager.addForgottenHash(patch.content_hash);
2230
+ }
2231
+ lockManager.clearPatches();
2232
+ lockManager.save();
2233
+ }
2234
+ return {
2235
+ initialized: true,
2236
+ removed,
2237
+ remaining: 0,
2238
+ notFound: false,
2239
+ alreadyForgotten: [],
2240
+ totalPatches,
2241
+ warnings
2242
+ };
2122
2243
  }
2123
- const removed = matchingPatches.map((p) => ({
2124
- id: p.id,
2125
- message: p.original_message,
2126
- files: p.files
2127
- }));
2128
- if (!options?.dryRun) {
2129
- for (const patch of matchingPatches) {
2130
- lockManager.removePatch(patch.id);
2244
+ if (options?.patchIds && options.patchIds.length > 0) {
2245
+ const removed = [];
2246
+ const alreadyForgotten = [];
2247
+ const patchesToRemove = [];
2248
+ for (const id of options.patchIds) {
2249
+ const patch = lock.patches.find((p) => p.id === id);
2250
+ if (patch) {
2251
+ removed.push(toMatchedPatch(patch));
2252
+ patchesToRemove.push(patch);
2253
+ } else {
2254
+ alreadyForgotten.push(id);
2255
+ }
2131
2256
  }
2132
- lockManager.save();
2257
+ const warnings = buildWarnings(patchesToRemove);
2258
+ if (!options.dryRun) {
2259
+ for (const patch of patchesToRemove) {
2260
+ lockManager.addForgottenHash(patch.content_hash);
2261
+ lockManager.removePatch(patch.id);
2262
+ }
2263
+ lockManager.save();
2264
+ }
2265
+ return {
2266
+ initialized: true,
2267
+ removed,
2268
+ remaining: totalPatches - removed.length,
2269
+ notFound: removed.length === 0 && alreadyForgotten.length > 0,
2270
+ alreadyForgotten,
2271
+ totalPatches,
2272
+ warnings
2273
+ };
2274
+ }
2275
+ if (options?.pattern) {
2276
+ const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
2277
+ return {
2278
+ initialized: true,
2279
+ removed: [],
2280
+ remaining: totalPatches,
2281
+ notFound: matched.length === 0,
2282
+ alreadyForgotten: [],
2283
+ totalPatches,
2284
+ warnings: [],
2285
+ matched
2286
+ };
2133
2287
  }
2134
- return { removed, notFound: false };
2288
+ return {
2289
+ initialized: true,
2290
+ removed: [],
2291
+ remaining: totalPatches,
2292
+ notFound: totalPatches === 0,
2293
+ alreadyForgotten: [],
2294
+ totalPatches,
2295
+ warnings: [],
2296
+ matched: lock.patches.map(toMatchedPatch)
2297
+ };
2135
2298
  }
2136
2299
 
2137
2300
  // src/commands/reset.ts
@@ -2253,24 +2416,49 @@ async function getChangedFiles(git, currentGen, files) {
2253
2416
  function status(outputDir) {
2254
2417
  const lockManager = new LockfileManager(outputDir);
2255
2418
  if (!lockManager.exists()) {
2256
- return { initialized: false, patches: [], lastGeneration: void 0 };
2419
+ return {
2420
+ initialized: false,
2421
+ generationCount: 0,
2422
+ lastGeneration: void 0,
2423
+ patches: [],
2424
+ unresolvedCount: 0,
2425
+ excludePatterns: []
2426
+ };
2257
2427
  }
2258
2428
  const lock = lockManager.read();
2259
2429
  const patches = lock.patches.map((patch) => ({
2260
- sha: patch.original_commit.slice(0, 7),
2261
- author: patch.original_author.split("<")[0]?.trim() ?? "unknown",
2430
+ id: patch.id,
2431
+ type: patch.patch_content.includes("new file mode") ? "added" : "modified",
2262
2432
  message: patch.original_message,
2263
- files: patch.files
2433
+ author: patch.original_author.split("<")[0]?.trim() || "unknown",
2434
+ sha: patch.original_commit.slice(0, 7),
2435
+ files: patch.files,
2436
+ fileCount: patch.files.length,
2437
+ ...patch.status ? { status: patch.status } : {}
2264
2438
  }));
2439
+ const unresolvedCount = lock.patches.filter(
2440
+ (p) => p.status === "unresolved" || p.status === "resolving"
2441
+ ).length;
2265
2442
  let lastGeneration;
2266
2443
  const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
2267
2444
  if (lastGen) {
2268
2445
  lastGeneration = {
2269
- sha: lastGen.commit_sha,
2270
- timestamp: lastGen.timestamp
2446
+ sha: lastGen.commit_sha.slice(0, 7),
2447
+ timestamp: lastGen.timestamp,
2448
+ cliVersion: lastGen.cli_version,
2449
+ generatorVersions: lastGen.generator_versions
2271
2450
  };
2272
2451
  }
2273
- return { initialized: true, patches, lastGeneration };
2452
+ const config = lockManager.getCustomizationsConfig();
2453
+ const excludePatterns = config.exclude ?? [];
2454
+ return {
2455
+ initialized: true,
2456
+ generationCount: lock.generations.length,
2457
+ lastGeneration,
2458
+ patches,
2459
+ unresolvedCount,
2460
+ excludePatterns
2461
+ };
2274
2462
  }
2275
2463
  // Annotate the CommonJS export names for ESM import in node:
2276
2464
  0 && (module.exports = {