@fern-api/replay 0.16.0 → 0.16.2
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 +2532 -2311
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +280 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +280 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -533,6 +533,7 @@ function parseRevertedMessage(subject) {
|
|
|
533
533
|
// src/LockfileManager.ts
|
|
534
534
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
535
535
|
import { join, dirname } from "path";
|
|
536
|
+
import { minimatch } from "minimatch";
|
|
536
537
|
import { stringify, parse } from "yaml";
|
|
537
538
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
538
539
|
var LockfileNotFoundError = class extends Error {
|
|
@@ -544,10 +545,23 @@ var LockfileNotFoundError = class extends Error {
|
|
|
544
545
|
var LockfileManager = class {
|
|
545
546
|
outputDir;
|
|
546
547
|
lock = null;
|
|
548
|
+
fernignorePatterns = [];
|
|
547
549
|
warnings = [];
|
|
548
550
|
constructor(outputDir) {
|
|
549
551
|
this.outputDir = outputDir;
|
|
550
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Inform the lockfile of the customer's `.fernignore` patterns so that
|
|
555
|
+
* `save()` can suppress its "Patch X removed from lockfile" warning when
|
|
556
|
+
* every file in the stripped patch is already `.fernignore`-protected.
|
|
557
|
+
* The strip itself still happens (credentials never persist); only the
|
|
558
|
+
* customer-visible warning is suppressed in that case, because the
|
|
559
|
+
* protected files survive the generator wipe regardless of whether the
|
|
560
|
+
* lockfile tracks them, so there's nothing for the customer to action.
|
|
561
|
+
*/
|
|
562
|
+
setFernignorePatterns(patterns) {
|
|
563
|
+
this.fernignorePatterns = patterns;
|
|
564
|
+
}
|
|
551
565
|
get lockfilePath() {
|
|
552
566
|
return join(this.outputDir, ".fern", "replay.lock");
|
|
553
567
|
}
|
|
@@ -616,9 +630,14 @@ var LockfileManager = class {
|
|
|
616
630
|
if (sensitiveFiles.length > 0) {
|
|
617
631
|
reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
|
|
618
632
|
}
|
|
619
|
-
|
|
620
|
-
|
|
633
|
+
const allProtected = patch.files.length > 0 && patch.files.every(
|
|
634
|
+
(f) => this.fernignorePatterns.some((p) => f === p || minimatch(f, p))
|
|
621
635
|
);
|
|
636
|
+
if (!allProtected) {
|
|
637
|
+
this.warnings.push(
|
|
638
|
+
`Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
622
641
|
} else {
|
|
623
642
|
cleanPatches.push(patch);
|
|
624
643
|
}
|
|
@@ -727,6 +746,7 @@ var LockfileManager = class {
|
|
|
727
746
|
|
|
728
747
|
// src/ReplayDetector.ts
|
|
729
748
|
import { createHash } from "crypto";
|
|
749
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
730
750
|
|
|
731
751
|
// src/shared/binary.ts
|
|
732
752
|
import { extname } from "path";
|
|
@@ -786,6 +806,15 @@ function isBinaryFile(filePath) {
|
|
|
786
806
|
|
|
787
807
|
// src/ReplayDetector.ts
|
|
788
808
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
809
|
+
function matchesFernignorePattern(filePath, patterns) {
|
|
810
|
+
if (patterns.length === 0) return false;
|
|
811
|
+
return patterns.some((p) => {
|
|
812
|
+
if (filePath === p) return true;
|
|
813
|
+
if (minimatch2(filePath, p)) return true;
|
|
814
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
815
|
+
return false;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
789
818
|
function parsePatchFileHeaders(patchContent) {
|
|
790
819
|
const results = [];
|
|
791
820
|
let currentPath = null;
|
|
@@ -826,21 +855,25 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
826
855
|
}
|
|
827
856
|
return snapshot;
|
|
828
857
|
}
|
|
829
|
-
function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
|
|
858
|
+
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
830
859
|
const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
831
860
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
832
|
-
const
|
|
833
|
-
|
|
861
|
+
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
862
|
+
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
863
|
+
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
864
|
+
return { files, sensitiveFiles, fernignoredFiles };
|
|
834
865
|
}
|
|
835
866
|
var ReplayDetector = class {
|
|
836
867
|
git;
|
|
837
868
|
lockManager;
|
|
838
869
|
sdkOutputDir;
|
|
870
|
+
fernignorePatterns;
|
|
839
871
|
warnings = [];
|
|
840
|
-
constructor(git, lockManager, sdkOutputDir) {
|
|
872
|
+
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
841
873
|
this.git = git;
|
|
842
874
|
this.lockManager = lockManager;
|
|
843
875
|
this.sdkOutputDir = sdkOutputDir;
|
|
876
|
+
this.fernignorePatterns = fernignorePatterns;
|
|
844
877
|
}
|
|
845
878
|
/**
|
|
846
879
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
@@ -947,22 +980,30 @@ var ReplayDetector = class {
|
|
|
947
980
|
continue;
|
|
948
981
|
}
|
|
949
982
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
950
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
983
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
984
|
+
filesOutput,
|
|
985
|
+
this.fernignorePatterns
|
|
986
|
+
);
|
|
951
987
|
if (sensitiveFiles.length > 0) {
|
|
952
988
|
this.warnings.push(
|
|
953
989
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
954
990
|
);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
991
|
+
}
|
|
992
|
+
if (fernignoredFiles.length > 0) {
|
|
993
|
+
this.warnings.push(
|
|
994
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
998
|
+
const parentSha = parents[0];
|
|
999
|
+
if (parentSha) {
|
|
1000
|
+
try {
|
|
1001
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
1002
|
+
} catch {
|
|
1003
|
+
continue;
|
|
965
1004
|
}
|
|
1005
|
+
if (!patchContent.trim()) continue;
|
|
1006
|
+
contentHash = this.computeContentHash(patchContent);
|
|
966
1007
|
}
|
|
967
1008
|
}
|
|
968
1009
|
if (files.length === 0) {
|
|
@@ -1161,12 +1202,20 @@ var ReplayDetector = class {
|
|
|
1161
1202
|
resolvedBaseGeneration = fallback;
|
|
1162
1203
|
}
|
|
1163
1204
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
1164
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1205
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1206
|
+
filesOutput,
|
|
1207
|
+
this.fernignorePatterns
|
|
1208
|
+
);
|
|
1165
1209
|
if (sensitiveFiles.length > 0) {
|
|
1166
1210
|
this.warnings.push(
|
|
1167
1211
|
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1168
1212
|
);
|
|
1169
1213
|
}
|
|
1214
|
+
if (fernignoredFiles.length > 0) {
|
|
1215
|
+
this.warnings.push(
|
|
1216
|
+
`.fernignore-protected file(s) excluded from composite patch: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1170
1219
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
1171
1220
|
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
1172
1221
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
@@ -1249,23 +1298,31 @@ var ReplayDetector = class {
|
|
|
1249
1298
|
continue;
|
|
1250
1299
|
}
|
|
1251
1300
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1252
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1301
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1302
|
+
filesOutput,
|
|
1303
|
+
this.fernignorePatterns
|
|
1304
|
+
);
|
|
1253
1305
|
if (sensitiveFiles.length > 0) {
|
|
1254
1306
|
this.warnings.push(
|
|
1255
1307
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1256
1308
|
);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1309
|
+
}
|
|
1310
|
+
if (fernignoredFiles.length > 0) {
|
|
1311
|
+
this.warnings.push(
|
|
1312
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
1316
|
+
if (parents.length === 0) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
1321
|
+
} catch {
|
|
1322
|
+
continue;
|
|
1268
1323
|
}
|
|
1324
|
+
if (!patchContent.trim()) continue;
|
|
1325
|
+
contentHash = this.computeContentHash(patchContent);
|
|
1269
1326
|
}
|
|
1270
1327
|
if (files.length === 0) {
|
|
1271
1328
|
continue;
|
|
@@ -1501,7 +1558,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
1501
1558
|
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1502
1559
|
import { tmpdir } from "os";
|
|
1503
1560
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1504
|
-
import { minimatch } from "minimatch";
|
|
1561
|
+
import { minimatch as minimatch3 } from "minimatch";
|
|
1505
1562
|
|
|
1506
1563
|
// src/accumulator/MergeAccumulator.ts
|
|
1507
1564
|
var MergeAccumulator = class {
|
|
@@ -2217,13 +2274,13 @@ var ReplayApplicator = class {
|
|
|
2217
2274
|
isExcluded(patch) {
|
|
2218
2275
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2219
2276
|
if (!config.exclude) return false;
|
|
2220
|
-
return patch.files.some((file) => config.exclude.some((pattern) =>
|
|
2277
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch3(file, pattern)));
|
|
2221
2278
|
}
|
|
2222
2279
|
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
2223
2280
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2224
2281
|
if (config.moves) {
|
|
2225
2282
|
for (const move of config.moves) {
|
|
2226
|
-
if (
|
|
2283
|
+
if (minimatch3(filePath, move.from) || filePath === move.from) {
|
|
2227
2284
|
if (filePath === move.from) {
|
|
2228
2285
|
return move.to;
|
|
2229
2286
|
}
|
|
@@ -2514,7 +2571,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2514
2571
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2515
2572
|
import { readFile as readFileAsync } from "fs/promises";
|
|
2516
2573
|
import { join as join6 } from "path";
|
|
2517
|
-
import { minimatch as
|
|
2574
|
+
import { minimatch as minimatch5 } from "minimatch";
|
|
2518
2575
|
init_GitClient();
|
|
2519
2576
|
|
|
2520
2577
|
// src/PatchRegionDiff.ts
|
|
@@ -2625,12 +2682,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
|
|
|
2625
2682
|
unlocatableFiles.push(file);
|
|
2626
2683
|
continue;
|
|
2627
2684
|
}
|
|
2628
|
-
const locInCurrentGen =
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2685
|
+
const locInCurrentGen = [];
|
|
2686
|
+
const locInWorkingTree = [];
|
|
2687
|
+
let spanResolutionFailed = false;
|
|
2688
|
+
for (let i = 0; i < locInCurrentGenRaw.length; i++) {
|
|
2689
|
+
const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
|
|
2690
|
+
const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
|
|
2691
|
+
if (cg === null || wt === null) {
|
|
2692
|
+
spanResolutionFailed = true;
|
|
2693
|
+
break;
|
|
2694
|
+
}
|
|
2695
|
+
locInCurrentGen.push(cg);
|
|
2696
|
+
locInWorkingTree.push(wt);
|
|
2697
|
+
}
|
|
2698
|
+
if (spanResolutionFailed) {
|
|
2699
|
+
unlocatableFiles.push(file);
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2634
2702
|
const overlay = overlayRegions(
|
|
2635
2703
|
currentGenLines,
|
|
2636
2704
|
locInCurrentGen,
|
|
@@ -2692,11 +2760,11 @@ function resolveSpan(loc, oursLines, prefer) {
|
|
|
2692
2760
|
if (prefer === "old") {
|
|
2693
2761
|
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2694
2762
|
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2695
|
-
|
|
2696
|
-
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2697
|
-
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2763
|
+
return loc;
|
|
2698
2764
|
}
|
|
2699
|
-
return loc;
|
|
2765
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2766
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2767
|
+
return null;
|
|
2700
2768
|
}
|
|
2701
2769
|
function sequenceMatches(needle, haystack, offset) {
|
|
2702
2770
|
if (offset + needle.length > haystack.length) return false;
|
|
@@ -2810,7 +2878,7 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
2810
2878
|
}
|
|
2811
2879
|
|
|
2812
2880
|
// src/classifier/FileOwnership.ts
|
|
2813
|
-
import { minimatch as
|
|
2881
|
+
import { minimatch as minimatch4 } from "minimatch";
|
|
2814
2882
|
var FileOwnership = class {
|
|
2815
2883
|
constructor(git) {
|
|
2816
2884
|
this.git = git;
|
|
@@ -2834,7 +2902,7 @@ var FileOwnership = class {
|
|
|
2834
2902
|
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
2835
2903
|
if (patch.user_owned) return true;
|
|
2836
2904
|
if (file === ".fernignore") return true;
|
|
2837
|
-
if (fernignorePatterns.some((p) => file === p ||
|
|
2905
|
+
if (fernignorePatterns.some((p) => file === p || minimatch4(file, p))) return true;
|
|
2838
2906
|
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
2839
2907
|
return true;
|
|
2840
2908
|
}
|
|
@@ -2913,6 +2981,7 @@ var FirstGenerationFlow = class {
|
|
|
2913
2981
|
};
|
|
2914
2982
|
}
|
|
2915
2983
|
async apply(_prep) {
|
|
2984
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2916
2985
|
this.service.lockManager.save();
|
|
2917
2986
|
return firstGenerationReport();
|
|
2918
2987
|
}
|
|
@@ -2971,6 +3040,7 @@ var SkipApplicationFlow = class {
|
|
|
2971
3040
|
};
|
|
2972
3041
|
}
|
|
2973
3042
|
async apply(_prep, options) {
|
|
3043
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2974
3044
|
this.service.lockManager.save();
|
|
2975
3045
|
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
2976
3046
|
if (!options?.stageOnly) {
|
|
@@ -3094,6 +3164,7 @@ var NoPatchesFlow = class {
|
|
|
3094
3164
|
this.service.lockManager.addPatch(patch);
|
|
3095
3165
|
}
|
|
3096
3166
|
}
|
|
3167
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3097
3168
|
this.service.lockManager.save();
|
|
3098
3169
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3099
3170
|
if (newPatches.length > 0) {
|
|
@@ -3256,6 +3327,7 @@ var NormalRegenerationFlow = class {
|
|
|
3256
3327
|
}
|
|
3257
3328
|
}
|
|
3258
3329
|
}
|
|
3330
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3259
3331
|
this.service.lockManager.save();
|
|
3260
3332
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3261
3333
|
if (options?.stageOnly) {
|
|
@@ -3336,7 +3408,8 @@ var ReplayService = class {
|
|
|
3336
3408
|
this.git = git;
|
|
3337
3409
|
this.outputDir = outputDir;
|
|
3338
3410
|
this.lockManager = new LockfileManager(outputDir);
|
|
3339
|
-
|
|
3411
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
3412
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
|
|
3340
3413
|
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
3341
3414
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
3342
3415
|
this.fileOwnership = new FileOwnership(git);
|
|
@@ -3356,8 +3429,60 @@ var ReplayService = class {
|
|
|
3356
3429
|
async prepareReplay(options) {
|
|
3357
3430
|
this.warnings = [];
|
|
3358
3431
|
this.detector.warnings.length = 0;
|
|
3432
|
+
this.cleanseLegacyFernignoreEntries();
|
|
3359
3433
|
return this.selectFlow(options).prepare(options);
|
|
3360
3434
|
}
|
|
3435
|
+
/**
|
|
3436
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
3437
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
3438
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
3439
|
+
* end up with no surviving files are removed entirely.
|
|
3440
|
+
*
|
|
3441
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
3442
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
3443
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
3444
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
3445
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
3446
|
+
*/
|
|
3447
|
+
cleanseLegacyFernignoreEntries() {
|
|
3448
|
+
const patterns = this.readFernignorePatterns();
|
|
3449
|
+
if (patterns.length === 0) return;
|
|
3450
|
+
if (!this.lockManager.exists()) return;
|
|
3451
|
+
this.lockManager.read();
|
|
3452
|
+
let lockfileMutated = false;
|
|
3453
|
+
const patches = this.lockManager.getPatches();
|
|
3454
|
+
for (const patch of patches) {
|
|
3455
|
+
try {
|
|
3456
|
+
const result = computeLegacyFernignoreCleanse(
|
|
3457
|
+
patch,
|
|
3458
|
+
patterns,
|
|
3459
|
+
(content) => this.detector.computeContentHash(content)
|
|
3460
|
+
);
|
|
3461
|
+
if (result.unchanged) continue;
|
|
3462
|
+
if (result.remove) {
|
|
3463
|
+
this.lockManager.removePatch(patch.id);
|
|
3464
|
+
this.warnings.push(
|
|
3465
|
+
`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.)`
|
|
3466
|
+
);
|
|
3467
|
+
} else {
|
|
3468
|
+
this.lockManager.updatePatch(patch.id, {
|
|
3469
|
+
files: result.files,
|
|
3470
|
+
patch_content: result.patch_content,
|
|
3471
|
+
content_hash: result.content_hash,
|
|
3472
|
+
...result.theirs_snapshot !== void 0 ? { theirs_snapshot: result.theirs_snapshot } : {}
|
|
3473
|
+
});
|
|
3474
|
+
this.warnings.push(
|
|
3475
|
+
`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.)`
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
lockfileMutated = true;
|
|
3479
|
+
} catch {
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
if (lockfileMutated) {
|
|
3483
|
+
this.lockManager.save();
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3361
3486
|
/**
|
|
3362
3487
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3363
3488
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3489,7 +3614,7 @@ var ReplayService = class {
|
|
|
3489
3614
|
)
|
|
3490
3615
|
);
|
|
3491
3616
|
const hasProtectedFiles = patch.files.some(
|
|
3492
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
3617
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
3493
3618
|
);
|
|
3494
3619
|
const allUserOwned = isUserOwnedPerFile.every(Boolean);
|
|
3495
3620
|
if (allUserOwned && patch.files.length > 0) {
|
|
@@ -3929,7 +4054,7 @@ var ReplayService = class {
|
|
|
3929
4054
|
);
|
|
3930
4055
|
if (snapHasStaleMarkers) continue;
|
|
3931
4056
|
const isProtectedSnap = patch.files.some(
|
|
3932
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
4057
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
3933
4058
|
);
|
|
3934
4059
|
if (isProtectedSnap) continue;
|
|
3935
4060
|
const snapHash = this.detector.computeContentHash(snapshotDiff);
|
|
@@ -3949,6 +4074,11 @@ var ReplayService = class {
|
|
|
3949
4074
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
3950
4075
|
);
|
|
3951
4076
|
if (ppResult === null) continue;
|
|
4077
|
+
if (ppResult.hadFallback) {
|
|
4078
|
+
this.warnings.push(
|
|
4079
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
3952
4082
|
const diff = ppResult.diff;
|
|
3953
4083
|
if (!diff.trim()) {
|
|
3954
4084
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
@@ -3964,7 +4094,7 @@ var ReplayService = class {
|
|
|
3964
4094
|
continue;
|
|
3965
4095
|
}
|
|
3966
4096
|
const isProtected = patch.files.some(
|
|
3967
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
4097
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
3968
4098
|
);
|
|
3969
4099
|
if (isProtected) {
|
|
3970
4100
|
continue;
|
|
@@ -4036,6 +4166,11 @@ var ReplayService = class {
|
|
|
4036
4166
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4037
4167
|
);
|
|
4038
4168
|
if (ppResult === null) continue;
|
|
4169
|
+
if (ppResult.hadFallback) {
|
|
4170
|
+
this.warnings.push(
|
|
4171
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4039
4174
|
const diff = ppResult.diff;
|
|
4040
4175
|
if (!diff.trim()) {
|
|
4041
4176
|
if (await allPatchFilesUserOwned(patch)) {
|
|
@@ -4123,13 +4258,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4123
4258
|
if (files.length === 0) return;
|
|
4124
4259
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
4125
4260
|
for (const file of files) {
|
|
4126
|
-
if (fernignorePatterns.some((pattern) =>
|
|
4261
|
+
if (fernignorePatterns.some((pattern) => minimatch5(file, pattern))) continue;
|
|
4127
4262
|
try {
|
|
4128
4263
|
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
4129
4264
|
} catch {
|
|
4130
4265
|
}
|
|
4131
4266
|
}
|
|
4132
4267
|
}
|
|
4268
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4133
4269
|
readFernignorePatterns() {
|
|
4134
4270
|
const fernignorePath = join6(this.outputDir, ".fernignore");
|
|
4135
4271
|
if (!existsSync2(fernignorePath)) return [];
|
|
@@ -4207,12 +4343,99 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
4207
4343
|
}
|
|
4208
4344
|
return { applied, unresolved, absorbed };
|
|
4209
4345
|
}
|
|
4346
|
+
function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeContentHash2) {
|
|
4347
|
+
const matchesFernignore = (filePath) => fernignorePatterns.some((p) => {
|
|
4348
|
+
if (filePath === p) return true;
|
|
4349
|
+
if (minimatch5(filePath, p)) return true;
|
|
4350
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
4351
|
+
return false;
|
|
4352
|
+
});
|
|
4353
|
+
const strippedFromFiles = patch.files.filter(matchesFernignore);
|
|
4354
|
+
const survivingFiles = patch.files.filter((f) => !matchesFernignore(f));
|
|
4355
|
+
let survivingSnapshot = void 0;
|
|
4356
|
+
let snapshotChanged = false;
|
|
4357
|
+
if (patch.theirs_snapshot) {
|
|
4358
|
+
survivingSnapshot = {};
|
|
4359
|
+
for (const [key, value] of Object.entries(patch.theirs_snapshot)) {
|
|
4360
|
+
if (matchesFernignore(key)) {
|
|
4361
|
+
snapshotChanged = true;
|
|
4362
|
+
} else {
|
|
4363
|
+
survivingSnapshot[key] = value;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch.patch_content, matchesFernignore);
|
|
4368
|
+
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
4369
|
+
if (unchanged) {
|
|
4370
|
+
return {
|
|
4371
|
+
unchanged: true,
|
|
4372
|
+
remove: false,
|
|
4373
|
+
files: patch.files,
|
|
4374
|
+
patch_content: patch.patch_content,
|
|
4375
|
+
content_hash: patch.content_hash,
|
|
4376
|
+
theirs_snapshot: patch.theirs_snapshot,
|
|
4377
|
+
strippedPaths: []
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
const strippedPaths = Array.from(/* @__PURE__ */ new Set([...strippedFromFiles, ...strippedSectionPaths]));
|
|
4381
|
+
if (survivingFiles.length === 0 || newPatchContent.trim() === "") {
|
|
4382
|
+
return {
|
|
4383
|
+
unchanged: false,
|
|
4384
|
+
remove: true,
|
|
4385
|
+
files: [],
|
|
4386
|
+
patch_content: "",
|
|
4387
|
+
content_hash: "",
|
|
4388
|
+
strippedPaths
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
const newContentHash = patchContentStripped ? computeContentHash2(newPatchContent) : patch.content_hash;
|
|
4392
|
+
return {
|
|
4393
|
+
unchanged: false,
|
|
4394
|
+
remove: false,
|
|
4395
|
+
files: survivingFiles,
|
|
4396
|
+
patch_content: newPatchContent,
|
|
4397
|
+
content_hash: newContentHash,
|
|
4398
|
+
...survivingSnapshot !== void 0 ? { theirs_snapshot: survivingSnapshot } : {},
|
|
4399
|
+
strippedPaths
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
4403
|
+
const lines = patchContent.split("\n");
|
|
4404
|
+
const sectionStarts = [];
|
|
4405
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4406
|
+
const line = lines[i];
|
|
4407
|
+
if (line.startsWith("diff --git ")) {
|
|
4408
|
+
const match = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
4409
|
+
const path = match ? match[2] : null;
|
|
4410
|
+
sectionStarts.push({ idx: i, path });
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
if (sectionStarts.length === 0) {
|
|
4414
|
+
return { stripped: false, content: patchContent, strippedSectionPaths: [] };
|
|
4415
|
+
}
|
|
4416
|
+
const preamble = lines.slice(0, sectionStarts[0].idx);
|
|
4417
|
+
const newLines = [...preamble];
|
|
4418
|
+
const strippedSectionPaths = [];
|
|
4419
|
+
let stripped = false;
|
|
4420
|
+
for (let i = 0; i < sectionStarts.length; i++) {
|
|
4421
|
+
const section = sectionStarts[i];
|
|
4422
|
+
const next = sectionStarts[i + 1];
|
|
4423
|
+
const endIdx = next ? next.idx : lines.length;
|
|
4424
|
+
if (section.path !== null && matchesFernignore(section.path)) {
|
|
4425
|
+
stripped = true;
|
|
4426
|
+
strippedSectionPaths.push(section.path);
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
newLines.push(...lines.slice(section.idx, endIdx));
|
|
4430
|
+
}
|
|
4431
|
+
return { stripped, content: newLines.join("\n"), strippedSectionPaths };
|
|
4432
|
+
}
|
|
4210
4433
|
|
|
4211
4434
|
// src/FernignoreMigrator.ts
|
|
4212
4435
|
import { createHash as createHash2 } from "crypto";
|
|
4213
4436
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4214
4437
|
import { dirname as dirname5, join as join7 } from "path";
|
|
4215
|
-
import { minimatch as
|
|
4438
|
+
import { minimatch as minimatch6 } from "minimatch";
|
|
4216
4439
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
4217
4440
|
var FernignoreMigrator = class {
|
|
4218
4441
|
git;
|
|
@@ -4244,7 +4467,7 @@ var FernignoreMigrator = class {
|
|
|
4244
4467
|
const commitsOnly = [];
|
|
4245
4468
|
const syntheticPatches = [];
|
|
4246
4469
|
for (const pattern of patterns) {
|
|
4247
|
-
const matchingPatch = patches.find((p) => p.files.some((f) =>
|
|
4470
|
+
const matchingPatch = patches.find((p) => p.files.some((f) => minimatch6(f, pattern) || f === pattern));
|
|
4248
4471
|
if (matchingPatch) {
|
|
4249
4472
|
trackedByBoth.push({
|
|
4250
4473
|
file: pattern,
|
|
@@ -4317,7 +4540,7 @@ var FernignoreMigrator = class {
|
|
|
4317
4540
|
fernignoreOnly.push(...remainingFernignoreOnly);
|
|
4318
4541
|
}
|
|
4319
4542
|
for (const patch of patches) {
|
|
4320
|
-
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) =>
|
|
4543
|
+
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch6(f, p) || f === p));
|
|
4321
4544
|
if (hasUnprotectedFiles) {
|
|
4322
4545
|
commitsOnly.push(patch);
|
|
4323
4546
|
}
|
|
@@ -4329,7 +4552,7 @@ var FernignoreMigrator = class {
|
|
|
4329
4552
|
const results = [];
|
|
4330
4553
|
for (const pattern of patterns) {
|
|
4331
4554
|
const matching = allFiles.filter(
|
|
4332
|
-
(f) =>
|
|
4555
|
+
(f) => minimatch6(f, pattern) || f === pattern || f.startsWith(pattern + "/")
|
|
4333
4556
|
);
|
|
4334
4557
|
results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
|
|
4335
4558
|
}
|
|
@@ -4642,7 +4865,7 @@ function computeContentHash(patchContent) {
|
|
|
4642
4865
|
}
|
|
4643
4866
|
|
|
4644
4867
|
// src/commands/forget.ts
|
|
4645
|
-
import { minimatch as
|
|
4868
|
+
import { minimatch as minimatch7 } from "minimatch";
|
|
4646
4869
|
function parseDiffStat(patchContent) {
|
|
4647
4870
|
let additions = 0;
|
|
4648
4871
|
let deletions = 0;
|
|
@@ -4672,7 +4895,7 @@ function toMatchedPatch(patch) {
|
|
|
4672
4895
|
}
|
|
4673
4896
|
function matchesPatch(patch, pattern) {
|
|
4674
4897
|
const fileMatch = patch.files.some(
|
|
4675
|
-
(file) => file === pattern ||
|
|
4898
|
+
(file) => file === pattern || minimatch7(file, pattern)
|
|
4676
4899
|
);
|
|
4677
4900
|
if (fileMatch) return true;
|
|
4678
4901
|
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|