@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/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 files = allFiles.filter((f) => !isSensitiveFile(f));
852
- return { files, sensitiveFiles };
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(filesOutput);
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
- if (files.length > 0) {
975
- const parentSha = parents[0];
976
- if (parentSha) {
977
- try {
978
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
979
- } catch {
980
- continue;
981
- }
982
- if (!patchContent.trim()) continue;
983
- contentHash = this.computeContentHash(patchContent);
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(filesOutput);
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(filesOutput);
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
- if (files.length > 0) {
1277
- if (parents.length === 0) {
1278
- continue;
1279
- }
1280
- try {
1281
- patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
1282
- } catch {
1283
- continue;
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 minimatch2 } from "minimatch";
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) => minimatch2(file, 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 (minimatch2(filePath, move.from) || filePath === move.from) {
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 minimatch4 } from "minimatch";
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 minimatch3 } from "minimatch";
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 || minimatch3(file, p))) return true;
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
- this.detector = new ReplayDetector(git, this.lockManager, outputDir);
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 || minimatch4(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 || minimatch4(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 || minimatch4(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) => minimatch4(file, pattern))) continue;
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 minimatch5 } from "minimatch";
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) => minimatch5(f, pattern) || f === pattern));
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) => minimatch5(f, p) || f === 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) => minimatch5(f, pattern) || f === pattern || f.startsWith(pattern + "/")
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 minimatch6 } from "minimatch";
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 || minimatch6(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());