@fern-api/replay 0.9.0 → 0.9.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/cli.cjs CHANGED
@@ -5335,6 +5335,14 @@ var init_GitClient = __esm({
5335
5335
  return false;
5336
5336
  }
5337
5337
  }
5338
+ async treeExists(treeHash) {
5339
+ try {
5340
+ const type = await this.exec(["cat-file", "-t", treeHash]);
5341
+ return type.trim() === "tree";
5342
+ } catch {
5343
+ return false;
5344
+ }
5345
+ }
5338
5346
  async getCommitBody(commitSha) {
5339
5347
  return this.exec(["log", "-1", "--format=%B", commitSha]);
5340
5348
  }
@@ -12843,6 +12851,255 @@ var require_brace_expansion = __commonJS({
12843
12851
  }
12844
12852
  });
12845
12853
 
12854
+ // src/HybridReconstruction.ts
12855
+ var HybridReconstruction_exports = {};
12856
+ __export(HybridReconstruction_exports, {
12857
+ assembleHybrid: () => assembleHybrid,
12858
+ locateHunksInOurs: () => locateHunksInOurs,
12859
+ parseHunks: () => parseHunks,
12860
+ reconstructFromGhostPatch: () => reconstructFromGhostPatch
12861
+ });
12862
+ function parseHunks(fileDiff) {
12863
+ const lines = fileDiff.split("\n");
12864
+ const hunks = [];
12865
+ let currentHunk = null;
12866
+ for (const line of lines) {
12867
+ const headerMatch = line.match(
12868
+ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
12869
+ );
12870
+ if (headerMatch) {
12871
+ if (currentHunk) {
12872
+ hunks.push(currentHunk);
12873
+ }
12874
+ currentHunk = {
12875
+ oldStart: parseInt(headerMatch[1], 10),
12876
+ oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
12877
+ newStart: parseInt(headerMatch[3], 10),
12878
+ newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
12879
+ lines: []
12880
+ };
12881
+ continue;
12882
+ }
12883
+ if (!currentHunk) continue;
12884
+ if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("new file mode") || line.startsWith("deleted file mode")) {
12885
+ continue;
12886
+ }
12887
+ if (line === "\") {
12888
+ continue;
12889
+ }
12890
+ if (line.startsWith("-")) {
12891
+ currentHunk.lines.push({ type: "remove", content: line.slice(1) });
12892
+ } else if (line.startsWith("+")) {
12893
+ currentHunk.lines.push({ type: "add", content: line.slice(1) });
12894
+ } else if (line.startsWith(" ") || line === "") {
12895
+ currentHunk.lines.push({
12896
+ type: "context",
12897
+ content: line.startsWith(" ") ? line.slice(1) : line
12898
+ });
12899
+ }
12900
+ }
12901
+ if (currentHunk) {
12902
+ hunks.push(currentHunk);
12903
+ }
12904
+ return hunks;
12905
+ }
12906
+ function extractLeadingContext(hunk) {
12907
+ const result = [];
12908
+ for (const line of hunk.lines) {
12909
+ if (line.type !== "context") break;
12910
+ result.push(line.content);
12911
+ }
12912
+ return result;
12913
+ }
12914
+ function extractTrailingContext(hunk) {
12915
+ const result = [];
12916
+ for (let i = hunk.lines.length - 1; i >= 0; i--) {
12917
+ if (hunk.lines[i].type !== "context") break;
12918
+ result.unshift(hunk.lines[i].content);
12919
+ }
12920
+ return result;
12921
+ }
12922
+ function countOursLinesBeforeTrailing(hunk) {
12923
+ let count = 0;
12924
+ const trailingStart = findTrailingContextStart(hunk);
12925
+ for (let i = 0; i < trailingStart; i++) {
12926
+ if (hunk.lines[i].type === "context") count++;
12927
+ }
12928
+ return count;
12929
+ }
12930
+ function findTrailingContextStart(hunk) {
12931
+ let i = hunk.lines.length - 1;
12932
+ while (i >= 0 && hunk.lines[i].type === "context") {
12933
+ i--;
12934
+ }
12935
+ return i + 1;
12936
+ }
12937
+ function matchesAt(needle, haystack, offset) {
12938
+ for (let i = 0; i < needle.length; i++) {
12939
+ if (haystack[offset + i] !== needle[i]) return false;
12940
+ }
12941
+ return true;
12942
+ }
12943
+ function findContextInOurs(contextLines, oursLines, minIndex, hint) {
12944
+ const SEARCH_WINDOW = 200;
12945
+ const maxStart = oursLines.length - contextLines.length;
12946
+ const clampedHint = Math.max(minIndex, Math.min(hint, maxStart));
12947
+ if (clampedHint >= minIndex && clampedHint <= maxStart) {
12948
+ if (matchesAt(contextLines, oursLines, clampedHint)) {
12949
+ return clampedHint;
12950
+ }
12951
+ }
12952
+ for (let delta = 1; delta <= SEARCH_WINDOW; delta++) {
12953
+ for (const sign of [1, -1]) {
12954
+ const idx = clampedHint + delta * sign;
12955
+ if (idx < minIndex || idx > maxStart) continue;
12956
+ if (matchesAt(contextLines, oursLines, idx)) {
12957
+ return idx;
12958
+ }
12959
+ }
12960
+ }
12961
+ return -1;
12962
+ }
12963
+ function computeOursSpan(hunk, oursLines, oursOffset) {
12964
+ const leading = extractLeadingContext(hunk);
12965
+ const trailing = extractTrailingContext(hunk);
12966
+ if (trailing.length === 0) {
12967
+ const contextCount2 = hunk.lines.filter(
12968
+ (l) => l.type === "context"
12969
+ ).length;
12970
+ return Math.min(contextCount2, oursLines.length - oursOffset);
12971
+ }
12972
+ const searchStart = oursOffset + leading.length;
12973
+ for (let i = searchStart; i <= oursLines.length - trailing.length; i++) {
12974
+ if (matchesAt(trailing, oursLines, i)) {
12975
+ return i + trailing.length - oursOffset;
12976
+ }
12977
+ }
12978
+ const contextCount = hunk.lines.filter(
12979
+ (l) => l.type === "context"
12980
+ ).length;
12981
+ return Math.min(contextCount, oursLines.length - oursOffset);
12982
+ }
12983
+ function locateHunksInOurs(hunks, oursLines) {
12984
+ const located = [];
12985
+ let minOursIndex = 0;
12986
+ for (const hunk of hunks) {
12987
+ const contextLines = extractLeadingContext(hunk);
12988
+ let oursOffset;
12989
+ if (contextLines.length > 0) {
12990
+ const found = findContextInOurs(
12991
+ contextLines,
12992
+ oursLines,
12993
+ minOursIndex,
12994
+ hunk.newStart - 1
12995
+ );
12996
+ if (found === -1) {
12997
+ const trailingContext = extractTrailingContext(hunk);
12998
+ if (trailingContext.length > 0) {
12999
+ const trailingFound = findContextInOurs(
13000
+ trailingContext,
13001
+ oursLines,
13002
+ minOursIndex,
13003
+ hunk.newStart - 1
13004
+ );
13005
+ if (trailingFound === -1) return null;
13006
+ const nonTrailingCount = countOursLinesBeforeTrailing(hunk);
13007
+ oursOffset = trailingFound - nonTrailingCount;
13008
+ if (oursOffset < minOursIndex) return null;
13009
+ } else {
13010
+ return null;
13011
+ }
13012
+ } else {
13013
+ oursOffset = found;
13014
+ }
13015
+ } else if (hunk.oldStart === 1 && hunk.oldCount === 0) {
13016
+ oursOffset = 0;
13017
+ } else {
13018
+ oursOffset = Math.max(hunk.newStart - 1, minOursIndex);
13019
+ }
13020
+ const oursSpan = computeOursSpan(hunk, oursLines, oursOffset);
13021
+ located.push({ hunk, oursOffset, oursSpan });
13022
+ minOursIndex = oursOffset + oursSpan;
13023
+ }
13024
+ return located;
13025
+ }
13026
+ function assembleHybrid(locatedHunks, oursLines) {
13027
+ const baseLines = [];
13028
+ const theirsLines = [];
13029
+ let oursCursor = 0;
13030
+ for (const { hunk, oursOffset, oursSpan } of locatedHunks) {
13031
+ if (oursOffset > oursCursor) {
13032
+ const gapLines = oursLines.slice(oursCursor, oursOffset);
13033
+ baseLines.push(...gapLines);
13034
+ theirsLines.push(...gapLines);
13035
+ }
13036
+ for (const line of hunk.lines) {
13037
+ switch (line.type) {
13038
+ case "context":
13039
+ baseLines.push(line.content);
13040
+ theirsLines.push(line.content);
13041
+ break;
13042
+ case "remove":
13043
+ baseLines.push(line.content);
13044
+ break;
13045
+ case "add":
13046
+ theirsLines.push(line.content);
13047
+ break;
13048
+ }
13049
+ }
13050
+ oursCursor = oursOffset + oursSpan;
13051
+ }
13052
+ if (oursCursor < oursLines.length) {
13053
+ const gapLines = oursLines.slice(oursCursor);
13054
+ baseLines.push(...gapLines);
13055
+ theirsLines.push(...gapLines);
13056
+ }
13057
+ return {
13058
+ base: baseLines.join("\n"),
13059
+ theirs: theirsLines.join("\n")
13060
+ };
13061
+ }
13062
+ function reconstructFromGhostPatch(fileDiff, ours) {
13063
+ const hunks = parseHunks(fileDiff);
13064
+ if (hunks.length === 0) {
13065
+ return null;
13066
+ }
13067
+ const isPureAddition = hunks.every(
13068
+ (h) => h.oldCount === 0 && h.lines.every((l) => l.type !== "remove")
13069
+ );
13070
+ if (isPureAddition) {
13071
+ return null;
13072
+ }
13073
+ const isPureDeletion = hunks.every(
13074
+ (h) => h.newCount === 0 && h.lines.every((l) => l.type !== "add")
13075
+ );
13076
+ if (isPureDeletion) {
13077
+ const baseLines = [];
13078
+ for (const hunk of hunks) {
13079
+ for (const line of hunk.lines) {
13080
+ if (line.type === "context" || line.type === "remove") {
13081
+ baseLines.push(line.content);
13082
+ }
13083
+ }
13084
+ }
13085
+ return {
13086
+ base: baseLines.join("\n"),
13087
+ theirs: ""
13088
+ };
13089
+ }
13090
+ const oursLines = ours.split("\n");
13091
+ const located = locateHunksInOurs(hunks, oursLines);
13092
+ if (!located) {
13093
+ return null;
13094
+ }
13095
+ return assembleHybrid(located, oursLines);
13096
+ }
13097
+ var init_HybridReconstruction = __esm({
13098
+ "src/HybridReconstruction.ts"() {
13099
+ "use strict";
13100
+ }
13101
+ });
13102
+
12846
13103
  // src/cli.ts
12847
13104
  var import_node_path7 = require("path");
12848
13105
  var import_node_fs6 = require("fs");
@@ -14891,6 +15148,7 @@ var ReplayApplicator = class {
14891
15148
  lockManager;
14892
15149
  outputDir;
14893
15150
  renameCache = /* @__PURE__ */ new Map();
15151
+ treeExistsCache = /* @__PURE__ */ new Map();
14894
15152
  fileTheirsAccumulator = /* @__PURE__ */ new Map();
14895
15153
  constructor(git, lockManager, outputDir) {
14896
15154
  this.git = git;
@@ -15087,14 +15345,33 @@ var ReplayApplicator = class {
15087
15345
  }
15088
15346
  const oursPath = (0, import_node_path3.join)(this.outputDir, resolvedPath);
15089
15347
  const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
15090
- let theirs = await this.applyPatchToContent(
15091
- base,
15092
- patch.patch_content,
15093
- filePath,
15094
- tempGit,
15095
- tempDir,
15096
- renameSourcePath
15097
- );
15348
+ let ghostReconstructed = false;
15349
+ let theirs = null;
15350
+ if (!base && ours && !renameSourcePath) {
15351
+ const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
15352
+ if (!treeReachable) {
15353
+ const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
15354
+ if (fileDiff) {
15355
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
15356
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
15357
+ if (result) {
15358
+ base = result.base;
15359
+ theirs = result.theirs;
15360
+ ghostReconstructed = true;
15361
+ }
15362
+ }
15363
+ }
15364
+ }
15365
+ if (!ghostReconstructed) {
15366
+ theirs = await this.applyPatchToContent(
15367
+ base,
15368
+ patch.patch_content,
15369
+ filePath,
15370
+ tempGit,
15371
+ tempDir,
15372
+ renameSourcePath
15373
+ );
15374
+ }
15098
15375
  let useAccumulatorAsMergeBase = false;
15099
15376
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
15100
15377
  if (!theirs && accumulatorEntry) {
@@ -15127,13 +15404,13 @@ var ReplayApplicator = class {
15127
15404
  baseMismatchSkipped = true;
15128
15405
  }
15129
15406
  }
15130
- if (!base && !ours && effective_theirs) {
15407
+ if (base == null && !ours && effective_theirs) {
15131
15408
  const outDir2 = (0, import_node_path3.dirname)(oursPath);
15132
15409
  await (0, import_promises.mkdir)(outDir2, { recursive: true });
15133
15410
  await (0, import_promises.writeFile)(oursPath, effective_theirs);
15134
15411
  return { file: resolvedPath, status: "merged", reason: "new-file" };
15135
15412
  }
15136
- if (!base && ours && effective_theirs) {
15413
+ if (base == null && ours && effective_theirs) {
15137
15414
  const merged2 = threeWayMerge("", ours, effective_theirs);
15138
15415
  const outDir2 = (0, import_node_path3.dirname)(oursPath);
15139
15416
  await (0, import_promises.mkdir)(outDir2, { recursive: true });
@@ -15156,7 +15433,7 @@ var ReplayApplicator = class {
15156
15433
  reason: "missing-content"
15157
15434
  };
15158
15435
  }
15159
- if (!base && !useAccumulatorAsMergeBase || !ours) {
15436
+ if (base == null && !useAccumulatorAsMergeBase || !ours) {
15160
15437
  return {
15161
15438
  file: resolvedPath,
15162
15439
  status: "skipped",
@@ -15199,6 +15476,14 @@ var ReplayApplicator = class {
15199
15476
  };
15200
15477
  }
15201
15478
  }
15479
+ async isTreeReachable(treeHash) {
15480
+ let result = this.treeExistsCache.get(treeHash);
15481
+ if (result === void 0) {
15482
+ result = await this.git.treeExists(treeHash);
15483
+ this.treeExistsCache.set(treeHash, result);
15484
+ }
15485
+ return result;
15486
+ }
15202
15487
  isExcluded(patch) {
15203
15488
  const config = this.lockManager.getCustomizationsConfig();
15204
15489
  if (!config.exclude) return false;
@@ -16268,6 +16553,7 @@ async function bootstrap(outputDir, options) {
16268
16553
  }
16269
16554
  lockManager.save();
16270
16555
  const fernignoreUpdated = ensureFernignoreEntries(outputDir);
16556
+ ensureGitattributesEntries(outputDir);
16271
16557
  if (migrator.fernignoreExists() && fernignorePatterns.length > 0) {
16272
16558
  const action = options?.fernignoreAction ?? "skip";
16273
16559
  if (action === "migrate") {
@@ -16349,7 +16635,7 @@ function parseGitLog(log) {
16349
16635
  return { sha, authorName, authorEmail, message };
16350
16636
  });
16351
16637
  }
16352
- var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml"];
16638
+ var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
16353
16639
  function ensureFernignoreEntries(outputDir) {
16354
16640
  const fernignorePath = (0, import_node_path6.join)(outputDir, ".fernignore");
16355
16641
  let content = "";
@@ -16373,6 +16659,29 @@ function ensureFernignoreEntries(outputDir) {
16373
16659
  (0, import_node_fs4.writeFileSync)(fernignorePath, content, "utf-8");
16374
16660
  return true;
16375
16661
  }
16662
+ var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
16663
+ function ensureGitattributesEntries(outputDir) {
16664
+ const gitattributesPath = (0, import_node_path6.join)(outputDir, ".gitattributes");
16665
+ let content = "";
16666
+ if ((0, import_node_fs4.existsSync)(gitattributesPath)) {
16667
+ content = (0, import_node_fs4.readFileSync)(gitattributesPath, "utf-8");
16668
+ }
16669
+ const lines = content.split("\n");
16670
+ const toAdd = [];
16671
+ for (const entry of GITATTRIBUTES_ENTRIES) {
16672
+ if (!lines.some((line) => line.trim() === entry)) {
16673
+ toAdd.push(entry);
16674
+ }
16675
+ }
16676
+ if (toAdd.length === 0) {
16677
+ return;
16678
+ }
16679
+ if (content && !content.endsWith("\n")) {
16680
+ content += "\n";
16681
+ }
16682
+ content += toAdd.join("\n") + "\n";
16683
+ (0, import_node_fs4.writeFileSync)(gitattributesPath, content, "utf-8");
16684
+ }
16376
16685
  function computeContentHash(patchContent) {
16377
16686
  const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
16378
16687
  return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex")}`;