@fern-api/replay 0.16.1 → 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 +206 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +223 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +223 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -266,8 +266,9 @@ declare class ReplayDetector {
|
|
|
266
266
|
private git;
|
|
267
267
|
private lockManager;
|
|
268
268
|
private sdkOutputDir;
|
|
269
|
+
private fernignorePatterns;
|
|
269
270
|
readonly warnings: string[];
|
|
270
|
-
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
271
|
+
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string, fernignorePatterns?: readonly string[]);
|
|
271
272
|
/**
|
|
272
273
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
273
274
|
* `lock.current_generation`.
|
|
@@ -737,6 +738,19 @@ declare class ReplayService {
|
|
|
737
738
|
* `applyPreparedReplay` (e.g., `[fern-autoversion]`).
|
|
738
739
|
*/
|
|
739
740
|
prepareReplay(options?: ReplayOptions): Promise<ReplayPreparation>;
|
|
741
|
+
/**
|
|
742
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
743
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
744
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
745
|
+
* end up with no surviving files are removed entirely.
|
|
746
|
+
*
|
|
747
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
748
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
749
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
750
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
751
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
752
|
+
*/
|
|
753
|
+
private cleanseLegacyFernignoreEntries;
|
|
740
754
|
/**
|
|
741
755
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
742
756
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
package/dist/index.d.ts
CHANGED
|
@@ -266,8 +266,9 @@ declare class ReplayDetector {
|
|
|
266
266
|
private git;
|
|
267
267
|
private lockManager;
|
|
268
268
|
private sdkOutputDir;
|
|
269
|
+
private fernignorePatterns;
|
|
269
270
|
readonly warnings: string[];
|
|
270
|
-
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
271
|
+
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string, fernignorePatterns?: readonly string[]);
|
|
271
272
|
/**
|
|
272
273
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
273
274
|
* `lock.current_generation`.
|
|
@@ -737,6 +738,19 @@ declare class ReplayService {
|
|
|
737
738
|
* `applyPreparedReplay` (e.g., `[fern-autoversion]`).
|
|
738
739
|
*/
|
|
739
740
|
prepareReplay(options?: ReplayOptions): Promise<ReplayPreparation>;
|
|
741
|
+
/**
|
|
742
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
743
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
744
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
745
|
+
* end up with no surviving files are removed entirely.
|
|
746
|
+
*
|
|
747
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
748
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
749
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
750
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
751
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
752
|
+
*/
|
|
753
|
+
private cleanseLegacyFernignoreEntries;
|
|
740
754
|
/**
|
|
741
755
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
742
756
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
package/dist/index.js
CHANGED
|
@@ -746,6 +746,7 @@ var LockfileManager = class {
|
|
|
746
746
|
|
|
747
747
|
// src/ReplayDetector.ts
|
|
748
748
|
import { createHash } from "crypto";
|
|
749
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
749
750
|
|
|
750
751
|
// src/shared/binary.ts
|
|
751
752
|
import { extname } from "path";
|
|
@@ -805,6 +806,15 @@ function isBinaryFile(filePath) {
|
|
|
805
806
|
|
|
806
807
|
// src/ReplayDetector.ts
|
|
807
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
|
+
}
|
|
808
818
|
function parsePatchFileHeaders(patchContent) {
|
|
809
819
|
const results = [];
|
|
810
820
|
let currentPath = null;
|
|
@@ -845,21 +855,25 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
845
855
|
}
|
|
846
856
|
return snapshot;
|
|
847
857
|
}
|
|
848
|
-
function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
|
|
858
|
+
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
849
859
|
const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
|
|
850
860
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
851
|
-
const
|
|
852
|
-
|
|
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 };
|
|
853
865
|
}
|
|
854
866
|
var ReplayDetector = class {
|
|
855
867
|
git;
|
|
856
868
|
lockManager;
|
|
857
869
|
sdkOutputDir;
|
|
870
|
+
fernignorePatterns;
|
|
858
871
|
warnings = [];
|
|
859
|
-
constructor(git, lockManager, sdkOutputDir) {
|
|
872
|
+
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
860
873
|
this.git = git;
|
|
861
874
|
this.lockManager = lockManager;
|
|
862
875
|
this.sdkOutputDir = sdkOutputDir;
|
|
876
|
+
this.fernignorePatterns = fernignorePatterns;
|
|
863
877
|
}
|
|
864
878
|
/**
|
|
865
879
|
* Derive the previous-generation SHA from git history instead of trusting
|
|
@@ -966,22 +980,30 @@ var ReplayDetector = class {
|
|
|
966
980
|
continue;
|
|
967
981
|
}
|
|
968
982
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
969
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
983
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
984
|
+
filesOutput,
|
|
985
|
+
this.fernignorePatterns
|
|
986
|
+
);
|
|
970
987
|
if (sensitiveFiles.length > 0) {
|
|
971
988
|
this.warnings.push(
|
|
972
989
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
973
990
|
);
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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;
|
|
984
1004
|
}
|
|
1005
|
+
if (!patchContent.trim()) continue;
|
|
1006
|
+
contentHash = this.computeContentHash(patchContent);
|
|
985
1007
|
}
|
|
986
1008
|
}
|
|
987
1009
|
if (files.length === 0) {
|
|
@@ -1180,12 +1202,20 @@ var ReplayDetector = class {
|
|
|
1180
1202
|
resolvedBaseGeneration = fallback;
|
|
1181
1203
|
}
|
|
1182
1204
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
1183
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1205
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1206
|
+
filesOutput,
|
|
1207
|
+
this.fernignorePatterns
|
|
1208
|
+
);
|
|
1184
1209
|
if (sensitiveFiles.length > 0) {
|
|
1185
1210
|
this.warnings.push(
|
|
1186
1211
|
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1187
1212
|
);
|
|
1188
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
|
+
}
|
|
1189
1219
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
1190
1220
|
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
1191
1221
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
@@ -1268,23 +1298,31 @@ var ReplayDetector = class {
|
|
|
1268
1298
|
continue;
|
|
1269
1299
|
}
|
|
1270
1300
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
1271
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1301
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
1302
|
+
filesOutput,
|
|
1303
|
+
this.fernignorePatterns
|
|
1304
|
+
);
|
|
1272
1305
|
if (sensitiveFiles.length > 0) {
|
|
1273
1306
|
this.warnings.push(
|
|
1274
1307
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
1275
1308
|
);
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (!patchContent.trim()) continue;
|
|
1286
|
-
contentHash = this.computeContentHash(patchContent);
|
|
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;
|
|
1287
1318
|
}
|
|
1319
|
+
try {
|
|
1320
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
1321
|
+
} catch {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (!patchContent.trim()) continue;
|
|
1325
|
+
contentHash = this.computeContentHash(patchContent);
|
|
1288
1326
|
}
|
|
1289
1327
|
if (files.length === 0) {
|
|
1290
1328
|
continue;
|
|
@@ -1520,7 +1558,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
1520
1558
|
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1521
1559
|
import { tmpdir } from "os";
|
|
1522
1560
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1523
|
-
import { minimatch as
|
|
1561
|
+
import { minimatch as minimatch3 } from "minimatch";
|
|
1524
1562
|
|
|
1525
1563
|
// src/accumulator/MergeAccumulator.ts
|
|
1526
1564
|
var MergeAccumulator = class {
|
|
@@ -2236,13 +2274,13 @@ var ReplayApplicator = class {
|
|
|
2236
2274
|
isExcluded(patch) {
|
|
2237
2275
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2238
2276
|
if (!config.exclude) return false;
|
|
2239
|
-
return patch.files.some((file) => config.exclude.some((pattern) =>
|
|
2277
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch3(file, pattern)));
|
|
2240
2278
|
}
|
|
2241
2279
|
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
2242
2280
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2243
2281
|
if (config.moves) {
|
|
2244
2282
|
for (const move of config.moves) {
|
|
2245
|
-
if (
|
|
2283
|
+
if (minimatch3(filePath, move.from) || filePath === move.from) {
|
|
2246
2284
|
if (filePath === move.from) {
|
|
2247
2285
|
return move.to;
|
|
2248
2286
|
}
|
|
@@ -2533,7 +2571,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2533
2571
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2534
2572
|
import { readFile as readFileAsync } from "fs/promises";
|
|
2535
2573
|
import { join as join6 } from "path";
|
|
2536
|
-
import { minimatch as
|
|
2574
|
+
import { minimatch as minimatch5 } from "minimatch";
|
|
2537
2575
|
init_GitClient();
|
|
2538
2576
|
|
|
2539
2577
|
// src/PatchRegionDiff.ts
|
|
@@ -2840,7 +2878,7 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
2840
2878
|
}
|
|
2841
2879
|
|
|
2842
2880
|
// src/classifier/FileOwnership.ts
|
|
2843
|
-
import { minimatch as
|
|
2881
|
+
import { minimatch as minimatch4 } from "minimatch";
|
|
2844
2882
|
var FileOwnership = class {
|
|
2845
2883
|
constructor(git) {
|
|
2846
2884
|
this.git = git;
|
|
@@ -2864,7 +2902,7 @@ var FileOwnership = class {
|
|
|
2864
2902
|
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
2865
2903
|
if (patch.user_owned) return true;
|
|
2866
2904
|
if (file === ".fernignore") return true;
|
|
2867
|
-
if (fernignorePatterns.some((p) => file === p ||
|
|
2905
|
+
if (fernignorePatterns.some((p) => file === p || minimatch4(file, p))) return true;
|
|
2868
2906
|
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
2869
2907
|
return true;
|
|
2870
2908
|
}
|
|
@@ -3370,7 +3408,8 @@ var ReplayService = class {
|
|
|
3370
3408
|
this.git = git;
|
|
3371
3409
|
this.outputDir = outputDir;
|
|
3372
3410
|
this.lockManager = new LockfileManager(outputDir);
|
|
3373
|
-
|
|
3411
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
3412
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
|
|
3374
3413
|
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
3375
3414
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
3376
3415
|
this.fileOwnership = new FileOwnership(git);
|
|
@@ -3390,8 +3429,60 @@ var ReplayService = class {
|
|
|
3390
3429
|
async prepareReplay(options) {
|
|
3391
3430
|
this.warnings = [];
|
|
3392
3431
|
this.detector.warnings.length = 0;
|
|
3432
|
+
this.cleanseLegacyFernignoreEntries();
|
|
3393
3433
|
return this.selectFlow(options).prepare(options);
|
|
3394
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
|
+
}
|
|
3395
3486
|
/**
|
|
3396
3487
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
3397
3488
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -3523,7 +3614,7 @@ var ReplayService = class {
|
|
|
3523
3614
|
)
|
|
3524
3615
|
);
|
|
3525
3616
|
const hasProtectedFiles = patch.files.some(
|
|
3526
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
3617
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
3527
3618
|
);
|
|
3528
3619
|
const allUserOwned = isUserOwnedPerFile.every(Boolean);
|
|
3529
3620
|
if (allUserOwned && patch.files.length > 0) {
|
|
@@ -3963,7 +4054,7 @@ var ReplayService = class {
|
|
|
3963
4054
|
);
|
|
3964
4055
|
if (snapHasStaleMarkers) continue;
|
|
3965
4056
|
const isProtectedSnap = patch.files.some(
|
|
3966
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
4057
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
3967
4058
|
);
|
|
3968
4059
|
if (isProtectedSnap) continue;
|
|
3969
4060
|
const snapHash = this.detector.computeContentHash(snapshotDiff);
|
|
@@ -4003,7 +4094,7 @@ var ReplayService = class {
|
|
|
4003
4094
|
continue;
|
|
4004
4095
|
}
|
|
4005
4096
|
const isProtected = patch.files.some(
|
|
4006
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
4097
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch5(file, p))
|
|
4007
4098
|
);
|
|
4008
4099
|
if (isProtected) {
|
|
4009
4100
|
continue;
|
|
@@ -4167,7 +4258,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4167
4258
|
if (files.length === 0) return;
|
|
4168
4259
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
4169
4260
|
for (const file of files) {
|
|
4170
|
-
if (fernignorePatterns.some((pattern) =>
|
|
4261
|
+
if (fernignorePatterns.some((pattern) => minimatch5(file, pattern))) continue;
|
|
4171
4262
|
try {
|
|
4172
4263
|
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
4173
4264
|
} catch {
|
|
@@ -4252,12 +4343,99 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
4252
4343
|
}
|
|
4253
4344
|
return { applied, unresolved, absorbed };
|
|
4254
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
|
+
}
|
|
4255
4433
|
|
|
4256
4434
|
// src/FernignoreMigrator.ts
|
|
4257
4435
|
import { createHash as createHash2 } from "crypto";
|
|
4258
4436
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4259
4437
|
import { dirname as dirname5, join as join7 } from "path";
|
|
4260
|
-
import { minimatch as
|
|
4438
|
+
import { minimatch as minimatch6 } from "minimatch";
|
|
4261
4439
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
4262
4440
|
var FernignoreMigrator = class {
|
|
4263
4441
|
git;
|
|
@@ -4289,7 +4467,7 @@ var FernignoreMigrator = class {
|
|
|
4289
4467
|
const commitsOnly = [];
|
|
4290
4468
|
const syntheticPatches = [];
|
|
4291
4469
|
for (const pattern of patterns) {
|
|
4292
|
-
const matchingPatch = patches.find((p) => p.files.some((f) =>
|
|
4470
|
+
const matchingPatch = patches.find((p) => p.files.some((f) => minimatch6(f, pattern) || f === pattern));
|
|
4293
4471
|
if (matchingPatch) {
|
|
4294
4472
|
trackedByBoth.push({
|
|
4295
4473
|
file: pattern,
|
|
@@ -4362,7 +4540,7 @@ var FernignoreMigrator = class {
|
|
|
4362
4540
|
fernignoreOnly.push(...remainingFernignoreOnly);
|
|
4363
4541
|
}
|
|
4364
4542
|
for (const patch of patches) {
|
|
4365
|
-
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) =>
|
|
4543
|
+
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch6(f, p) || f === p));
|
|
4366
4544
|
if (hasUnprotectedFiles) {
|
|
4367
4545
|
commitsOnly.push(patch);
|
|
4368
4546
|
}
|
|
@@ -4374,7 +4552,7 @@ var FernignoreMigrator = class {
|
|
|
4374
4552
|
const results = [];
|
|
4375
4553
|
for (const pattern of patterns) {
|
|
4376
4554
|
const matching = allFiles.filter(
|
|
4377
|
-
(f) =>
|
|
4555
|
+
(f) => minimatch6(f, pattern) || f === pattern || f.startsWith(pattern + "/")
|
|
4378
4556
|
);
|
|
4379
4557
|
results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
|
|
4380
4558
|
}
|
|
@@ -4687,7 +4865,7 @@ function computeContentHash(patchContent) {
|
|
|
4687
4865
|
}
|
|
4688
4866
|
|
|
4689
4867
|
// src/commands/forget.ts
|
|
4690
|
-
import { minimatch as
|
|
4868
|
+
import { minimatch as minimatch7 } from "minimatch";
|
|
4691
4869
|
function parseDiffStat(patchContent) {
|
|
4692
4870
|
let additions = 0;
|
|
4693
4871
|
let deletions = 0;
|
|
@@ -4717,7 +4895,7 @@ function toMatchedPatch(patch) {
|
|
|
4717
4895
|
}
|
|
4718
4896
|
function matchesPatch(patch, pattern) {
|
|
4719
4897
|
const fileMatch = patch.files.some(
|
|
4720
|
-
(file) => file === pattern ||
|
|
4898
|
+
(file) => file === pattern || minimatch7(file, pattern)
|
|
4721
4899
|
);
|
|
4722
4900
|
if (fileMatch) return true;
|
|
4723
4901
|
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|