@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.js CHANGED
@@ -142,7 +142,7 @@ function isReplayCommit(commit) {
142
142
  return commit.message.startsWith("[fern-replay]");
143
143
  }
144
144
  function isRevertCommit(message) {
145
- return message.startsWith('Revert "');
145
+ return /^Revert ".+"$/.test(message);
146
146
  }
147
147
  function parseRevertedSha(fullBody) {
148
148
  const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
@@ -243,6 +243,15 @@ var LockfileManager = class {
243
243
  this.ensureLoaded();
244
244
  this.lock.patches = [];
245
245
  }
246
+ addForgottenHash(hash) {
247
+ this.ensureLoaded();
248
+ if (!this.lock.forgotten_hashes) {
249
+ this.lock.forgotten_hashes = [];
250
+ }
251
+ if (!this.lock.forgotten_hashes.includes(hash)) {
252
+ this.lock.forgotten_hashes.push(hash);
253
+ }
254
+ }
246
255
  getUnresolvedPatches() {
247
256
  this.ensureLoaded();
248
257
  return this.lock.patches.filter((p) => p.status === "unresolved");
@@ -339,6 +348,7 @@ var ReplayDetector = class {
339
348
  }
340
349
  const commits = this.parseGitLog(log);
341
350
  const newPatches = [];
351
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
342
352
  for (const commit of commits) {
343
353
  if (isGenerationCommit(commit)) {
344
354
  continue;
@@ -352,7 +362,7 @@ var ReplayDetector = class {
352
362
  }
353
363
  const patchContent = await this.git.formatPatch(commit.sha);
354
364
  const contentHash = this.computeContentHash(patchContent);
355
- if (lock.patches.find((p) => p.content_hash === contentHash)) {
365
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
356
366
  continue;
357
367
  }
358
368
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
@@ -422,6 +432,9 @@ var ReplayDetector = class {
422
432
  revertIndicesToRemove.add(idx);
423
433
  }
424
434
  }
435
+ if (!matchedExisting && !matchedNew) {
436
+ revertIndicesToRemove.add(i);
437
+ }
425
438
  }
426
439
  const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
427
440
  return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
@@ -448,7 +461,7 @@ var ReplayDetector = class {
448
461
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
449
462
  const contentHash = this.computeContentHash(diff);
450
463
  const lock = this.lockManager.read();
451
- if (lock.patches.some((p) => p.content_hash === contentHash)) {
464
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
452
465
  return { patches: [], revertedPatchIds: [] };
453
466
  }
454
467
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
@@ -518,6 +531,36 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
518
531
  import { tmpdir } from "os";
519
532
  import { dirname as dirname2, extname, join as join2 } from "path";
520
533
  import { minimatch } from "minimatch";
534
+
535
+ // src/conflict-utils.ts
536
+ function stripConflictMarkers(content) {
537
+ const lines = content.split("\n");
538
+ const result = [];
539
+ let inConflict = false;
540
+ let inOurs = false;
541
+ for (const line of lines) {
542
+ if (line.startsWith("<<<<<<< ")) {
543
+ inConflict = true;
544
+ inOurs = true;
545
+ continue;
546
+ }
547
+ if (inConflict && line === "=======") {
548
+ inOurs = false;
549
+ continue;
550
+ }
551
+ if (inConflict && line.startsWith(">>>>>>> ")) {
552
+ inConflict = false;
553
+ inOurs = false;
554
+ continue;
555
+ }
556
+ if (!inConflict || inOurs) {
557
+ result.push(line);
558
+ }
559
+ }
560
+ return result.join("\n");
561
+ }
562
+
563
+ // src/ReplayApplicator.ts
521
564
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
522
565
  ".png",
523
566
  ".jpg",
@@ -589,7 +632,8 @@ var ReplayApplicator = class {
589
632
  async applyPatches(patches) {
590
633
  this.resetAccumulator();
591
634
  const results = [];
592
- for (const patch of patches) {
635
+ for (let i = 0; i < patches.length; i++) {
636
+ const patch = patches[i];
593
637
  if (this.isExcluded(patch)) {
594
638
  results.push({
595
639
  patch,
@@ -600,6 +644,33 @@ var ReplayApplicator = class {
600
644
  }
601
645
  const result = await this.applyPatchWithFallback(patch);
602
646
  results.push(result);
647
+ if (result.status === "conflict" && result.fileResults) {
648
+ const laterFiles = /* @__PURE__ */ new Set();
649
+ for (let j = i + 1; j < patches.length; j++) {
650
+ for (const f of patches[j].files) {
651
+ laterFiles.add(f);
652
+ }
653
+ }
654
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
655
+ if (result.resolvedFiles) {
656
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
657
+ resolvedToOriginal.set(resolved, orig);
658
+ }
659
+ }
660
+ for (const fileResult of result.fileResults) {
661
+ if (fileResult.status !== "conflict") continue;
662
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
663
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
664
+ const filePath = join2(this.outputDir, fileResult.file);
665
+ try {
666
+ const content = await readFile(filePath, "utf-8");
667
+ const stripped = stripConflictMarkers(content);
668
+ await writeFile(filePath, stripped);
669
+ } catch {
670
+ }
671
+ }
672
+ }
673
+ }
603
674
  }
604
675
  return results;
605
676
  }
@@ -618,7 +689,7 @@ var ReplayApplicator = class {
618
689
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
619
690
  const base = await this.git.showFile(baseGen.tree_hash, filePath);
620
691
  const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
621
- if (theirs && base) {
692
+ if (theirs) {
622
693
  this.fileTheirsAccumulator.set(resolvedPath, {
623
694
  content: theirs,
624
695
  baseGeneration: patch.base_generation
@@ -750,7 +821,7 @@ var ReplayApplicator = class {
750
821
  );
751
822
  let useAccumulatorAsMergeBase = false;
752
823
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
753
- if (!theirs && base && accumulatorEntry) {
824
+ if (!theirs && accumulatorEntry) {
754
825
  theirs = await this.applyPatchToContent(
755
826
  accumulatorEntry.content,
756
827
  patch.patch_content,
@@ -809,7 +880,7 @@ var ReplayApplicator = class {
809
880
  reason: "missing-content"
810
881
  };
811
882
  }
812
- if (!base || !ours) {
883
+ if (!base && !useAccumulatorAsMergeBase || !ours) {
813
884
  return {
814
885
  file: resolvedPath,
815
886
  status: "skipped",
@@ -817,11 +888,18 @@ var ReplayApplicator = class {
817
888
  };
818
889
  }
819
890
  const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
891
+ if (mergeBase == null) {
892
+ return {
893
+ file: resolvedPath,
894
+ status: "skipped",
895
+ reason: "missing-content"
896
+ };
897
+ }
820
898
  const merged = threeWayMerge(mergeBase, ours, effective_theirs);
821
899
  const outDir = dirname2(oursPath);
822
900
  await mkdir(outDir, { recursive: true });
823
901
  await writeFile(oursPath, merged.content);
824
- if (effective_theirs && base) {
902
+ if (effective_theirs) {
825
903
  this.fileTheirsAccumulator.set(resolvedPath, {
826
904
  content: effective_theirs,
827
905
  baseGeneration: patch.base_generation
@@ -1064,36 +1142,6 @@ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync
1064
1142
  import { join as join3 } from "path";
1065
1143
  import { minimatch as minimatch2 } from "minimatch";
1066
1144
  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
1097
1145
  var ReplayService = class {
1098
1146
  git;
1099
1147
  detector;
@@ -2061,30 +2109,145 @@ function computeContentHash(patchContent) {
2061
2109
 
2062
2110
  // src/commands/forget.ts
2063
2111
  import { minimatch as minimatch4 } from "minimatch";
2064
- function forget(outputDir, filePattern, options) {
2112
+ function parseDiffStat(patchContent) {
2113
+ let additions = 0;
2114
+ let deletions = 0;
2115
+ let inDiffHunk = false;
2116
+ for (const line of patchContent.split("\n")) {
2117
+ if (line.startsWith("diff --git ")) {
2118
+ inDiffHunk = true;
2119
+ continue;
2120
+ }
2121
+ if (!inDiffHunk) continue;
2122
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2123
+ additions++;
2124
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2125
+ deletions++;
2126
+ }
2127
+ }
2128
+ return { additions, deletions };
2129
+ }
2130
+ function toMatchedPatch(patch) {
2131
+ return {
2132
+ id: patch.id,
2133
+ message: patch.original_message,
2134
+ files: patch.files,
2135
+ diffstat: parseDiffStat(patch.patch_content),
2136
+ ...patch.status ? { status: patch.status } : {}
2137
+ };
2138
+ }
2139
+ function matchesPatch(patch, pattern) {
2140
+ const fileMatch = patch.files.some(
2141
+ (file) => file === pattern || minimatch4(file, pattern)
2142
+ );
2143
+ if (fileMatch) return true;
2144
+ return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
2145
+ }
2146
+ function buildWarnings(patches) {
2147
+ const warnings = [];
2148
+ for (const patch of patches) {
2149
+ if (patch.status === "resolving") {
2150
+ warnings.push(
2151
+ `patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
2152
+ );
2153
+ } else if (patch.status === "unresolved") {
2154
+ warnings.push(
2155
+ `patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
2156
+ );
2157
+ }
2158
+ }
2159
+ return warnings;
2160
+ }
2161
+ var EMPTY_RESULT = {
2162
+ initialized: false,
2163
+ removed: [],
2164
+ remaining: 0,
2165
+ notFound: false,
2166
+ alreadyForgotten: [],
2167
+ totalPatches: 0,
2168
+ warnings: []
2169
+ };
2170
+ function forget(outputDir, options) {
2065
2171
  const lockManager = new LockfileManager(outputDir);
2066
2172
  if (!lockManager.exists()) {
2067
- return { removed: [], notFound: true };
2173
+ return { ...EMPTY_RESULT };
2068
2174
  }
2069
2175
  const lock = lockManager.read();
2070
- const matchingPatches = lock.patches.filter(
2071
- (patch) => patch.files.some((file) => file === filePattern || minimatch4(file, filePattern))
2072
- );
2073
- if (matchingPatches.length === 0) {
2074
- return { removed: [], notFound: true };
2176
+ const totalPatches = lock.patches.length;
2177
+ if (options?.all) {
2178
+ const removed = lock.patches.map(toMatchedPatch);
2179
+ const warnings = buildWarnings(lock.patches);
2180
+ if (!options.dryRun) {
2181
+ for (const patch of lock.patches) {
2182
+ lockManager.addForgottenHash(patch.content_hash);
2183
+ }
2184
+ lockManager.clearPatches();
2185
+ lockManager.save();
2186
+ }
2187
+ return {
2188
+ initialized: true,
2189
+ removed,
2190
+ remaining: 0,
2191
+ notFound: false,
2192
+ alreadyForgotten: [],
2193
+ totalPatches,
2194
+ warnings
2195
+ };
2075
2196
  }
2076
- const removed = matchingPatches.map((p) => ({
2077
- id: p.id,
2078
- message: p.original_message,
2079
- files: p.files
2080
- }));
2081
- if (!options?.dryRun) {
2082
- for (const patch of matchingPatches) {
2083
- lockManager.removePatch(patch.id);
2197
+ if (options?.patchIds && options.patchIds.length > 0) {
2198
+ const removed = [];
2199
+ const alreadyForgotten = [];
2200
+ const patchesToRemove = [];
2201
+ for (const id of options.patchIds) {
2202
+ const patch = lock.patches.find((p) => p.id === id);
2203
+ if (patch) {
2204
+ removed.push(toMatchedPatch(patch));
2205
+ patchesToRemove.push(patch);
2206
+ } else {
2207
+ alreadyForgotten.push(id);
2208
+ }
2084
2209
  }
2085
- lockManager.save();
2210
+ const warnings = buildWarnings(patchesToRemove);
2211
+ if (!options.dryRun) {
2212
+ for (const patch of patchesToRemove) {
2213
+ lockManager.addForgottenHash(patch.content_hash);
2214
+ lockManager.removePatch(patch.id);
2215
+ }
2216
+ lockManager.save();
2217
+ }
2218
+ return {
2219
+ initialized: true,
2220
+ removed,
2221
+ remaining: totalPatches - removed.length,
2222
+ notFound: removed.length === 0 && alreadyForgotten.length > 0,
2223
+ alreadyForgotten,
2224
+ totalPatches,
2225
+ warnings
2226
+ };
2227
+ }
2228
+ if (options?.pattern) {
2229
+ const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
2230
+ return {
2231
+ initialized: true,
2232
+ removed: [],
2233
+ remaining: totalPatches,
2234
+ notFound: matched.length === 0,
2235
+ alreadyForgotten: [],
2236
+ totalPatches,
2237
+ warnings: [],
2238
+ matched
2239
+ };
2086
2240
  }
2087
- return { removed, notFound: false };
2241
+ return {
2242
+ initialized: true,
2243
+ removed: [],
2244
+ remaining: totalPatches,
2245
+ notFound: totalPatches === 0,
2246
+ alreadyForgotten: [],
2247
+ totalPatches,
2248
+ warnings: [],
2249
+ matched: lock.patches.map(toMatchedPatch)
2250
+ };
2088
2251
  }
2089
2252
 
2090
2253
  // src/commands/reset.ts
@@ -2206,24 +2369,49 @@ async function getChangedFiles(git, currentGen, files) {
2206
2369
  function status(outputDir) {
2207
2370
  const lockManager = new LockfileManager(outputDir);
2208
2371
  if (!lockManager.exists()) {
2209
- return { initialized: false, patches: [], lastGeneration: void 0 };
2372
+ return {
2373
+ initialized: false,
2374
+ generationCount: 0,
2375
+ lastGeneration: void 0,
2376
+ patches: [],
2377
+ unresolvedCount: 0,
2378
+ excludePatterns: []
2379
+ };
2210
2380
  }
2211
2381
  const lock = lockManager.read();
2212
2382
  const patches = lock.patches.map((patch) => ({
2213
- sha: patch.original_commit.slice(0, 7),
2214
- author: patch.original_author.split("<")[0]?.trim() ?? "unknown",
2383
+ id: patch.id,
2384
+ type: patch.patch_content.includes("new file mode") ? "added" : "modified",
2215
2385
  message: patch.original_message,
2216
- files: patch.files
2386
+ author: patch.original_author.split("<")[0]?.trim() || "unknown",
2387
+ sha: patch.original_commit.slice(0, 7),
2388
+ files: patch.files,
2389
+ fileCount: patch.files.length,
2390
+ ...patch.status ? { status: patch.status } : {}
2217
2391
  }));
2392
+ const unresolvedCount = lock.patches.filter(
2393
+ (p) => p.status === "unresolved" || p.status === "resolving"
2394
+ ).length;
2218
2395
  let lastGeneration;
2219
2396
  const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
2220
2397
  if (lastGen) {
2221
2398
  lastGeneration = {
2222
- sha: lastGen.commit_sha,
2223
- timestamp: lastGen.timestamp
2399
+ sha: lastGen.commit_sha.slice(0, 7),
2400
+ timestamp: lastGen.timestamp,
2401
+ cliVersion: lastGen.cli_version,
2402
+ generatorVersions: lastGen.generator_versions
2224
2403
  };
2225
2404
  }
2226
- return { initialized: true, patches, lastGeneration };
2405
+ const config = lockManager.getCustomizationsConfig();
2406
+ const excludePatterns = config.exclude ?? [];
2407
+ return {
2408
+ initialized: true,
2409
+ generationCount: lock.generations.length,
2410
+ lastGeneration,
2411
+ patches,
2412
+ unresolvedCount,
2413
+ excludePatterns
2414
+ };
2227
2415
  }
2228
2416
  export {
2229
2417
  FERN_BOT_EMAIL,