@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/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
|
|
15026
|
-
|
|
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
|
|
@@ -15140,22 +15153,30 @@ var ReplayDetector = class {
|
|
|
15140
15153
|
continue;
|
|
15141
15154
|
}
|
|
15142
15155
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15143
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15156
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15157
|
+
filesOutput,
|
|
15158
|
+
this.fernignorePatterns
|
|
15159
|
+
);
|
|
15144
15160
|
if (sensitiveFiles.length > 0) {
|
|
15145
15161
|
this.warnings.push(
|
|
15146
15162
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15147
15163
|
);
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15164
|
+
}
|
|
15165
|
+
if (fernignoredFiles.length > 0) {
|
|
15166
|
+
this.warnings.push(
|
|
15167
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15168
|
+
);
|
|
15169
|
+
}
|
|
15170
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
15171
|
+
const parentSha = parents[0];
|
|
15172
|
+
if (parentSha) {
|
|
15173
|
+
try {
|
|
15174
|
+
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
15175
|
+
} catch {
|
|
15176
|
+
continue;
|
|
15158
15177
|
}
|
|
15178
|
+
if (!patchContent.trim()) continue;
|
|
15179
|
+
contentHash = this.computeContentHash(patchContent);
|
|
15159
15180
|
}
|
|
15160
15181
|
}
|
|
15161
15182
|
if (files.length === 0) {
|
|
@@ -15354,12 +15375,20 @@ var ReplayDetector = class {
|
|
|
15354
15375
|
resolvedBaseGeneration = fallback;
|
|
15355
15376
|
}
|
|
15356
15377
|
const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
|
|
15357
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15378
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15379
|
+
filesOutput,
|
|
15380
|
+
this.fernignorePatterns
|
|
15381
|
+
);
|
|
15358
15382
|
if (sensitiveFiles.length > 0) {
|
|
15359
15383
|
this.warnings.push(
|
|
15360
15384
|
`Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15361
15385
|
);
|
|
15362
15386
|
}
|
|
15387
|
+
if (fernignoredFiles.length > 0) {
|
|
15388
|
+
this.warnings.push(
|
|
15389
|
+
`.fernignore-protected file(s) excluded from composite patch: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15390
|
+
);
|
|
15391
|
+
}
|
|
15363
15392
|
if (files.length === 0) return { patches: [], revertedPatchIds: [] };
|
|
15364
15393
|
const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
|
|
15365
15394
|
if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
|
|
@@ -15442,23 +15471,31 @@ var ReplayDetector = class {
|
|
|
15442
15471
|
continue;
|
|
15443
15472
|
}
|
|
15444
15473
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15445
|
-
const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15474
|
+
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15475
|
+
filesOutput,
|
|
15476
|
+
this.fernignorePatterns
|
|
15477
|
+
);
|
|
15446
15478
|
if (sensitiveFiles.length > 0) {
|
|
15447
15479
|
this.warnings.push(
|
|
15448
15480
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15449
15481
|
);
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
}
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15482
|
+
}
|
|
15483
|
+
if (fernignoredFiles.length > 0) {
|
|
15484
|
+
this.warnings.push(
|
|
15485
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15486
|
+
);
|
|
15487
|
+
}
|
|
15488
|
+
if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
|
|
15489
|
+
if (parents.length === 0) {
|
|
15490
|
+
continue;
|
|
15491
|
+
}
|
|
15492
|
+
try {
|
|
15493
|
+
patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
|
|
15494
|
+
} catch {
|
|
15495
|
+
continue;
|
|
15461
15496
|
}
|
|
15497
|
+
if (!patchContent.trim()) continue;
|
|
15498
|
+
contentHash = this.computeContentHash(patchContent);
|
|
15462
15499
|
}
|
|
15463
15500
|
if (files.length === 0) {
|
|
15464
15501
|
continue;
|
|
@@ -17785,7 +17822,8 @@ var ReplayService = class {
|
|
|
17785
17822
|
this.git = git;
|
|
17786
17823
|
this.outputDir = outputDir;
|
|
17787
17824
|
this.lockManager = new LockfileManager(outputDir);
|
|
17788
|
-
|
|
17825
|
+
const fernignorePatterns = this.readFernignorePatterns();
|
|
17826
|
+
this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
|
|
17789
17827
|
this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
|
|
17790
17828
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
17791
17829
|
this.fileOwnership = new FileOwnership(git);
|
|
@@ -17805,8 +17843,60 @@ var ReplayService = class {
|
|
|
17805
17843
|
async prepareReplay(options) {
|
|
17806
17844
|
this.warnings = [];
|
|
17807
17845
|
this.detector.warnings.length = 0;
|
|
17846
|
+
this.cleanseLegacyFernignoreEntries();
|
|
17808
17847
|
return this.selectFlow(options).prepare(options);
|
|
17809
17848
|
}
|
|
17849
|
+
/**
|
|
17850
|
+
* ADR 0002 migration step. Strips `.fernignore`-matched entries from
|
|
17851
|
+
* `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
|
|
17852
|
+
* in `patch.patch_content` for every patch in the lockfile. Patches that
|
|
17853
|
+
* end up with no surviving files are removed entirely.
|
|
17854
|
+
*
|
|
17855
|
+
* Idempotent: a second run is a no-op because the first run already
|
|
17856
|
+
* matches the contract. Per-patch try/catch ensures one bad patch does
|
|
17857
|
+
* not block the rest. Each affected patch emits a warning naming the
|
|
17858
|
+
* stripped paths. `patch_content` is left untouched if no parseable
|
|
17859
|
+
* `diff --git` headers are found (fail-safe for exotic legacy formats).
|
|
17860
|
+
*/
|
|
17861
|
+
cleanseLegacyFernignoreEntries() {
|
|
17862
|
+
const patterns = this.readFernignorePatterns();
|
|
17863
|
+
if (patterns.length === 0) return;
|
|
17864
|
+
if (!this.lockManager.exists()) return;
|
|
17865
|
+
this.lockManager.read();
|
|
17866
|
+
let lockfileMutated = false;
|
|
17867
|
+
const patches = this.lockManager.getPatches();
|
|
17868
|
+
for (const patch of patches) {
|
|
17869
|
+
try {
|
|
17870
|
+
const result = computeLegacyFernignoreCleanse(
|
|
17871
|
+
patch,
|
|
17872
|
+
patterns,
|
|
17873
|
+
(content) => this.detector.computeContentHash(content)
|
|
17874
|
+
);
|
|
17875
|
+
if (result.unchanged) continue;
|
|
17876
|
+
if (result.remove) {
|
|
17877
|
+
this.lockManager.removePatch(patch.id);
|
|
17878
|
+
this.warnings.push(
|
|
17879
|
+
`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.)`
|
|
17880
|
+
);
|
|
17881
|
+
} else {
|
|
17882
|
+
this.lockManager.updatePatch(patch.id, {
|
|
17883
|
+
files: result.files,
|
|
17884
|
+
patch_content: result.patch_content,
|
|
17885
|
+
content_hash: result.content_hash,
|
|
17886
|
+
...result.theirs_snapshot !== void 0 ? { theirs_snapshot: result.theirs_snapshot } : {}
|
|
17887
|
+
});
|
|
17888
|
+
this.warnings.push(
|
|
17889
|
+
`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.)`
|
|
17890
|
+
);
|
|
17891
|
+
}
|
|
17892
|
+
lockfileMutated = true;
|
|
17893
|
+
} catch {
|
|
17894
|
+
}
|
|
17895
|
+
}
|
|
17896
|
+
if (lockfileMutated) {
|
|
17897
|
+
this.lockManager.save();
|
|
17898
|
+
}
|
|
17899
|
+
}
|
|
17810
17900
|
/**
|
|
17811
17901
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
17812
17902
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -18667,6 +18757,93 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
18667
18757
|
}
|
|
18668
18758
|
return { applied, unresolved, absorbed };
|
|
18669
18759
|
}
|
|
18760
|
+
function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeContentHash2) {
|
|
18761
|
+
const matchesFernignore = (filePath) => fernignorePatterns.some((p) => {
|
|
18762
|
+
if (filePath === p) return true;
|
|
18763
|
+
if (minimatch(filePath, p)) return true;
|
|
18764
|
+
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
18765
|
+
return false;
|
|
18766
|
+
});
|
|
18767
|
+
const strippedFromFiles = patch.files.filter(matchesFernignore);
|
|
18768
|
+
const survivingFiles = patch.files.filter((f) => !matchesFernignore(f));
|
|
18769
|
+
let survivingSnapshot = void 0;
|
|
18770
|
+
let snapshotChanged = false;
|
|
18771
|
+
if (patch.theirs_snapshot) {
|
|
18772
|
+
survivingSnapshot = {};
|
|
18773
|
+
for (const [key, value] of Object.entries(patch.theirs_snapshot)) {
|
|
18774
|
+
if (matchesFernignore(key)) {
|
|
18775
|
+
snapshotChanged = true;
|
|
18776
|
+
} else {
|
|
18777
|
+
survivingSnapshot[key] = value;
|
|
18778
|
+
}
|
|
18779
|
+
}
|
|
18780
|
+
}
|
|
18781
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch.patch_content, matchesFernignore);
|
|
18782
|
+
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
18783
|
+
if (unchanged) {
|
|
18784
|
+
return {
|
|
18785
|
+
unchanged: true,
|
|
18786
|
+
remove: false,
|
|
18787
|
+
files: patch.files,
|
|
18788
|
+
patch_content: patch.patch_content,
|
|
18789
|
+
content_hash: patch.content_hash,
|
|
18790
|
+
theirs_snapshot: patch.theirs_snapshot,
|
|
18791
|
+
strippedPaths: []
|
|
18792
|
+
};
|
|
18793
|
+
}
|
|
18794
|
+
const strippedPaths = Array.from(/* @__PURE__ */ new Set([...strippedFromFiles, ...strippedSectionPaths]));
|
|
18795
|
+
if (survivingFiles.length === 0 || newPatchContent.trim() === "") {
|
|
18796
|
+
return {
|
|
18797
|
+
unchanged: false,
|
|
18798
|
+
remove: true,
|
|
18799
|
+
files: [],
|
|
18800
|
+
patch_content: "",
|
|
18801
|
+
content_hash: "",
|
|
18802
|
+
strippedPaths
|
|
18803
|
+
};
|
|
18804
|
+
}
|
|
18805
|
+
const newContentHash = patchContentStripped ? computeContentHash2(newPatchContent) : patch.content_hash;
|
|
18806
|
+
return {
|
|
18807
|
+
unchanged: false,
|
|
18808
|
+
remove: false,
|
|
18809
|
+
files: survivingFiles,
|
|
18810
|
+
patch_content: newPatchContent,
|
|
18811
|
+
content_hash: newContentHash,
|
|
18812
|
+
...survivingSnapshot !== void 0 ? { theirs_snapshot: survivingSnapshot } : {},
|
|
18813
|
+
strippedPaths
|
|
18814
|
+
};
|
|
18815
|
+
}
|
|
18816
|
+
function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
18817
|
+
const lines = patchContent.split("\n");
|
|
18818
|
+
const sectionStarts = [];
|
|
18819
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18820
|
+
const line = lines[i];
|
|
18821
|
+
if (line.startsWith("diff --git ")) {
|
|
18822
|
+
const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
18823
|
+
const path2 = match2 ? match2[2] : null;
|
|
18824
|
+
sectionStarts.push({ idx: i, path: path2 });
|
|
18825
|
+
}
|
|
18826
|
+
}
|
|
18827
|
+
if (sectionStarts.length === 0) {
|
|
18828
|
+
return { stripped: false, content: patchContent, strippedSectionPaths: [] };
|
|
18829
|
+
}
|
|
18830
|
+
const preamble = lines.slice(0, sectionStarts[0].idx);
|
|
18831
|
+
const newLines = [...preamble];
|
|
18832
|
+
const strippedSectionPaths = [];
|
|
18833
|
+
let stripped = false;
|
|
18834
|
+
for (let i = 0; i < sectionStarts.length; i++) {
|
|
18835
|
+
const section = sectionStarts[i];
|
|
18836
|
+
const next = sectionStarts[i + 1];
|
|
18837
|
+
const endIdx = next ? next.idx : lines.length;
|
|
18838
|
+
if (section.path !== null && matchesFernignore(section.path)) {
|
|
18839
|
+
stripped = true;
|
|
18840
|
+
strippedSectionPaths.push(section.path);
|
|
18841
|
+
continue;
|
|
18842
|
+
}
|
|
18843
|
+
newLines.push(...lines.slice(section.idx, endIdx));
|
|
18844
|
+
}
|
|
18845
|
+
return { stripped, content: newLines.join("\n"), strippedSectionPaths };
|
|
18846
|
+
}
|
|
18670
18847
|
|
|
18671
18848
|
// src/FernignoreMigrator.ts
|
|
18672
18849
|
var import_node_crypto2 = require("crypto");
|