@fern-api/replay 0.8.1 → 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 +812 -137
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +566 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -16
- package/dist/index.d.ts +64 -16
- package/dist/index.js +566 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -133,6 +133,14 @@ var init_GitClient = __esm({
|
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
async treeExists(treeHash) {
|
|
137
|
+
try {
|
|
138
|
+
const type = await this.exec(["cat-file", "-t", treeHash]);
|
|
139
|
+
return type.trim() === "tree";
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
136
144
|
async getCommitBody(commitSha) {
|
|
137
145
|
return this.exec(["log", "-1", "--format=%B", commitSha]);
|
|
138
146
|
}
|
|
@@ -143,6 +151,255 @@ var init_GitClient = __esm({
|
|
|
143
151
|
}
|
|
144
152
|
});
|
|
145
153
|
|
|
154
|
+
// src/HybridReconstruction.ts
|
|
155
|
+
var HybridReconstruction_exports = {};
|
|
156
|
+
__export(HybridReconstruction_exports, {
|
|
157
|
+
assembleHybrid: () => assembleHybrid,
|
|
158
|
+
locateHunksInOurs: () => locateHunksInOurs,
|
|
159
|
+
parseHunks: () => parseHunks,
|
|
160
|
+
reconstructFromGhostPatch: () => reconstructFromGhostPatch
|
|
161
|
+
});
|
|
162
|
+
function parseHunks(fileDiff) {
|
|
163
|
+
const lines = fileDiff.split("\n");
|
|
164
|
+
const hunks = [];
|
|
165
|
+
let currentHunk = null;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const headerMatch = line.match(
|
|
168
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
169
|
+
);
|
|
170
|
+
if (headerMatch) {
|
|
171
|
+
if (currentHunk) {
|
|
172
|
+
hunks.push(currentHunk);
|
|
173
|
+
}
|
|
174
|
+
currentHunk = {
|
|
175
|
+
oldStart: parseInt(headerMatch[1], 10),
|
|
176
|
+
oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
|
|
177
|
+
newStart: parseInt(headerMatch[3], 10),
|
|
178
|
+
newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
|
|
179
|
+
lines: []
|
|
180
|
+
};
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!currentHunk) continue;
|
|
184
|
+
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")) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (line === "\") {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (line.startsWith("-")) {
|
|
191
|
+
currentHunk.lines.push({ type: "remove", content: line.slice(1) });
|
|
192
|
+
} else if (line.startsWith("+")) {
|
|
193
|
+
currentHunk.lines.push({ type: "add", content: line.slice(1) });
|
|
194
|
+
} else if (line.startsWith(" ") || line === "") {
|
|
195
|
+
currentHunk.lines.push({
|
|
196
|
+
type: "context",
|
|
197
|
+
content: line.startsWith(" ") ? line.slice(1) : line
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (currentHunk) {
|
|
202
|
+
hunks.push(currentHunk);
|
|
203
|
+
}
|
|
204
|
+
return hunks;
|
|
205
|
+
}
|
|
206
|
+
function extractLeadingContext(hunk) {
|
|
207
|
+
const result = [];
|
|
208
|
+
for (const line of hunk.lines) {
|
|
209
|
+
if (line.type !== "context") break;
|
|
210
|
+
result.push(line.content);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
function extractTrailingContext(hunk) {
|
|
215
|
+
const result = [];
|
|
216
|
+
for (let i = hunk.lines.length - 1; i >= 0; i--) {
|
|
217
|
+
if (hunk.lines[i].type !== "context") break;
|
|
218
|
+
result.unshift(hunk.lines[i].content);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function countOursLinesBeforeTrailing(hunk) {
|
|
223
|
+
let count = 0;
|
|
224
|
+
const trailingStart = findTrailingContextStart(hunk);
|
|
225
|
+
for (let i = 0; i < trailingStart; i++) {
|
|
226
|
+
if (hunk.lines[i].type === "context") count++;
|
|
227
|
+
}
|
|
228
|
+
return count;
|
|
229
|
+
}
|
|
230
|
+
function findTrailingContextStart(hunk) {
|
|
231
|
+
let i = hunk.lines.length - 1;
|
|
232
|
+
while (i >= 0 && hunk.lines[i].type === "context") {
|
|
233
|
+
i--;
|
|
234
|
+
}
|
|
235
|
+
return i + 1;
|
|
236
|
+
}
|
|
237
|
+
function matchesAt(needle, haystack, offset) {
|
|
238
|
+
for (let i = 0; i < needle.length; i++) {
|
|
239
|
+
if (haystack[offset + i] !== needle[i]) return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
function findContextInOurs(contextLines, oursLines, minIndex, hint) {
|
|
244
|
+
const SEARCH_WINDOW = 200;
|
|
245
|
+
const maxStart = oursLines.length - contextLines.length;
|
|
246
|
+
const clampedHint = Math.max(minIndex, Math.min(hint, maxStart));
|
|
247
|
+
if (clampedHint >= minIndex && clampedHint <= maxStart) {
|
|
248
|
+
if (matchesAt(contextLines, oursLines, clampedHint)) {
|
|
249
|
+
return clampedHint;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (let delta = 1; delta <= SEARCH_WINDOW; delta++) {
|
|
253
|
+
for (const sign of [1, -1]) {
|
|
254
|
+
const idx = clampedHint + delta * sign;
|
|
255
|
+
if (idx < minIndex || idx > maxStart) continue;
|
|
256
|
+
if (matchesAt(contextLines, oursLines, idx)) {
|
|
257
|
+
return idx;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return -1;
|
|
262
|
+
}
|
|
263
|
+
function computeOursSpan(hunk, oursLines, oursOffset) {
|
|
264
|
+
const leading = extractLeadingContext(hunk);
|
|
265
|
+
const trailing = extractTrailingContext(hunk);
|
|
266
|
+
if (trailing.length === 0) {
|
|
267
|
+
const contextCount2 = hunk.lines.filter(
|
|
268
|
+
(l) => l.type === "context"
|
|
269
|
+
).length;
|
|
270
|
+
return Math.min(contextCount2, oursLines.length - oursOffset);
|
|
271
|
+
}
|
|
272
|
+
const searchStart = oursOffset + leading.length;
|
|
273
|
+
for (let i = searchStart; i <= oursLines.length - trailing.length; i++) {
|
|
274
|
+
if (matchesAt(trailing, oursLines, i)) {
|
|
275
|
+
return i + trailing.length - oursOffset;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const contextCount = hunk.lines.filter(
|
|
279
|
+
(l) => l.type === "context"
|
|
280
|
+
).length;
|
|
281
|
+
return Math.min(contextCount, oursLines.length - oursOffset);
|
|
282
|
+
}
|
|
283
|
+
function locateHunksInOurs(hunks, oursLines) {
|
|
284
|
+
const located = [];
|
|
285
|
+
let minOursIndex = 0;
|
|
286
|
+
for (const hunk of hunks) {
|
|
287
|
+
const contextLines = extractLeadingContext(hunk);
|
|
288
|
+
let oursOffset;
|
|
289
|
+
if (contextLines.length > 0) {
|
|
290
|
+
const found = findContextInOurs(
|
|
291
|
+
contextLines,
|
|
292
|
+
oursLines,
|
|
293
|
+
minOursIndex,
|
|
294
|
+
hunk.newStart - 1
|
|
295
|
+
);
|
|
296
|
+
if (found === -1) {
|
|
297
|
+
const trailingContext = extractTrailingContext(hunk);
|
|
298
|
+
if (trailingContext.length > 0) {
|
|
299
|
+
const trailingFound = findContextInOurs(
|
|
300
|
+
trailingContext,
|
|
301
|
+
oursLines,
|
|
302
|
+
minOursIndex,
|
|
303
|
+
hunk.newStart - 1
|
|
304
|
+
);
|
|
305
|
+
if (trailingFound === -1) return null;
|
|
306
|
+
const nonTrailingCount = countOursLinesBeforeTrailing(hunk);
|
|
307
|
+
oursOffset = trailingFound - nonTrailingCount;
|
|
308
|
+
if (oursOffset < minOursIndex) return null;
|
|
309
|
+
} else {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
oursOffset = found;
|
|
314
|
+
}
|
|
315
|
+
} else if (hunk.oldStart === 1 && hunk.oldCount === 0) {
|
|
316
|
+
oursOffset = 0;
|
|
317
|
+
} else {
|
|
318
|
+
oursOffset = Math.max(hunk.newStart - 1, minOursIndex);
|
|
319
|
+
}
|
|
320
|
+
const oursSpan = computeOursSpan(hunk, oursLines, oursOffset);
|
|
321
|
+
located.push({ hunk, oursOffset, oursSpan });
|
|
322
|
+
minOursIndex = oursOffset + oursSpan;
|
|
323
|
+
}
|
|
324
|
+
return located;
|
|
325
|
+
}
|
|
326
|
+
function assembleHybrid(locatedHunks, oursLines) {
|
|
327
|
+
const baseLines = [];
|
|
328
|
+
const theirsLines = [];
|
|
329
|
+
let oursCursor = 0;
|
|
330
|
+
for (const { hunk, oursOffset, oursSpan } of locatedHunks) {
|
|
331
|
+
if (oursOffset > oursCursor) {
|
|
332
|
+
const gapLines = oursLines.slice(oursCursor, oursOffset);
|
|
333
|
+
baseLines.push(...gapLines);
|
|
334
|
+
theirsLines.push(...gapLines);
|
|
335
|
+
}
|
|
336
|
+
for (const line of hunk.lines) {
|
|
337
|
+
switch (line.type) {
|
|
338
|
+
case "context":
|
|
339
|
+
baseLines.push(line.content);
|
|
340
|
+
theirsLines.push(line.content);
|
|
341
|
+
break;
|
|
342
|
+
case "remove":
|
|
343
|
+
baseLines.push(line.content);
|
|
344
|
+
break;
|
|
345
|
+
case "add":
|
|
346
|
+
theirsLines.push(line.content);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
oursCursor = oursOffset + oursSpan;
|
|
351
|
+
}
|
|
352
|
+
if (oursCursor < oursLines.length) {
|
|
353
|
+
const gapLines = oursLines.slice(oursCursor);
|
|
354
|
+
baseLines.push(...gapLines);
|
|
355
|
+
theirsLines.push(...gapLines);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
base: baseLines.join("\n"),
|
|
359
|
+
theirs: theirsLines.join("\n")
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function reconstructFromGhostPatch(fileDiff, ours) {
|
|
363
|
+
const hunks = parseHunks(fileDiff);
|
|
364
|
+
if (hunks.length === 0) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const isPureAddition = hunks.every(
|
|
368
|
+
(h) => h.oldCount === 0 && h.lines.every((l) => l.type !== "remove")
|
|
369
|
+
);
|
|
370
|
+
if (isPureAddition) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const isPureDeletion = hunks.every(
|
|
374
|
+
(h) => h.newCount === 0 && h.lines.every((l) => l.type !== "add")
|
|
375
|
+
);
|
|
376
|
+
if (isPureDeletion) {
|
|
377
|
+
const baseLines = [];
|
|
378
|
+
for (const hunk of hunks) {
|
|
379
|
+
for (const line of hunk.lines) {
|
|
380
|
+
if (line.type === "context" || line.type === "remove") {
|
|
381
|
+
baseLines.push(line.content);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
base: baseLines.join("\n"),
|
|
387
|
+
theirs: ""
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const oursLines = ours.split("\n");
|
|
391
|
+
const located = locateHunksInOurs(hunks, oursLines);
|
|
392
|
+
if (!located) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
return assembleHybrid(located, oursLines);
|
|
396
|
+
}
|
|
397
|
+
var init_HybridReconstruction = __esm({
|
|
398
|
+
"src/HybridReconstruction.ts"() {
|
|
399
|
+
"use strict";
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
146
403
|
// src/index.ts
|
|
147
404
|
var index_exports = {};
|
|
148
405
|
__export(index_exports, {
|
|
@@ -290,6 +547,15 @@ var LockfileManager = class {
|
|
|
290
547
|
this.ensureLoaded();
|
|
291
548
|
this.lock.patches = [];
|
|
292
549
|
}
|
|
550
|
+
addForgottenHash(hash) {
|
|
551
|
+
this.ensureLoaded();
|
|
552
|
+
if (!this.lock.forgotten_hashes) {
|
|
553
|
+
this.lock.forgotten_hashes = [];
|
|
554
|
+
}
|
|
555
|
+
if (!this.lock.forgotten_hashes.includes(hash)) {
|
|
556
|
+
this.lock.forgotten_hashes.push(hash);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
293
559
|
getUnresolvedPatches() {
|
|
294
560
|
this.ensureLoaded();
|
|
295
561
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -386,6 +652,7 @@ var ReplayDetector = class {
|
|
|
386
652
|
}
|
|
387
653
|
const commits = this.parseGitLog(log);
|
|
388
654
|
const newPatches = [];
|
|
655
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
389
656
|
for (const commit of commits) {
|
|
390
657
|
if (isGenerationCommit(commit)) {
|
|
391
658
|
continue;
|
|
@@ -399,7 +666,7 @@ var ReplayDetector = class {
|
|
|
399
666
|
}
|
|
400
667
|
const patchContent = await this.git.formatPatch(commit.sha);
|
|
401
668
|
const contentHash = this.computeContentHash(patchContent);
|
|
402
|
-
if (lock.patches.find((p) => p.content_hash === contentHash)) {
|
|
669
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
403
670
|
continue;
|
|
404
671
|
}
|
|
405
672
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
@@ -498,7 +765,7 @@ var ReplayDetector = class {
|
|
|
498
765
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
499
766
|
const contentHash = this.computeContentHash(diff);
|
|
500
767
|
const lock = this.lockManager.read();
|
|
501
|
-
if (lock.patches.some((p) => p.content_hash === contentHash)) {
|
|
768
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
|
|
502
769
|
return { patches: [], revertedPatchIds: [] };
|
|
503
770
|
}
|
|
504
771
|
const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
@@ -568,6 +835,36 @@ var import_promises = require("fs/promises");
|
|
|
568
835
|
var import_node_os = require("os");
|
|
569
836
|
var import_node_path2 = require("path");
|
|
570
837
|
var import_minimatch = require("minimatch");
|
|
838
|
+
|
|
839
|
+
// src/conflict-utils.ts
|
|
840
|
+
function stripConflictMarkers(content) {
|
|
841
|
+
const lines = content.split("\n");
|
|
842
|
+
const result = [];
|
|
843
|
+
let inConflict = false;
|
|
844
|
+
let inOurs = false;
|
|
845
|
+
for (const line of lines) {
|
|
846
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
847
|
+
inConflict = true;
|
|
848
|
+
inOurs = true;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (inConflict && line === "=======") {
|
|
852
|
+
inOurs = false;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
856
|
+
inConflict = false;
|
|
857
|
+
inOurs = false;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (!inConflict || inOurs) {
|
|
861
|
+
result.push(line);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return result.join("\n");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/ReplayApplicator.ts
|
|
571
868
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
572
869
|
".png",
|
|
573
870
|
".jpg",
|
|
@@ -622,6 +919,7 @@ var ReplayApplicator = class {
|
|
|
622
919
|
lockManager;
|
|
623
920
|
outputDir;
|
|
624
921
|
renameCache = /* @__PURE__ */ new Map();
|
|
922
|
+
treeExistsCache = /* @__PURE__ */ new Map();
|
|
625
923
|
fileTheirsAccumulator = /* @__PURE__ */ new Map();
|
|
626
924
|
constructor(git, lockManager, outputDir) {
|
|
627
925
|
this.git = git;
|
|
@@ -639,7 +937,8 @@ var ReplayApplicator = class {
|
|
|
639
937
|
async applyPatches(patches) {
|
|
640
938
|
this.resetAccumulator();
|
|
641
939
|
const results = [];
|
|
642
|
-
for (
|
|
940
|
+
for (let i = 0; i < patches.length; i++) {
|
|
941
|
+
const patch = patches[i];
|
|
643
942
|
if (this.isExcluded(patch)) {
|
|
644
943
|
results.push({
|
|
645
944
|
patch,
|
|
@@ -650,6 +949,33 @@ var ReplayApplicator = class {
|
|
|
650
949
|
}
|
|
651
950
|
const result = await this.applyPatchWithFallback(patch);
|
|
652
951
|
results.push(result);
|
|
952
|
+
if (result.status === "conflict" && result.fileResults) {
|
|
953
|
+
const laterFiles = /* @__PURE__ */ new Set();
|
|
954
|
+
for (let j = i + 1; j < patches.length; j++) {
|
|
955
|
+
for (const f of patches[j].files) {
|
|
956
|
+
laterFiles.add(f);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const resolvedToOriginal = /* @__PURE__ */ new Map();
|
|
960
|
+
if (result.resolvedFiles) {
|
|
961
|
+
for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
|
|
962
|
+
resolvedToOriginal.set(resolved, orig);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const fileResult of result.fileResults) {
|
|
966
|
+
if (fileResult.status !== "conflict") continue;
|
|
967
|
+
const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
|
|
968
|
+
if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
|
|
969
|
+
const filePath = (0, import_node_path2.join)(this.outputDir, fileResult.file);
|
|
970
|
+
try {
|
|
971
|
+
const content = await (0, import_promises.readFile)(filePath, "utf-8");
|
|
972
|
+
const stripped = stripConflictMarkers(content);
|
|
973
|
+
await (0, import_promises.writeFile)(filePath, stripped);
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
653
979
|
}
|
|
654
980
|
return results;
|
|
655
981
|
}
|
|
@@ -668,7 +994,7 @@ var ReplayApplicator = class {
|
|
|
668
994
|
const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
|
|
669
995
|
const base = await this.git.showFile(baseGen.tree_hash, filePath);
|
|
670
996
|
const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
|
|
671
|
-
if (theirs
|
|
997
|
+
if (theirs) {
|
|
672
998
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
673
999
|
content: theirs,
|
|
674
1000
|
baseGeneration: patch.base_generation
|
|
@@ -790,17 +1116,36 @@ var ReplayApplicator = class {
|
|
|
790
1116
|
}
|
|
791
1117
|
const oursPath = (0, import_node_path2.join)(this.outputDir, resolvedPath);
|
|
792
1118
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
793
|
-
let
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1119
|
+
let ghostReconstructed = false;
|
|
1120
|
+
let theirs = null;
|
|
1121
|
+
if (!base && ours && !renameSourcePath) {
|
|
1122
|
+
const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
|
|
1123
|
+
if (!treeReachable) {
|
|
1124
|
+
const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
|
|
1125
|
+
if (fileDiff) {
|
|
1126
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
1127
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
1128
|
+
if (result) {
|
|
1129
|
+
base = result.base;
|
|
1130
|
+
theirs = result.theirs;
|
|
1131
|
+
ghostReconstructed = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (!ghostReconstructed) {
|
|
1137
|
+
theirs = await this.applyPatchToContent(
|
|
1138
|
+
base,
|
|
1139
|
+
patch.patch_content,
|
|
1140
|
+
filePath,
|
|
1141
|
+
tempGit,
|
|
1142
|
+
tempDir,
|
|
1143
|
+
renameSourcePath
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
801
1146
|
let useAccumulatorAsMergeBase = false;
|
|
802
1147
|
const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
|
|
803
|
-
if (!theirs &&
|
|
1148
|
+
if (!theirs && accumulatorEntry) {
|
|
804
1149
|
theirs = await this.applyPatchToContent(
|
|
805
1150
|
accumulatorEntry.content,
|
|
806
1151
|
patch.patch_content,
|
|
@@ -830,13 +1175,13 @@ var ReplayApplicator = class {
|
|
|
830
1175
|
baseMismatchSkipped = true;
|
|
831
1176
|
}
|
|
832
1177
|
}
|
|
833
|
-
if (
|
|
1178
|
+
if (base == null && !ours && effective_theirs) {
|
|
834
1179
|
const outDir2 = (0, import_node_path2.dirname)(oursPath);
|
|
835
1180
|
await (0, import_promises.mkdir)(outDir2, { recursive: true });
|
|
836
1181
|
await (0, import_promises.writeFile)(oursPath, effective_theirs);
|
|
837
1182
|
return { file: resolvedPath, status: "merged", reason: "new-file" };
|
|
838
1183
|
}
|
|
839
|
-
if (
|
|
1184
|
+
if (base == null && ours && effective_theirs) {
|
|
840
1185
|
const merged2 = threeWayMerge("", ours, effective_theirs);
|
|
841
1186
|
const outDir2 = (0, import_node_path2.dirname)(oursPath);
|
|
842
1187
|
await (0, import_promises.mkdir)(outDir2, { recursive: true });
|
|
@@ -859,7 +1204,7 @@ var ReplayApplicator = class {
|
|
|
859
1204
|
reason: "missing-content"
|
|
860
1205
|
};
|
|
861
1206
|
}
|
|
862
|
-
if (
|
|
1207
|
+
if (base == null && !useAccumulatorAsMergeBase || !ours) {
|
|
863
1208
|
return {
|
|
864
1209
|
file: resolvedPath,
|
|
865
1210
|
status: "skipped",
|
|
@@ -867,11 +1212,18 @@ var ReplayApplicator = class {
|
|
|
867
1212
|
};
|
|
868
1213
|
}
|
|
869
1214
|
const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
|
|
1215
|
+
if (mergeBase == null) {
|
|
1216
|
+
return {
|
|
1217
|
+
file: resolvedPath,
|
|
1218
|
+
status: "skipped",
|
|
1219
|
+
reason: "missing-content"
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
870
1222
|
const merged = threeWayMerge(mergeBase, ours, effective_theirs);
|
|
871
1223
|
const outDir = (0, import_node_path2.dirname)(oursPath);
|
|
872
1224
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
873
1225
|
await (0, import_promises.writeFile)(oursPath, merged.content);
|
|
874
|
-
if (effective_theirs
|
|
1226
|
+
if (effective_theirs) {
|
|
875
1227
|
this.fileTheirsAccumulator.set(resolvedPath, {
|
|
876
1228
|
content: effective_theirs,
|
|
877
1229
|
baseGeneration: patch.base_generation
|
|
@@ -895,6 +1247,14 @@ var ReplayApplicator = class {
|
|
|
895
1247
|
};
|
|
896
1248
|
}
|
|
897
1249
|
}
|
|
1250
|
+
async isTreeReachable(treeHash) {
|
|
1251
|
+
let result = this.treeExistsCache.get(treeHash);
|
|
1252
|
+
if (result === void 0) {
|
|
1253
|
+
result = await this.git.treeExists(treeHash);
|
|
1254
|
+
this.treeExistsCache.set(treeHash, result);
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
898
1258
|
isExcluded(patch) {
|
|
899
1259
|
const config = this.lockManager.getCustomizationsConfig();
|
|
900
1260
|
if (!config.exclude) return false;
|
|
@@ -1114,36 +1474,6 @@ var import_node_fs2 = require("fs");
|
|
|
1114
1474
|
var import_node_path3 = require("path");
|
|
1115
1475
|
var import_minimatch2 = require("minimatch");
|
|
1116
1476
|
init_GitClient();
|
|
1117
|
-
|
|
1118
|
-
// src/conflict-utils.ts
|
|
1119
|
-
function stripConflictMarkers(content) {
|
|
1120
|
-
const lines = content.split("\n");
|
|
1121
|
-
const result = [];
|
|
1122
|
-
let inConflict = false;
|
|
1123
|
-
let inOurs = false;
|
|
1124
|
-
for (const line of lines) {
|
|
1125
|
-
if (line.startsWith("<<<<<<< ")) {
|
|
1126
|
-
inConflict = true;
|
|
1127
|
-
inOurs = true;
|
|
1128
|
-
continue;
|
|
1129
|
-
}
|
|
1130
|
-
if (inConflict && line === "=======") {
|
|
1131
|
-
inOurs = false;
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
1135
|
-
inConflict = false;
|
|
1136
|
-
inOurs = false;
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
if (!inConflict || inOurs) {
|
|
1140
|
-
result.push(line);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return result.join("\n");
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// src/ReplayService.ts
|
|
1147
1477
|
var ReplayService = class {
|
|
1148
1478
|
git;
|
|
1149
1479
|
detector;
|
|
@@ -1999,6 +2329,7 @@ async function bootstrap(outputDir, options) {
|
|
|
1999
2329
|
}
|
|
2000
2330
|
lockManager.save();
|
|
2001
2331
|
const fernignoreUpdated = ensureFernignoreEntries(outputDir);
|
|
2332
|
+
ensureGitattributesEntries(outputDir);
|
|
2002
2333
|
if (migrator.fernignoreExists() && fernignorePatterns.length > 0) {
|
|
2003
2334
|
const action = options?.fernignoreAction ?? "skip";
|
|
2004
2335
|
if (action === "migrate") {
|
|
@@ -2080,7 +2411,7 @@ function parseGitLog(log) {
|
|
|
2080
2411
|
return { sha, authorName, authorEmail, message };
|
|
2081
2412
|
});
|
|
2082
2413
|
}
|
|
2083
|
-
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml"];
|
|
2414
|
+
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
2084
2415
|
function ensureFernignoreEntries(outputDir) {
|
|
2085
2416
|
const fernignorePath = (0, import_node_path5.join)(outputDir, ".fernignore");
|
|
2086
2417
|
let content = "";
|
|
@@ -2104,6 +2435,29 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
2104
2435
|
(0, import_node_fs4.writeFileSync)(fernignorePath, content, "utf-8");
|
|
2105
2436
|
return true;
|
|
2106
2437
|
}
|
|
2438
|
+
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
2439
|
+
function ensureGitattributesEntries(outputDir) {
|
|
2440
|
+
const gitattributesPath = (0, import_node_path5.join)(outputDir, ".gitattributes");
|
|
2441
|
+
let content = "";
|
|
2442
|
+
if ((0, import_node_fs4.existsSync)(gitattributesPath)) {
|
|
2443
|
+
content = (0, import_node_fs4.readFileSync)(gitattributesPath, "utf-8");
|
|
2444
|
+
}
|
|
2445
|
+
const lines = content.split("\n");
|
|
2446
|
+
const toAdd = [];
|
|
2447
|
+
for (const entry of GITATTRIBUTES_ENTRIES) {
|
|
2448
|
+
if (!lines.some((line) => line.trim() === entry)) {
|
|
2449
|
+
toAdd.push(entry);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
if (toAdd.length === 0) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (content && !content.endsWith("\n")) {
|
|
2456
|
+
content += "\n";
|
|
2457
|
+
}
|
|
2458
|
+
content += toAdd.join("\n") + "\n";
|
|
2459
|
+
(0, import_node_fs4.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
2460
|
+
}
|
|
2107
2461
|
function computeContentHash(patchContent) {
|
|
2108
2462
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
2109
2463
|
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
@@ -2111,30 +2465,145 @@ function computeContentHash(patchContent) {
|
|
|
2111
2465
|
|
|
2112
2466
|
// src/commands/forget.ts
|
|
2113
2467
|
var import_minimatch4 = require("minimatch");
|
|
2114
|
-
function
|
|
2468
|
+
function parseDiffStat(patchContent) {
|
|
2469
|
+
let additions = 0;
|
|
2470
|
+
let deletions = 0;
|
|
2471
|
+
let inDiffHunk = false;
|
|
2472
|
+
for (const line of patchContent.split("\n")) {
|
|
2473
|
+
if (line.startsWith("diff --git ")) {
|
|
2474
|
+
inDiffHunk = true;
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
if (!inDiffHunk) continue;
|
|
2478
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2479
|
+
additions++;
|
|
2480
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2481
|
+
deletions++;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return { additions, deletions };
|
|
2485
|
+
}
|
|
2486
|
+
function toMatchedPatch(patch) {
|
|
2487
|
+
return {
|
|
2488
|
+
id: patch.id,
|
|
2489
|
+
message: patch.original_message,
|
|
2490
|
+
files: patch.files,
|
|
2491
|
+
diffstat: parseDiffStat(patch.patch_content),
|
|
2492
|
+
...patch.status ? { status: patch.status } : {}
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
function matchesPatch(patch, pattern) {
|
|
2496
|
+
const fileMatch = patch.files.some(
|
|
2497
|
+
(file) => file === pattern || (0, import_minimatch4.minimatch)(file, pattern)
|
|
2498
|
+
);
|
|
2499
|
+
if (fileMatch) return true;
|
|
2500
|
+
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|
|
2501
|
+
}
|
|
2502
|
+
function buildWarnings(patches) {
|
|
2503
|
+
const warnings = [];
|
|
2504
|
+
for (const patch of patches) {
|
|
2505
|
+
if (patch.status === "resolving") {
|
|
2506
|
+
warnings.push(
|
|
2507
|
+
`patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
|
|
2508
|
+
);
|
|
2509
|
+
} else if (patch.status === "unresolved") {
|
|
2510
|
+
warnings.push(
|
|
2511
|
+
`patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return warnings;
|
|
2516
|
+
}
|
|
2517
|
+
var EMPTY_RESULT = {
|
|
2518
|
+
initialized: false,
|
|
2519
|
+
removed: [],
|
|
2520
|
+
remaining: 0,
|
|
2521
|
+
notFound: false,
|
|
2522
|
+
alreadyForgotten: [],
|
|
2523
|
+
totalPatches: 0,
|
|
2524
|
+
warnings: []
|
|
2525
|
+
};
|
|
2526
|
+
function forget(outputDir, options) {
|
|
2115
2527
|
const lockManager = new LockfileManager(outputDir);
|
|
2116
2528
|
if (!lockManager.exists()) {
|
|
2117
|
-
return {
|
|
2529
|
+
return { ...EMPTY_RESULT };
|
|
2118
2530
|
}
|
|
2119
2531
|
const lock = lockManager.read();
|
|
2120
|
-
const
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2532
|
+
const totalPatches = lock.patches.length;
|
|
2533
|
+
if (options?.all) {
|
|
2534
|
+
const removed = lock.patches.map(toMatchedPatch);
|
|
2535
|
+
const warnings = buildWarnings(lock.patches);
|
|
2536
|
+
if (!options.dryRun) {
|
|
2537
|
+
for (const patch of lock.patches) {
|
|
2538
|
+
lockManager.addForgottenHash(patch.content_hash);
|
|
2539
|
+
}
|
|
2540
|
+
lockManager.clearPatches();
|
|
2541
|
+
lockManager.save();
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
2544
|
+
initialized: true,
|
|
2545
|
+
removed,
|
|
2546
|
+
remaining: 0,
|
|
2547
|
+
notFound: false,
|
|
2548
|
+
alreadyForgotten: [],
|
|
2549
|
+
totalPatches,
|
|
2550
|
+
warnings
|
|
2551
|
+
};
|
|
2125
2552
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2553
|
+
if (options?.patchIds && options.patchIds.length > 0) {
|
|
2554
|
+
const removed = [];
|
|
2555
|
+
const alreadyForgotten = [];
|
|
2556
|
+
const patchesToRemove = [];
|
|
2557
|
+
for (const id of options.patchIds) {
|
|
2558
|
+
const patch = lock.patches.find((p) => p.id === id);
|
|
2559
|
+
if (patch) {
|
|
2560
|
+
removed.push(toMatchedPatch(patch));
|
|
2561
|
+
patchesToRemove.push(patch);
|
|
2562
|
+
} else {
|
|
2563
|
+
alreadyForgotten.push(id);
|
|
2564
|
+
}
|
|
2134
2565
|
}
|
|
2135
|
-
|
|
2566
|
+
const warnings = buildWarnings(patchesToRemove);
|
|
2567
|
+
if (!options.dryRun) {
|
|
2568
|
+
for (const patch of patchesToRemove) {
|
|
2569
|
+
lockManager.addForgottenHash(patch.content_hash);
|
|
2570
|
+
lockManager.removePatch(patch.id);
|
|
2571
|
+
}
|
|
2572
|
+
lockManager.save();
|
|
2573
|
+
}
|
|
2574
|
+
return {
|
|
2575
|
+
initialized: true,
|
|
2576
|
+
removed,
|
|
2577
|
+
remaining: totalPatches - removed.length,
|
|
2578
|
+
notFound: removed.length === 0 && alreadyForgotten.length > 0,
|
|
2579
|
+
alreadyForgotten,
|
|
2580
|
+
totalPatches,
|
|
2581
|
+
warnings
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
if (options?.pattern) {
|
|
2585
|
+
const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
|
|
2586
|
+
return {
|
|
2587
|
+
initialized: true,
|
|
2588
|
+
removed: [],
|
|
2589
|
+
remaining: totalPatches,
|
|
2590
|
+
notFound: matched.length === 0,
|
|
2591
|
+
alreadyForgotten: [],
|
|
2592
|
+
totalPatches,
|
|
2593
|
+
warnings: [],
|
|
2594
|
+
matched
|
|
2595
|
+
};
|
|
2136
2596
|
}
|
|
2137
|
-
return {
|
|
2597
|
+
return {
|
|
2598
|
+
initialized: true,
|
|
2599
|
+
removed: [],
|
|
2600
|
+
remaining: totalPatches,
|
|
2601
|
+
notFound: totalPatches === 0,
|
|
2602
|
+
alreadyForgotten: [],
|
|
2603
|
+
totalPatches,
|
|
2604
|
+
warnings: [],
|
|
2605
|
+
matched: lock.patches.map(toMatchedPatch)
|
|
2606
|
+
};
|
|
2138
2607
|
}
|
|
2139
2608
|
|
|
2140
2609
|
// src/commands/reset.ts
|
|
@@ -2256,24 +2725,49 @@ async function getChangedFiles(git, currentGen, files) {
|
|
|
2256
2725
|
function status(outputDir) {
|
|
2257
2726
|
const lockManager = new LockfileManager(outputDir);
|
|
2258
2727
|
if (!lockManager.exists()) {
|
|
2259
|
-
return {
|
|
2728
|
+
return {
|
|
2729
|
+
initialized: false,
|
|
2730
|
+
generationCount: 0,
|
|
2731
|
+
lastGeneration: void 0,
|
|
2732
|
+
patches: [],
|
|
2733
|
+
unresolvedCount: 0,
|
|
2734
|
+
excludePatterns: []
|
|
2735
|
+
};
|
|
2260
2736
|
}
|
|
2261
2737
|
const lock = lockManager.read();
|
|
2262
2738
|
const patches = lock.patches.map((patch) => ({
|
|
2263
|
-
|
|
2264
|
-
|
|
2739
|
+
id: patch.id,
|
|
2740
|
+
type: patch.patch_content.includes("new file mode") ? "added" : "modified",
|
|
2265
2741
|
message: patch.original_message,
|
|
2266
|
-
|
|
2742
|
+
author: patch.original_author.split("<")[0]?.trim() || "unknown",
|
|
2743
|
+
sha: patch.original_commit.slice(0, 7),
|
|
2744
|
+
files: patch.files,
|
|
2745
|
+
fileCount: patch.files.length,
|
|
2746
|
+
...patch.status ? { status: patch.status } : {}
|
|
2267
2747
|
}));
|
|
2748
|
+
const unresolvedCount = lock.patches.filter(
|
|
2749
|
+
(p) => p.status === "unresolved" || p.status === "resolving"
|
|
2750
|
+
).length;
|
|
2268
2751
|
let lastGeneration;
|
|
2269
2752
|
const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
2270
2753
|
if (lastGen) {
|
|
2271
2754
|
lastGeneration = {
|
|
2272
|
-
sha: lastGen.commit_sha,
|
|
2273
|
-
timestamp: lastGen.timestamp
|
|
2755
|
+
sha: lastGen.commit_sha.slice(0, 7),
|
|
2756
|
+
timestamp: lastGen.timestamp,
|
|
2757
|
+
cliVersion: lastGen.cli_version,
|
|
2758
|
+
generatorVersions: lastGen.generator_versions
|
|
2274
2759
|
};
|
|
2275
2760
|
}
|
|
2276
|
-
|
|
2761
|
+
const config = lockManager.getCustomizationsConfig();
|
|
2762
|
+
const excludePatterns = config.exclude ?? [];
|
|
2763
|
+
return {
|
|
2764
|
+
initialized: true,
|
|
2765
|
+
generationCount: lock.generations.length,
|
|
2766
|
+
lastGeneration,
|
|
2767
|
+
patches,
|
|
2768
|
+
unresolvedCount,
|
|
2769
|
+
excludePatterns
|
|
2770
|
+
};
|
|
2277
2771
|
}
|
|
2278
2772
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2279
2773
|
0 && (module.exports = {
|