@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 +321 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +321 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +321 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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")}`;
|