@fern-api/replay 0.9.0 → 0.10.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/cli.cjs +745 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +716 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +716 -77
- 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");
|
|
@@ -12854,6 +13111,12 @@ var import_node_fs = require("fs");
|
|
|
12854
13111
|
var import_node_path2 = require("path");
|
|
12855
13112
|
var import_yaml = __toESM(require_dist3(), 1);
|
|
12856
13113
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
13114
|
+
var LockfileNotFoundError = class extends Error {
|
|
13115
|
+
constructor(path2) {
|
|
13116
|
+
super(`Lockfile not found: ${path2}`);
|
|
13117
|
+
this.name = "LockfileNotFoundError";
|
|
13118
|
+
}
|
|
13119
|
+
};
|
|
12857
13120
|
var LockfileManager = class {
|
|
12858
13121
|
outputDir;
|
|
12859
13122
|
lock = null;
|
|
@@ -12873,12 +13136,16 @@ var LockfileManager = class {
|
|
|
12873
13136
|
if (this.lock) {
|
|
12874
13137
|
return this.lock;
|
|
12875
13138
|
}
|
|
12876
|
-
|
|
12877
|
-
|
|
13139
|
+
try {
|
|
13140
|
+
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
13141
|
+
this.lock = (0, import_yaml.parse)(content);
|
|
13142
|
+
return this.lock;
|
|
13143
|
+
} catch (error) {
|
|
13144
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
13145
|
+
throw new LockfileNotFoundError(this.lockfilePath);
|
|
13146
|
+
}
|
|
13147
|
+
throw error;
|
|
12878
13148
|
}
|
|
12879
|
-
const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
|
|
12880
|
-
this.lock = (0, import_yaml.parse)(content);
|
|
12881
|
-
return this.lock;
|
|
12882
13149
|
}
|
|
12883
13150
|
initialize(firstGeneration) {
|
|
12884
13151
|
this.initializeInMemory(firstGeneration);
|
|
@@ -13055,13 +13322,21 @@ var ReplayDetector = class {
|
|
|
13055
13322
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
13056
13323
|
if (!exists2) {
|
|
13057
13324
|
this.warnings.push(
|
|
13058
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history.
|
|
13325
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to alternate detection.`
|
|
13326
|
+
);
|
|
13327
|
+
return this.detectPatchesViaTreeDiff(
|
|
13328
|
+
lastGen,
|
|
13329
|
+
/* commitKnownMissing */
|
|
13330
|
+
true
|
|
13059
13331
|
);
|
|
13060
|
-
return { patches: [], revertedPatchIds: [] };
|
|
13061
13332
|
}
|
|
13062
13333
|
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
13063
13334
|
if (!isAncestor) {
|
|
13064
|
-
return this.detectPatchesViaTreeDiff(
|
|
13335
|
+
return this.detectPatchesViaTreeDiff(
|
|
13336
|
+
lastGen,
|
|
13337
|
+
/* commitKnownMissing */
|
|
13338
|
+
false
|
|
13339
|
+
);
|
|
13065
13340
|
}
|
|
13066
13341
|
const log = await this.git.exec([
|
|
13067
13342
|
"log",
|
|
@@ -13087,7 +13362,15 @@ var ReplayDetector = class {
|
|
|
13087
13362
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
13088
13363
|
continue;
|
|
13089
13364
|
}
|
|
13090
|
-
|
|
13365
|
+
let patchContent;
|
|
13366
|
+
try {
|
|
13367
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
13368
|
+
} catch {
|
|
13369
|
+
this.warnings.push(
|
|
13370
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
13371
|
+
);
|
|
13372
|
+
continue;
|
|
13373
|
+
}
|
|
13091
13374
|
const contentHash = this.computeContentHash(patchContent);
|
|
13092
13375
|
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
13093
13376
|
continue;
|
|
@@ -13180,11 +13463,15 @@ var ReplayDetector = class {
|
|
|
13180
13463
|
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
13181
13464
|
* patch from the aggregate diff — individual revert commits are not distinguishable.
|
|
13182
13465
|
*/
|
|
13183
|
-
async detectPatchesViaTreeDiff(lastGen) {
|
|
13184
|
-
const
|
|
13466
|
+
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
13467
|
+
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
13468
|
+
if (!diffBase) {
|
|
13469
|
+
return this.detectPatchesViaCommitScan();
|
|
13470
|
+
}
|
|
13471
|
+
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
13185
13472
|
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
13186
13473
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
13187
|
-
const diff = await this.git.exec(["diff",
|
|
13474
|
+
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
13188
13475
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
13189
13476
|
const contentHash = this.computeContentHash(diff);
|
|
13190
13477
|
const lock = this.lockManager.read();
|
|
@@ -13198,12 +13485,113 @@ var ReplayDetector = class {
|
|
|
13198
13485
|
original_commit: headSha,
|
|
13199
13486
|
original_message: "Customer customizations (composite)",
|
|
13200
13487
|
original_author: "composite",
|
|
13201
|
-
|
|
13488
|
+
// Use diffBase when commit is unreachable — the applicator needs a reachable
|
|
13489
|
+
// reference to find base file content. diffBase may be the tree_hash.
|
|
13490
|
+
base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
|
|
13202
13491
|
files,
|
|
13203
13492
|
patch_content: diff
|
|
13204
13493
|
};
|
|
13205
13494
|
return { patches: [compositePatch], revertedPatchIds: [] };
|
|
13206
13495
|
}
|
|
13496
|
+
/**
|
|
13497
|
+
* Last-resort detection when both generation commit and tree are unreachable.
|
|
13498
|
+
* Scans all commits from HEAD, filters against known lockfile patches, and
|
|
13499
|
+
* skips creation-only commits (squashed history after force push).
|
|
13500
|
+
*
|
|
13501
|
+
* Detected patches use the commit's parent as base_generation so the applicator
|
|
13502
|
+
* can find base file content from the parent's tree (which IS reachable).
|
|
13503
|
+
*/
|
|
13504
|
+
async detectPatchesViaCommitScan() {
|
|
13505
|
+
const lock = this.lockManager.read();
|
|
13506
|
+
const log = await this.git.exec([
|
|
13507
|
+
"log",
|
|
13508
|
+
"--max-count=200",
|
|
13509
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
13510
|
+
"HEAD",
|
|
13511
|
+
"--",
|
|
13512
|
+
this.sdkOutputDir
|
|
13513
|
+
]);
|
|
13514
|
+
if (!log.trim()) {
|
|
13515
|
+
return { patches: [], revertedPatchIds: [] };
|
|
13516
|
+
}
|
|
13517
|
+
const commits = this.parseGitLog(log);
|
|
13518
|
+
const newPatches = [];
|
|
13519
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
13520
|
+
const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
|
|
13521
|
+
const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
|
|
13522
|
+
for (const commit of commits) {
|
|
13523
|
+
if (isGenerationCommit(commit)) {
|
|
13524
|
+
continue;
|
|
13525
|
+
}
|
|
13526
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
13527
|
+
if (parents.length > 1) {
|
|
13528
|
+
continue;
|
|
13529
|
+
}
|
|
13530
|
+
if (existingCommits.has(commit.sha)) {
|
|
13531
|
+
continue;
|
|
13532
|
+
}
|
|
13533
|
+
let patchContent;
|
|
13534
|
+
try {
|
|
13535
|
+
patchContent = await this.git.formatPatch(commit.sha);
|
|
13536
|
+
} catch {
|
|
13537
|
+
continue;
|
|
13538
|
+
}
|
|
13539
|
+
const contentHash = this.computeContentHash(patchContent);
|
|
13540
|
+
if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
|
|
13541
|
+
continue;
|
|
13542
|
+
}
|
|
13543
|
+
if (this.isCreationOnlyPatch(patchContent)) {
|
|
13544
|
+
continue;
|
|
13545
|
+
}
|
|
13546
|
+
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
13547
|
+
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f));
|
|
13548
|
+
if (files.length === 0) {
|
|
13549
|
+
continue;
|
|
13550
|
+
}
|
|
13551
|
+
if (parents.length === 0) {
|
|
13552
|
+
continue;
|
|
13553
|
+
}
|
|
13554
|
+
const parentSha = parents[0];
|
|
13555
|
+
newPatches.push({
|
|
13556
|
+
id: `patch-${commit.sha.slice(0, 8)}`,
|
|
13557
|
+
content_hash: contentHash,
|
|
13558
|
+
original_commit: commit.sha,
|
|
13559
|
+
original_message: commit.message,
|
|
13560
|
+
original_author: `${commit.authorName} <${commit.authorEmail}>`,
|
|
13561
|
+
base_generation: parentSha,
|
|
13562
|
+
files,
|
|
13563
|
+
patch_content: patchContent
|
|
13564
|
+
});
|
|
13565
|
+
}
|
|
13566
|
+
newPatches.reverse();
|
|
13567
|
+
return { patches: newPatches, revertedPatchIds: [] };
|
|
13568
|
+
}
|
|
13569
|
+
/**
|
|
13570
|
+
* Check if a format-patch consists entirely of new-file creations.
|
|
13571
|
+
* Used to identify squashed commits after force push, which create all files
|
|
13572
|
+
* from scratch (--- /dev/null) rather than modifying existing files.
|
|
13573
|
+
*/
|
|
13574
|
+
isCreationOnlyPatch(patchContent) {
|
|
13575
|
+
const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
|
|
13576
|
+
if (diffOldHeaders.length === 0) {
|
|
13577
|
+
return false;
|
|
13578
|
+
}
|
|
13579
|
+
return diffOldHeaders.every((l) => l === "--- /dev/null");
|
|
13580
|
+
}
|
|
13581
|
+
/**
|
|
13582
|
+
* Resolve the best available diff base for a generation record.
|
|
13583
|
+
* Prefers commit_sha, falls back to tree_hash for unreachable commits.
|
|
13584
|
+
* When commitKnownMissing is true, skips the redundant commitExists check.
|
|
13585
|
+
*/
|
|
13586
|
+
async resolveDiffBase(gen, commitKnownMissing) {
|
|
13587
|
+
if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
|
|
13588
|
+
return gen.commit_sha;
|
|
13589
|
+
}
|
|
13590
|
+
if (await this.git.treeExists(gen.tree_hash)) {
|
|
13591
|
+
return gen.tree_hash;
|
|
13592
|
+
}
|
|
13593
|
+
return null;
|
|
13594
|
+
}
|
|
13207
13595
|
parseGitLog(log) {
|
|
13208
13596
|
return log.trim().split("\n").map((line) => {
|
|
13209
13597
|
const [sha, authorName, authorEmail, message] = line.split("\0");
|
|
@@ -14558,29 +14946,74 @@ var import_node_os = require("os");
|
|
|
14558
14946
|
var import_node_path3 = require("path");
|
|
14559
14947
|
|
|
14560
14948
|
// src/conflict-utils.ts
|
|
14949
|
+
var CONFLICT_OPENER = "<<<<<<< Generated";
|
|
14950
|
+
var CONFLICT_SEPARATOR = "=======";
|
|
14951
|
+
var CONFLICT_CLOSER = ">>>>>>> Your customization";
|
|
14952
|
+
function trimCR(line) {
|
|
14953
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
14954
|
+
}
|
|
14955
|
+
function findConflictRanges(lines) {
|
|
14956
|
+
const ranges = [];
|
|
14957
|
+
let i = 0;
|
|
14958
|
+
while (i < lines.length) {
|
|
14959
|
+
if (trimCR(lines[i]) === CONFLICT_OPENER) {
|
|
14960
|
+
let separatorIdx = -1;
|
|
14961
|
+
let j = i + 1;
|
|
14962
|
+
let found = false;
|
|
14963
|
+
while (j < lines.length) {
|
|
14964
|
+
const trimmed2 = trimCR(lines[j]);
|
|
14965
|
+
if (trimmed2 === CONFLICT_OPENER) {
|
|
14966
|
+
break;
|
|
14967
|
+
}
|
|
14968
|
+
if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
|
|
14969
|
+
separatorIdx = j;
|
|
14970
|
+
} else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
|
|
14971
|
+
ranges.push({ start: i, separator: separatorIdx, end: j });
|
|
14972
|
+
i = j;
|
|
14973
|
+
found = true;
|
|
14974
|
+
break;
|
|
14975
|
+
}
|
|
14976
|
+
j++;
|
|
14977
|
+
}
|
|
14978
|
+
if (!found) {
|
|
14979
|
+
i++;
|
|
14980
|
+
continue;
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14983
|
+
i++;
|
|
14984
|
+
}
|
|
14985
|
+
return ranges;
|
|
14986
|
+
}
|
|
14561
14987
|
function stripConflictMarkers(content) {
|
|
14562
|
-
const lines = content.split(
|
|
14988
|
+
const lines = content.split(/\r?\n/);
|
|
14989
|
+
const ranges = findConflictRanges(lines);
|
|
14990
|
+
if (ranges.length === 0) {
|
|
14991
|
+
return content;
|
|
14992
|
+
}
|
|
14563
14993
|
const result = [];
|
|
14564
|
-
let
|
|
14565
|
-
let
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14994
|
+
let rangeIdx = 0;
|
|
14995
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14996
|
+
if (rangeIdx < ranges.length) {
|
|
14997
|
+
const range = ranges[rangeIdx];
|
|
14998
|
+
if (i === range.start) {
|
|
14999
|
+
continue;
|
|
15000
|
+
}
|
|
15001
|
+
if (i > range.start && i < range.separator) {
|
|
15002
|
+
result.push(lines[i]);
|
|
15003
|
+
continue;
|
|
15004
|
+
}
|
|
15005
|
+
if (i === range.separator) {
|
|
15006
|
+
continue;
|
|
15007
|
+
}
|
|
15008
|
+
if (i > range.separator && i < range.end) {
|
|
15009
|
+
continue;
|
|
15010
|
+
}
|
|
15011
|
+
if (i === range.end) {
|
|
15012
|
+
rangeIdx++;
|
|
15013
|
+
continue;
|
|
15014
|
+
}
|
|
14583
15015
|
}
|
|
15016
|
+
result.push(lines[i]);
|
|
14584
15017
|
}
|
|
14585
15018
|
return result.join("\n");
|
|
14586
15019
|
}
|
|
@@ -14651,6 +15084,37 @@ function diffIndices(buffer1, buffer2) {
|
|
|
14651
15084
|
result.reverse();
|
|
14652
15085
|
return result;
|
|
14653
15086
|
}
|
|
15087
|
+
function diffPatch(buffer1, buffer2) {
|
|
15088
|
+
const lcs = LCS(buffer1, buffer2);
|
|
15089
|
+
let result = [];
|
|
15090
|
+
let tail1 = buffer1.length;
|
|
15091
|
+
let tail2 = buffer2.length;
|
|
15092
|
+
function chunkDescription(buffer, offset, length) {
|
|
15093
|
+
let chunk = [];
|
|
15094
|
+
for (let i = 0; i < length; i++) {
|
|
15095
|
+
chunk.push(buffer[offset + i]);
|
|
15096
|
+
}
|
|
15097
|
+
return {
|
|
15098
|
+
offset,
|
|
15099
|
+
length,
|
|
15100
|
+
chunk
|
|
15101
|
+
};
|
|
15102
|
+
}
|
|
15103
|
+
for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {
|
|
15104
|
+
const mismatchLength1 = tail1 - candidate.buffer1index - 1;
|
|
15105
|
+
const mismatchLength2 = tail2 - candidate.buffer2index - 1;
|
|
15106
|
+
tail1 = candidate.buffer1index;
|
|
15107
|
+
tail2 = candidate.buffer2index;
|
|
15108
|
+
if (mismatchLength1 || mismatchLength2) {
|
|
15109
|
+
result.push({
|
|
15110
|
+
buffer1: chunkDescription(buffer1, candidate.buffer1index + 1, mismatchLength1),
|
|
15111
|
+
buffer2: chunkDescription(buffer2, candidate.buffer2index + 1, mismatchLength2)
|
|
15112
|
+
});
|
|
15113
|
+
}
|
|
15114
|
+
}
|
|
15115
|
+
result.reverse();
|
|
15116
|
+
return result;
|
|
15117
|
+
}
|
|
14654
15118
|
function diff3MergeRegions(a, o, b) {
|
|
14655
15119
|
let hunks = [];
|
|
14656
15120
|
function addHunk(h, ab) {
|
|
@@ -14813,20 +15277,33 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
14813
15277
|
outputLines.push(...region.ok);
|
|
14814
15278
|
currentLine += region.ok.length;
|
|
14815
15279
|
} else if (region.conflict) {
|
|
14816
|
-
const
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
15280
|
+
const resolved = tryResolveConflict(
|
|
15281
|
+
region.conflict.a,
|
|
15282
|
+
// ours (generator)
|
|
15283
|
+
region.conflict.o,
|
|
15284
|
+
// base
|
|
15285
|
+
region.conflict.b
|
|
15286
|
+
// theirs (user)
|
|
15287
|
+
);
|
|
15288
|
+
if (resolved !== null) {
|
|
15289
|
+
outputLines.push(...resolved);
|
|
15290
|
+
currentLine += resolved.length;
|
|
15291
|
+
} else {
|
|
15292
|
+
const startLine = currentLine;
|
|
15293
|
+
outputLines.push("<<<<<<< Generated");
|
|
15294
|
+
outputLines.push(...region.conflict.a);
|
|
15295
|
+
outputLines.push("=======");
|
|
15296
|
+
outputLines.push(...region.conflict.b);
|
|
15297
|
+
outputLines.push(">>>>>>> Your customization");
|
|
15298
|
+
const conflictLines = region.conflict.a.length + region.conflict.b.length + 3;
|
|
15299
|
+
conflicts2.push({
|
|
15300
|
+
startLine,
|
|
15301
|
+
endLine: startLine + conflictLines - 1,
|
|
15302
|
+
ours: region.conflict.a,
|
|
15303
|
+
theirs: region.conflict.b
|
|
15304
|
+
});
|
|
15305
|
+
currentLine += conflictLines;
|
|
15306
|
+
}
|
|
14830
15307
|
}
|
|
14831
15308
|
}
|
|
14832
15309
|
return {
|
|
@@ -14835,6 +15312,62 @@ function threeWayMerge(base, ours, theirs) {
|
|
|
14835
15312
|
conflicts: conflicts2
|
|
14836
15313
|
};
|
|
14837
15314
|
}
|
|
15315
|
+
function tryResolveConflict(oursLines, baseLines, theirsLines) {
|
|
15316
|
+
if (baseLines.length === 0) {
|
|
15317
|
+
return null;
|
|
15318
|
+
}
|
|
15319
|
+
const oursPatches = diffPatch(baseLines, oursLines);
|
|
15320
|
+
const theirsPatches = diffPatch(baseLines, theirsLines);
|
|
15321
|
+
if (oursPatches.length === 0) return theirsLines;
|
|
15322
|
+
if (theirsPatches.length === 0) return oursLines;
|
|
15323
|
+
if (patchesOverlap(oursPatches, theirsPatches)) {
|
|
15324
|
+
return null;
|
|
15325
|
+
}
|
|
15326
|
+
return applyBothPatches(baseLines, oursPatches, theirsPatches);
|
|
15327
|
+
}
|
|
15328
|
+
function patchesOverlap(oursPatches, theirsPatches) {
|
|
15329
|
+
for (const op of oursPatches) {
|
|
15330
|
+
const oStart = op.buffer1.offset;
|
|
15331
|
+
const oEnd = oStart + op.buffer1.length;
|
|
15332
|
+
for (const tp of theirsPatches) {
|
|
15333
|
+
const tStart = tp.buffer1.offset;
|
|
15334
|
+
const tEnd = tStart + tp.buffer1.length;
|
|
15335
|
+
if (op.buffer1.length === 0 && tp.buffer1.length === 0 && oStart === tStart) {
|
|
15336
|
+
return true;
|
|
15337
|
+
}
|
|
15338
|
+
if (tp.buffer1.length === 0 && tStart === oEnd) {
|
|
15339
|
+
return true;
|
|
15340
|
+
}
|
|
15341
|
+
if (op.buffer1.length === 0 && oStart === tEnd) {
|
|
15342
|
+
return true;
|
|
15343
|
+
}
|
|
15344
|
+
if (oStart < tEnd && tStart < oEnd) {
|
|
15345
|
+
return true;
|
|
15346
|
+
}
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15349
|
+
return false;
|
|
15350
|
+
}
|
|
15351
|
+
function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
15352
|
+
const allPatches = [
|
|
15353
|
+
...oursPatches.map((p) => ({
|
|
15354
|
+
offset: p.buffer1.offset,
|
|
15355
|
+
length: p.buffer1.length,
|
|
15356
|
+
replacement: p.buffer2.chunk
|
|
15357
|
+
})),
|
|
15358
|
+
...theirsPatches.map((p) => ({
|
|
15359
|
+
offset: p.buffer1.offset,
|
|
15360
|
+
length: p.buffer1.length,
|
|
15361
|
+
replacement: p.buffer2.chunk
|
|
15362
|
+
}))
|
|
15363
|
+
];
|
|
15364
|
+
allPatches.sort((a, b) => b.offset - a.offset);
|
|
15365
|
+
const result = [...baseLines];
|
|
15366
|
+
for (const p of allPatches) {
|
|
15367
|
+
result.splice(p.offset, p.length, ...p.replacement);
|
|
15368
|
+
}
|
|
15369
|
+
return result;
|
|
15370
|
+
}
|
|
14838
15371
|
|
|
14839
15372
|
// src/ReplayApplicator.ts
|
|
14840
15373
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -14891,12 +15424,34 @@ var ReplayApplicator = class {
|
|
|
14891
15424
|
lockManager;
|
|
14892
15425
|
outputDir;
|
|
14893
15426
|
renameCache = /* @__PURE__ */ new Map();
|
|
15427
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
14894
15428
|
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
14895
15429
|
constructor(git, lockManager, outputDir) {
|
|
14896
15430
|
this.git = git;
|
|
14897
15431
|
this.lockManager = lockManager;
|
|
14898
15432
|
this.outputDir = outputDir;
|
|
14899
15433
|
}
|
|
15434
|
+
/**
|
|
15435
|
+
* Resolve the GenerationRecord for a patch's base_generation.
|
|
15436
|
+
* Falls back to constructing an ad-hoc record from the commit's tree
|
|
15437
|
+
* when base_generation isn't a tracked generation (commit-scan patches).
|
|
15438
|
+
*/
|
|
15439
|
+
async resolveBaseGeneration(baseGeneration) {
|
|
15440
|
+
const gen = this.lockManager.getGeneration(baseGeneration);
|
|
15441
|
+
if (gen) return gen;
|
|
15442
|
+
try {
|
|
15443
|
+
const treeHash = await this.git.getTreeHash(baseGeneration);
|
|
15444
|
+
return {
|
|
15445
|
+
commit_sha: baseGeneration,
|
|
15446
|
+
tree_hash: treeHash,
|
|
15447
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15448
|
+
cli_version: "unknown",
|
|
15449
|
+
generator_versions: {}
|
|
15450
|
+
};
|
|
15451
|
+
} catch {
|
|
15452
|
+
return void 0;
|
|
15453
|
+
}
|
|
15454
|
+
}
|
|
14900
15455
|
/** Reset inter-patch accumulator for a new cycle. */
|
|
14901
15456
|
resetAccumulator() {
|
|
14902
15457
|
this.fileTheirsAccumulator.clear();
|
|
@@ -14978,7 +15533,7 @@ var ReplayApplicator = class {
|
|
|
14978
15533
|
}
|
|
14979
15534
|
}
|
|
14980
15535
|
async applyPatchWithFallback(patch) {
|
|
14981
|
-
const baseGen = this.
|
|
15536
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
14982
15537
|
const lock = this.lockManager.read();
|
|
14983
15538
|
const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
14984
15539
|
const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
|
|
@@ -15062,7 +15617,7 @@ var ReplayApplicator = class {
|
|
|
15062
15617
|
}
|
|
15063
15618
|
async mergeFile(patch, filePath, tempGit, tempDir) {
|
|
15064
15619
|
try {
|
|
15065
|
-
const baseGen = this.
|
|
15620
|
+
const baseGen = await this.resolveBaseGeneration(patch.base_generation);
|
|
15066
15621
|
if (!baseGen) {
|
|
15067
15622
|
return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
|
|
15068
15623
|
}
|
|
@@ -15087,14 +15642,44 @@ var ReplayApplicator = class {
|
|
|
15087
15642
|
}
|
|
15088
15643
|
const oursPath = (0, import_node_path3.join)(this.outputDir, resolvedPath);
|
|
15089
15644
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
15090
|
-
let
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15645
|
+
let ghostReconstructed = false;
|
|
15646
|
+
let theirs = null;
|
|
15647
|
+
if (!base && ours && !renameSourcePath) {
|
|
15648
|
+
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
15649
|
+
if (!treeReachable) {
|
|
15650
|
+
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
15651
|
+
if (fileDiff) {
|
|
15652
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
15653
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
15654
|
+
if (result) {
|
|
15655
|
+
base = result.base;
|
|
15656
|
+
theirs = result.theirs;
|
|
15657
|
+
ghostReconstructed = true;
|
|
15658
|
+
}
|
|
15659
|
+
}
|
|
15660
|
+
}
|
|
15661
|
+
}
|
|
15662
|
+
if (!ghostReconstructed) {
|
|
15663
|
+
theirs = await this.applyPatchToContent(
|
|
15664
|
+
base,
|
|
15665
|
+
patch.patch_content,
|
|
15666
|
+
filePath,
|
|
15667
|
+
tempGit,
|
|
15668
|
+
tempDir,
|
|
15669
|
+
renameSourcePath
|
|
15670
|
+
);
|
|
15671
|
+
}
|
|
15672
|
+
if (theirs) {
|
|
15673
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
15674
|
+
const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
|
|
15675
|
+
if (theirsHasMarkers && !baseHasMarkers) {
|
|
15676
|
+
return {
|
|
15677
|
+
file: resolvedPath,
|
|
15678
|
+
status: "skipped",
|
|
15679
|
+
reason: "stale-conflict-markers"
|
|
15680
|
+
};
|
|
15681
|
+
}
|
|
15682
|
+
}
|
|
15098
15683
|
let useAccumulatorAsMergeBase = false;
|
|
15099
15684
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
15100
15685
|
if (!theirs && accumulatorEntry) {
|
|
@@ -15109,6 +15694,17 @@ var ReplayApplicator = class {
|
|
|
15109
15694
|
useAccumulatorAsMergeBase = true;
|
|
15110
15695
|
}
|
|
15111
15696
|
}
|
|
15697
|
+
if (theirs) {
|
|
15698
|
+
const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
|
|
15699
|
+
const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
|
|
15700
|
+
if (theirsHasMarkers && !accBaseHasMarkers && !(base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization")))) {
|
|
15701
|
+
return {
|
|
15702
|
+
file: resolvedPath,
|
|
15703
|
+
status: "skipped",
|
|
15704
|
+
reason: "stale-conflict-markers"
|
|
15705
|
+
};
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15112
15708
|
let effective_theirs = theirs;
|
|
15113
15709
|
let baseMismatchSkipped = false;
|
|
15114
15710
|
if (theirs && base && !useAccumulatorAsMergeBase) {
|
|
@@ -15127,13 +15723,13 @@ var ReplayApplicator = class {
|
|
|
15127
15723
|
baseMismatchSkipped = true;
|
|
15128
15724
|
}
|
|
15129
15725
|
}
|
|
15130
|
-
if (
|
|
15726
|
+
if (base == null && !ours && effective_theirs) {
|
|
15131
15727
|
const outDir2 = (0, import_node_path3.dirname)(oursPath);
|
|
15132
15728
|
await (0, import_promises.mkdir)(outDir2, { recursive: true });
|
|
15133
15729
|
await (0, import_promises.writeFile)(oursPath, effective_theirs);
|
|
15134
15730
|
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
15135
15731
|
}
|
|
15136
|
-
if (
|
|
15732
|
+
if (base == null && ours && effective_theirs) {
|
|
15137
15733
|
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
15138
15734
|
const outDir2 = (0, import_node_path3.dirname)(oursPath);
|
|
15139
15735
|
await (0, import_promises.mkdir)(outDir2, { recursive: true });
|
|
@@ -15156,7 +15752,7 @@ var ReplayApplicator = class {
|
|
|
15156
15752
|
reason: "missing-content"
|
|
15157
15753
|
};
|
|
15158
15754
|
}
|
|
15159
|
-
if (
|
|
15755
|
+
if (base == null && !useAccumulatorAsMergeBase || !ours) {
|
|
15160
15756
|
return {
|
|
15161
15757
|
file: resolvedPath,
|
|
15162
15758
|
status: "skipped",
|
|
@@ -15175,7 +15771,7 @@ var ReplayApplicator = class {
|
|
|
15175
15771
|
const outDir = (0, import_node_path3.dirname)(oursPath);
|
|
15176
15772
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
15177
15773
|
await (0, import_promises.writeFile)(oursPath, merged.content);
|
|
15178
|
-
if (effective_theirs) {
|
|
15774
|
+
if (effective_theirs && !merged.hasConflicts) {
|
|
15179
15775
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
15180
15776
|
content: effective_theirs,
|
|
15181
15777
|
baseGeneration: patch.base_generation
|
|
@@ -15199,6 +15795,14 @@ var ReplayApplicator = class {
|
|
|
15199
15795
|
};
|
|
15200
15796
|
}
|
|
15201
15797
|
}
|
|
15798
|
+
async isTreeReachable(treeHash) {
|
|
15799
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
15800
|
+
if (result === void 0) {
|
|
15801
|
+
result = await this.git.treeExists(treeHash);
|
|
15802
|
+
this.treeExistsCache.set(treeHash, result);
|
|
15803
|
+
}
|
|
15804
|
+
return result;
|
|
15805
|
+
}
|
|
15202
15806
|
isExcluded(patch) {
|
|
15203
15807
|
const config = this.lockManager.getCustomizationsConfig();
|
|
15204
15808
|
if (!config.exclude) return false;
|
|
@@ -15465,6 +16069,7 @@ var ReplayService = class {
|
|
|
15465
16069
|
generator_versions: options?.generatorVersions ?? {},
|
|
15466
16070
|
base_branch_head: options?.baseBranchHead
|
|
15467
16071
|
};
|
|
16072
|
+
let resolvedPatches;
|
|
15468
16073
|
if (!this.lockManager.exists()) {
|
|
15469
16074
|
this.lockManager.initializeInMemory(record);
|
|
15470
16075
|
} else {
|
|
@@ -15473,13 +16078,13 @@ var ReplayService = class {
|
|
|
15473
16078
|
...this.lockManager.getUnresolvedPatches(),
|
|
15474
16079
|
...this.lockManager.getResolvingPatches()
|
|
15475
16080
|
];
|
|
16081
|
+
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
15476
16082
|
this.lockManager.addGeneration(record);
|
|
15477
16083
|
this.lockManager.clearPatches();
|
|
15478
16084
|
for (const patch of unresolvedPatches) {
|
|
15479
16085
|
this.lockManager.addPatch(patch);
|
|
15480
16086
|
}
|
|
15481
16087
|
}
|
|
15482
|
-
this.lockManager.save();
|
|
15483
16088
|
try {
|
|
15484
16089
|
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
15485
16090
|
if (redetectedPatches.length > 0) {
|
|
@@ -15493,20 +16098,27 @@ var ReplayService = class {
|
|
|
15493
16098
|
for (const patch of redetectedPatches) {
|
|
15494
16099
|
this.lockManager.addPatch(patch);
|
|
15495
16100
|
}
|
|
15496
|
-
this.lockManager.save();
|
|
15497
16101
|
}
|
|
15498
|
-
} catch {
|
|
16102
|
+
} catch (error) {
|
|
16103
|
+
for (const patch of resolvedPatches ?? []) {
|
|
16104
|
+
this.lockManager.addPatch(patch);
|
|
16105
|
+
}
|
|
16106
|
+
this.detector.warnings.push(
|
|
16107
|
+
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
16108
|
+
);
|
|
15499
16109
|
}
|
|
16110
|
+
this.lockManager.save();
|
|
15500
16111
|
}
|
|
15501
16112
|
determineFlow() {
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
16113
|
+
try {
|
|
16114
|
+
const lock = this.lockManager.read();
|
|
16115
|
+
return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
|
|
16116
|
+
} catch (error) {
|
|
16117
|
+
if (error instanceof LockfileNotFoundError) {
|
|
16118
|
+
return "first-generation";
|
|
16119
|
+
}
|
|
16120
|
+
throw error;
|
|
15508
16121
|
}
|
|
15509
|
-
return "normal-regeneration";
|
|
15510
16122
|
}
|
|
15511
16123
|
async handleFirstGeneration(options) {
|
|
15512
16124
|
if (options?.dryRun) {
|
|
@@ -15548,12 +16160,17 @@ var ReplayService = class {
|
|
|
15548
16160
|
baseBranchHead: options.baseBranchHead
|
|
15549
16161
|
} : void 0;
|
|
15550
16162
|
await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
16163
|
+
await this.cleanupStaleConflictMarkers();
|
|
15551
16164
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15552
|
-
|
|
16165
|
+
try {
|
|
15553
16166
|
this.lockManager.read();
|
|
15554
16167
|
this.lockManager.addGeneration(genRecord);
|
|
15555
|
-
}
|
|
15556
|
-
|
|
16168
|
+
} catch (error) {
|
|
16169
|
+
if (error instanceof LockfileNotFoundError) {
|
|
16170
|
+
this.lockManager.initializeInMemory(genRecord);
|
|
16171
|
+
} else {
|
|
16172
|
+
throw error;
|
|
16173
|
+
}
|
|
15557
16174
|
}
|
|
15558
16175
|
this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
15559
16176
|
this.lockManager.save();
|
|
@@ -15599,6 +16216,7 @@ var ReplayService = class {
|
|
|
15599
16216
|
baseBranchHead: options.baseBranchHead
|
|
15600
16217
|
} : void 0;
|
|
15601
16218
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
16219
|
+
await this.cleanupStaleConflictMarkers();
|
|
15602
16220
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15603
16221
|
this.lockManager.addGeneration(genRecord);
|
|
15604
16222
|
let results = [];
|
|
@@ -15692,6 +16310,7 @@ var ReplayService = class {
|
|
|
15692
16310
|
baseBranchHead: options.baseBranchHead
|
|
15693
16311
|
} : void 0;
|
|
15694
16312
|
await this.committer.commitGeneration("Update SDK", commitOpts);
|
|
16313
|
+
await this.cleanupStaleConflictMarkers();
|
|
15695
16314
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15696
16315
|
this.lockManager.addGeneration(genRecord);
|
|
15697
16316
|
const results = await this.applicator.applyPatches(allPatches);
|
|
@@ -15882,6 +16501,13 @@ var ReplayService = class {
|
|
|
15882
16501
|
contentRefreshed++;
|
|
15883
16502
|
continue;
|
|
15884
16503
|
}
|
|
16504
|
+
const diffLines = diff.split("\n");
|
|
16505
|
+
const hasStaleMarkers = diffLines.some(
|
|
16506
|
+
(l) => l.startsWith("+<<<<<<< Generated") || l.startsWith("+>>>>>>> Your customization")
|
|
16507
|
+
);
|
|
16508
|
+
if (hasStaleMarkers) {
|
|
16509
|
+
continue;
|
|
16510
|
+
}
|
|
15885
16511
|
const newContentHash = this.detector.computeContentHash(diff);
|
|
15886
16512
|
if (newContentHash !== patch.content_hash) {
|
|
15887
16513
|
const filesOutput = await this.git.exec(["diff", "--name-only", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -15902,7 +16528,7 @@ var ReplayService = class {
|
|
|
15902
16528
|
continue;
|
|
15903
16529
|
}
|
|
15904
16530
|
try {
|
|
15905
|
-
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<<", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
16531
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "HEAD", "--", ...patch.files]).catch(() => "");
|
|
15906
16532
|
if (markerFiles.trim()) continue;
|
|
15907
16533
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
15908
16534
|
if (diff === null) continue;
|
|
@@ -15942,6 +16568,25 @@ var ReplayService = class {
|
|
|
15942
16568
|
}
|
|
15943
16569
|
}
|
|
15944
16570
|
}
|
|
16571
|
+
/**
|
|
16572
|
+
* Clean up stale conflict markers left by a previous crashed run.
|
|
16573
|
+
* Called after commitGeneration() when HEAD is the [fern-generated] commit.
|
|
16574
|
+
* Restores files to their clean generated state from HEAD.
|
|
16575
|
+
* Skips .fernignore-protected files to prevent overwriting user content.
|
|
16576
|
+
*/
|
|
16577
|
+
async cleanupStaleConflictMarkers() {
|
|
16578
|
+
const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
|
|
16579
|
+
const files = markerFiles.trim().split("\n").filter(Boolean);
|
|
16580
|
+
if (files.length === 0) return;
|
|
16581
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
16582
|
+
for (const file of files) {
|
|
16583
|
+
if (fernignorePatterns.some((pattern) => minimatch(file, pattern))) continue;
|
|
16584
|
+
try {
|
|
16585
|
+
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
16586
|
+
} catch {
|
|
16587
|
+
}
|
|
16588
|
+
}
|
|
16589
|
+
}
|
|
15945
16590
|
readFernignorePatterns() {
|
|
15946
16591
|
const fernignorePath = (0, import_node_path4.join)(this.outputDir, ".fernignore");
|
|
15947
16592
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -16268,6 +16913,7 @@ async function bootstrap(outputDir, options) {
|
|
|
16268
16913
|
}
|
|
16269
16914
|
lockManager.save();
|
|
16270
16915
|
const fernignoreUpdated = ensureFernignoreEntries(outputDir);
|
|
16916
|
+
ensureGitattributesEntries(outputDir);
|
|
16271
16917
|
if (migrator.fernignoreExists() && fernignorePatterns.length > 0) {
|
|
16272
16918
|
const action = options?.fernignoreAction ?? "skip";
|
|
16273
16919
|
if (action === "migrate") {
|
|
@@ -16349,7 +16995,7 @@ function parseGitLog(log) {
|
|
|
16349
16995
|
return { sha, authorName, authorEmail, message };
|
|
16350
16996
|
});
|
|
16351
16997
|
}
|
|
16352
|
-
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml"];
|
|
16998
|
+
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
16353
16999
|
function ensureFernignoreEntries(outputDir) {
|
|
16354
17000
|
const fernignorePath = (0, import_node_path6.join)(outputDir, ".fernignore");
|
|
16355
17001
|
let content = "";
|
|
@@ -16373,6 +17019,29 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
16373
17019
|
(0, import_node_fs4.writeFileSync)(fernignorePath, content, "utf-8");
|
|
16374
17020
|
return true;
|
|
16375
17021
|
}
|
|
17022
|
+
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
17023
|
+
function ensureGitattributesEntries(outputDir) {
|
|
17024
|
+
const gitattributesPath = (0, import_node_path6.join)(outputDir, ".gitattributes");
|
|
17025
|
+
let content = "";
|
|
17026
|
+
if ((0, import_node_fs4.existsSync)(gitattributesPath)) {
|
|
17027
|
+
content = (0, import_node_fs4.readFileSync)(gitattributesPath, "utf-8");
|
|
17028
|
+
}
|
|
17029
|
+
const lines = content.split("\n");
|
|
17030
|
+
const toAdd = [];
|
|
17031
|
+
for (const entry of GITATTRIBUTES_ENTRIES) {
|
|
17032
|
+
if (!lines.some((line) => line.trim() === entry)) {
|
|
17033
|
+
toAdd.push(entry);
|
|
17034
|
+
}
|
|
17035
|
+
}
|
|
17036
|
+
if (toAdd.length === 0) {
|
|
17037
|
+
return;
|
|
17038
|
+
}
|
|
17039
|
+
if (content && !content.endsWith("\n")) {
|
|
17040
|
+
content += "\n";
|
|
17041
|
+
}
|
|
17042
|
+
content += toAdd.join("\n") + "\n";
|
|
17043
|
+
(0, import_node_fs4.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
17044
|
+
}
|
|
16376
17045
|
function computeContentHash(patchContent) {
|
|
16377
17046
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
16378
17047
|
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex")}`;
|