@fern-api/replay 0.16.1 → 0.17.4

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