@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.cjs
CHANGED
|
@@ -588,6 +588,7 @@ function parseRevertedMessage(subject) {
|
|
|
588
588
|
// src/LockfileManager.ts
|
|
589
589
|
var import_node_fs = require("fs");
|
|
590
590
|
var import_node_path = require("path");
|
|
591
|
+
var import_minimatch = require("minimatch");
|
|
591
592
|
var import_yaml = require("yaml");
|
|
592
593
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
593
594
|
var LockfileNotFoundError = class extends Error {
|
|
@@ -599,10 +600,23 @@ var LockfileNotFoundError = class extends Error {
|
|
|
599
600
|
var LockfileManager = class {
|
|
600
601
|
outputDir;
|
|
601
602
|
lock = null;
|
|
603
|
+
fernignorePatterns = [];
|
|
602
604
|
warnings = [];
|
|
603
605
|
constructor(outputDir) {
|
|
604
606
|
this.outputDir = outputDir;
|
|
605
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Inform the lockfile of the customer's `.fernignore` patterns so that
|
|
610
|
+
* `save()` can suppress its "Patch X removed from lockfile" warning when
|
|
611
|
+
* every file in the stripped patch is already `.fernignore`-protected.
|
|
612
|
+
* The strip itself still happens (credentials never persist); only the
|
|
613
|
+
* customer-visible warning is suppressed in that case, because the
|
|
614
|
+
* protected files survive the generator wipe regardless of whether the
|
|
615
|
+
* lockfile tracks them, so there's nothing for the customer to action.
|
|
616
|
+
*/
|
|
617
|
+
setFernignorePatterns(patterns) {
|
|
618
|
+
this.fernignorePatterns = patterns;
|
|
619
|
+
}
|
|
606
620
|
get lockfilePath() {
|
|
607
621
|
return (0, import_node_path.join)(this.outputDir, ".fern", "replay.lock");
|
|
608
622
|
}
|
|
@@ -671,9 +685,14 @@ var LockfileManager = class {
|
|
|
671
685
|
if (sensitiveFiles.length > 0) {
|
|
672
686
|
reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
|
|
673
687
|
}
|
|
674
|
-
|
|
675
|
-
|
|
688
|
+
const allProtected = patch.files.length > 0 && patch.files.every(
|
|
689
|
+
(f) => this.fernignorePatterns.some((p) => f === p || (0, import_minimatch.minimatch)(f, p))
|
|
676
690
|
);
|
|
691
|
+
if (!allProtected) {
|
|
692
|
+
this.warnings.push(
|
|
693
|
+
`Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
677
696
|
} else {
|
|
678
697
|
cleanPatches.push(patch);
|
|
679
698
|
}
|
|
@@ -782,6 +801,7 @@ var LockfileManager = class {
|
|
|
782
801
|
|
|
783
802
|
// src/ReplayDetector.ts
|
|
784
803
|
var import_node_crypto = require("crypto");
|
|
804
|
+
var import_minimatch2 = require("minimatch");
|
|
785
805
|
|
|
786
806
|
// src/shared/binary.ts
|
|
787
807
|
var import_node_path2 = require("path");
|
|
@@ -841,6 +861,15 @@ function isBinaryFile(filePath) {
|
|
|
841
861
|
|
|
842
862
|
// src/ReplayDetector.ts
|
|
843
863
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
864
|
+
function matchesFernignorePattern(filePath, patterns) {
|
|
865
|
+
if (patterns.length === 0) return false;
|
|
866
|
+
return patterns.some((p) => {
|
|
867
|
+
if (filePath === p) return true;
|
|
868
|
+
if ((0, import_minimatch2.minimatch)(filePath, p)) return true;
|
|
869
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
870
|
+
return false;
|
|
871
|
+
});
|
|
872
|
+
}
|
|
844
873
|
function parsePatchFileHeaders(patchContent) {
|
|
845
874
|
const results = [];
|
|
846
875
|
let currentPath = null;
|
|
@@ -881,21 +910,25 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
881
910
|
}
|
|
882
911
|
return snapshot;
|
|
883
912
|
}
|
|
884
|
-
function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
|
|
913
|
+
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
885
914
|
const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
886
915
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
887
|
-
const
|
|
888
|
-
|
|
916
|
+
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
917
|
+
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
918
|
+
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
919
|
+
return { files, sensitiveFiles, fernignoredFiles };
|
|
889
920
|
}
|
|
890
921
|
var ReplayDetector = class {
|
|
891
922
|
git;
|
|
892
923
|
lockManager;
|
|
893
924
|
sdkOutputDir;
|
|
925
|
+
fernignorePatterns;
|
|
894
926
|
warnings = [];
|
|
895
|
-
constructor(git, lockManager, sdkOutputDir) {
|
|
927
|
+
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
896
928
|
this.git = git;
|
|
897
929
|
this.lockManager = lockManager;
|
|
898
930
|
this.sdkOutputDir = sdkOutputDir;
|
|
931
|
+
this.fernignorePatterns = fernignorePatterns;
|
|
899
932
|
}
|
|
900
933
|
/**
|
|
901
934
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
@@ -1002,22 +1035,30 @@ var ReplayDetector = class {
|
|
|
1002
1035
|
continue;
|
|
1003
1036
|
}
|
|
1004
1037
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1005
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1038
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1039
|
+
filesOutput,
|
|
1040
|
+
this.fernignorePatterns
|
|
1041
|
+
);
|
|
1006
1042
|
if (sensitiveFiles.length > 0) {
|
|
1007
1043
|
this.warnings.push(
|
|
1008
1044
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1009
1045
|
);
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1046
|
+
}
|
|
1047
|
+
if (fernignoredFiles.length > 0) {
|
|
1048
|
+
this.warnings.push(
|
|
1049
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
1053
|
+
const parentSha = parents[0];
|
|
1054
|
+
if (parentSha) {
|
|
1055
|
+
try {
|
|
1056
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
1057
|
+
} catch {
|
|
1058
|
+
continue;
|
|
1020
1059
|
}
|
|
1060
|
+
if (!patchContent.trim()) continue;
|
|
1061
|
+
contentHash = this.computeContentHash(patchContent);
|
|
1021
1062
|
}
|
|
1022
1063
|
}
|
|
1023
1064
|
if (files.length === 0) {
|
|
@@ -1216,12 +1257,20 @@ var ReplayDetector = class {
|
|
|
1216
1257
|
resolvedBaseGeneration = fallback;
|
|
1217
1258
|
}
|
|
1218
1259
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
1219
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1260
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1261
|
+
filesOutput,
|
|
1262
|
+
this.fernignorePatterns
|
|
1263
|
+
);
|
|
1220
1264
|
if (sensitiveFiles.length > 0) {
|
|
1221
1265
|
this.warnings.push(
|
|
1222
1266
|
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1223
1267
|
);
|
|
1224
1268
|
}
|
|
1269
|
+
if (fernignoredFiles.length > 0) {
|
|
1270
|
+
this.warnings.push(
|
|
1271
|
+
`.fernignore-protected file(s) excluded from composite patch: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1225
1274
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
1226
1275
|
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
1227
1276
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
@@ -1304,23 +1353,31 @@ var ReplayDetector = class {
|
|
|
1304
1353
|
continue;
|
|
1305
1354
|
}
|
|
1306
1355
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1307
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1356
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1357
|
+
filesOutput,
|
|
1358
|
+
this.fernignorePatterns
|
|
1359
|
+
);
|
|
1308
1360
|
if (sensitiveFiles.length > 0) {
|
|
1309
1361
|
this.warnings.push(
|
|
1310
1362
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1311
1363
|
);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1364
|
+
}
|
|
1365
|
+
if (fernignoredFiles.length > 0) {
|
|
1366
|
+
this.warnings.push(
|
|
1367
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
1371
|
+
if (parents.length === 0) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
1376
|
+
} catch {
|
|
1377
|
+
continue;
|
|
1323
1378
|
}
|
|
1379
|
+
if (!patchContent.trim()) continue;
|
|
1380
|
+
contentHash = this.computeContentHash(patchContent);
|
|
1324
1381
|
}
|
|
1325
1382
|
if (files.length === 0) {
|
|
1326
1383
|
continue;
|
|
@@ -1556,7 +1613,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
1556
1613
|
var import_promises3 = require("fs/promises");
|
|
1557
1614
|
var import_node_os = require("os");
|
|
1558
1615
|
var import_node_path5 = require("path");
|
|
1559
|
-
var
|
|
1616
|
+
var import_minimatch3 = require("minimatch");
|
|
1560
1617
|
|
|
1561
1618
|
// src/accumulator/MergeAccumulator.ts
|
|
1562
1619
|
var MergeAccumulator = class {
|
|
@@ -2272,13 +2329,13 @@ var ReplayApplicator = class {
|
|
|
2272
2329
|
isExcluded(patch) {
|
|
2273
2330
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2274
2331
|
if (!config.exclude) return false;
|
|
2275
|
-
return patch.files.some((file) => config.exclude.some((pattern) => (0,
|
|
2332
|
+
return patch.files.some((file) => config.exclude.some((pattern) => (0, import_minimatch3.minimatch)(file, pattern)));
|
|
2276
2333
|
}
|
|
2277
2334
|
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
2278
2335
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2279
2336
|
if (config.moves) {
|
|
2280
2337
|
for (const move of config.moves) {
|
|
2281
|
-
if ((0,
|
|
2338
|
+
if ((0, import_minimatch3.minimatch)(filePath, move.from) || filePath === move.from) {
|
|
2282
2339
|
if (filePath === move.from) {
|
|
2283
2340
|
return move.to;
|
|
2284
2341
|
}
|
|
@@ -2569,7 +2626,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2569
2626
|
var import_node_fs2 = require("fs");
|
|
2570
2627
|
var import_promises6 = require("fs/promises");
|
|
2571
2628
|
var import_node_path8 = require("path");
|
|
2572
|
-
var
|
|
2629
|
+
var import_minimatch5 = require("minimatch");
|
|
2573
2630
|
init_GitClient();
|
|
2574
2631
|
|
|
2575
2632
|
// src/PatchRegionDiff.ts
|
|
@@ -2680,12 +2737,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
|
|
|
2680
2737
|
unlocatableFiles.push(file);
|
|
2681
2738
|
continue;
|
|
2682
2739
|
}
|
|
2683
|
-
const locInCurrentGen =
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2740
|
+
const locInCurrentGen = [];
|
|
2741
|
+
const locInWorkingTree = [];
|
|
2742
|
+
let spanResolutionFailed = false;
|
|
2743
|
+
for (let i = 0; i < locInCurrentGenRaw.length; i++) {
|
|
2744
|
+
const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
|
|
2745
|
+
const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
|
|
2746
|
+
if (cg === null || wt === null) {
|
|
2747
|
+
spanResolutionFailed = true;
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
locInCurrentGen.push(cg);
|
|
2751
|
+
locInWorkingTree.push(wt);
|
|
2752
|
+
}
|
|
2753
|
+
if (spanResolutionFailed) {
|
|
2754
|
+
unlocatableFiles.push(file);
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2689
2757
|
const overlay = overlayRegions(
|
|
2690
2758
|
currentGenLines,
|
|
2691
2759
|
locInCurrentGen,
|
|
@@ -2747,11 +2815,11 @@ function resolveSpan(loc, oursLines, prefer) {
|
|
|
2747
2815
|
if (prefer === "old") {
|
|
2748
2816
|
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2749
2817
|
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2750
|
-
|
|
2751
|
-
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2752
|
-
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2818
|
+
return loc;
|
|
2753
2819
|
}
|
|
2754
|
-
return loc;
|
|
2820
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2821
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2822
|
+
return null;
|
|
2755
2823
|
}
|
|
2756
2824
|
function sequenceMatches(needle, haystack, offset) {
|
|
2757
2825
|
if (offset + needle.length > haystack.length) return false;
|
|
@@ -2865,7 +2933,7 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
2865
2933
|
}
|
|
2866
2934
|
|
|
2867
2935
|
// src/classifier/FileOwnership.ts
|
|
2868
|
-
var
|
|
2936
|
+
var import_minimatch4 = require("minimatch");
|
|
2869
2937
|
var FileOwnership = class {
|
|
2870
2938
|
constructor(git) {
|
|
2871
2939
|
this.git = git;
|
|
@@ -2889,7 +2957,7 @@ var FileOwnership = class {
|
|
|
2889
2957
|
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
2890
2958
|
if (patch.user_owned) return true;
|
|
2891
2959
|
if (file === ".fernignore") return true;
|
|
2892
|
-
if (fernignorePatterns.some((p) => file === p || (0,
|
|
2960
|
+
if (fernignorePatterns.some((p) => file === p || (0, import_minimatch4.minimatch)(file, p))) return true;
|
|
2893
2961
|
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
2894
2962
|
return true;
|
|
2895
2963
|
}
|
|
@@ -2968,6 +3036,7 @@ var FirstGenerationFlow = class {
|
|
|
2968
3036
|
};
|
|
2969
3037
|
}
|
|
2970
3038
|
async apply(_prep) {
|
|
3039
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2971
3040
|
this.service.lockManager.save();
|
|
2972
3041
|
return firstGenerationReport();
|
|
2973
3042
|
}
|
|
@@ -3026,6 +3095,7 @@ var SkipApplicationFlow = class {
|
|
|
3026
3095
|
};
|
|
3027
3096
|
}
|
|
3028
3097
|
async apply(_prep, options) {
|
|
3098
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3029
3099
|
this.service.lockManager.save();
|
|
3030
3100
|
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
3031
3101
|
if (!options?.stageOnly) {
|
|
@@ -3149,6 +3219,7 @@ var NoPatchesFlow = class {
|
|
|
3149
3219
|
this.service.lockManager.addPatch(patch);
|
|
3150
3220
|
}
|
|
3151
3221
|
}
|
|
3222
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3152
3223
|
this.service.lockManager.save();
|
|
3153
3224
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3154
3225
|
if (newPatches.length > 0) {
|
|
@@ -3311,6 +3382,7 @@ var NormalRegenerationFlow = class {
|
|
|
3311
3382
|
}
|
|
3312
3383
|
}
|
|
3313
3384
|
}
|
|
3385
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3314
3386
|
this.service.lockManager.save();
|
|
3315
3387
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3316
3388
|
if (options?.stageOnly) {
|
|
@@ -3391,7 +3463,8 @@ var ReplayService = class {
|
|
|
3391
3463
|
this.git = git;
|
|
3392
3464
|
this.outputDir = outputDir;
|
|
3393
3465
|
this.lockManager = new LockfileManager(outputDir);
|
|
3394
|
-
|
|
3466
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
3467
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
|
|
3395
3468
|
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
3396
3469
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
3397
3470
|
this.fileOwnership = new FileOwnership(git);
|
|
@@ -3411,8 +3484,60 @@ var ReplayService = class {
|
|
|
3411
3484
|
async prepareReplay(options) {
|
|
3412
3485
|
this.warnings = [];
|
|
3413
3486
|
this.detector.warnings.length = 0;
|
|
3487
|
+
this.cleanseLegacyFernignoreEntries();
|
|
3414
3488
|
return this.selectFlow(options).prepare(options);
|
|
3415
3489
|
}
|
|
3490
|
+
/**
|
|
3491
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
3492
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
3493
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
3494
|
+
* end up with no surviving files are removed entirely.
|
|
3495
|
+
*
|
|
3496
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
3497
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
3498
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
3499
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
3500
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
3501
|
+
*/
|
|
3502
|
+
cleanseLegacyFernignoreEntries() {
|
|
3503
|
+
const patterns = this.readFernignorePatterns();
|
|
3504
|
+
if (patterns.length === 0) return;
|
|
3505
|
+
if (!this.lockManager.exists()) return;
|
|
3506
|
+
this.lockManager.read();
|
|
3507
|
+
let lockfileMutated = false;
|
|
3508
|
+
const patches = this.lockManager.getPatches();
|
|
3509
|
+
for (const patch of patches) {
|
|
3510
|
+
try {
|
|
3511
|
+
const result = computeLegacyFernignoreCleanse(
|
|
3512
|
+
patch,
|
|
3513
|
+
patterns,
|
|
3514
|
+
(content) => this.detector.computeContentHash(content)
|
|
3515
|
+
);
|
|
3516
|
+
if (result.unchanged) continue;
|
|
3517
|
+
if (result.remove) {
|
|
3518
|
+
this.lockManager.removePatch(patch.id);
|
|
3519
|
+
this.warnings.push(
|
|
3520
|
+
`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.)`
|
|
3521
|
+
);
|
|
3522
|
+
} else {
|
|
3523
|
+
this.lockManager.updatePatch(patch.id, {
|
|
3524
|
+
files: result.files,
|
|
3525
|
+
patch_content: result.patch_content,
|
|
3526
|
+
content_hash: result.content_hash,
|
|
3527
|
+
...result.theirs_snapshot !== void 0 ? { theirs_snapshot: result.theirs_snapshot } : {}
|
|
3528
|
+
});
|
|
3529
|
+
this.warnings.push(
|
|
3530
|
+
`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.)`
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
lockfileMutated = true;
|
|
3534
|
+
} catch {
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
if (lockfileMutated) {
|
|
3538
|
+
this.lockManager.save();
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3416
3541
|
/**
|
|
3417
3542
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3418
3543
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3544,7 +3669,7 @@ var ReplayService = class {
|
|
|
3544
3669
|
)
|
|
3545
3670
|
);
|
|
3546
3671
|
const hasProtectedFiles = patch.files.some(
|
|
3547
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
3672
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch5.minimatch)(file, p))
|
|
3548
3673
|
);
|
|
3549
3674
|
const allUserOwned = isUserOwnedPerFile.every(Boolean);
|
|
3550
3675
|
if (allUserOwned && patch.files.length > 0) {
|
|
@@ -3984,7 +4109,7 @@ var ReplayService = class {
|
|
|
3984
4109
|
);
|
|
3985
4110
|
if (snapHasStaleMarkers) continue;
|
|
3986
4111
|
const isProtectedSnap = patch.files.some(
|
|
3987
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
4112
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch5.minimatch)(file, p))
|
|
3988
4113
|
);
|
|
3989
4114
|
if (isProtectedSnap) continue;
|
|
3990
4115
|
const snapHash = this.detector.computeContentHash(snapshotDiff);
|
|
@@ -4004,6 +4129,11 @@ var ReplayService = class {
|
|
|
4004
4129
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4005
4130
|
);
|
|
4006
4131
|
if (ppResult === null) continue;
|
|
4132
|
+
if (ppResult.hadFallback) {
|
|
4133
|
+
this.warnings.push(
|
|
4134
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4007
4137
|
const diff = ppResult.diff;
|
|
4008
4138
|
if (!diff.trim()) {
|
|
4009
4139
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
@@ -4019,7 +4149,7 @@ var ReplayService = class {
|
|
|
4019
4149
|
continue;
|
|
4020
4150
|
}
|
|
4021
4151
|
const isProtected = patch.files.some(
|
|
4022
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
4152
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch5.minimatch)(file, p))
|
|
4023
4153
|
);
|
|
4024
4154
|
if (isProtected) {
|
|
4025
4155
|
continue;
|
|
@@ -4091,6 +4221,11 @@ var ReplayService = class {
|
|
|
4091
4221
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4092
4222
|
);
|
|
4093
4223
|
if (ppResult === null) continue;
|
|
4224
|
+
if (ppResult.hadFallback) {
|
|
4225
|
+
this.warnings.push(
|
|
4226
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
4094
4229
|
const diff = ppResult.diff;
|
|
4095
4230
|
if (!diff.trim()) {
|
|
4096
4231
|
if (await allPatchFilesUserOwned(patch)) {
|
|
@@ -4178,13 +4313,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4178
4313
|
if (files.length === 0) return;
|
|
4179
4314
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
4180
4315
|
for (const file of files) {
|
|
4181
|
-
if (fernignorePatterns.some((pattern) => (0,
|
|
4316
|
+
if (fernignorePatterns.some((pattern) => (0, import_minimatch5.minimatch)(file, pattern))) continue;
|
|
4182
4317
|
try {
|
|
4183
4318
|
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
4184
4319
|
} catch {
|
|
4185
4320
|
}
|
|
4186
4321
|
}
|
|
4187
4322
|
}
|
|
4323
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4188
4324
|
readFernignorePatterns() {
|
|
4189
4325
|
const fernignorePath = (0, import_node_path8.join)(this.outputDir, ".fernignore");
|
|
4190
4326
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -4262,12 +4398,99 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
4262
4398
|
}
|
|
4263
4399
|
return { applied, unresolved, absorbed };
|
|
4264
4400
|
}
|
|
4401
|
+
function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeContentHash2) {
|
|
4402
|
+
const matchesFernignore = (filePath) => fernignorePatterns.some((p) => {
|
|
4403
|
+
if (filePath === p) return true;
|
|
4404
|
+
if ((0, import_minimatch5.minimatch)(filePath, p)) return true;
|
|
4405
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
4406
|
+
return false;
|
|
4407
|
+
});
|
|
4408
|
+
const strippedFromFiles = patch.files.filter(matchesFernignore);
|
|
4409
|
+
const survivingFiles = patch.files.filter((f) => !matchesFernignore(f));
|
|
4410
|
+
let survivingSnapshot = void 0;
|
|
4411
|
+
let snapshotChanged = false;
|
|
4412
|
+
if (patch.theirs_snapshot) {
|
|
4413
|
+
survivingSnapshot = {};
|
|
4414
|
+
for (const [key, value] of Object.entries(patch.theirs_snapshot)) {
|
|
4415
|
+
if (matchesFernignore(key)) {
|
|
4416
|
+
snapshotChanged = true;
|
|
4417
|
+
} else {
|
|
4418
|
+
survivingSnapshot[key] = value;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch.patch_content, matchesFernignore);
|
|
4423
|
+
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
4424
|
+
if (unchanged) {
|
|
4425
|
+
return {
|
|
4426
|
+
unchanged: true,
|
|
4427
|
+
remove: false,
|
|
4428
|
+
files: patch.files,
|
|
4429
|
+
patch_content: patch.patch_content,
|
|
4430
|
+
content_hash: patch.content_hash,
|
|
4431
|
+
theirs_snapshot: patch.theirs_snapshot,
|
|
4432
|
+
strippedPaths: []
|
|
4433
|
+
};
|
|
4434
|
+
}
|
|
4435
|
+
const strippedPaths = Array.from(/* @__PURE__ */ new Set([...strippedFromFiles, ...strippedSectionPaths]));
|
|
4436
|
+
if (survivingFiles.length === 0 || newPatchContent.trim() === "") {
|
|
4437
|
+
return {
|
|
4438
|
+
unchanged: false,
|
|
4439
|
+
remove: true,
|
|
4440
|
+
files: [],
|
|
4441
|
+
patch_content: "",
|
|
4442
|
+
content_hash: "",
|
|
4443
|
+
strippedPaths
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4446
|
+
const newContentHash = patchContentStripped ? computeContentHash2(newPatchContent) : patch.content_hash;
|
|
4447
|
+
return {
|
|
4448
|
+
unchanged: false,
|
|
4449
|
+
remove: false,
|
|
4450
|
+
files: survivingFiles,
|
|
4451
|
+
patch_content: newPatchContent,
|
|
4452
|
+
content_hash: newContentHash,
|
|
4453
|
+
...survivingSnapshot !== void 0 ? { theirs_snapshot: survivingSnapshot } : {},
|
|
4454
|
+
strippedPaths
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
4458
|
+
const lines = patchContent.split("\n");
|
|
4459
|
+
const sectionStarts = [];
|
|
4460
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4461
|
+
const line = lines[i];
|
|
4462
|
+
if (line.startsWith("diff --git ")) {
|
|
4463
|
+
const match = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
4464
|
+
const path = match ? match[2] : null;
|
|
4465
|
+
sectionStarts.push({ idx: i, path });
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
if (sectionStarts.length === 0) {
|
|
4469
|
+
return { stripped: false, content: patchContent, strippedSectionPaths: [] };
|
|
4470
|
+
}
|
|
4471
|
+
const preamble = lines.slice(0, sectionStarts[0].idx);
|
|
4472
|
+
const newLines = [...preamble];
|
|
4473
|
+
const strippedSectionPaths = [];
|
|
4474
|
+
let stripped = false;
|
|
4475
|
+
for (let i = 0; i < sectionStarts.length; i++) {
|
|
4476
|
+
const section = sectionStarts[i];
|
|
4477
|
+
const next = sectionStarts[i + 1];
|
|
4478
|
+
const endIdx = next ? next.idx : lines.length;
|
|
4479
|
+
if (section.path !== null && matchesFernignore(section.path)) {
|
|
4480
|
+
stripped = true;
|
|
4481
|
+
strippedSectionPaths.push(section.path);
|
|
4482
|
+
continue;
|
|
4483
|
+
}
|
|
4484
|
+
newLines.push(...lines.slice(section.idx, endIdx));
|
|
4485
|
+
}
|
|
4486
|
+
return { stripped, content: newLines.join("\n"), strippedSectionPaths };
|
|
4487
|
+
}
|
|
4265
4488
|
|
|
4266
4489
|
// src/FernignoreMigrator.ts
|
|
4267
4490
|
var import_node_crypto2 = require("crypto");
|
|
4268
4491
|
var import_node_fs3 = require("fs");
|
|
4269
4492
|
var import_node_path9 = require("path");
|
|
4270
|
-
var
|
|
4493
|
+
var import_minimatch6 = require("minimatch");
|
|
4271
4494
|
var import_yaml2 = require("yaml");
|
|
4272
4495
|
var FernignoreMigrator = class {
|
|
4273
4496
|
git;
|
|
@@ -4299,7 +4522,7 @@ var FernignoreMigrator = class {
|
|
|
4299
4522
|
const commitsOnly = [];
|
|
4300
4523
|
const syntheticPatches = [];
|
|
4301
4524
|
for (const pattern of patterns) {
|
|
4302
|
-
const matchingPatch = patches.find((p) => p.files.some((f) => (0,
|
|
4525
|
+
const matchingPatch = patches.find((p) => p.files.some((f) => (0, import_minimatch6.minimatch)(f, pattern) || f === pattern));
|
|
4303
4526
|
if (matchingPatch) {
|
|
4304
4527
|
trackedByBoth.push({
|
|
4305
4528
|
file: pattern,
|
|
@@ -4372,7 +4595,7 @@ var FernignoreMigrator = class {
|
|
|
4372
4595
|
fernignoreOnly.push(...remainingFernignoreOnly);
|
|
4373
4596
|
}
|
|
4374
4597
|
for (const patch of patches) {
|
|
4375
|
-
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => (0,
|
|
4598
|
+
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => (0, import_minimatch6.minimatch)(f, p) || f === p));
|
|
4376
4599
|
if (hasUnprotectedFiles) {
|
|
4377
4600
|
commitsOnly.push(patch);
|
|
4378
4601
|
}
|
|
@@ -4384,7 +4607,7 @@ var FernignoreMigrator = class {
|
|
|
4384
4607
|
const results = [];
|
|
4385
4608
|
for (const pattern of patterns) {
|
|
4386
4609
|
const matching = allFiles.filter(
|
|
4387
|
-
(f) => (0,
|
|
4610
|
+
(f) => (0, import_minimatch6.minimatch)(f, pattern) || f === pattern || f.startsWith(pattern + "/")
|
|
4388
4611
|
);
|
|
4389
4612
|
results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
|
|
4390
4613
|
}
|
|
@@ -4697,7 +4920,7 @@ function computeContentHash(patchContent) {
|
|
|
4697
4920
|
}
|
|
4698
4921
|
|
|
4699
4922
|
// src/commands/forget.ts
|
|
4700
|
-
var
|
|
4923
|
+
var import_minimatch7 = require("minimatch");
|
|
4701
4924
|
function parseDiffStat(patchContent) {
|
|
4702
4925
|
let additions = 0;
|
|
4703
4926
|
let deletions = 0;
|
|
@@ -4727,7 +4950,7 @@ function toMatchedPatch(patch) {
|
|
|
4727
4950
|
}
|
|
4728
4951
|
function matchesPatch(patch, pattern) {
|
|
4729
4952
|
const fileMatch = patch.files.some(
|
|
4730
|
-
(file) => file === pattern || (0,
|
|
4953
|
+
(file) => file === pattern || (0, import_minimatch7.minimatch)(file, pattern)
|
|
4731
4954
|
);
|
|
4732
4955
|
if (fileMatch) return true;
|
|
4733
4956
|
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|