@fern-api/replay 0.17.4 → 0.19.0
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 +436 -116
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +381 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -3
- package/dist/index.d.ts +193 -3
- package/dist/index.js +364 -90
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/cli.cjs
CHANGED
|
@@ -5379,6 +5379,11 @@ var init_GitClient = __esm({
|
|
|
5379
5379
|
proc.stderr.on("data", (data) => {
|
|
5380
5380
|
stderr += data.toString();
|
|
5381
5381
|
});
|
|
5382
|
+
proc.on("error", (err) => {
|
|
5383
|
+
reject(new Error(`git ${args.join(" ")} failed to spawn: ${err.message}`));
|
|
5384
|
+
});
|
|
5385
|
+
proc.stdin.on("error", () => {
|
|
5386
|
+
});
|
|
5382
5387
|
proc.on("close", (code) => {
|
|
5383
5388
|
if (code === 0) {
|
|
5384
5389
|
resolve2(stdout);
|
|
@@ -13240,15 +13245,15 @@ __export(PatchApplyTheirs_exports, {
|
|
|
13240
13245
|
async function applyPatchToContent(base, patchContent, filePath) {
|
|
13241
13246
|
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
13242
13247
|
if (!fileDiff) return null;
|
|
13243
|
-
const tempDir = await (0, import_promises5.mkdtemp)((0,
|
|
13248
|
+
const tempDir = await (0, import_promises5.mkdtemp)((0, import_node_path9.join)((0, import_node_os3.tmpdir)(), "replay-migrate-"));
|
|
13244
13249
|
const tempGit = new GitClient(tempDir);
|
|
13245
13250
|
try {
|
|
13246
13251
|
await tempGit.exec(["init"]);
|
|
13247
13252
|
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
13248
13253
|
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
13249
13254
|
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
13250
|
-
const tempFilePath = (0,
|
|
13251
|
-
await (0, import_promises5.mkdir)((0,
|
|
13255
|
+
const tempFilePath = (0, import_node_path9.join)(tempDir, filePath);
|
|
13256
|
+
await (0, import_promises5.mkdir)((0, import_node_path9.dirname)(tempFilePath), { recursive: true });
|
|
13252
13257
|
await (0, import_promises5.writeFile)(tempFilePath, base);
|
|
13253
13258
|
await tempGit.exec(["add", filePath]);
|
|
13254
13259
|
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
@@ -13279,20 +13284,20 @@ function extractFileDiff(patchContent, filePath) {
|
|
|
13279
13284
|
}
|
|
13280
13285
|
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
13281
13286
|
}
|
|
13282
|
-
var import_promises5, import_node_os3,
|
|
13287
|
+
var import_promises5, import_node_os3, import_node_path9;
|
|
13283
13288
|
var init_PatchApplyTheirs = __esm({
|
|
13284
13289
|
"src/PatchApplyTheirs.ts"() {
|
|
13285
13290
|
"use strict";
|
|
13286
13291
|
import_promises5 = require("fs/promises");
|
|
13287
13292
|
import_node_os3 = require("os");
|
|
13288
|
-
|
|
13293
|
+
import_node_path9 = require("path");
|
|
13289
13294
|
init_GitClient();
|
|
13290
13295
|
}
|
|
13291
13296
|
});
|
|
13292
13297
|
|
|
13293
13298
|
// src/cli.ts
|
|
13294
|
-
var
|
|
13295
|
-
var
|
|
13299
|
+
var import_node_path14 = require("path");
|
|
13300
|
+
var import_node_fs7 = require("fs");
|
|
13296
13301
|
var import_node_readline = require("readline");
|
|
13297
13302
|
init_GitClient();
|
|
13298
13303
|
|
|
@@ -14822,6 +14827,22 @@ var LockfileManager = class {
|
|
|
14822
14827
|
this.lock.forgotten_hashes.push(hash);
|
|
14823
14828
|
}
|
|
14824
14829
|
}
|
|
14830
|
+
/**
|
|
14831
|
+
* Record a dismissed customization: visibility metadata in `dismissals`
|
|
14832
|
+
* plus a tombstone in `forgotten_hashes` for every content hash.
|
|
14833
|
+
* `forgotten_hashes` stays the only list the detector consults; the
|
|
14834
|
+
* metadata exists so `status` can show what was dismissed.
|
|
14835
|
+
*/
|
|
14836
|
+
addDismissal(record) {
|
|
14837
|
+
this.ensureLoaded();
|
|
14838
|
+
if (!this.lock.dismissals) {
|
|
14839
|
+
this.lock.dismissals = [];
|
|
14840
|
+
}
|
|
14841
|
+
this.lock.dismissals.push(record);
|
|
14842
|
+
for (const hash of record.content_hashes) {
|
|
14843
|
+
this.addForgottenHash(hash);
|
|
14844
|
+
}
|
|
14845
|
+
}
|
|
14825
14846
|
getUnresolvedPatches() {
|
|
14826
14847
|
this.ensureLoaded();
|
|
14827
14848
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -14981,7 +15002,8 @@ function parseRevertedMessage(subject) {
|
|
|
14981
15002
|
var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
|
|
14982
15003
|
function matchesFernignorePattern(filePath, patterns) {
|
|
14983
15004
|
if (patterns.length === 0) return false;
|
|
14984
|
-
return patterns.some((
|
|
15005
|
+
return patterns.some((rawPattern) => {
|
|
15006
|
+
const p = rawPattern.endsWith("/") ? rawPattern.slice(0, -1) : rawPattern;
|
|
14985
15007
|
if (filePath === p) return true;
|
|
14986
15008
|
if (minimatch(filePath, p)) return true;
|
|
14987
15009
|
if (!p.includes("*") && !p.includes("?") && filePath.startsWith(p + "/")) return true;
|
|
@@ -15029,12 +15051,14 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
15029
15051
|
return snapshot;
|
|
15030
15052
|
}
|
|
15031
15053
|
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
15032
|
-
const
|
|
15054
|
+
const rawFiles = rawFilesOutput.trim().split("\n").filter(Boolean);
|
|
15055
|
+
const infrastructureFiles = rawFiles.filter((f) => INFRASTRUCTURE_FILES.has(f) || f.startsWith(".fern/"));
|
|
15056
|
+
const allFiles = rawFiles.filter((f) => !INFRASTRUCTURE_FILES.has(f) && !f.startsWith(".fern/"));
|
|
15033
15057
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
15034
15058
|
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
15035
15059
|
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
15036
15060
|
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
15037
|
-
return { files, sensitiveFiles, fernignoredFiles };
|
|
15061
|
+
return { files, sensitiveFiles, fernignoredFiles, infrastructureFiles };
|
|
15038
15062
|
}
|
|
15039
15063
|
var ReplayDetector = class {
|
|
15040
15064
|
git;
|
|
@@ -15042,6 +15066,15 @@ var ReplayDetector = class {
|
|
|
15042
15066
|
sdkOutputDir;
|
|
15043
15067
|
fernignorePatterns;
|
|
15044
15068
|
warnings = [];
|
|
15069
|
+
/**
|
|
15070
|
+
* Structured degraded-state channel, mirroring `warnings`. Populated when
|
|
15071
|
+
* detection cannot guarantee customizations were carried forward (baseline
|
|
15072
|
+
* unreachable in this clone). Cleared per-run by `ReplayService.prepareReplay`
|
|
15073
|
+
* alongside `warnings`. Merged into `ReplayReport.degradedReasons` by each
|
|
15074
|
+
* flow handler. Degraded is observability only: detection results are
|
|
15075
|
+
* unchanged.
|
|
15076
|
+
*/
|
|
15077
|
+
degradedReasons = [];
|
|
15045
15078
|
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
15046
15079
|
this.git = git;
|
|
15047
15080
|
this.lockManager = lockManager;
|
|
@@ -15112,6 +15145,10 @@ var ReplayDetector = class {
|
|
|
15112
15145
|
this.warnings.push(
|
|
15113
15146
|
`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
15147
|
);
|
|
15148
|
+
this.degradedReasons.push({
|
|
15149
|
+
code: "baseline-unresolvable",
|
|
15150
|
+
message: `The previous generation recorded in the lockfile (${lock.current_generation.slice(0, 7)}) could not be resolved to any reachable baseline during this run. Customizations may not have been carried forward \u2014 review the result before merging.`
|
|
15151
|
+
});
|
|
15115
15152
|
}
|
|
15116
15153
|
return { patches: [], revertedPatchIds: [] };
|
|
15117
15154
|
}
|
|
@@ -15120,6 +15157,12 @@ var ReplayDetector = class {
|
|
|
15120
15157
|
this.warnings.push(
|
|
15121
15158
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
15122
15159
|
);
|
|
15160
|
+
if (lock.patches.length > 0) {
|
|
15161
|
+
this.degradedReasons.push({
|
|
15162
|
+
code: "generation-anchor-unreachable",
|
|
15163
|
+
message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run, and ${lock.patches.length} tracked customization(s) could not be verified against it. Customizations may not have been carried forward \u2014 review the result before merging.`
|
|
15164
|
+
});
|
|
15165
|
+
}
|
|
15123
15166
|
return this.detectPatchesViaTreeDiff(
|
|
15124
15167
|
lastGen,
|
|
15125
15168
|
/* commitKnownMissing */
|
|
@@ -15159,48 +15202,28 @@ var ReplayDetector = class {
|
|
|
15159
15202
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
15160
15203
|
continue;
|
|
15161
15204
|
}
|
|
15162
|
-
const
|
|
15163
|
-
|
|
15164
|
-
try {
|
|
15165
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
15166
|
-
} catch {
|
|
15205
|
+
const materialized = await this.materializeCommitPatch(commit.sha);
|
|
15206
|
+
if (materialized.sensitiveFiles.length > 0) {
|
|
15167
15207
|
this.warnings.push(
|
|
15168
|
-
`
|
|
15208
|
+
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
15169
15209
|
);
|
|
15170
|
-
continue;
|
|
15171
15210
|
}
|
|
15172
|
-
|
|
15173
|
-
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
15174
|
-
continue;
|
|
15175
|
-
}
|
|
15176
|
-
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15177
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15178
|
-
filesOutput,
|
|
15179
|
-
this.fernignorePatterns
|
|
15180
|
-
);
|
|
15181
|
-
if (sensitiveFiles.length > 0) {
|
|
15211
|
+
if (materialized.fernignoredFiles.length > 0) {
|
|
15182
15212
|
this.warnings.push(
|
|
15183
|
-
|
|
15213
|
+
`.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${materialized.fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
|
|
15184
15214
|
);
|
|
15185
15215
|
}
|
|
15186
|
-
if (
|
|
15216
|
+
if (materialized.status === "unreachable") {
|
|
15187
15217
|
this.warnings.push(
|
|
15188
|
-
|
|
15218
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
15189
15219
|
);
|
|
15220
|
+
continue;
|
|
15190
15221
|
}
|
|
15191
|
-
if (
|
|
15192
|
-
|
|
15193
|
-
if (parentSha) {
|
|
15194
|
-
try {
|
|
15195
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
15196
|
-
} catch {
|
|
15197
|
-
continue;
|
|
15198
|
-
}
|
|
15199
|
-
if (!patchContent.trim()) continue;
|
|
15200
|
-
contentHash = this.computeContentHash(patchContent);
|
|
15201
|
-
}
|
|
15222
|
+
if (materialized.status === "empty") {
|
|
15223
|
+
continue;
|
|
15202
15224
|
}
|
|
15203
|
-
|
|
15225
|
+
const { files, patchContent, contentHash } = materialized;
|
|
15226
|
+
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
15204
15227
|
continue;
|
|
15205
15228
|
}
|
|
15206
15229
|
const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
|
|
@@ -15276,6 +15299,63 @@ var ReplayDetector = class {
|
|
|
15276
15299
|
const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
15277
15300
|
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
15278
15301
|
}
|
|
15302
|
+
/**
|
|
15303
|
+
* Materialize a commit into patch content exactly as detection does.
|
|
15304
|
+
*
|
|
15305
|
+
* Pipeline: `diff-tree --name-only` → filter infrastructure / sensitive /
|
|
15306
|
+
* `.fernignore`d files → scoped `git diff` when files were stripped,
|
|
15307
|
+
* otherwise full `git format-patch` → content hash.
|
|
15308
|
+
*
|
|
15309
|
+
* This is THE definition of a commit's re-emission identity: the content
|
|
15310
|
+
* hash this method returns is what `detectPatchesInRange` will compute if
|
|
15311
|
+
* the commit re-enters the scan range (e.g. after a regen PR is merged
|
|
15312
|
+
* with a merge commit). Callers that tombstone a dismissed patch must use
|
|
15313
|
+
* this hash — the stored `content_hash` drifts every time the patch is
|
|
15314
|
+
* carried forward, so it does not identify the commit on re-detection.
|
|
15315
|
+
*
|
|
15316
|
+
* Never throws; failures are reported through the `status` discriminant.
|
|
15317
|
+
*/
|
|
15318
|
+
async materializeCommitPatch(sha) {
|
|
15319
|
+
let parents;
|
|
15320
|
+
let filesOutput;
|
|
15321
|
+
try {
|
|
15322
|
+
parents = await this.git.getCommitParents(sha);
|
|
15323
|
+
filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", sha]);
|
|
15324
|
+
} catch {
|
|
15325
|
+
return { status: "unreachable", sensitiveFiles: [], fernignoredFiles: [] };
|
|
15326
|
+
}
|
|
15327
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
15328
|
+
if (files.length === 0) {
|
|
15329
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
15330
|
+
}
|
|
15331
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
15332
|
+
const parentSha = parents[0];
|
|
15333
|
+
let patchContent;
|
|
15334
|
+
if (wasFiltered && parentSha) {
|
|
15335
|
+
try {
|
|
15336
|
+
patchContent = await this.git.exec(["diff", parentSha, sha, "--", ...files]);
|
|
15337
|
+
} catch {
|
|
15338
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
15339
|
+
}
|
|
15340
|
+
if (!patchContent.trim()) {
|
|
15341
|
+
return { status: "empty", sensitiveFiles, fernignoredFiles };
|
|
15342
|
+
}
|
|
15343
|
+
} else {
|
|
15344
|
+
try {
|
|
15345
|
+
patchContent = await this.git.formatPatch(sha);
|
|
15346
|
+
} catch {
|
|
15347
|
+
return { status: "unreachable", sensitiveFiles, fernignoredFiles };
|
|
15348
|
+
}
|
|
15349
|
+
}
|
|
15350
|
+
return {
|
|
15351
|
+
status: "ok",
|
|
15352
|
+
files,
|
|
15353
|
+
sensitiveFiles,
|
|
15354
|
+
fernignoredFiles,
|
|
15355
|
+
patchContent,
|
|
15356
|
+
contentHash: this.computeContentHash(patchContent)
|
|
15357
|
+
};
|
|
15358
|
+
}
|
|
15279
15359
|
/**
|
|
15280
15360
|
* Compute content hash for deduplication.
|
|
15281
15361
|
*
|
|
@@ -15380,6 +15460,13 @@ var ReplayDetector = class {
|
|
|
15380
15460
|
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
15381
15461
|
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
15382
15462
|
if (!diffBase) {
|
|
15463
|
+
this.warnings.push(
|
|
15464
|
+
`Neither the previous generation's commit (${lastGen.commit_sha.slice(0, 7)}) nor its recorded tree was reachable during this run. Falling back to a bounded commit scan, which cannot see customizations older than the clone window. Widen the clone window (e.g. \`git fetch --unshallow\`) and re-run replay to verify customizations were carried forward.`
|
|
15465
|
+
);
|
|
15466
|
+
this.degradedReasons.push({
|
|
15467
|
+
code: "generation-tree-unreachable",
|
|
15468
|
+
message: `The previous generation (${lastGen.commit_sha.slice(0, 7)}) was not reachable during this run \u2014 neither its commit nor its recorded tree. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
|
|
15469
|
+
});
|
|
15383
15470
|
return this.detectPatchesViaCommitScan();
|
|
15384
15471
|
}
|
|
15385
15472
|
const lockForGuard = this.lockManager.read();
|
|
@@ -15492,10 +15579,7 @@ var ReplayDetector = class {
|
|
|
15492
15579
|
continue;
|
|
15493
15580
|
}
|
|
15494
15581
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15495
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15496
|
-
filesOutput,
|
|
15497
|
-
this.fernignorePatterns
|
|
15498
|
-
);
|
|
15582
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
15499
15583
|
if (sensitiveFiles.length > 0) {
|
|
15500
15584
|
this.warnings.push(
|
|
15501
15585
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
@@ -15506,7 +15590,8 @@ var ReplayDetector = class {
|
|
|
15506
15590
|
`.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
15591
|
);
|
|
15508
15592
|
}
|
|
15509
|
-
|
|
15593
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
15594
|
+
if (wasFiltered && files.length > 0) {
|
|
15510
15595
|
if (parents.length === 0) {
|
|
15511
15596
|
continue;
|
|
15512
15597
|
}
|
|
@@ -15610,9 +15695,9 @@ var ReplayDetector = class {
|
|
|
15610
15695
|
};
|
|
15611
15696
|
|
|
15612
15697
|
// src/ReplayService.ts
|
|
15613
|
-
var
|
|
15698
|
+
var import_node_fs3 = require("fs");
|
|
15614
15699
|
var import_promises6 = require("fs/promises");
|
|
15615
|
-
var
|
|
15700
|
+
var import_node_path10 = require("path");
|
|
15616
15701
|
init_GitClient();
|
|
15617
15702
|
|
|
15618
15703
|
// src/ReplayApplicator.ts
|
|
@@ -16922,6 +17007,15 @@ function isDiffLineForFile(diffLine, filePath) {
|
|
|
16922
17007
|
return match2 !== null && match2[1] === filePath;
|
|
16923
17008
|
}
|
|
16924
17009
|
|
|
17010
|
+
// src/shared/fernignore.ts
|
|
17011
|
+
var import_node_fs2 = require("fs");
|
|
17012
|
+
var import_node_path7 = require("path");
|
|
17013
|
+
function readFernignorePatterns(outputDir) {
|
|
17014
|
+
const fernignorePath = (0, import_node_path7.join)(outputDir, ".fernignore");
|
|
17015
|
+
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
17016
|
+
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
17017
|
+
}
|
|
17018
|
+
|
|
16925
17019
|
// src/ReplayCommitter.ts
|
|
16926
17020
|
var ReplayCommitter = class {
|
|
16927
17021
|
git;
|
|
@@ -17027,7 +17121,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
17027
17121
|
// src/PatchRegionDiff.ts
|
|
17028
17122
|
var import_promises4 = require("fs/promises");
|
|
17029
17123
|
var import_node_os2 = require("os");
|
|
17030
|
-
var
|
|
17124
|
+
var import_node_path8 = require("path");
|
|
17031
17125
|
var import_node_child_process = require("child_process");
|
|
17032
17126
|
init_HybridReconstruction();
|
|
17033
17127
|
function normalizeHunkBody(hunk) {
|
|
@@ -17245,10 +17339,10 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
17245
17339
|
}
|
|
17246
17340
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
17247
17341
|
if (beforeContent === afterContent) return "";
|
|
17248
|
-
const tmp = await (0, import_promises4.mkdtemp)((0,
|
|
17342
|
+
const tmp = await (0, import_promises4.mkdtemp)((0, import_node_path8.join)((0, import_node_os2.tmpdir)(), "fern-replay-pp-"));
|
|
17249
17343
|
try {
|
|
17250
|
-
const beforePath = (0,
|
|
17251
|
-
const afterPath = (0,
|
|
17344
|
+
const beforePath = (0, import_node_path8.join)(tmp, "before");
|
|
17345
|
+
const afterPath = (0, import_node_path8.join)(tmp, "after");
|
|
17252
17346
|
await (0, import_promises4.writeFile)(beforePath, beforeContent, "utf-8");
|
|
17253
17347
|
await (0, import_promises4.writeFile)(afterPath, afterContent, "utf-8");
|
|
17254
17348
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
@@ -17423,6 +17517,7 @@ var FirstGenerationFlow = class {
|
|
|
17423
17517
|
patchesToApply: [],
|
|
17424
17518
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
17425
17519
|
detectorWarnings: [],
|
|
17520
|
+
detectorDegraded: [],
|
|
17426
17521
|
revertedCount: 0,
|
|
17427
17522
|
genSha: genRecord.commit_sha,
|
|
17428
17523
|
optionsSnapshot: options
|
|
@@ -17482,6 +17577,7 @@ var SkipApplicationFlow = class {
|
|
|
17482
17577
|
patchesToApply: [],
|
|
17483
17578
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
17484
17579
|
detectorWarnings: [],
|
|
17580
|
+
detectorDegraded: [],
|
|
17485
17581
|
revertedCount: 0,
|
|
17486
17582
|
genSha: genRecord.commit_sha,
|
|
17487
17583
|
optionsSnapshot: options
|
|
@@ -17519,8 +17615,10 @@ var NoPatchesFlow = class {
|
|
|
17519
17615
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
17520
17616
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
17521
17617
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
17618
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
17522
17619
|
if (options?.dryRun) {
|
|
17523
17620
|
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
17621
|
+
const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
|
|
17524
17622
|
const preparedReport = {
|
|
17525
17623
|
flow: "no-patches",
|
|
17526
17624
|
patchesDetected: newPatches.length,
|
|
@@ -17530,7 +17628,9 @@ var NoPatchesFlow = class {
|
|
|
17530
17628
|
patchesReverted: revertedPatchIds.length,
|
|
17531
17629
|
conflicts: [],
|
|
17532
17630
|
wouldApply: newPatches,
|
|
17533
|
-
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
17631
|
+
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0,
|
|
17632
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
17633
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
17534
17634
|
};
|
|
17535
17635
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
17536
17636
|
return {
|
|
@@ -17584,6 +17684,7 @@ var NoPatchesFlow = class {
|
|
|
17584
17684
|
patchesToApply: newPatches,
|
|
17585
17685
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
17586
17686
|
detectorWarnings,
|
|
17687
|
+
detectorDegraded,
|
|
17587
17688
|
revertedCount: revertedPatchIds.length,
|
|
17588
17689
|
genSha: genRecord.commit_sha,
|
|
17589
17690
|
optionsSnapshot: options
|
|
@@ -17627,6 +17728,7 @@ var NoPatchesFlow = class {
|
|
|
17627
17728
|
}
|
|
17628
17729
|
}
|
|
17629
17730
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
17731
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
17630
17732
|
return this.service.buildReport(
|
|
17631
17733
|
"no-patches",
|
|
17632
17734
|
newPatches,
|
|
@@ -17635,7 +17737,8 @@ var NoPatchesFlow = class {
|
|
|
17635
17737
|
warnings,
|
|
17636
17738
|
rebaseCounts,
|
|
17637
17739
|
void 0,
|
|
17638
|
-
prep._prepared.revertedCount
|
|
17740
|
+
prep._prepared.revertedCount,
|
|
17741
|
+
degradedReasons
|
|
17639
17742
|
);
|
|
17640
17743
|
}
|
|
17641
17744
|
};
|
|
@@ -17652,6 +17755,10 @@ var NormalRegenerationFlow = class {
|
|
|
17652
17755
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
17653
17756
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
17654
17757
|
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
17758
|
+
const dryRunDegraded = dedupeDegradedReasons([
|
|
17759
|
+
...this.service.detector.degradedReasons,
|
|
17760
|
+
...this.service.degradedReasons
|
|
17761
|
+
]);
|
|
17655
17762
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
17656
17763
|
const preparedReport = {
|
|
17657
17764
|
flow: "normal-regeneration",
|
|
@@ -17662,7 +17769,9 @@ var NormalRegenerationFlow = class {
|
|
|
17662
17769
|
patchesReverted: dryRunReverted.length,
|
|
17663
17770
|
conflicts: [],
|
|
17664
17771
|
wouldApply: allPatches2,
|
|
17665
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
17772
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
17773
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
17774
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
17666
17775
|
};
|
|
17667
17776
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
17668
17777
|
return {
|
|
@@ -17701,6 +17810,7 @@ var NormalRegenerationFlow = class {
|
|
|
17701
17810
|
existingPatches = this.service.lockManager.getPatches();
|
|
17702
17811
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
17703
17812
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
17813
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
17704
17814
|
if (removedByPreRebase.length > 0) {
|
|
17705
17815
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
17706
17816
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
@@ -17741,6 +17851,7 @@ var NormalRegenerationFlow = class {
|
|
|
17741
17851
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
17742
17852
|
preRebaseCounts,
|
|
17743
17853
|
detectorWarnings,
|
|
17854
|
+
detectorDegraded,
|
|
17744
17855
|
revertedCount: revertedPatchIds.length,
|
|
17745
17856
|
genSha: genRecord.commit_sha,
|
|
17746
17857
|
optionsSnapshot: options
|
|
@@ -17789,6 +17900,7 @@ var NormalRegenerationFlow = class {
|
|
|
17789
17900
|
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
17790
17901
|
}
|
|
17791
17902
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
17903
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
17792
17904
|
return this.service.buildReport(
|
|
17793
17905
|
"normal-regeneration",
|
|
17794
17906
|
allPatches,
|
|
@@ -17797,7 +17909,8 @@ var NormalRegenerationFlow = class {
|
|
|
17797
17909
|
warnings,
|
|
17798
17910
|
rebaseCounts,
|
|
17799
17911
|
prep._prepared.preRebaseCounts,
|
|
17800
|
-
prep._prepared.revertedCount
|
|
17912
|
+
prep._prepared.revertedCount,
|
|
17913
|
+
degradedReasons
|
|
17801
17914
|
);
|
|
17802
17915
|
}
|
|
17803
17916
|
};
|
|
@@ -17837,6 +17950,16 @@ var ReplayService = class {
|
|
|
17837
17950
|
* @internal Used by Flow implementations in `src/flows/`.
|
|
17838
17951
|
*/
|
|
17839
17952
|
warnings = [];
|
|
17953
|
+
/**
|
|
17954
|
+
* Service-level structured degraded reasons, mirroring `warnings`.
|
|
17955
|
+
* Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
|
|
17956
|
+
* patch's base-generation tree is unreachable). Merged with
|
|
17957
|
+
* `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
|
|
17958
|
+
* flow handler. Cleared at the top of `prepareReplay()`.
|
|
17959
|
+
*
|
|
17960
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
17961
|
+
*/
|
|
17962
|
+
degradedReasons = [];
|
|
17840
17963
|
/**
|
|
17841
17964
|
* @internal Test-only accessor.
|
|
17842
17965
|
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
@@ -17880,7 +18003,10 @@ var ReplayService = class {
|
|
|
17880
18003
|
async prepareReplay(options) {
|
|
17881
18004
|
this.warnings = [];
|
|
17882
18005
|
this.detector.warnings.length = 0;
|
|
18006
|
+
this.degradedReasons = [];
|
|
18007
|
+
this.detector.degradedReasons.length = 0;
|
|
17883
18008
|
this.cleanseLegacyFernignoreEntries();
|
|
18009
|
+
this.cleanseInfrastructureFileEntries();
|
|
17884
18010
|
return this.selectFlow(options).prepare(options);
|
|
17885
18011
|
}
|
|
17886
18012
|
/**
|
|
@@ -17934,6 +18060,50 @@ var ReplayService = class {
|
|
|
17934
18060
|
this.lockManager.save();
|
|
17935
18061
|
}
|
|
17936
18062
|
}
|
|
18063
|
+
/**
|
|
18064
|
+
* Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
|
|
18065
|
+
* from existing patches' `patch_content`. These diffs were bundled by older
|
|
18066
|
+
* detection logic that did not scope `git diff` when only infrastructure files
|
|
18067
|
+
* were stripped. Patches that end up with no surviving diff sections are
|
|
18068
|
+
* removed entirely.
|
|
18069
|
+
*
|
|
18070
|
+
* Idempotent: a second run is a no-op because the first run already removed
|
|
18071
|
+
* all infrastructure sections. Per-patch try/catch ensures isolation.
|
|
18072
|
+
*/
|
|
18073
|
+
cleanseInfrastructureFileEntries() {
|
|
18074
|
+
if (!this.lockManager.exists()) return;
|
|
18075
|
+
this.lockManager.read();
|
|
18076
|
+
let lockfileMutated = false;
|
|
18077
|
+
const patches = this.lockManager.getPatches();
|
|
18078
|
+
for (const patch of patches) {
|
|
18079
|
+
try {
|
|
18080
|
+
const result = computeInfrastructureFileCleanse(
|
|
18081
|
+
patch,
|
|
18082
|
+
(content) => this.detector.computeContentHash(content)
|
|
18083
|
+
);
|
|
18084
|
+
if (result.unchanged) continue;
|
|
18085
|
+
if (result.remove) {
|
|
18086
|
+
this.lockManager.removePatch(patch.id);
|
|
18087
|
+
this.warnings.push(
|
|
18088
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: patch contained only infrastructure file changes (${result.strippedPaths.join(", ")}) \u2014 removed.`
|
|
18089
|
+
);
|
|
18090
|
+
} else {
|
|
18091
|
+
this.lockManager.updatePatch(patch.id, {
|
|
18092
|
+
patch_content: result.patch_content,
|
|
18093
|
+
content_hash: result.content_hash
|
|
18094
|
+
});
|
|
18095
|
+
this.warnings.push(
|
|
18096
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: removed bundled diffs for ${result.strippedPaths.join(", ")}.`
|
|
18097
|
+
);
|
|
18098
|
+
}
|
|
18099
|
+
lockfileMutated = true;
|
|
18100
|
+
} catch {
|
|
18101
|
+
}
|
|
18102
|
+
}
|
|
18103
|
+
if (lockfileMutated) {
|
|
18104
|
+
this.lockManager.save();
|
|
18105
|
+
}
|
|
18106
|
+
}
|
|
17937
18107
|
/**
|
|
17938
18108
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
17939
18109
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -18189,7 +18359,7 @@ var ReplayService = class {
|
|
|
18189
18359
|
for (const file of files) {
|
|
18190
18360
|
if (isBinaryFile(file)) continue;
|
|
18191
18361
|
try {
|
|
18192
|
-
const content = await (0, import_promises6.readFile)((0,
|
|
18362
|
+
const content = await (0, import_promises6.readFile)((0, import_node_path10.join)(this.outputDir, file), "utf-8");
|
|
18193
18363
|
snapshot[file] = content;
|
|
18194
18364
|
} catch {
|
|
18195
18365
|
}
|
|
@@ -18706,6 +18876,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18706
18876
|
);
|
|
18707
18877
|
}
|
|
18708
18878
|
}
|
|
18879
|
+
if (warned.size > 0) {
|
|
18880
|
+
this.degradedReasons.push({
|
|
18881
|
+
code: "patch-base-unreachable",
|
|
18882
|
+
message: `${warned.size} customization file(s) could not be checked against the generation they were made on because that generation's tree was not reachable during this run. Customizations may not have been carried forward \u2014 review the result before merging, or widen the clone window and re-run replay.`
|
|
18883
|
+
});
|
|
18884
|
+
}
|
|
18709
18885
|
}
|
|
18710
18886
|
/**
|
|
18711
18887
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
@@ -18717,11 +18893,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18717
18893
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
18718
18894
|
for (const fileResult of result.fileResults) {
|
|
18719
18895
|
if (fileResult.status !== "conflict") continue;
|
|
18720
|
-
const filePath = (0,
|
|
18896
|
+
const filePath = (0, import_node_path10.join)(this.outputDir, fileResult.file);
|
|
18721
18897
|
try {
|
|
18722
|
-
const content = (0,
|
|
18898
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
18723
18899
|
const stripped = stripConflictMarkers(content);
|
|
18724
|
-
(0,
|
|
18900
|
+
(0, import_node_fs3.writeFileSync)(filePath, stripped);
|
|
18725
18901
|
} catch {
|
|
18726
18902
|
}
|
|
18727
18903
|
}
|
|
@@ -18749,12 +18925,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18749
18925
|
}
|
|
18750
18926
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18751
18927
|
readFernignorePatterns() {
|
|
18752
|
-
|
|
18753
|
-
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
18754
|
-
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
18928
|
+
return readFernignorePatterns(this.outputDir);
|
|
18755
18929
|
}
|
|
18756
18930
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18757
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
18931
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted, degradedReasons) {
|
|
18932
|
+
const degraded = dedupeDegradedReasons(degradedReasons ?? []);
|
|
18758
18933
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
18759
18934
|
const conflictDetails = conflictResults.map((r) => {
|
|
18760
18935
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -18794,10 +18969,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18794
18969
|
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
18795
18970
|
})) : void 0,
|
|
18796
18971
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
18797
|
-
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
18972
|
+
warnings: warnings && warnings.length > 0 ? warnings : void 0,
|
|
18973
|
+
degraded: degraded.length > 0 ? true : void 0,
|
|
18974
|
+
degradedReasons: degraded.length > 0 ? degraded : void 0
|
|
18798
18975
|
};
|
|
18799
18976
|
}
|
|
18800
18977
|
};
|
|
18978
|
+
function dedupeDegradedReasons(reasons) {
|
|
18979
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18980
|
+
const out = [];
|
|
18981
|
+
for (const reason of reasons) {
|
|
18982
|
+
const key = `${reason.code}\0${reason.message}`;
|
|
18983
|
+
if (seen.has(key)) continue;
|
|
18984
|
+
seen.add(key);
|
|
18985
|
+
out.push(reason);
|
|
18986
|
+
}
|
|
18987
|
+
return out;
|
|
18988
|
+
}
|
|
18801
18989
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
18802
18990
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
18803
18991
|
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
@@ -18846,7 +19034,7 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
18846
19034
|
}
|
|
18847
19035
|
}
|
|
18848
19036
|
}
|
|
18849
|
-
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } =
|
|
19037
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, matchesFernignore);
|
|
18850
19038
|
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
18851
19039
|
if (unchanged) {
|
|
18852
19040
|
return {
|
|
@@ -18881,7 +19069,38 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
18881
19069
|
strippedPaths
|
|
18882
19070
|
};
|
|
18883
19071
|
}
|
|
18884
|
-
function
|
|
19072
|
+
function isInfrastructureFile(filePath) {
|
|
19073
|
+
return filePath === ".fernignore" || filePath.startsWith(".fern/");
|
|
19074
|
+
}
|
|
19075
|
+
function computeInfrastructureFileCleanse(patch, computeContentHash2) {
|
|
19076
|
+
const { stripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, isInfrastructureFile);
|
|
19077
|
+
if (!stripped) {
|
|
19078
|
+
return {
|
|
19079
|
+
unchanged: true,
|
|
19080
|
+
remove: false,
|
|
19081
|
+
patch_content: patch.patch_content,
|
|
19082
|
+
content_hash: patch.content_hash,
|
|
19083
|
+
strippedPaths: []
|
|
19084
|
+
};
|
|
19085
|
+
}
|
|
19086
|
+
if (newPatchContent.trim() === "" || patch.files.length === 0) {
|
|
19087
|
+
return {
|
|
19088
|
+
unchanged: false,
|
|
19089
|
+
remove: true,
|
|
19090
|
+
patch_content: "",
|
|
19091
|
+
content_hash: "",
|
|
19092
|
+
strippedPaths: strippedSectionPaths
|
|
19093
|
+
};
|
|
19094
|
+
}
|
|
19095
|
+
return {
|
|
19096
|
+
unchanged: false,
|
|
19097
|
+
remove: false,
|
|
19098
|
+
patch_content: newPatchContent,
|
|
19099
|
+
content_hash: computeContentHash2(newPatchContent),
|
|
19100
|
+
strippedPaths: strippedSectionPaths
|
|
19101
|
+
};
|
|
19102
|
+
}
|
|
19103
|
+
function stripDiffSectionsByPredicate(patchContent, shouldStrip) {
|
|
18885
19104
|
const lines = patchContent.split("\n");
|
|
18886
19105
|
const sectionStarts = [];
|
|
18887
19106
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -18903,7 +19122,7 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
18903
19122
|
const section = sectionStarts[i];
|
|
18904
19123
|
const next = sectionStarts[i + 1];
|
|
18905
19124
|
const endIdx = next ? next.idx : lines.length;
|
|
18906
|
-
if (section.path !== null &&
|
|
19125
|
+
if (section.path !== null && shouldStrip(section.path)) {
|
|
18907
19126
|
stripped = true;
|
|
18908
19127
|
strippedSectionPaths.push(section.path);
|
|
18909
19128
|
continue;
|
|
@@ -18915,8 +19134,8 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
18915
19134
|
|
|
18916
19135
|
// src/FernignoreMigrator.ts
|
|
18917
19136
|
var import_node_crypto2 = require("crypto");
|
|
18918
|
-
var
|
|
18919
|
-
var
|
|
19137
|
+
var import_node_fs4 = require("fs");
|
|
19138
|
+
var import_node_path11 = require("path");
|
|
18920
19139
|
var import_yaml2 = __toESM(require_dist3(), 1);
|
|
18921
19140
|
var FernignoreMigrator = class {
|
|
18922
19141
|
git;
|
|
@@ -18928,14 +19147,14 @@ var FernignoreMigrator = class {
|
|
|
18928
19147
|
this.outputDir = outputDir;
|
|
18929
19148
|
}
|
|
18930
19149
|
fernignoreExists() {
|
|
18931
|
-
return (0,
|
|
19150
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path11.join)(this.outputDir, ".fernignore"));
|
|
18932
19151
|
}
|
|
18933
19152
|
readFernignorePatterns() {
|
|
18934
|
-
const fernignorePath = (0,
|
|
18935
|
-
if (!(0,
|
|
19153
|
+
const fernignorePath = (0, import_node_path11.join)(this.outputDir, ".fernignore");
|
|
19154
|
+
if (!(0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
18936
19155
|
return [];
|
|
18937
19156
|
}
|
|
18938
|
-
const content = (0,
|
|
19157
|
+
const content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
18939
19158
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
18940
19159
|
}
|
|
18941
19160
|
/** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
|
|
@@ -19040,16 +19259,16 @@ var FernignoreMigrator = class {
|
|
|
19040
19259
|
return results;
|
|
19041
19260
|
}
|
|
19042
19261
|
readFileContent(filePath) {
|
|
19043
|
-
const fullPath = (0,
|
|
19044
|
-
if (!(0,
|
|
19262
|
+
const fullPath = (0, import_node_path11.join)(this.outputDir, filePath);
|
|
19263
|
+
if (!(0, import_node_fs4.existsSync)(fullPath)) {
|
|
19045
19264
|
return null;
|
|
19046
19265
|
}
|
|
19047
19266
|
try {
|
|
19048
|
-
const stat = (0,
|
|
19267
|
+
const stat = (0, import_node_fs4.statSync)(fullPath);
|
|
19049
19268
|
if (stat.isDirectory()) {
|
|
19050
19269
|
return null;
|
|
19051
19270
|
}
|
|
19052
|
-
return (0,
|
|
19271
|
+
return (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
19053
19272
|
} catch {
|
|
19054
19273
|
return null;
|
|
19055
19274
|
}
|
|
@@ -19105,27 +19324,27 @@ var FernignoreMigrator = class {
|
|
|
19105
19324
|
};
|
|
19106
19325
|
}
|
|
19107
19326
|
movePatternsToReplayYml(patterns) {
|
|
19108
|
-
const replayYmlPath = (0,
|
|
19327
|
+
const replayYmlPath = (0, import_node_path11.join)(this.outputDir, ".fern", "replay.yml");
|
|
19109
19328
|
let config = {};
|
|
19110
|
-
if ((0,
|
|
19111
|
-
const content = (0,
|
|
19329
|
+
if ((0, import_node_fs4.existsSync)(replayYmlPath)) {
|
|
19330
|
+
const content = (0, import_node_fs4.readFileSync)(replayYmlPath, "utf-8");
|
|
19112
19331
|
config = (0, import_yaml2.parse)(content) ?? {};
|
|
19113
19332
|
}
|
|
19114
19333
|
const existing = config.exclude ?? [];
|
|
19115
19334
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
19116
19335
|
config.exclude = merged;
|
|
19117
|
-
const dir = (0,
|
|
19118
|
-
if (!(0,
|
|
19119
|
-
(0,
|
|
19336
|
+
const dir = (0, import_node_path11.dirname)(replayYmlPath);
|
|
19337
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
19338
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
19120
19339
|
}
|
|
19121
|
-
(0,
|
|
19340
|
+
(0, import_node_fs4.writeFileSync)(replayYmlPath, (0, import_yaml2.stringify)(config, { lineWidth: 0 }), "utf-8");
|
|
19122
19341
|
}
|
|
19123
19342
|
};
|
|
19124
19343
|
|
|
19125
19344
|
// src/commands/bootstrap.ts
|
|
19126
19345
|
var import_node_crypto3 = require("crypto");
|
|
19127
|
-
var
|
|
19128
|
-
var
|
|
19346
|
+
var import_node_fs5 = require("fs");
|
|
19347
|
+
var import_node_path12 = require("path");
|
|
19129
19348
|
init_GitClient();
|
|
19130
19349
|
async function bootstrap(outputDir, options) {
|
|
19131
19350
|
const git = new GitClient(outputDir);
|
|
@@ -19292,10 +19511,10 @@ function parseGitLog(log) {
|
|
|
19292
19511
|
}
|
|
19293
19512
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
19294
19513
|
function ensureFernignoreEntries(outputDir) {
|
|
19295
|
-
const fernignorePath = (0,
|
|
19514
|
+
const fernignorePath = (0, import_node_path12.join)(outputDir, ".fernignore");
|
|
19296
19515
|
let content = "";
|
|
19297
|
-
if ((0,
|
|
19298
|
-
content = (0,
|
|
19516
|
+
if ((0, import_node_fs5.existsSync)(fernignorePath)) {
|
|
19517
|
+
content = (0, import_node_fs5.readFileSync)(fernignorePath, "utf-8");
|
|
19299
19518
|
}
|
|
19300
19519
|
const lines = content.split("\n");
|
|
19301
19520
|
const toAdd = [];
|
|
@@ -19311,15 +19530,15 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
19311
19530
|
content += "\n";
|
|
19312
19531
|
}
|
|
19313
19532
|
content += toAdd.join("\n") + "\n";
|
|
19314
|
-
(0,
|
|
19533
|
+
(0, import_node_fs5.writeFileSync)(fernignorePath, content, "utf-8");
|
|
19315
19534
|
return true;
|
|
19316
19535
|
}
|
|
19317
19536
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
19318
19537
|
function ensureGitattributesEntries(outputDir) {
|
|
19319
|
-
const gitattributesPath = (0,
|
|
19538
|
+
const gitattributesPath = (0, import_node_path12.join)(outputDir, ".gitattributes");
|
|
19320
19539
|
let content = "";
|
|
19321
|
-
if ((0,
|
|
19322
|
-
content = (0,
|
|
19540
|
+
if ((0, import_node_fs5.existsSync)(gitattributesPath)) {
|
|
19541
|
+
content = (0, import_node_fs5.readFileSync)(gitattributesPath, "utf-8");
|
|
19323
19542
|
}
|
|
19324
19543
|
const lines = content.split("\n");
|
|
19325
19544
|
const toAdd = [];
|
|
@@ -19335,7 +19554,7 @@ function ensureGitattributesEntries(outputDir) {
|
|
|
19335
19554
|
content += "\n";
|
|
19336
19555
|
}
|
|
19337
19556
|
content += toAdd.join("\n") + "\n";
|
|
19338
|
-
(0,
|
|
19557
|
+
(0, import_node_fs5.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
19339
19558
|
}
|
|
19340
19559
|
function computeContentHash(patchContent) {
|
|
19341
19560
|
const lines = patchContent.split("\n");
|
|
@@ -19346,6 +19565,7 @@ function computeContentHash(patchContent) {
|
|
|
19346
19565
|
}
|
|
19347
19566
|
|
|
19348
19567
|
// src/commands/forget.ts
|
|
19568
|
+
init_GitClient();
|
|
19349
19569
|
function parseDiffStat(patchContent) {
|
|
19350
19570
|
let additions = 0;
|
|
19351
19571
|
let deletions = 0;
|
|
@@ -19404,7 +19624,28 @@ var EMPTY_RESULT = {
|
|
|
19404
19624
|
totalPatches: 0,
|
|
19405
19625
|
warnings: []
|
|
19406
19626
|
};
|
|
19407
|
-
function
|
|
19627
|
+
async function dismissPatches(outputDir, lockManager, patches) {
|
|
19628
|
+
const git = new GitClient(outputDir);
|
|
19629
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
19630
|
+
for (const patch of patches) {
|
|
19631
|
+
const hashes = [patch.content_hash];
|
|
19632
|
+
if (patch.original_commit) {
|
|
19633
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
19634
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
19635
|
+
hashes.push(materialized.contentHash);
|
|
19636
|
+
}
|
|
19637
|
+
}
|
|
19638
|
+
lockManager.addDismissal({
|
|
19639
|
+
patch_id: patch.id,
|
|
19640
|
+
original_commit: patch.original_commit,
|
|
19641
|
+
original_message: patch.original_message,
|
|
19642
|
+
content_hashes: hashes,
|
|
19643
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19644
|
+
via: "forget"
|
|
19645
|
+
});
|
|
19646
|
+
}
|
|
19647
|
+
}
|
|
19648
|
+
async function forget(outputDir, options) {
|
|
19408
19649
|
const lockManager = new LockfileManager(outputDir);
|
|
19409
19650
|
if (!lockManager.exists()) {
|
|
19410
19651
|
return { ...EMPTY_RESULT };
|
|
@@ -19415,9 +19656,7 @@ function forget(outputDir, options) {
|
|
|
19415
19656
|
const removed = lock.patches.map(toMatchedPatch);
|
|
19416
19657
|
const warnings = buildWarnings(lock.patches);
|
|
19417
19658
|
if (!options.dryRun) {
|
|
19418
|
-
|
|
19419
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
19420
|
-
}
|
|
19659
|
+
await dismissPatches(outputDir, lockManager, lock.patches);
|
|
19421
19660
|
lockManager.clearPatches();
|
|
19422
19661
|
lockManager.save();
|
|
19423
19662
|
}
|
|
@@ -19446,8 +19685,8 @@ function forget(outputDir, options) {
|
|
|
19446
19685
|
}
|
|
19447
19686
|
const warnings = buildWarnings(patchesToRemove);
|
|
19448
19687
|
if (!options.dryRun) {
|
|
19688
|
+
await dismissPatches(outputDir, lockManager, patchesToRemove);
|
|
19449
19689
|
for (const patch of patchesToRemove) {
|
|
19450
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
19451
19690
|
lockManager.removePatch(patch.id);
|
|
19452
19691
|
}
|
|
19453
19692
|
lockManager.save();
|
|
@@ -19488,7 +19727,7 @@ function forget(outputDir, options) {
|
|
|
19488
19727
|
}
|
|
19489
19728
|
|
|
19490
19729
|
// src/commands/reset.ts
|
|
19491
|
-
var
|
|
19730
|
+
var import_node_fs6 = require("fs");
|
|
19492
19731
|
function reset(outputDir, options) {
|
|
19493
19732
|
const lockManager = new LockfileManager(outputDir);
|
|
19494
19733
|
if (!lockManager.exists()) {
|
|
@@ -19502,7 +19741,7 @@ function reset(outputDir, options) {
|
|
|
19502
19741
|
const lock = lockManager.read();
|
|
19503
19742
|
const patchCount = lock.patches.length;
|
|
19504
19743
|
if (!options?.dryRun) {
|
|
19505
|
-
(0,
|
|
19744
|
+
(0, import_node_fs6.unlinkSync)(lockManager.lockfilePath);
|
|
19506
19745
|
}
|
|
19507
19746
|
return {
|
|
19508
19747
|
success: true,
|
|
@@ -19514,7 +19753,7 @@ function reset(outputDir, options) {
|
|
|
19514
19753
|
|
|
19515
19754
|
// src/commands/resolve.ts
|
|
19516
19755
|
var import_promises7 = require("fs/promises");
|
|
19517
|
-
var
|
|
19756
|
+
var import_node_path13 = require("path");
|
|
19518
19757
|
init_GitClient();
|
|
19519
19758
|
async function resolve(outputDir, options) {
|
|
19520
19759
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -19555,15 +19794,16 @@ async function resolve(outputDir, options) {
|
|
|
19555
19794
|
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
19556
19795
|
if (patchesToCommit.length > 0) {
|
|
19557
19796
|
const currentGen = lock.current_generation;
|
|
19558
|
-
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
19797
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
19559
19798
|
const warnings = [];
|
|
19799
|
+
const patchesDismissed = [];
|
|
19560
19800
|
let patchesResolved = 0;
|
|
19561
19801
|
const pass1 = [];
|
|
19562
19802
|
for (const patch of patchesToCommit) {
|
|
19563
19803
|
const result = await computePerPatchDiffWithFallback(
|
|
19564
19804
|
patch,
|
|
19565
19805
|
(f) => git.showFile(currentGen, f),
|
|
19566
|
-
(f) => (0, import_promises7.readFile)((0,
|
|
19806
|
+
(f) => (0, import_promises7.readFile)((0, import_node_path13.join)(outputDir, f), "utf-8").catch(() => null),
|
|
19567
19807
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
19568
19808
|
);
|
|
19569
19809
|
if (result === null) {
|
|
@@ -19597,7 +19837,27 @@ async function resolve(outputDir, options) {
|
|
|
19597
19837
|
const r = pass1[i];
|
|
19598
19838
|
if (r.kind === "skip") continue;
|
|
19599
19839
|
if (r.kind === "revert") {
|
|
19840
|
+
const hashes = [patch.content_hash];
|
|
19841
|
+
if (patch.original_commit) {
|
|
19842
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
19843
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
19844
|
+
hashes.push(materialized.contentHash);
|
|
19845
|
+
} else if (materialized.status === "unreachable") {
|
|
19846
|
+
warnings.push(
|
|
19847
|
+
`Patch ${patch.id}: original commit ${patch.original_commit.slice(0, 7)} is unreachable; dismissed using the stored content hash only. If this commit reappears in history, the customization may be re-detected once.`
|
|
19848
|
+
);
|
|
19849
|
+
}
|
|
19850
|
+
}
|
|
19851
|
+
lockManager.addDismissal({
|
|
19852
|
+
patch_id: patch.id,
|
|
19853
|
+
original_commit: patch.original_commit,
|
|
19854
|
+
original_message: patch.original_message,
|
|
19855
|
+
content_hashes: hashes,
|
|
19856
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19857
|
+
via: "resolve"
|
|
19858
|
+
});
|
|
19600
19859
|
lockManager.removePatch(patch.id);
|
|
19860
|
+
patchesDismissed.push({ id: patch.id, message: patch.original_message });
|
|
19601
19861
|
continue;
|
|
19602
19862
|
}
|
|
19603
19863
|
if (lastIndexByHash.get(r.hash) !== i) {
|
|
@@ -19619,7 +19879,7 @@ async function resolve(outputDir, options) {
|
|
|
19619
19879
|
const theirsSnapshot = {};
|
|
19620
19880
|
for (const file of filesForSnapshot) {
|
|
19621
19881
|
if (isBinaryFile(file)) continue;
|
|
19622
|
-
const content = await (0, import_promises7.readFile)((0,
|
|
19882
|
+
const content = await (0, import_promises7.readFile)((0, import_node_path13.join)(outputDir, file), "utf-8").catch(() => null);
|
|
19623
19883
|
if (content != null) {
|
|
19624
19884
|
theirsSnapshot[file] = content;
|
|
19625
19885
|
}
|
|
@@ -19646,6 +19906,7 @@ async function resolve(outputDir, options) {
|
|
|
19646
19906
|
commitSha: commitSha2,
|
|
19647
19907
|
phase: "committed",
|
|
19648
19908
|
patchesResolved,
|
|
19909
|
+
patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
|
|
19649
19910
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
19650
19911
|
};
|
|
19651
19912
|
}
|
|
@@ -19693,7 +19954,9 @@ function status(outputDir) {
|
|
|
19693
19954
|
lastGeneration: void 0,
|
|
19694
19955
|
patches: [],
|
|
19695
19956
|
unresolvedCount: 0,
|
|
19696
|
-
excludePatterns: []
|
|
19957
|
+
excludePatterns: [],
|
|
19958
|
+
dismissed: [],
|
|
19959
|
+
legacyDismissedCount: 0
|
|
19697
19960
|
};
|
|
19698
19961
|
}
|
|
19699
19962
|
const lock = lockManager.read();
|
|
@@ -19722,13 +19985,24 @@ function status(outputDir) {
|
|
|
19722
19985
|
}
|
|
19723
19986
|
const config = lockManager.getCustomizationsConfig();
|
|
19724
19987
|
const excludePatterns = config.exclude ?? [];
|
|
19988
|
+
const dismissals = lock.dismissals ?? [];
|
|
19989
|
+
const dismissed = dismissals.map((d) => ({
|
|
19990
|
+
message: d.original_message,
|
|
19991
|
+
sha: d.original_commit.slice(0, 7),
|
|
19992
|
+
via: d.via,
|
|
19993
|
+
dismissedAt: d.dismissed_at
|
|
19994
|
+
}));
|
|
19995
|
+
const coveredHashes = new Set(dismissals.flatMap((d) => d.content_hashes));
|
|
19996
|
+
const legacyDismissedCount = (lock.forgotten_hashes ?? []).filter((h) => !coveredHashes.has(h)).length;
|
|
19725
19997
|
return {
|
|
19726
19998
|
initialized: true,
|
|
19727
19999
|
generationCount: lock.generations.length,
|
|
19728
20000
|
lastGeneration,
|
|
19729
20001
|
patches,
|
|
19730
20002
|
unresolvedCount,
|
|
19731
|
-
excludePatterns
|
|
20003
|
+
excludePatterns,
|
|
20004
|
+
dismissed,
|
|
20005
|
+
legacyDismissedCount
|
|
19732
20006
|
};
|
|
19733
20007
|
}
|
|
19734
20008
|
|
|
@@ -19816,7 +20090,7 @@ function parseArgs(argv) {
|
|
|
19816
20090
|
positionals.push(arg);
|
|
19817
20091
|
}
|
|
19818
20092
|
}
|
|
19819
|
-
return { command, dir: (0,
|
|
20093
|
+
return { command, dir: (0, import_node_path14.resolve)(dir), flags, positionals };
|
|
19820
20094
|
}
|
|
19821
20095
|
async function runBootstrap(dir, flags) {
|
|
19822
20096
|
const dryRun = !!flags.dryRun;
|
|
@@ -19894,6 +20168,7 @@ async function runStatus(dir, flags) {
|
|
|
19894
20168
|
}
|
|
19895
20169
|
if (result.patches.length === 0) {
|
|
19896
20170
|
console.log("\nNo customizations tracked. Edit SDK files and commit \u2014 Replay will detect them on next generation.");
|
|
20171
|
+
printDismissedSection(result);
|
|
19897
20172
|
return;
|
|
19898
20173
|
}
|
|
19899
20174
|
const unresolvedSuffix = result.unresolvedCount > 0 ? `, ${result.unresolvedCount} unresolved` : "";
|
|
@@ -19931,6 +20206,26 @@ ${prefix}${patch.id} [${patch.type}] "${patch.message}"`);
|
|
|
19931
20206
|
console.log(` ... and ${result.patches.length - maxDisplay} more`);
|
|
19932
20207
|
}
|
|
19933
20208
|
}
|
|
20209
|
+
printDismissedSection(result);
|
|
20210
|
+
}
|
|
20211
|
+
function printDismissedSection(result) {
|
|
20212
|
+
if (result.dismissed.length === 0 && result.legacyDismissedCount === 0) {
|
|
20213
|
+
return;
|
|
20214
|
+
}
|
|
20215
|
+
const total = result.dismissed.length + result.legacyDismissedCount;
|
|
20216
|
+
console.log(`
|
|
20217
|
+
Dismissed customizations: ${total}`);
|
|
20218
|
+
for (const d of result.dismissed) {
|
|
20219
|
+
const shaSuffix = d.sha ? ` (${d.sha})` : "";
|
|
20220
|
+
const date = d.dismissedAt ? d.dismissedAt.slice(0, 10) : "";
|
|
20221
|
+
const dateSuffix = date ? ` on ${date}` : "";
|
|
20222
|
+
console.log(` "${d.message}"${shaSuffix} \u2014 dismissed via ${d.via}${dateSuffix}`);
|
|
20223
|
+
}
|
|
20224
|
+
if (result.legacyDismissedCount > 0) {
|
|
20225
|
+
console.log(
|
|
20226
|
+
` ... and ${result.legacyDismissedCount} dismissed before this version \u2014 details unavailable`
|
|
20227
|
+
);
|
|
20228
|
+
}
|
|
19934
20229
|
}
|
|
19935
20230
|
async function runDetect(dir) {
|
|
19936
20231
|
const lockManager = new LockfileManager(dir);
|
|
@@ -19942,6 +20237,7 @@ async function runDetect(dir) {
|
|
|
19942
20237
|
const git = new GitClient(dir);
|
|
19943
20238
|
const detector = new ReplayDetector(git, lockManager, dir);
|
|
19944
20239
|
const { patches, revertedPatchIds } = await detector.detectNewPatches();
|
|
20240
|
+
printDegradedBanner(detector.degradedReasons);
|
|
19945
20241
|
if (detector.warnings.length > 0) {
|
|
19946
20242
|
for (const w of detector.warnings) console.log(`Warning: ${w}`);
|
|
19947
20243
|
console.log();
|
|
@@ -19972,7 +20268,20 @@ async function runReplay(dir, flags) {
|
|
|
19972
20268
|
const report = await service.runReplay({ dryRun });
|
|
19973
20269
|
printReport(report);
|
|
19974
20270
|
}
|
|
20271
|
+
function printDegradedBanner(reasons) {
|
|
20272
|
+
if (reasons.length === 0) return;
|
|
20273
|
+
console.log("============================================================");
|
|
20274
|
+
console.log(" REPLAY DEGRADED \u2014 customizations may not have been");
|
|
20275
|
+
console.log(" carried forward in this run. Review the result before");
|
|
20276
|
+
console.log(" merging.");
|
|
20277
|
+
console.log("============================================================");
|
|
20278
|
+
for (const reason of reasons) {
|
|
20279
|
+
console.log(` [${reason.code}] ${reason.message}`);
|
|
20280
|
+
}
|
|
20281
|
+
console.log();
|
|
20282
|
+
}
|
|
19975
20283
|
function printReport(report) {
|
|
20284
|
+
printDegradedBanner(report.degradedReasons ?? []);
|
|
19976
20285
|
console.log(`Flow: ${report.flow}`);
|
|
19977
20286
|
console.log(`Patches detected: ${report.patchesDetected}`);
|
|
19978
20287
|
console.log(`Patches applied: ${report.patchesApplied}`);
|
|
@@ -20140,7 +20449,7 @@ async function runForget(dir, flags, positionals) {
|
|
|
20140
20449
|
const pattern = !isPatchIdMode && positionals.length > 0 ? positionals[0] : void 0;
|
|
20141
20450
|
const patchIds = isPatchIdMode ? positionals : void 0;
|
|
20142
20451
|
if (all) {
|
|
20143
|
-
const preview = forget(dir, { all: true, dryRun: true });
|
|
20452
|
+
const preview = await forget(dir, { all: true, dryRun: true });
|
|
20144
20453
|
if (!preview.initialized) {
|
|
20145
20454
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20146
20455
|
process.exit(1);
|
|
@@ -20159,14 +20468,14 @@ async function runForget(dir, flags, positionals) {
|
|
|
20159
20468
|
return;
|
|
20160
20469
|
}
|
|
20161
20470
|
}
|
|
20162
|
-
const finalResult = dryRun ? preview : forget(dir, { all: true });
|
|
20471
|
+
const finalResult = dryRun ? preview : await forget(dir, { all: true });
|
|
20163
20472
|
printForgetWarnings(finalResult.warnings);
|
|
20164
20473
|
const verb2 = dryRun ? "Would remove" : "Removed";
|
|
20165
20474
|
console.log(`${verb2} ${finalResult.removed.length} patches. ${finalResult.remaining} patches remaining.`);
|
|
20166
20475
|
return;
|
|
20167
20476
|
}
|
|
20168
20477
|
if (isPatchIdMode && patchIds) {
|
|
20169
|
-
const result2 = forget(dir, { patchIds, dryRun });
|
|
20478
|
+
const result2 = await forget(dir, { patchIds, dryRun });
|
|
20170
20479
|
if (!result2.initialized) {
|
|
20171
20480
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20172
20481
|
process.exit(1);
|
|
@@ -20193,7 +20502,7 @@ async function runForget(dir, flags, positionals) {
|
|
|
20193
20502
|
console.log(`${result2.remaining} patches remaining.`);
|
|
20194
20503
|
return;
|
|
20195
20504
|
}
|
|
20196
|
-
const searchResult = pattern ? forget(dir, { pattern }) : forget(dir);
|
|
20505
|
+
const searchResult = pattern ? await forget(dir, { pattern }) : await forget(dir);
|
|
20197
20506
|
if (!searchResult.initialized) {
|
|
20198
20507
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20199
20508
|
process.exit(1);
|
|
@@ -20241,7 +20550,7 @@ This will remove ${selectedIds.length} patch(es). Affected files will be overwri
|
|
|
20241
20550
|
return;
|
|
20242
20551
|
}
|
|
20243
20552
|
}
|
|
20244
|
-
const result = forget(dir, { patchIds: selectedIds, dryRun });
|
|
20553
|
+
const result = await forget(dir, { patchIds: selectedIds, dryRun });
|
|
20245
20554
|
printForgetWarnings(result.warnings);
|
|
20246
20555
|
const verb = dryRun ? "Would remove" : "Removed";
|
|
20247
20556
|
console.log(`
|
|
@@ -20308,12 +20617,23 @@ Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' agai
|
|
|
20308
20617
|
if (result.patchesResolved) {
|
|
20309
20618
|
console.log(`Resolution complete. Updated ${result.patchesResolved} patch(es) in lockfile.`);
|
|
20310
20619
|
}
|
|
20620
|
+
if (result.patchesDismissed && result.patchesDismissed.length > 0) {
|
|
20621
|
+
console.log(`Permanently dismissed ${result.patchesDismissed.length} customization(s) (you kept the generated code):`);
|
|
20622
|
+
for (const d of result.patchesDismissed) {
|
|
20623
|
+
console.log(` "${d.message}" (${d.id}) \u2014 will not return on future generations`);
|
|
20624
|
+
}
|
|
20625
|
+
}
|
|
20626
|
+
if (result.warnings) {
|
|
20627
|
+
for (const w of result.warnings) {
|
|
20628
|
+
console.log(`Warning: ${w}`);
|
|
20629
|
+
}
|
|
20630
|
+
}
|
|
20311
20631
|
console.log(`Created [fern-replay] commit: ${result.commitSha?.slice(0, 7)}`);
|
|
20312
20632
|
console.log("Push to update the PR.");
|
|
20313
20633
|
}
|
|
20314
20634
|
async function main() {
|
|
20315
20635
|
const { command, dir, flags, positionals } = parseArgs(process.argv);
|
|
20316
|
-
if (!(0,
|
|
20636
|
+
if (!(0, import_node_fs7.existsSync)(dir)) {
|
|
20317
20637
|
console.error(`Directory not found: ${dir}`);
|
|
20318
20638
|
process.exit(1);
|
|
20319
20639
|
}
|