@fern-api/replay 0.16.1 → 0.17.4
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 +286 -41
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +303 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +303 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -14979,6 +14979,15 @@ function parseRevertedMessage(subject) {
|
|
|
14979
14979
|
|
|
14980
14980
|
// src/ReplayDetector.ts
|
|
14981
14981
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
14982
|
+
function matchesFernignorePattern(filePath, patterns) {
|
|
14983
|
+
if (patterns.length === 0) return false;
|
|
14984
|
+
return patterns.some((p) => {
|
|
14985
|
+
if (filePath === p) return true;
|
|
14986
|
+
if (minimatch(filePath, p)) return true;
|
|
14987
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
14988
|
+
return false;
|
|
14989
|
+
});
|
|
14990
|
+
}
|
|
14982
14991
|
function parsePatchFileHeaders(patchContent) {
|
|
14983
14992
|
const results = [];
|
|
14984
14993
|
let currentPath = null;
|
|
@@ -15019,21 +15028,25 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
15019
15028
|
}
|
|
15020
15029
|
return snapshot;
|
|
15021
15030
|
}
|
|
15022
|
-
function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
|
|
15031
|
+
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
15023
15032
|
const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
15024
15033
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
15025
|
-
const
|
|
15026
|
-
|
|
15034
|
+
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
15035
|
+
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
15036
|
+
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
15037
|
+
return { files, sensitiveFiles, fernignoredFiles };
|
|
15027
15038
|
}
|
|
15028
15039
|
var ReplayDetector = class {
|
|
15029
15040
|
git;
|
|
15030
15041
|
lockManager;
|
|
15031
15042
|
sdkOutputDir;
|
|
15043
|
+
fernignorePatterns;
|
|
15032
15044
|
warnings = [];
|
|
15033
|
-
constructor(git, lockManager, sdkOutputDir) {
|
|
15045
|
+
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
15034
15046
|
this.git = git;
|
|
15035
15047
|
this.lockManager = lockManager;
|
|
15036
15048
|
this.sdkOutputDir = sdkOutputDir;
|
|
15049
|
+
this.fernignorePatterns = fernignorePatterns;
|
|
15037
15050
|
}
|
|
15038
15051
|
/**
|
|
15039
15052
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
@@ -15068,6 +15081,22 @@ var ReplayDetector = class {
|
|
|
15068
15081
|
return commit.sha;
|
|
15069
15082
|
}
|
|
15070
15083
|
}
|
|
15084
|
+
let fullLog;
|
|
15085
|
+
try {
|
|
15086
|
+
fullLog = await this.git.exec([
|
|
15087
|
+
"log",
|
|
15088
|
+
"--no-merges",
|
|
15089
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
15090
|
+
"HEAD"
|
|
15091
|
+
]);
|
|
15092
|
+
} catch {
|
|
15093
|
+
return null;
|
|
15094
|
+
}
|
|
15095
|
+
for (const commit of this.parseGitLog(fullLog)) {
|
|
15096
|
+
if (isGenerationBoundary(commit)) {
|
|
15097
|
+
return commit.sha;
|
|
15098
|
+
}
|
|
15099
|
+
}
|
|
15071
15100
|
return null;
|
|
15072
15101
|
}
|
|
15073
15102
|
async detectNewPatches() {
|
|
@@ -15079,6 +15108,11 @@ var ReplayDetector = class {
|
|
|
15079
15108
|
}
|
|
15080
15109
|
const lastGen = this.getLastGeneration(lock);
|
|
15081
15110
|
if (!lastGen) {
|
|
15111
|
+
if (lock.current_generation) {
|
|
15112
|
+
this.warnings.push(
|
|
15113
|
+
`No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
|
|
15114
|
+
);
|
|
15115
|
+
}
|
|
15082
15116
|
return { patches: [], revertedPatchIds: [] };
|
|
15083
15117
|
}
|
|
15084
15118
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
@@ -15140,22 +15174,30 @@ var ReplayDetector = class {
|
|
|
15140
15174
|
continue;
|
|
15141
15175
|
}
|
|
15142
15176
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15143
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15177
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15178
|
+
filesOutput,
|
|
15179
|
+
this.fernignorePatterns
|
|
15180
|
+
);
|
|
15144
15181
|
if (sensitiveFiles.length > 0) {
|
|
15145
15182
|
this.warnings.push(
|
|
15146
15183
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15147
15184
|
);
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15185
|
+
}
|
|
15186
|
+
if (fernignoredFiles.length > 0) {
|
|
15187
|
+
this.warnings.push(
|
|
15188
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15189
|
+
);
|
|
15190
|
+
}
|
|
15191
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
15192
|
+
const parentSha = parents[0];
|
|
15193
|
+
if (parentSha) {
|
|
15194
|
+
try {
|
|
15195
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
15196
|
+
} catch {
|
|
15197
|
+
continue;
|
|
15158
15198
|
}
|
|
15199
|
+
if (!patchContent.trim()) continue;
|
|
15200
|
+
contentHash = this.computeContentHash(patchContent);
|
|
15159
15201
|
}
|
|
15160
15202
|
}
|
|
15161
15203
|
if (files.length === 0) {
|
|
@@ -15354,12 +15396,20 @@ var ReplayDetector = class {
|
|
|
15354
15396
|
resolvedBaseGeneration = fallback;
|
|
15355
15397
|
}
|
|
15356
15398
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
15357
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15399
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15400
|
+
filesOutput,
|
|
15401
|
+
this.fernignorePatterns
|
|
15402
|
+
);
|
|
15358
15403
|
if (sensitiveFiles.length > 0) {
|
|
15359
15404
|
this.warnings.push(
|
|
15360
15405
|
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15361
15406
|
);
|
|
15362
15407
|
}
|
|
15408
|
+
if (fernignoredFiles.length > 0) {
|
|
15409
|
+
this.warnings.push(
|
|
15410
|
+
`.fernignore-protected file(s) excluded from composite patch: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15411
|
+
);
|
|
15412
|
+
}
|
|
15363
15413
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
15364
15414
|
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
15365
15415
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
@@ -15442,23 +15492,31 @@ var ReplayDetector = class {
|
|
|
15442
15492
|
continue;
|
|
15443
15493
|
}
|
|
15444
15494
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15445
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15495
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15496
|
+
filesOutput,
|
|
15497
|
+
this.fernignorePatterns
|
|
15498
|
+
);
|
|
15446
15499
|
if (sensitiveFiles.length > 0) {
|
|
15447
15500
|
this.warnings.push(
|
|
15448
15501
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15449
15502
|
);
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
}
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15503
|
+
}
|
|
15504
|
+
if (fernignoredFiles.length > 0) {
|
|
15505
|
+
this.warnings.push(
|
|
15506
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15507
|
+
);
|
|
15508
|
+
}
|
|
15509
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
15510
|
+
if (parents.length === 0) {
|
|
15511
|
+
continue;
|
|
15512
|
+
}
|
|
15513
|
+
try {
|
|
15514
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
15515
|
+
} catch {
|
|
15516
|
+
continue;
|
|
15461
15517
|
}
|
|
15518
|
+
if (!patchContent.trim()) continue;
|
|
15519
|
+
contentHash = this.computeContentHash(patchContent);
|
|
15462
15520
|
}
|
|
15463
15521
|
if (files.length === 0) {
|
|
15464
15522
|
continue;
|
|
@@ -15675,16 +15733,20 @@ async function resolveInputs(args) {
|
|
|
15675
15733
|
const oursPath = (0, import_node_path4.join)(outputDir, resolvedPath);
|
|
15676
15734
|
const ours = await (0, import_promises.readFile)(oursPath, "utf-8").catch(() => null);
|
|
15677
15735
|
let ghostTheirs = null;
|
|
15678
|
-
|
|
15736
|
+
let baseTreeUnreachable = false;
|
|
15737
|
+
if (!base && !renameSourcePath) {
|
|
15679
15738
|
const treeReachable = await isTreeReachable(baseGen.tree_hash);
|
|
15680
15739
|
if (!treeReachable) {
|
|
15681
|
-
|
|
15682
|
-
if (
|
|
15683
|
-
const
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
|
|
15740
|
+
baseTreeUnreachable = true;
|
|
15741
|
+
if (ours) {
|
|
15742
|
+
const fileDiff = extractFileDiff2(patch.patch_content, filePath);
|
|
15743
|
+
if (fileDiff) {
|
|
15744
|
+
const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
|
|
15745
|
+
const result = reconstructFromGhostPatch2(fileDiff, ours);
|
|
15746
|
+
if (result) {
|
|
15747
|
+
base = result.base;
|
|
15748
|
+
ghostTheirs = result.theirs;
|
|
15749
|
+
}
|
|
15688
15750
|
}
|
|
15689
15751
|
}
|
|
15690
15752
|
}
|
|
@@ -15696,7 +15758,11 @@ async function resolveInputs(args) {
|
|
|
15696
15758
|
ours,
|
|
15697
15759
|
oursPath,
|
|
15698
15760
|
renameSourcePath,
|
|
15699
|
-
ghostTheirs
|
|
15761
|
+
ghostTheirs,
|
|
15762
|
+
// Only meaningful when ghost reconstruction did NOT recover a base; if it
|
|
15763
|
+
// did, `base` is non-null and the merge proceeds normally. We still set
|
|
15764
|
+
// the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
|
|
15765
|
+
baseTreeUnreachable
|
|
15700
15766
|
};
|
|
15701
15767
|
}
|
|
15702
15768
|
|
|
@@ -16262,7 +16328,11 @@ async function buildMergePlan(args) {
|
|
|
16262
16328
|
return { kind: "new-file-only-user", theirs: effective_theirs };
|
|
16263
16329
|
}
|
|
16264
16330
|
if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
|
|
16265
|
-
return {
|
|
16331
|
+
return {
|
|
16332
|
+
kind: "new-file-both",
|
|
16333
|
+
theirs: effective_theirs,
|
|
16334
|
+
...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
|
|
16335
|
+
};
|
|
16266
16336
|
}
|
|
16267
16337
|
if (effective_theirs == null) {
|
|
16268
16338
|
return { kind: "skipped", reason: "missing-content" };
|
|
@@ -16310,17 +16380,19 @@ async function runMergeAndRecord(args) {
|
|
|
16310
16380
|
const merged2 = threeWayMerge("", ours, plan.theirs);
|
|
16311
16381
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
16312
16382
|
await (0, import_promises2.writeFile)(oursPath, merged2.content);
|
|
16383
|
+
const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
|
|
16313
16384
|
if (merged2.hasConflicts) {
|
|
16314
16385
|
return {
|
|
16315
16386
|
file: resolvedPath,
|
|
16316
16387
|
status: "conflict",
|
|
16317
16388
|
conflicts: merged2.conflicts,
|
|
16318
16389
|
conflictReason: "new-file-both",
|
|
16319
|
-
conflictMetadata: metadata
|
|
16390
|
+
conflictMetadata: metadata,
|
|
16391
|
+
...unreachableFlag
|
|
16320
16392
|
};
|
|
16321
16393
|
}
|
|
16322
16394
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
16323
|
-
return { file: resolvedPath, status: "merged" };
|
|
16395
|
+
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
16324
16396
|
}
|
|
16325
16397
|
const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
|
|
16326
16398
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
@@ -17528,6 +17600,7 @@ var NoPatchesFlow = class {
|
|
|
17528
17600
|
results = await this.service.applicator.applyPatches(newPatches);
|
|
17529
17601
|
this.service._lastApplyResults = results;
|
|
17530
17602
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17603
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17531
17604
|
this.service.revertConflictingFiles(results);
|
|
17532
17605
|
for (const result of results) {
|
|
17533
17606
|
if (result.status === "conflict") {
|
|
@@ -17683,6 +17756,7 @@ var NormalRegenerationFlow = class {
|
|
|
17683
17756
|
const results = await this.service.applicator.applyPatches(allPatches);
|
|
17684
17757
|
this.service._lastApplyResults = results;
|
|
17685
17758
|
this.service.recordDroppedContextLineWarnings(results);
|
|
17759
|
+
this.service.recordUnreachableBaseWarnings(results);
|
|
17686
17760
|
this.service.revertConflictingFiles(results);
|
|
17687
17761
|
for (const result of results) {
|
|
17688
17762
|
if (result.status === "conflict") {
|
|
@@ -17785,7 +17859,8 @@ var ReplayService = class {
|
|
|
17785
17859
|
this.git = git;
|
|
17786
17860
|
this.outputDir = outputDir;
|
|
17787
17861
|
this.lockManager = new LockfileManager(outputDir);
|
|
17788
|
-
|
|
17862
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
17863
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
|
|
17789
17864
|
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
17790
17865
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
17791
17866
|
this.fileOwnership = new FileOwnership(git);
|
|
@@ -17805,8 +17880,60 @@ var ReplayService = class {
|
|
|
17805
17880
|
async prepareReplay(options) {
|
|
17806
17881
|
this.warnings = [];
|
|
17807
17882
|
this.detector.warnings.length = 0;
|
|
17883
|
+
this.cleanseLegacyFernignoreEntries();
|
|
17808
17884
|
return this.selectFlow(options).prepare(options);
|
|
17809
17885
|
}
|
|
17886
|
+
/**
|
|
17887
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
17888
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
17889
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
17890
|
+
* end up with no surviving files are removed entirely.
|
|
17891
|
+
*
|
|
17892
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
17893
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
17894
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
17895
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
17896
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
17897
|
+
*/
|
|
17898
|
+
cleanseLegacyFernignoreEntries() {
|
|
17899
|
+
const patterns = this.readFernignorePatterns();
|
|
17900
|
+
if (patterns.length === 0) return;
|
|
17901
|
+
if (!this.lockManager.exists()) return;
|
|
17902
|
+
this.lockManager.read();
|
|
17903
|
+
let lockfileMutated = false;
|
|
17904
|
+
const patches = this.lockManager.getPatches();
|
|
17905
|
+
for (const patch of patches) {
|
|
17906
|
+
try {
|
|
17907
|
+
const result = computeLegacyFernignoreCleanse(
|
|
17908
|
+
patch,
|
|
17909
|
+
patterns,
|
|
17910
|
+
(content) => this.detector.computeContentHash(content)
|
|
17911
|
+
);
|
|
17912
|
+
if (result.unchanged) continue;
|
|
17913
|
+
if (result.remove) {
|
|
17914
|
+
this.lockManager.removePatch(patch.id);
|
|
17915
|
+
this.warnings.push(
|
|
17916
|
+
`Cleansed legacy patch ${patch.id}: all referenced files match .fernignore \u2014 patch removed. Customer's on-disk content for those files is preserved by .fernignore. (ADR 0002: .fernignore-protected files are not tracked by Replay.)`
|
|
17917
|
+
);
|
|
17918
|
+
} else {
|
|
17919
|
+
this.lockManager.updatePatch(patch.id, {
|
|
17920
|
+
files: result.files,
|
|
17921
|
+
patch_content: result.patch_content,
|
|
17922
|
+
content_hash: result.content_hash,
|
|
17923
|
+
...result.theirs_snapshot !== void 0 ? { theirs_snapshot: result.theirs_snapshot } : {}
|
|
17924
|
+
});
|
|
17925
|
+
this.warnings.push(
|
|
17926
|
+
`Cleansed legacy patch ${patch.id}: stripped .fernignore-matched entries (${result.strippedPaths.join(", ")}). Customer's on-disk content for those files is preserved by .fernignore. (ADR 0002.)`
|
|
17927
|
+
);
|
|
17928
|
+
}
|
|
17929
|
+
lockfileMutated = true;
|
|
17930
|
+
} catch {
|
|
17931
|
+
}
|
|
17932
|
+
}
|
|
17933
|
+
if (lockfileMutated) {
|
|
17934
|
+
this.lockManager.save();
|
|
17935
|
+
}
|
|
17936
|
+
}
|
|
17810
17937
|
/**
|
|
17811
17938
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
17812
17939
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -18549,6 +18676,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18549
18676
|
}
|
|
18550
18677
|
}
|
|
18551
18678
|
}
|
|
18679
|
+
/**
|
|
18680
|
+
* After applyPatches(), surface a warning for each (patch × file) where a
|
|
18681
|
+
* customization's BASE could not be read because the patch's
|
|
18682
|
+
* base-generation tree is unreachable in this clone (shallow-clone window
|
|
18683
|
+
* too small / pruned history). In that state the merge pipeline falls
|
|
18684
|
+
* through to `new-file-both` and the customer's edit can be dropped with NO
|
|
18685
|
+
* conflict and NO error — the INC-866 silent-loss class. This converts that
|
|
18686
|
+
* silence into an explicit, actionable warning naming the file and the
|
|
18687
|
+
* likely cause (insufficient clone window).
|
|
18688
|
+
*
|
|
18689
|
+
* Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
|
|
18690
|
+
* drop" contract. De-duplicated per file path so a single insufficient
|
|
18691
|
+
* window doesn't spam one warning per touched file repeatedly.
|
|
18692
|
+
*/
|
|
18693
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18694
|
+
recordUnreachableBaseWarnings(results) {
|
|
18695
|
+
const warned = /* @__PURE__ */ new Set();
|
|
18696
|
+
for (const result of results) {
|
|
18697
|
+
if (!result.fileResults) continue;
|
|
18698
|
+
for (const fr of result.fileResults) {
|
|
18699
|
+
if (!fr.baseTreeUnreachable) continue;
|
|
18700
|
+
if (warned.has(fr.file)) continue;
|
|
18701
|
+
warned.add(fr.file);
|
|
18702
|
+
const base = fr.conflictMetadata?.baseGeneration;
|
|
18703
|
+
const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
|
|
18704
|
+
this.warnings.push(
|
|
18705
|
+
`${fr.file}: could not read the prior generation's version of this file${baseRef} because that generation's tree is outside the clone window (insufficient shallow-clone depth or pruned history). The customization was treated as a new file, which can drop your edits silently. Re-run with the prior generation in the clone window \u2014 e.g. \`git fetch --shallow-since=<date just before the prior [fern-generated] commit>\` or \`git fetch --unshallow\` \u2014 then re-run replay.`
|
|
18706
|
+
);
|
|
18707
|
+
}
|
|
18708
|
+
}
|
|
18709
|
+
}
|
|
18552
18710
|
/**
|
|
18553
18711
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
18554
18712
|
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
@@ -18667,6 +18825,93 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
18667
18825
|
}
|
|
18668
18826
|
return { applied, unresolved, absorbed };
|
|
18669
18827
|
}
|
|
18828
|
+
function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeContentHash2) {
|
|
18829
|
+
const matchesFernignore = (filePath) => fernignorePatterns.some((p) => {
|
|
18830
|
+
if (filePath === p) return true;
|
|
18831
|
+
if (minimatch(filePath, p)) return true;
|
|
18832
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
18833
|
+
return false;
|
|
18834
|
+
});
|
|
18835
|
+
const strippedFromFiles = patch.files.filter(matchesFernignore);
|
|
18836
|
+
const survivingFiles = patch.files.filter((f) => !matchesFernignore(f));
|
|
18837
|
+
let survivingSnapshot = void 0;
|
|
18838
|
+
let snapshotChanged = false;
|
|
18839
|
+
if (patch.theirs_snapshot) {
|
|
18840
|
+
survivingSnapshot = {};
|
|
18841
|
+
for (const [key, value] of Object.entries(patch.theirs_snapshot)) {
|
|
18842
|
+
if (matchesFernignore(key)) {
|
|
18843
|
+
snapshotChanged = true;
|
|
18844
|
+
} else {
|
|
18845
|
+
survivingSnapshot[key] = value;
|
|
18846
|
+
}
|
|
18847
|
+
}
|
|
18848
|
+
}
|
|
18849
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch.patch_content, matchesFernignore);
|
|
18850
|
+
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
18851
|
+
if (unchanged) {
|
|
18852
|
+
return {
|
|
18853
|
+
unchanged: true,
|
|
18854
|
+
remove: false,
|
|
18855
|
+
files: patch.files,
|
|
18856
|
+
patch_content: patch.patch_content,
|
|
18857
|
+
content_hash: patch.content_hash,
|
|
18858
|
+
theirs_snapshot: patch.theirs_snapshot,
|
|
18859
|
+
strippedPaths: []
|
|
18860
|
+
};
|
|
18861
|
+
}
|
|
18862
|
+
const strippedPaths = Array.from(/* @__PURE__ */ new Set([...strippedFromFiles, ...strippedSectionPaths]));
|
|
18863
|
+
if (survivingFiles.length === 0 || newPatchContent.trim() === "") {
|
|
18864
|
+
return {
|
|
18865
|
+
unchanged: false,
|
|
18866
|
+
remove: true,
|
|
18867
|
+
files: [],
|
|
18868
|
+
patch_content: "",
|
|
18869
|
+
content_hash: "",
|
|
18870
|
+
strippedPaths
|
|
18871
|
+
};
|
|
18872
|
+
}
|
|
18873
|
+
const newContentHash = patchContentStripped ? computeContentHash2(newPatchContent) : patch.content_hash;
|
|
18874
|
+
return {
|
|
18875
|
+
unchanged: false,
|
|
18876
|
+
remove: false,
|
|
18877
|
+
files: survivingFiles,
|
|
18878
|
+
patch_content: newPatchContent,
|
|
18879
|
+
content_hash: newContentHash,
|
|
18880
|
+
...survivingSnapshot !== void 0 ? { theirs_snapshot: survivingSnapshot } : {},
|
|
18881
|
+
strippedPaths
|
|
18882
|
+
};
|
|
18883
|
+
}
|
|
18884
|
+
function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
18885
|
+
const lines = patchContent.split("\n");
|
|
18886
|
+
const sectionStarts = [];
|
|
18887
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18888
|
+
const line = lines[i];
|
|
18889
|
+
if (line.startsWith("diff --git ")) {
|
|
18890
|
+
const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
18891
|
+
const path2 = match2 ? match2[2] : null;
|
|
18892
|
+
sectionStarts.push({ idx: i, path: path2 });
|
|
18893
|
+
}
|
|
18894
|
+
}
|
|
18895
|
+
if (sectionStarts.length === 0) {
|
|
18896
|
+
return { stripped: false, content: patchContent, strippedSectionPaths: [] };
|
|
18897
|
+
}
|
|
18898
|
+
const preamble = lines.slice(0, sectionStarts[0].idx);
|
|
18899
|
+
const newLines = [...preamble];
|
|
18900
|
+
const strippedSectionPaths = [];
|
|
18901
|
+
let stripped = false;
|
|
18902
|
+
for (let i = 0; i < sectionStarts.length; i++) {
|
|
18903
|
+
const section = sectionStarts[i];
|
|
18904
|
+
const next = sectionStarts[i + 1];
|
|
18905
|
+
const endIdx = next ? next.idx : lines.length;
|
|
18906
|
+
if (section.path !== null && matchesFernignore(section.path)) {
|
|
18907
|
+
stripped = true;
|
|
18908
|
+
strippedSectionPaths.push(section.path);
|
|
18909
|
+
continue;
|
|
18910
|
+
}
|
|
18911
|
+
newLines.push(...lines.slice(section.idx, endIdx));
|
|
18912
|
+
}
|
|
18913
|
+
return { stripped, content: newLines.join("\n"), strippedSectionPaths };
|
|
18914
|
+
}
|
|
18670
18915
|
|
|
18671
18916
|
// src/FernignoreMigrator.ts
|
|
18672
18917
|
var import_node_crypto2 = require("crypto");
|