@fern-api/replay 0.18.0 → 0.19.1
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 +522 -125
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +467 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -3
- package/dist/index.d.ts +208 -3
- package/dist/index.js +450 -99
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/cli.cjs
CHANGED
|
@@ -13245,15 +13245,15 @@ __export(PatchApplyTheirs_exports, {
|
|
|
13245
13245
|
async function applyPatchToContent(base, patchContent, filePath) {
|
|
13246
13246
|
const fileDiff = extractFileDiff(patchContent, filePath);
|
|
13247
13247
|
if (!fileDiff) return null;
|
|
13248
|
-
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-"));
|
|
13249
13249
|
const tempGit = new GitClient(tempDir);
|
|
13250
13250
|
try {
|
|
13251
13251
|
await tempGit.exec(["init"]);
|
|
13252
13252
|
await tempGit.exec(["config", "user.email", "migrate@fern.com"]);
|
|
13253
13253
|
await tempGit.exec(["config", "user.name", "Fern Replay Migration"]);
|
|
13254
13254
|
await tempGit.exec(["config", "commit.gpgSign", "false"]);
|
|
13255
|
-
const tempFilePath = (0,
|
|
13256
|
-
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 });
|
|
13257
13257
|
await (0, import_promises5.writeFile)(tempFilePath, base);
|
|
13258
13258
|
await tempGit.exec(["add", filePath]);
|
|
13259
13259
|
await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
|
|
@@ -13284,20 +13284,20 @@ function extractFileDiff(patchContent, filePath) {
|
|
|
13284
13284
|
}
|
|
13285
13285
|
return out.length > 0 ? out.join("\n") + "\n" : null;
|
|
13286
13286
|
}
|
|
13287
|
-
var import_promises5, import_node_os3,
|
|
13287
|
+
var import_promises5, import_node_os3, import_node_path9;
|
|
13288
13288
|
var init_PatchApplyTheirs = __esm({
|
|
13289
13289
|
"src/PatchApplyTheirs.ts"() {
|
|
13290
13290
|
"use strict";
|
|
13291
13291
|
import_promises5 = require("fs/promises");
|
|
13292
13292
|
import_node_os3 = require("os");
|
|
13293
|
-
|
|
13293
|
+
import_node_path9 = require("path");
|
|
13294
13294
|
init_GitClient();
|
|
13295
13295
|
}
|
|
13296
13296
|
});
|
|
13297
13297
|
|
|
13298
13298
|
// src/cli.ts
|
|
13299
|
-
var
|
|
13300
|
-
var
|
|
13299
|
+
var import_node_path14 = require("path");
|
|
13300
|
+
var import_node_fs7 = require("fs");
|
|
13301
13301
|
var import_node_readline = require("readline");
|
|
13302
13302
|
init_GitClient();
|
|
13303
13303
|
|
|
@@ -14827,6 +14827,22 @@ var LockfileManager = class {
|
|
|
14827
14827
|
this.lock.forgotten_hashes.push(hash);
|
|
14828
14828
|
}
|
|
14829
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
|
+
}
|
|
14830
14846
|
getUnresolvedPatches() {
|
|
14831
14847
|
this.ensureLoaded();
|
|
14832
14848
|
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
@@ -15035,12 +15051,14 @@ async function capturePatchSnapshot(git, files, treeish) {
|
|
|
15035
15051
|
return snapshot;
|
|
15036
15052
|
}
|
|
15037
15053
|
function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
|
|
15038
|
-
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/"));
|
|
15039
15057
|
const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
|
|
15040
15058
|
const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
|
|
15041
15059
|
const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
|
|
15042
15060
|
const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
|
|
15043
|
-
return { files, sensitiveFiles, fernignoredFiles };
|
|
15061
|
+
return { files, sensitiveFiles, fernignoredFiles, infrastructureFiles };
|
|
15044
15062
|
}
|
|
15045
15063
|
var ReplayDetector = class {
|
|
15046
15064
|
git;
|
|
@@ -15048,6 +15066,15 @@ var ReplayDetector = class {
|
|
|
15048
15066
|
sdkOutputDir;
|
|
15049
15067
|
fernignorePatterns;
|
|
15050
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 = [];
|
|
15051
15078
|
constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
|
|
15052
15079
|
this.git = git;
|
|
15053
15080
|
this.lockManager = lockManager;
|
|
@@ -15118,6 +15145,10 @@ var ReplayDetector = class {
|
|
|
15118
15145
|
this.warnings.push(
|
|
15119
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.`
|
|
15120
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
|
+
});
|
|
15121
15152
|
}
|
|
15122
15153
|
return { patches: [], revertedPatchIds: [] };
|
|
15123
15154
|
}
|
|
@@ -15126,6 +15157,12 @@ var ReplayDetector = class {
|
|
|
15126
15157
|
this.warnings.push(
|
|
15127
15158
|
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
15128
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
|
+
}
|
|
15129
15166
|
return this.detectPatchesViaTreeDiff(
|
|
15130
15167
|
lastGen,
|
|
15131
15168
|
/* commitKnownMissing */
|
|
@@ -15165,54 +15202,27 @@ var ReplayDetector = class {
|
|
|
15165
15202
|
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
15166
15203
|
continue;
|
|
15167
15204
|
}
|
|
15168
|
-
const
|
|
15169
|
-
|
|
15170
|
-
try {
|
|
15171
|
-
filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15172
|
-
} catch {
|
|
15205
|
+
const materialized = await this.materializeCommitPatch(commit.sha);
|
|
15206
|
+
if (materialized.sensitiveFiles.length > 0) {
|
|
15173
15207
|
this.warnings.push(
|
|
15174
|
-
`
|
|
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.`
|
|
15175
15209
|
);
|
|
15176
|
-
continue;
|
|
15177
15210
|
}
|
|
15178
|
-
|
|
15179
|
-
filesOutput,
|
|
15180
|
-
this.fernignorePatterns
|
|
15181
|
-
);
|
|
15182
|
-
if (sensitiveFiles.length > 0) {
|
|
15211
|
+
if (materialized.fernignoredFiles.length > 0) {
|
|
15183
15212
|
this.warnings.push(
|
|
15184
|
-
|
|
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.`
|
|
15185
15214
|
);
|
|
15186
15215
|
}
|
|
15187
|
-
if (
|
|
15216
|
+
if (materialized.status === "unreachable") {
|
|
15188
15217
|
this.warnings.push(
|
|
15189
|
-
|
|
15218
|
+
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
15190
15219
|
);
|
|
15191
|
-
}
|
|
15192
|
-
if (files.length === 0) {
|
|
15193
15220
|
continue;
|
|
15194
15221
|
}
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
let patchContent;
|
|
15198
|
-
if (wasFiltered && parentSha) {
|
|
15199
|
-
try {
|
|
15200
|
-
patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
|
|
15201
|
-
} catch {
|
|
15202
|
-
continue;
|
|
15203
|
-
}
|
|
15204
|
-
if (!patchContent.trim()) continue;
|
|
15205
|
-
} else {
|
|
15206
|
-
try {
|
|
15207
|
-
patchContent = await this.git.formatPatch(commit.sha);
|
|
15208
|
-
} catch {
|
|
15209
|
-
this.warnings.push(
|
|
15210
|
-
`Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
|
|
15211
|
-
);
|
|
15212
|
-
continue;
|
|
15213
|
-
}
|
|
15222
|
+
if (materialized.status === "empty") {
|
|
15223
|
+
continue;
|
|
15214
15224
|
}
|
|
15215
|
-
const contentHash =
|
|
15225
|
+
const { files, patchContent, contentHash } = materialized;
|
|
15216
15226
|
if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
15217
15227
|
continue;
|
|
15218
15228
|
}
|
|
@@ -15289,6 +15299,63 @@ var ReplayDetector = class {
|
|
|
15289
15299
|
const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
|
|
15290
15300
|
return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
|
|
15291
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
|
+
}
|
|
15292
15359
|
/**
|
|
15293
15360
|
* Compute content hash for deduplication.
|
|
15294
15361
|
*
|
|
@@ -15393,6 +15460,13 @@ var ReplayDetector = class {
|
|
|
15393
15460
|
async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
|
|
15394
15461
|
const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
|
|
15395
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
|
+
});
|
|
15396
15470
|
return this.detectPatchesViaCommitScan();
|
|
15397
15471
|
}
|
|
15398
15472
|
const lockForGuard = this.lockManager.read();
|
|
@@ -15505,10 +15579,7 @@ var ReplayDetector = class {
|
|
|
15505
15579
|
continue;
|
|
15506
15580
|
}
|
|
15507
15581
|
const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
|
|
15508
|
-
const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
|
|
15509
|
-
filesOutput,
|
|
15510
|
-
this.fernignorePatterns
|
|
15511
|
-
);
|
|
15582
|
+
const { files, sensitiveFiles, fernignoredFiles, infrastructureFiles } = filterInfrastructureAndSensitiveFiles(filesOutput, this.fernignorePatterns);
|
|
15512
15583
|
if (sensitiveFiles.length > 0) {
|
|
15513
15584
|
this.warnings.push(
|
|
15514
15585
|
`Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
|
|
@@ -15519,7 +15590,8 @@ var ReplayDetector = class {
|
|
|
15519
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.`
|
|
15520
15591
|
);
|
|
15521
15592
|
}
|
|
15522
|
-
|
|
15593
|
+
const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0 || infrastructureFiles.length > 0;
|
|
15594
|
+
if (wasFiltered && files.length > 0) {
|
|
15523
15595
|
if (parents.length === 0) {
|
|
15524
15596
|
continue;
|
|
15525
15597
|
}
|
|
@@ -15623,9 +15695,9 @@ var ReplayDetector = class {
|
|
|
15623
15695
|
};
|
|
15624
15696
|
|
|
15625
15697
|
// src/ReplayService.ts
|
|
15626
|
-
var
|
|
15698
|
+
var import_node_fs3 = require("fs");
|
|
15627
15699
|
var import_promises6 = require("fs/promises");
|
|
15628
|
-
var
|
|
15700
|
+
var import_node_path10 = require("path");
|
|
15629
15701
|
init_GitClient();
|
|
15630
15702
|
|
|
15631
15703
|
// src/ReplayApplicator.ts
|
|
@@ -16377,6 +16449,51 @@ async function buildMergePlan(args) {
|
|
|
16377
16449
|
// src/applicator/runMergeAndRecord.ts
|
|
16378
16450
|
var import_promises2 = require("fs/promises");
|
|
16379
16451
|
var import_node_path5 = require("path");
|
|
16452
|
+
|
|
16453
|
+
// src/autoversion.ts
|
|
16454
|
+
var AUTOVERSION_PLACEHOLDERS = [
|
|
16455
|
+
"v0.0.0-fern-placeholder",
|
|
16456
|
+
// Go -- a superstring of the default; check first
|
|
16457
|
+
"0.0.0-fern-placeholder",
|
|
16458
|
+
// TS/Java/C#/Ruby/PHP/Rust/default
|
|
16459
|
+
"0.0.0.dev0"
|
|
16460
|
+
// Python (PEP 440 -- Poetry rejects hyphens)
|
|
16461
|
+
];
|
|
16462
|
+
var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
|
|
16463
|
+
var VERSION_MASK = "@@FERN_VERSION@@";
|
|
16464
|
+
function maskVersions(line) {
|
|
16465
|
+
return line.replace(VERSION_TOKEN, VERSION_MASK);
|
|
16466
|
+
}
|
|
16467
|
+
function containsPlaceholder(content) {
|
|
16468
|
+
if (!content) return false;
|
|
16469
|
+
return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
|
|
16470
|
+
}
|
|
16471
|
+
function remapManagedLines(genBase, target, theirs) {
|
|
16472
|
+
if (!containsPlaceholder(genBase)) return theirs;
|
|
16473
|
+
const managedMasks = /* @__PURE__ */ new Set();
|
|
16474
|
+
for (const line of genBase.split("\n")) {
|
|
16475
|
+
if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
|
|
16476
|
+
}
|
|
16477
|
+
if (managedMasks.size === 0) return theirs;
|
|
16478
|
+
const targetByMask = /* @__PURE__ */ new Map();
|
|
16479
|
+
for (const line of target.split("\n")) {
|
|
16480
|
+
const mask = maskVersions(line);
|
|
16481
|
+
if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
|
|
16482
|
+
}
|
|
16483
|
+
return theirs.split("\n").map((line) => {
|
|
16484
|
+
const mask = maskVersions(line);
|
|
16485
|
+
const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
|
|
16486
|
+
return replacement !== void 0 && replacement !== line ? replacement : line;
|
|
16487
|
+
}).join("\n");
|
|
16488
|
+
}
|
|
16489
|
+
function neutralizeAutoversionTheirs(genBase, theirs) {
|
|
16490
|
+
return remapManagedLines(genBase, genBase, theirs);
|
|
16491
|
+
}
|
|
16492
|
+
function alignAutoversionToOurs(genBase, ours, theirs) {
|
|
16493
|
+
return remapManagedLines(genBase, ours, theirs);
|
|
16494
|
+
}
|
|
16495
|
+
|
|
16496
|
+
// src/applicator/runMergeAndRecord.ts
|
|
16380
16497
|
async function runMergeAndRecord(args) {
|
|
16381
16498
|
const { plan, inputs, patch, accumulator } = args;
|
|
16382
16499
|
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
@@ -16407,11 +16524,13 @@ async function runMergeAndRecord(args) {
|
|
|
16407
16524
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
16408
16525
|
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
16409
16526
|
}
|
|
16410
|
-
const
|
|
16527
|
+
const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
|
|
16528
|
+
const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
|
|
16529
|
+
const merged = threeWayMerge(mergeBase, ours, theirs);
|
|
16411
16530
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
16412
16531
|
await (0, import_promises2.writeFile)(oursPath, merged.content);
|
|
16413
16532
|
if (merged.hasConflicts) {
|
|
16414
|
-
accumulator.recordConflict(resolvedPath,
|
|
16533
|
+
accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
|
|
16415
16534
|
return {
|
|
16416
16535
|
file: resolvedPath,
|
|
16417
16536
|
status: "conflict",
|
|
@@ -16420,7 +16539,7 @@ async function runMergeAndRecord(args) {
|
|
|
16420
16539
|
conflictMetadata: metadata
|
|
16421
16540
|
};
|
|
16422
16541
|
}
|
|
16423
|
-
accumulator.recordMerge(resolvedPath,
|
|
16542
|
+
accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
|
|
16424
16543
|
return {
|
|
16425
16544
|
file: resolvedPath,
|
|
16426
16545
|
status: "merged",
|
|
@@ -16935,6 +17054,15 @@ function isDiffLineForFile(diffLine, filePath) {
|
|
|
16935
17054
|
return match2 !== null && match2[1] === filePath;
|
|
16936
17055
|
}
|
|
16937
17056
|
|
|
17057
|
+
// src/shared/fernignore.ts
|
|
17058
|
+
var import_node_fs2 = require("fs");
|
|
17059
|
+
var import_node_path7 = require("path");
|
|
17060
|
+
function readFernignorePatterns(outputDir) {
|
|
17061
|
+
const fernignorePath = (0, import_node_path7.join)(outputDir, ".fernignore");
|
|
17062
|
+
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
17063
|
+
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
17064
|
+
}
|
|
17065
|
+
|
|
16938
17066
|
// src/ReplayCommitter.ts
|
|
16939
17067
|
var ReplayCommitter = class {
|
|
16940
17068
|
git;
|
|
@@ -17040,7 +17168,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
17040
17168
|
// src/PatchRegionDiff.ts
|
|
17041
17169
|
var import_promises4 = require("fs/promises");
|
|
17042
17170
|
var import_node_os2 = require("os");
|
|
17043
|
-
var
|
|
17171
|
+
var import_node_path8 = require("path");
|
|
17044
17172
|
var import_node_child_process = require("child_process");
|
|
17045
17173
|
init_HybridReconstruction();
|
|
17046
17174
|
function normalizeHunkBody(hunk) {
|
|
@@ -17258,10 +17386,10 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
|
|
|
17258
17386
|
}
|
|
17259
17387
|
async function unifiedDiff(beforeContent, afterContent, filePath) {
|
|
17260
17388
|
if (beforeContent === afterContent) return "";
|
|
17261
|
-
const tmp = await (0, import_promises4.mkdtemp)((0,
|
|
17389
|
+
const tmp = await (0, import_promises4.mkdtemp)((0, import_node_path8.join)((0, import_node_os2.tmpdir)(), "fern-replay-pp-"));
|
|
17262
17390
|
try {
|
|
17263
|
-
const beforePath = (0,
|
|
17264
|
-
const afterPath = (0,
|
|
17391
|
+
const beforePath = (0, import_node_path8.join)(tmp, "before");
|
|
17392
|
+
const afterPath = (0, import_node_path8.join)(tmp, "after");
|
|
17265
17393
|
await (0, import_promises4.writeFile)(beforePath, beforeContent, "utf-8");
|
|
17266
17394
|
await (0, import_promises4.writeFile)(afterPath, afterContent, "utf-8");
|
|
17267
17395
|
const raw = await runGitDiffNoIndex(beforePath, afterPath);
|
|
@@ -17436,6 +17564,7 @@ var FirstGenerationFlow = class {
|
|
|
17436
17564
|
patchesToApply: [],
|
|
17437
17565
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
17438
17566
|
detectorWarnings: [],
|
|
17567
|
+
detectorDegraded: [],
|
|
17439
17568
|
revertedCount: 0,
|
|
17440
17569
|
genSha: genRecord.commit_sha,
|
|
17441
17570
|
optionsSnapshot: options
|
|
@@ -17495,6 +17624,7 @@ var SkipApplicationFlow = class {
|
|
|
17495
17624
|
patchesToApply: [],
|
|
17496
17625
|
newPatchIds: /* @__PURE__ */ new Set(),
|
|
17497
17626
|
detectorWarnings: [],
|
|
17627
|
+
detectorDegraded: [],
|
|
17498
17628
|
revertedCount: 0,
|
|
17499
17629
|
genSha: genRecord.commit_sha,
|
|
17500
17630
|
optionsSnapshot: options
|
|
@@ -17532,8 +17662,10 @@ var NoPatchesFlow = class {
|
|
|
17532
17662
|
const previousGenerationSha = preLock.current_generation ?? null;
|
|
17533
17663
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
17534
17664
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
17665
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
17535
17666
|
if (options?.dryRun) {
|
|
17536
17667
|
const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
|
|
17668
|
+
const dryRunDegraded = dedupeDegradedReasons([...detectorDegraded, ...this.service.degradedReasons]);
|
|
17537
17669
|
const preparedReport = {
|
|
17538
17670
|
flow: "no-patches",
|
|
17539
17671
|
patchesDetected: newPatches.length,
|
|
@@ -17543,7 +17675,9 @@ var NoPatchesFlow = class {
|
|
|
17543
17675
|
patchesReverted: revertedPatchIds.length,
|
|
17544
17676
|
conflicts: [],
|
|
17545
17677
|
wouldApply: newPatches,
|
|
17546
|
-
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
|
|
17678
|
+
warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0,
|
|
17679
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
17680
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
17547
17681
|
};
|
|
17548
17682
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
17549
17683
|
return {
|
|
@@ -17597,6 +17731,7 @@ var NoPatchesFlow = class {
|
|
|
17597
17731
|
patchesToApply: newPatches,
|
|
17598
17732
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
17599
17733
|
detectorWarnings,
|
|
17734
|
+
detectorDegraded,
|
|
17600
17735
|
revertedCount: revertedPatchIds.length,
|
|
17601
17736
|
genSha: genRecord.commit_sha,
|
|
17602
17737
|
optionsSnapshot: options
|
|
@@ -17640,6 +17775,7 @@ var NoPatchesFlow = class {
|
|
|
17640
17775
|
}
|
|
17641
17776
|
}
|
|
17642
17777
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
17778
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
17643
17779
|
return this.service.buildReport(
|
|
17644
17780
|
"no-patches",
|
|
17645
17781
|
newPatches,
|
|
@@ -17648,7 +17784,8 @@ var NoPatchesFlow = class {
|
|
|
17648
17784
|
warnings,
|
|
17649
17785
|
rebaseCounts,
|
|
17650
17786
|
void 0,
|
|
17651
|
-
prep._prepared.revertedCount
|
|
17787
|
+
prep._prepared.revertedCount,
|
|
17788
|
+
degradedReasons
|
|
17652
17789
|
);
|
|
17653
17790
|
}
|
|
17654
17791
|
};
|
|
@@ -17665,6 +17802,10 @@ var NormalRegenerationFlow = class {
|
|
|
17665
17802
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
17666
17803
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
17667
17804
|
const warnings = [...this.service.detector.warnings, ...this.service.warnings];
|
|
17805
|
+
const dryRunDegraded = dedupeDegradedReasons([
|
|
17806
|
+
...this.service.detector.degradedReasons,
|
|
17807
|
+
...this.service.degradedReasons
|
|
17808
|
+
]);
|
|
17668
17809
|
const allPatches2 = [...existingPatches2, ...newPatches2];
|
|
17669
17810
|
const preparedReport = {
|
|
17670
17811
|
flow: "normal-regeneration",
|
|
@@ -17675,7 +17816,9 @@ var NormalRegenerationFlow = class {
|
|
|
17675
17816
|
patchesReverted: dryRunReverted.length,
|
|
17676
17817
|
conflicts: [],
|
|
17677
17818
|
wouldApply: allPatches2,
|
|
17678
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
17819
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
17820
|
+
degraded: dryRunDegraded.length > 0 ? true : void 0,
|
|
17821
|
+
degradedReasons: dryRunDegraded.length > 0 ? dryRunDegraded : void 0
|
|
17679
17822
|
};
|
|
17680
17823
|
const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
|
|
17681
17824
|
return {
|
|
@@ -17714,6 +17857,7 @@ var NormalRegenerationFlow = class {
|
|
|
17714
17857
|
existingPatches = this.service.lockManager.getPatches();
|
|
17715
17858
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
17716
17859
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
17860
|
+
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
17717
17861
|
if (removedByPreRebase.length > 0) {
|
|
17718
17862
|
const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
|
|
17719
17863
|
const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
|
|
@@ -17754,6 +17898,7 @@ var NormalRegenerationFlow = class {
|
|
|
17754
17898
|
newPatchIds: new Set(newPatches.map((p) => p.id)),
|
|
17755
17899
|
preRebaseCounts,
|
|
17756
17900
|
detectorWarnings,
|
|
17901
|
+
detectorDegraded,
|
|
17757
17902
|
revertedCount: revertedPatchIds.length,
|
|
17758
17903
|
genSha: genRecord.commit_sha,
|
|
17759
17904
|
optionsSnapshot: options
|
|
@@ -17802,6 +17947,7 @@ var NormalRegenerationFlow = class {
|
|
|
17802
17947
|
await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
|
|
17803
17948
|
}
|
|
17804
17949
|
const warnings = [...detectorWarnings, ...this.service.warnings];
|
|
17950
|
+
const degradedReasons = [...prep._prepared.detectorDegraded, ...this.service.degradedReasons];
|
|
17805
17951
|
return this.service.buildReport(
|
|
17806
17952
|
"normal-regeneration",
|
|
17807
17953
|
allPatches,
|
|
@@ -17810,7 +17956,8 @@ var NormalRegenerationFlow = class {
|
|
|
17810
17956
|
warnings,
|
|
17811
17957
|
rebaseCounts,
|
|
17812
17958
|
prep._prepared.preRebaseCounts,
|
|
17813
|
-
prep._prepared.revertedCount
|
|
17959
|
+
prep._prepared.revertedCount,
|
|
17960
|
+
degradedReasons
|
|
17814
17961
|
);
|
|
17815
17962
|
}
|
|
17816
17963
|
};
|
|
@@ -17850,6 +17997,16 @@ var ReplayService = class {
|
|
|
17850
17997
|
* @internal Used by Flow implementations in `src/flows/`.
|
|
17851
17998
|
*/
|
|
17852
17999
|
warnings = [];
|
|
18000
|
+
/**
|
|
18001
|
+
* Service-level structured degraded reasons, mirroring `warnings`.
|
|
18002
|
+
* Populated at apply time (e.g. `recordUnreachableBaseWarnings` when a
|
|
18003
|
+
* patch's base-generation tree is unreachable). Merged with
|
|
18004
|
+
* `detector.degradedReasons` into `ReplayReport.degradedReasons` by each
|
|
18005
|
+
* flow handler. Cleared at the top of `prepareReplay()`.
|
|
18006
|
+
*
|
|
18007
|
+
* @internal Used by Flow implementations in `src/flows/`.
|
|
18008
|
+
*/
|
|
18009
|
+
degradedReasons = [];
|
|
17853
18010
|
/**
|
|
17854
18011
|
* @internal Test-only accessor.
|
|
17855
18012
|
* Returns the ReplayApplicator instance used for the last run. Tests use this
|
|
@@ -17893,7 +18050,10 @@ var ReplayService = class {
|
|
|
17893
18050
|
async prepareReplay(options) {
|
|
17894
18051
|
this.warnings = [];
|
|
17895
18052
|
this.detector.warnings.length = 0;
|
|
18053
|
+
this.degradedReasons = [];
|
|
18054
|
+
this.detector.degradedReasons.length = 0;
|
|
17896
18055
|
this.cleanseLegacyFernignoreEntries();
|
|
18056
|
+
this.cleanseInfrastructureFileEntries();
|
|
17897
18057
|
return this.selectFlow(options).prepare(options);
|
|
17898
18058
|
}
|
|
17899
18059
|
/**
|
|
@@ -17947,6 +18107,50 @@ var ReplayService = class {
|
|
|
17947
18107
|
this.lockManager.save();
|
|
17948
18108
|
}
|
|
17949
18109
|
}
|
|
18110
|
+
/**
|
|
18111
|
+
* Migration step: strips infrastructure file diffs (`.fernignore`, `.fern/*`)
|
|
18112
|
+
* from existing patches' `patch_content`. These diffs were bundled by older
|
|
18113
|
+
* detection logic that did not scope `git diff` when only infrastructure files
|
|
18114
|
+
* were stripped. Patches that end up with no surviving diff sections are
|
|
18115
|
+
* removed entirely.
|
|
18116
|
+
*
|
|
18117
|
+
* Idempotent: a second run is a no-op because the first run already removed
|
|
18118
|
+
* all infrastructure sections. Per-patch try/catch ensures isolation.
|
|
18119
|
+
*/
|
|
18120
|
+
cleanseInfrastructureFileEntries() {
|
|
18121
|
+
if (!this.lockManager.exists()) return;
|
|
18122
|
+
this.lockManager.read();
|
|
18123
|
+
let lockfileMutated = false;
|
|
18124
|
+
const patches = this.lockManager.getPatches();
|
|
18125
|
+
for (const patch of patches) {
|
|
18126
|
+
try {
|
|
18127
|
+
const result = computeInfrastructureFileCleanse(
|
|
18128
|
+
patch,
|
|
18129
|
+
(content) => this.detector.computeContentHash(content)
|
|
18130
|
+
);
|
|
18131
|
+
if (result.unchanged) continue;
|
|
18132
|
+
if (result.remove) {
|
|
18133
|
+
this.lockManager.removePatch(patch.id);
|
|
18134
|
+
this.warnings.push(
|
|
18135
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: patch contained only infrastructure file changes (${result.strippedPaths.join(", ")}) \u2014 removed.`
|
|
18136
|
+
);
|
|
18137
|
+
} else {
|
|
18138
|
+
this.lockManager.updatePatch(patch.id, {
|
|
18139
|
+
patch_content: result.patch_content,
|
|
18140
|
+
content_hash: result.content_hash
|
|
18141
|
+
});
|
|
18142
|
+
this.warnings.push(
|
|
18143
|
+
`Stripped infrastructure file diffs from patch ${patch.id}: removed bundled diffs for ${result.strippedPaths.join(", ")}.`
|
|
18144
|
+
);
|
|
18145
|
+
}
|
|
18146
|
+
lockfileMutated = true;
|
|
18147
|
+
} catch {
|
|
18148
|
+
}
|
|
18149
|
+
}
|
|
18150
|
+
if (lockfileMutated) {
|
|
18151
|
+
this.lockManager.save();
|
|
18152
|
+
}
|
|
18153
|
+
}
|
|
17950
18154
|
/**
|
|
17951
18155
|
* Phase 2 of the two-phase replay flow. Applies patches, rebases them against
|
|
17952
18156
|
* the generation, saves the lockfile, and commits `[fern-replay]` (or stages
|
|
@@ -18113,7 +18317,9 @@ var ReplayService = class {
|
|
|
18113
18317
|
if (baseChanged) repointed++;
|
|
18114
18318
|
continue;
|
|
18115
18319
|
}
|
|
18116
|
-
const
|
|
18320
|
+
const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
18321
|
+
const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
|
|
18322
|
+
const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
|
|
18117
18323
|
if (diff === null) continue;
|
|
18118
18324
|
if (!diff.trim()) {
|
|
18119
18325
|
if (hasProtectedFiles) {
|
|
@@ -18169,7 +18375,7 @@ var ReplayService = class {
|
|
|
18169
18375
|
patch.base_generation = currentGenSha;
|
|
18170
18376
|
patch.patch_content = diff;
|
|
18171
18377
|
patch.content_hash = newContentHash;
|
|
18172
|
-
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
18378
|
+
const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
|
|
18173
18379
|
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
18174
18380
|
if (snapshotIsNonEmpty) {
|
|
18175
18381
|
patch.theirs_snapshot = snapshot;
|
|
@@ -18202,13 +18408,54 @@ var ReplayService = class {
|
|
|
18202
18408
|
for (const file of files) {
|
|
18203
18409
|
if (isBinaryFile(file)) continue;
|
|
18204
18410
|
try {
|
|
18205
|
-
const content = await (0, import_promises6.readFile)((0,
|
|
18411
|
+
const content = await (0, import_promises6.readFile)((0, import_node_path10.join)(this.outputDir, file), "utf-8");
|
|
18206
18412
|
snapshot[file] = content;
|
|
18207
18413
|
} catch {
|
|
18208
18414
|
}
|
|
18209
18415
|
}
|
|
18210
18416
|
return snapshot;
|
|
18211
18417
|
}
|
|
18418
|
+
/**
|
|
18419
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
18420
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
18421
|
+
*
|
|
18422
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
18423
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
18424
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
18425
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
18426
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
18427
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
18428
|
+
*
|
|
18429
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
18430
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
18431
|
+
*/
|
|
18432
|
+
async computeAutoversionNeutralizedRebase(files, currentGenSha) {
|
|
18433
|
+
const genByFile = /* @__PURE__ */ new Map();
|
|
18434
|
+
let anyPlaceholder = false;
|
|
18435
|
+
for (const file of files) {
|
|
18436
|
+
const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
|
|
18437
|
+
genByFile.set(file, gen);
|
|
18438
|
+
if (containsPlaceholder(gen)) anyPlaceholder = true;
|
|
18439
|
+
}
|
|
18440
|
+
if (!anyPlaceholder) return null;
|
|
18441
|
+
const snapshot = {};
|
|
18442
|
+
const fragments = [];
|
|
18443
|
+
for (const file of files) {
|
|
18444
|
+
if (isBinaryFile(file)) continue;
|
|
18445
|
+
const disk = await (0, import_promises6.readFile)((0, import_node_path10.join)(this.outputDir, file), "utf-8").catch(() => null);
|
|
18446
|
+
if (disk == null) continue;
|
|
18447
|
+
const gen = genByFile.get(file) ?? null;
|
|
18448
|
+
const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
|
|
18449
|
+
snapshot[file] = neutralized;
|
|
18450
|
+
if (gen == null) {
|
|
18451
|
+
fragments.push(synthesizeNewFileDiff(file, neutralized));
|
|
18452
|
+
} else if (gen !== neutralized) {
|
|
18453
|
+
const fileDiff = await unifiedDiff(gen, neutralized, file);
|
|
18454
|
+
if (fileDiff.trim()) fragments.push(fileDiff);
|
|
18455
|
+
}
|
|
18456
|
+
}
|
|
18457
|
+
return { diff: fragments.join(""), snapshot };
|
|
18458
|
+
}
|
|
18212
18459
|
/**
|
|
18213
18460
|
* Detects autoversion contamination — returns true when a
|
|
18214
18461
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|
|
@@ -18719,6 +18966,12 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18719
18966
|
);
|
|
18720
18967
|
}
|
|
18721
18968
|
}
|
|
18969
|
+
if (warned.size > 0) {
|
|
18970
|
+
this.degradedReasons.push({
|
|
18971
|
+
code: "patch-base-unreachable",
|
|
18972
|
+
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.`
|
|
18973
|
+
});
|
|
18974
|
+
}
|
|
18722
18975
|
}
|
|
18723
18976
|
/**
|
|
18724
18977
|
* After applyPatches(), strip conflict markers from conflicting files
|
|
@@ -18730,11 +18983,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18730
18983
|
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
18731
18984
|
for (const fileResult of result.fileResults) {
|
|
18732
18985
|
if (fileResult.status !== "conflict") continue;
|
|
18733
|
-
const filePath = (0,
|
|
18986
|
+
const filePath = (0, import_node_path10.join)(this.outputDir, fileResult.file);
|
|
18734
18987
|
try {
|
|
18735
|
-
const content = (0,
|
|
18988
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
18736
18989
|
const stripped = stripConflictMarkers(content);
|
|
18737
|
-
(0,
|
|
18990
|
+
(0, import_node_fs3.writeFileSync)(filePath, stripped);
|
|
18738
18991
|
} catch {
|
|
18739
18992
|
}
|
|
18740
18993
|
}
|
|
@@ -18762,12 +19015,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18762
19015
|
}
|
|
18763
19016
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18764
19017
|
readFernignorePatterns() {
|
|
18765
|
-
|
|
18766
|
-
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
18767
|
-
return (0, import_node_fs2.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
19018
|
+
return readFernignorePatterns(this.outputDir);
|
|
18768
19019
|
}
|
|
18769
19020
|
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
18770
|
-
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
|
|
19021
|
+
buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted, degradedReasons) {
|
|
19022
|
+
const degraded = dedupeDegradedReasons(degradedReasons ?? []);
|
|
18771
19023
|
const conflictResults = results.filter((r) => r.status === "conflict");
|
|
18772
19024
|
const conflictDetails = conflictResults.map((r) => {
|
|
18773
19025
|
const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
|
|
@@ -18807,10 +19059,23 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
18807
19059
|
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
18808
19060
|
})) : void 0,
|
|
18809
19061
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
18810
|
-
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
19062
|
+
warnings: warnings && warnings.length > 0 ? warnings : void 0,
|
|
19063
|
+
degraded: degraded.length > 0 ? true : void 0,
|
|
19064
|
+
degradedReasons: degraded.length > 0 ? degraded : void 0
|
|
18811
19065
|
};
|
|
18812
19066
|
}
|
|
18813
19067
|
};
|
|
19068
|
+
function dedupeDegradedReasons(reasons) {
|
|
19069
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19070
|
+
const out = [];
|
|
19071
|
+
for (const reason of reasons) {
|
|
19072
|
+
const key = `${reason.code}\0${reason.message}`;
|
|
19073
|
+
if (seen.has(key)) continue;
|
|
19074
|
+
seen.add(key);
|
|
19075
|
+
out.push(reason);
|
|
19076
|
+
}
|
|
19077
|
+
return out;
|
|
19078
|
+
}
|
|
18814
19079
|
function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
|
|
18815
19080
|
const resultByPatchId = /* @__PURE__ */ new Map();
|
|
18816
19081
|
for (const r of results) resultByPatchId.set(r.patch.id, r);
|
|
@@ -18859,7 +19124,7 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
18859
19124
|
}
|
|
18860
19125
|
}
|
|
18861
19126
|
}
|
|
18862
|
-
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } =
|
|
19127
|
+
const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, matchesFernignore);
|
|
18863
19128
|
const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
|
|
18864
19129
|
if (unchanged) {
|
|
18865
19130
|
return {
|
|
@@ -18894,7 +19159,38 @@ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeConten
|
|
|
18894
19159
|
strippedPaths
|
|
18895
19160
|
};
|
|
18896
19161
|
}
|
|
18897
|
-
function
|
|
19162
|
+
function isInfrastructureFile(filePath) {
|
|
19163
|
+
return filePath === ".fernignore" || filePath.startsWith(".fern/");
|
|
19164
|
+
}
|
|
19165
|
+
function computeInfrastructureFileCleanse(patch, computeContentHash2) {
|
|
19166
|
+
const { stripped, content: newPatchContent, strippedSectionPaths } = stripDiffSectionsByPredicate(patch.patch_content, isInfrastructureFile);
|
|
19167
|
+
if (!stripped) {
|
|
19168
|
+
return {
|
|
19169
|
+
unchanged: true,
|
|
19170
|
+
remove: false,
|
|
19171
|
+
patch_content: patch.patch_content,
|
|
19172
|
+
content_hash: patch.content_hash,
|
|
19173
|
+
strippedPaths: []
|
|
19174
|
+
};
|
|
19175
|
+
}
|
|
19176
|
+
if (newPatchContent.trim() === "" || patch.files.length === 0) {
|
|
19177
|
+
return {
|
|
19178
|
+
unchanged: false,
|
|
19179
|
+
remove: true,
|
|
19180
|
+
patch_content: "",
|
|
19181
|
+
content_hash: "",
|
|
19182
|
+
strippedPaths: strippedSectionPaths
|
|
19183
|
+
};
|
|
19184
|
+
}
|
|
19185
|
+
return {
|
|
19186
|
+
unchanged: false,
|
|
19187
|
+
remove: false,
|
|
19188
|
+
patch_content: newPatchContent,
|
|
19189
|
+
content_hash: computeContentHash2(newPatchContent),
|
|
19190
|
+
strippedPaths: strippedSectionPaths
|
|
19191
|
+
};
|
|
19192
|
+
}
|
|
19193
|
+
function stripDiffSectionsByPredicate(patchContent, shouldStrip) {
|
|
18898
19194
|
const lines = patchContent.split("\n");
|
|
18899
19195
|
const sectionStarts = [];
|
|
18900
19196
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -18916,7 +19212,7 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
18916
19212
|
const section = sectionStarts[i];
|
|
18917
19213
|
const next = sectionStarts[i + 1];
|
|
18918
19214
|
const endIdx = next ? next.idx : lines.length;
|
|
18919
|
-
if (section.path !== null &&
|
|
19215
|
+
if (section.path !== null && shouldStrip(section.path)) {
|
|
18920
19216
|
stripped = true;
|
|
18921
19217
|
strippedSectionPaths.push(section.path);
|
|
18922
19218
|
continue;
|
|
@@ -18928,8 +19224,8 @@ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
|
|
|
18928
19224
|
|
|
18929
19225
|
// src/FernignoreMigrator.ts
|
|
18930
19226
|
var import_node_crypto2 = require("crypto");
|
|
18931
|
-
var
|
|
18932
|
-
var
|
|
19227
|
+
var import_node_fs4 = require("fs");
|
|
19228
|
+
var import_node_path11 = require("path");
|
|
18933
19229
|
var import_yaml2 = __toESM(require_dist3(), 1);
|
|
18934
19230
|
var FernignoreMigrator = class {
|
|
18935
19231
|
git;
|
|
@@ -18941,14 +19237,14 @@ var FernignoreMigrator = class {
|
|
|
18941
19237
|
this.outputDir = outputDir;
|
|
18942
19238
|
}
|
|
18943
19239
|
fernignoreExists() {
|
|
18944
|
-
return (0,
|
|
19240
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path11.join)(this.outputDir, ".fernignore"));
|
|
18945
19241
|
}
|
|
18946
19242
|
readFernignorePatterns() {
|
|
18947
|
-
const fernignorePath = (0,
|
|
18948
|
-
if (!(0,
|
|
19243
|
+
const fernignorePath = (0, import_node_path11.join)(this.outputDir, ".fernignore");
|
|
19244
|
+
if (!(0, import_node_fs4.existsSync)(fernignorePath)) {
|
|
18949
19245
|
return [];
|
|
18950
19246
|
}
|
|
18951
|
-
const content = (0,
|
|
19247
|
+
const content = (0, import_node_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
18952
19248
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
18953
19249
|
}
|
|
18954
19250
|
/** Analyze .fernignore patterns vs git history. Creates synthetic patches for files differing from pristine generation. */
|
|
@@ -19053,16 +19349,16 @@ var FernignoreMigrator = class {
|
|
|
19053
19349
|
return results;
|
|
19054
19350
|
}
|
|
19055
19351
|
readFileContent(filePath) {
|
|
19056
|
-
const fullPath = (0,
|
|
19057
|
-
if (!(0,
|
|
19352
|
+
const fullPath = (0, import_node_path11.join)(this.outputDir, filePath);
|
|
19353
|
+
if (!(0, import_node_fs4.existsSync)(fullPath)) {
|
|
19058
19354
|
return null;
|
|
19059
19355
|
}
|
|
19060
19356
|
try {
|
|
19061
|
-
const stat = (0,
|
|
19357
|
+
const stat = (0, import_node_fs4.statSync)(fullPath);
|
|
19062
19358
|
if (stat.isDirectory()) {
|
|
19063
19359
|
return null;
|
|
19064
19360
|
}
|
|
19065
|
-
return (0,
|
|
19361
|
+
return (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
19066
19362
|
} catch {
|
|
19067
19363
|
return null;
|
|
19068
19364
|
}
|
|
@@ -19118,27 +19414,27 @@ var FernignoreMigrator = class {
|
|
|
19118
19414
|
};
|
|
19119
19415
|
}
|
|
19120
19416
|
movePatternsToReplayYml(patterns) {
|
|
19121
|
-
const replayYmlPath = (0,
|
|
19417
|
+
const replayYmlPath = (0, import_node_path11.join)(this.outputDir, ".fern", "replay.yml");
|
|
19122
19418
|
let config = {};
|
|
19123
|
-
if ((0,
|
|
19124
|
-
const content = (0,
|
|
19419
|
+
if ((0, import_node_fs4.existsSync)(replayYmlPath)) {
|
|
19420
|
+
const content = (0, import_node_fs4.readFileSync)(replayYmlPath, "utf-8");
|
|
19125
19421
|
config = (0, import_yaml2.parse)(content) ?? {};
|
|
19126
19422
|
}
|
|
19127
19423
|
const existing = config.exclude ?? [];
|
|
19128
19424
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
|
|
19129
19425
|
config.exclude = merged;
|
|
19130
|
-
const dir = (0,
|
|
19131
|
-
if (!(0,
|
|
19132
|
-
(0,
|
|
19426
|
+
const dir = (0, import_node_path11.dirname)(replayYmlPath);
|
|
19427
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
19428
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
19133
19429
|
}
|
|
19134
|
-
(0,
|
|
19430
|
+
(0, import_node_fs4.writeFileSync)(replayYmlPath, (0, import_yaml2.stringify)(config, { lineWidth: 0 }), "utf-8");
|
|
19135
19431
|
}
|
|
19136
19432
|
};
|
|
19137
19433
|
|
|
19138
19434
|
// src/commands/bootstrap.ts
|
|
19139
19435
|
var import_node_crypto3 = require("crypto");
|
|
19140
|
-
var
|
|
19141
|
-
var
|
|
19436
|
+
var import_node_fs5 = require("fs");
|
|
19437
|
+
var import_node_path12 = require("path");
|
|
19142
19438
|
init_GitClient();
|
|
19143
19439
|
async function bootstrap(outputDir, options) {
|
|
19144
19440
|
const git = new GitClient(outputDir);
|
|
@@ -19305,10 +19601,10 @@ function parseGitLog(log) {
|
|
|
19305
19601
|
}
|
|
19306
19602
|
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
19307
19603
|
function ensureFernignoreEntries(outputDir) {
|
|
19308
|
-
const fernignorePath = (0,
|
|
19604
|
+
const fernignorePath = (0, import_node_path12.join)(outputDir, ".fernignore");
|
|
19309
19605
|
let content = "";
|
|
19310
|
-
if ((0,
|
|
19311
|
-
content = (0,
|
|
19606
|
+
if ((0, import_node_fs5.existsSync)(fernignorePath)) {
|
|
19607
|
+
content = (0, import_node_fs5.readFileSync)(fernignorePath, "utf-8");
|
|
19312
19608
|
}
|
|
19313
19609
|
const lines = content.split("\n");
|
|
19314
19610
|
const toAdd = [];
|
|
@@ -19324,15 +19620,15 @@ function ensureFernignoreEntries(outputDir) {
|
|
|
19324
19620
|
content += "\n";
|
|
19325
19621
|
}
|
|
19326
19622
|
content += toAdd.join("\n") + "\n";
|
|
19327
|
-
(0,
|
|
19623
|
+
(0, import_node_fs5.writeFileSync)(fernignorePath, content, "utf-8");
|
|
19328
19624
|
return true;
|
|
19329
19625
|
}
|
|
19330
19626
|
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
19331
19627
|
function ensureGitattributesEntries(outputDir) {
|
|
19332
|
-
const gitattributesPath = (0,
|
|
19628
|
+
const gitattributesPath = (0, import_node_path12.join)(outputDir, ".gitattributes");
|
|
19333
19629
|
let content = "";
|
|
19334
|
-
if ((0,
|
|
19335
|
-
content = (0,
|
|
19630
|
+
if ((0, import_node_fs5.existsSync)(gitattributesPath)) {
|
|
19631
|
+
content = (0, import_node_fs5.readFileSync)(gitattributesPath, "utf-8");
|
|
19336
19632
|
}
|
|
19337
19633
|
const lines = content.split("\n");
|
|
19338
19634
|
const toAdd = [];
|
|
@@ -19348,7 +19644,7 @@ function ensureGitattributesEntries(outputDir) {
|
|
|
19348
19644
|
content += "\n";
|
|
19349
19645
|
}
|
|
19350
19646
|
content += toAdd.join("\n") + "\n";
|
|
19351
|
-
(0,
|
|
19647
|
+
(0, import_node_fs5.writeFileSync)(gitattributesPath, content, "utf-8");
|
|
19352
19648
|
}
|
|
19353
19649
|
function computeContentHash(patchContent) {
|
|
19354
19650
|
const lines = patchContent.split("\n");
|
|
@@ -19359,6 +19655,7 @@ function computeContentHash(patchContent) {
|
|
|
19359
19655
|
}
|
|
19360
19656
|
|
|
19361
19657
|
// src/commands/forget.ts
|
|
19658
|
+
init_GitClient();
|
|
19362
19659
|
function parseDiffStat(patchContent) {
|
|
19363
19660
|
let additions = 0;
|
|
19364
19661
|
let deletions = 0;
|
|
@@ -19417,7 +19714,28 @@ var EMPTY_RESULT = {
|
|
|
19417
19714
|
totalPatches: 0,
|
|
19418
19715
|
warnings: []
|
|
19419
19716
|
};
|
|
19420
|
-
function
|
|
19717
|
+
async function dismissPatches(outputDir, lockManager, patches) {
|
|
19718
|
+
const git = new GitClient(outputDir);
|
|
19719
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
19720
|
+
for (const patch of patches) {
|
|
19721
|
+
const hashes = [patch.content_hash];
|
|
19722
|
+
if (patch.original_commit) {
|
|
19723
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
19724
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
19725
|
+
hashes.push(materialized.contentHash);
|
|
19726
|
+
}
|
|
19727
|
+
}
|
|
19728
|
+
lockManager.addDismissal({
|
|
19729
|
+
patch_id: patch.id,
|
|
19730
|
+
original_commit: patch.original_commit,
|
|
19731
|
+
original_message: patch.original_message,
|
|
19732
|
+
content_hashes: hashes,
|
|
19733
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19734
|
+
via: "forget"
|
|
19735
|
+
});
|
|
19736
|
+
}
|
|
19737
|
+
}
|
|
19738
|
+
async function forget(outputDir, options) {
|
|
19421
19739
|
const lockManager = new LockfileManager(outputDir);
|
|
19422
19740
|
if (!lockManager.exists()) {
|
|
19423
19741
|
return { ...EMPTY_RESULT };
|
|
@@ -19428,9 +19746,7 @@ function forget(outputDir, options) {
|
|
|
19428
19746
|
const removed = lock.patches.map(toMatchedPatch);
|
|
19429
19747
|
const warnings = buildWarnings(lock.patches);
|
|
19430
19748
|
if (!options.dryRun) {
|
|
19431
|
-
|
|
19432
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
19433
|
-
}
|
|
19749
|
+
await dismissPatches(outputDir, lockManager, lock.patches);
|
|
19434
19750
|
lockManager.clearPatches();
|
|
19435
19751
|
lockManager.save();
|
|
19436
19752
|
}
|
|
@@ -19459,8 +19775,8 @@ function forget(outputDir, options) {
|
|
|
19459
19775
|
}
|
|
19460
19776
|
const warnings = buildWarnings(patchesToRemove);
|
|
19461
19777
|
if (!options.dryRun) {
|
|
19778
|
+
await dismissPatches(outputDir, lockManager, patchesToRemove);
|
|
19462
19779
|
for (const patch of patchesToRemove) {
|
|
19463
|
-
lockManager.addForgottenHash(patch.content_hash);
|
|
19464
19780
|
lockManager.removePatch(patch.id);
|
|
19465
19781
|
}
|
|
19466
19782
|
lockManager.save();
|
|
@@ -19501,7 +19817,7 @@ function forget(outputDir, options) {
|
|
|
19501
19817
|
}
|
|
19502
19818
|
|
|
19503
19819
|
// src/commands/reset.ts
|
|
19504
|
-
var
|
|
19820
|
+
var import_node_fs6 = require("fs");
|
|
19505
19821
|
function reset(outputDir, options) {
|
|
19506
19822
|
const lockManager = new LockfileManager(outputDir);
|
|
19507
19823
|
if (!lockManager.exists()) {
|
|
@@ -19515,7 +19831,7 @@ function reset(outputDir, options) {
|
|
|
19515
19831
|
const lock = lockManager.read();
|
|
19516
19832
|
const patchCount = lock.patches.length;
|
|
19517
19833
|
if (!options?.dryRun) {
|
|
19518
|
-
(0,
|
|
19834
|
+
(0, import_node_fs6.unlinkSync)(lockManager.lockfilePath);
|
|
19519
19835
|
}
|
|
19520
19836
|
return {
|
|
19521
19837
|
success: true,
|
|
@@ -19527,7 +19843,7 @@ function reset(outputDir, options) {
|
|
|
19527
19843
|
|
|
19528
19844
|
// src/commands/resolve.ts
|
|
19529
19845
|
var import_promises7 = require("fs/promises");
|
|
19530
|
-
var
|
|
19846
|
+
var import_node_path13 = require("path");
|
|
19531
19847
|
init_GitClient();
|
|
19532
19848
|
async function resolve(outputDir, options) {
|
|
19533
19849
|
const lockManager = new LockfileManager(outputDir);
|
|
@@ -19568,15 +19884,16 @@ async function resolve(outputDir, options) {
|
|
|
19568
19884
|
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
19569
19885
|
if (patchesToCommit.length > 0) {
|
|
19570
19886
|
const currentGen = lock.current_generation;
|
|
19571
|
-
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
19887
|
+
const detector = new ReplayDetector(git, lockManager, outputDir, readFernignorePatterns(outputDir));
|
|
19572
19888
|
const warnings = [];
|
|
19889
|
+
const patchesDismissed = [];
|
|
19573
19890
|
let patchesResolved = 0;
|
|
19574
19891
|
const pass1 = [];
|
|
19575
19892
|
for (const patch of patchesToCommit) {
|
|
19576
19893
|
const result = await computePerPatchDiffWithFallback(
|
|
19577
19894
|
patch,
|
|
19578
19895
|
(f) => git.showFile(currentGen, f),
|
|
19579
|
-
(f) => (0, import_promises7.readFile)((0,
|
|
19896
|
+
(f) => (0, import_promises7.readFile)((0, import_node_path13.join)(outputDir, f), "utf-8").catch(() => null),
|
|
19580
19897
|
(files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
|
|
19581
19898
|
);
|
|
19582
19899
|
if (result === null) {
|
|
@@ -19610,7 +19927,27 @@ async function resolve(outputDir, options) {
|
|
|
19610
19927
|
const r = pass1[i];
|
|
19611
19928
|
if (r.kind === "skip") continue;
|
|
19612
19929
|
if (r.kind === "revert") {
|
|
19930
|
+
const hashes = [patch.content_hash];
|
|
19931
|
+
if (patch.original_commit) {
|
|
19932
|
+
const materialized = await detector.materializeCommitPatch(patch.original_commit);
|
|
19933
|
+
if (materialized.status === "ok" && !hashes.includes(materialized.contentHash)) {
|
|
19934
|
+
hashes.push(materialized.contentHash);
|
|
19935
|
+
} else if (materialized.status === "unreachable") {
|
|
19936
|
+
warnings.push(
|
|
19937
|
+
`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.`
|
|
19938
|
+
);
|
|
19939
|
+
}
|
|
19940
|
+
}
|
|
19941
|
+
lockManager.addDismissal({
|
|
19942
|
+
patch_id: patch.id,
|
|
19943
|
+
original_commit: patch.original_commit,
|
|
19944
|
+
original_message: patch.original_message,
|
|
19945
|
+
content_hashes: hashes,
|
|
19946
|
+
dismissed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19947
|
+
via: "resolve"
|
|
19948
|
+
});
|
|
19613
19949
|
lockManager.removePatch(patch.id);
|
|
19950
|
+
patchesDismissed.push({ id: patch.id, message: patch.original_message });
|
|
19614
19951
|
continue;
|
|
19615
19952
|
}
|
|
19616
19953
|
if (lastIndexByHash.get(r.hash) !== i) {
|
|
@@ -19632,7 +19969,7 @@ async function resolve(outputDir, options) {
|
|
|
19632
19969
|
const theirsSnapshot = {};
|
|
19633
19970
|
for (const file of filesForSnapshot) {
|
|
19634
19971
|
if (isBinaryFile(file)) continue;
|
|
19635
|
-
const content = await (0, import_promises7.readFile)((0,
|
|
19972
|
+
const content = await (0, import_promises7.readFile)((0, import_node_path13.join)(outputDir, file), "utf-8").catch(() => null);
|
|
19636
19973
|
if (content != null) {
|
|
19637
19974
|
theirsSnapshot[file] = content;
|
|
19638
19975
|
}
|
|
@@ -19659,6 +19996,7 @@ async function resolve(outputDir, options) {
|
|
|
19659
19996
|
commitSha: commitSha2,
|
|
19660
19997
|
phase: "committed",
|
|
19661
19998
|
patchesResolved,
|
|
19999
|
+
patchesDismissed: patchesDismissed.length > 0 ? patchesDismissed : void 0,
|
|
19662
20000
|
warnings: warnings.length > 0 ? warnings : void 0
|
|
19663
20001
|
};
|
|
19664
20002
|
}
|
|
@@ -19706,7 +20044,9 @@ function status(outputDir) {
|
|
|
19706
20044
|
lastGeneration: void 0,
|
|
19707
20045
|
patches: [],
|
|
19708
20046
|
unresolvedCount: 0,
|
|
19709
|
-
excludePatterns: []
|
|
20047
|
+
excludePatterns: [],
|
|
20048
|
+
dismissed: [],
|
|
20049
|
+
legacyDismissedCount: 0
|
|
19710
20050
|
};
|
|
19711
20051
|
}
|
|
19712
20052
|
const lock = lockManager.read();
|
|
@@ -19735,13 +20075,24 @@ function status(outputDir) {
|
|
|
19735
20075
|
}
|
|
19736
20076
|
const config = lockManager.getCustomizationsConfig();
|
|
19737
20077
|
const excludePatterns = config.exclude ?? [];
|
|
20078
|
+
const dismissals = lock.dismissals ?? [];
|
|
20079
|
+
const dismissed = dismissals.map((d) => ({
|
|
20080
|
+
message: d.original_message,
|
|
20081
|
+
sha: d.original_commit.slice(0, 7),
|
|
20082
|
+
via: d.via,
|
|
20083
|
+
dismissedAt: d.dismissed_at
|
|
20084
|
+
}));
|
|
20085
|
+
const coveredHashes = new Set(dismissals.flatMap((d) => d.content_hashes));
|
|
20086
|
+
const legacyDismissedCount = (lock.forgotten_hashes ?? []).filter((h) => !coveredHashes.has(h)).length;
|
|
19738
20087
|
return {
|
|
19739
20088
|
initialized: true,
|
|
19740
20089
|
generationCount: lock.generations.length,
|
|
19741
20090
|
lastGeneration,
|
|
19742
20091
|
patches,
|
|
19743
20092
|
unresolvedCount,
|
|
19744
|
-
excludePatterns
|
|
20093
|
+
excludePatterns,
|
|
20094
|
+
dismissed,
|
|
20095
|
+
legacyDismissedCount
|
|
19745
20096
|
};
|
|
19746
20097
|
}
|
|
19747
20098
|
|
|
@@ -19829,7 +20180,7 @@ function parseArgs(argv) {
|
|
|
19829
20180
|
positionals.push(arg);
|
|
19830
20181
|
}
|
|
19831
20182
|
}
|
|
19832
|
-
return { command, dir: (0,
|
|
20183
|
+
return { command, dir: (0, import_node_path14.resolve)(dir), flags, positionals };
|
|
19833
20184
|
}
|
|
19834
20185
|
async function runBootstrap(dir, flags) {
|
|
19835
20186
|
const dryRun = !!flags.dryRun;
|
|
@@ -19907,6 +20258,7 @@ async function runStatus(dir, flags) {
|
|
|
19907
20258
|
}
|
|
19908
20259
|
if (result.patches.length === 0) {
|
|
19909
20260
|
console.log("\nNo customizations tracked. Edit SDK files and commit \u2014 Replay will detect them on next generation.");
|
|
20261
|
+
printDismissedSection(result);
|
|
19910
20262
|
return;
|
|
19911
20263
|
}
|
|
19912
20264
|
const unresolvedSuffix = result.unresolvedCount > 0 ? `, ${result.unresolvedCount} unresolved` : "";
|
|
@@ -19944,6 +20296,26 @@ ${prefix}${patch.id} [${patch.type}] "${patch.message}"`);
|
|
|
19944
20296
|
console.log(` ... and ${result.patches.length - maxDisplay} more`);
|
|
19945
20297
|
}
|
|
19946
20298
|
}
|
|
20299
|
+
printDismissedSection(result);
|
|
20300
|
+
}
|
|
20301
|
+
function printDismissedSection(result) {
|
|
20302
|
+
if (result.dismissed.length === 0 && result.legacyDismissedCount === 0) {
|
|
20303
|
+
return;
|
|
20304
|
+
}
|
|
20305
|
+
const total = result.dismissed.length + result.legacyDismissedCount;
|
|
20306
|
+
console.log(`
|
|
20307
|
+
Dismissed customizations: ${total}`);
|
|
20308
|
+
for (const d of result.dismissed) {
|
|
20309
|
+
const shaSuffix = d.sha ? ` (${d.sha})` : "";
|
|
20310
|
+
const date = d.dismissedAt ? d.dismissedAt.slice(0, 10) : "";
|
|
20311
|
+
const dateSuffix = date ? ` on ${date}` : "";
|
|
20312
|
+
console.log(` "${d.message}"${shaSuffix} \u2014 dismissed via ${d.via}${dateSuffix}`);
|
|
20313
|
+
}
|
|
20314
|
+
if (result.legacyDismissedCount > 0) {
|
|
20315
|
+
console.log(
|
|
20316
|
+
` ... and ${result.legacyDismissedCount} dismissed before this version \u2014 details unavailable`
|
|
20317
|
+
);
|
|
20318
|
+
}
|
|
19947
20319
|
}
|
|
19948
20320
|
async function runDetect(dir) {
|
|
19949
20321
|
const lockManager = new LockfileManager(dir);
|
|
@@ -19955,6 +20327,7 @@ async function runDetect(dir) {
|
|
|
19955
20327
|
const git = new GitClient(dir);
|
|
19956
20328
|
const detector = new ReplayDetector(git, lockManager, dir);
|
|
19957
20329
|
const { patches, revertedPatchIds } = await detector.detectNewPatches();
|
|
20330
|
+
printDegradedBanner(detector.degradedReasons);
|
|
19958
20331
|
if (detector.warnings.length > 0) {
|
|
19959
20332
|
for (const w of detector.warnings) console.log(`Warning: ${w}`);
|
|
19960
20333
|
console.log();
|
|
@@ -19985,7 +20358,20 @@ async function runReplay(dir, flags) {
|
|
|
19985
20358
|
const report = await service.runReplay({ dryRun });
|
|
19986
20359
|
printReport(report);
|
|
19987
20360
|
}
|
|
20361
|
+
function printDegradedBanner(reasons) {
|
|
20362
|
+
if (reasons.length === 0) return;
|
|
20363
|
+
console.log("============================================================");
|
|
20364
|
+
console.log(" REPLAY DEGRADED \u2014 customizations may not have been");
|
|
20365
|
+
console.log(" carried forward in this run. Review the result before");
|
|
20366
|
+
console.log(" merging.");
|
|
20367
|
+
console.log("============================================================");
|
|
20368
|
+
for (const reason of reasons) {
|
|
20369
|
+
console.log(` [${reason.code}] ${reason.message}`);
|
|
20370
|
+
}
|
|
20371
|
+
console.log();
|
|
20372
|
+
}
|
|
19988
20373
|
function printReport(report) {
|
|
20374
|
+
printDegradedBanner(report.degradedReasons ?? []);
|
|
19989
20375
|
console.log(`Flow: ${report.flow}`);
|
|
19990
20376
|
console.log(`Patches detected: ${report.patchesDetected}`);
|
|
19991
20377
|
console.log(`Patches applied: ${report.patchesApplied}`);
|
|
@@ -20153,7 +20539,7 @@ async function runForget(dir, flags, positionals) {
|
|
|
20153
20539
|
const pattern = !isPatchIdMode && positionals.length > 0 ? positionals[0] : void 0;
|
|
20154
20540
|
const patchIds = isPatchIdMode ? positionals : void 0;
|
|
20155
20541
|
if (all) {
|
|
20156
|
-
const preview = forget(dir, { all: true, dryRun: true });
|
|
20542
|
+
const preview = await forget(dir, { all: true, dryRun: true });
|
|
20157
20543
|
if (!preview.initialized) {
|
|
20158
20544
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20159
20545
|
process.exit(1);
|
|
@@ -20172,14 +20558,14 @@ async function runForget(dir, flags, positionals) {
|
|
|
20172
20558
|
return;
|
|
20173
20559
|
}
|
|
20174
20560
|
}
|
|
20175
|
-
const finalResult = dryRun ? preview : forget(dir, { all: true });
|
|
20561
|
+
const finalResult = dryRun ? preview : await forget(dir, { all: true });
|
|
20176
20562
|
printForgetWarnings(finalResult.warnings);
|
|
20177
20563
|
const verb2 = dryRun ? "Would remove" : "Removed";
|
|
20178
20564
|
console.log(`${verb2} ${finalResult.removed.length} patches. ${finalResult.remaining} patches remaining.`);
|
|
20179
20565
|
return;
|
|
20180
20566
|
}
|
|
20181
20567
|
if (isPatchIdMode && patchIds) {
|
|
20182
|
-
const result2 = forget(dir, { patchIds, dryRun });
|
|
20568
|
+
const result2 = await forget(dir, { patchIds, dryRun });
|
|
20183
20569
|
if (!result2.initialized) {
|
|
20184
20570
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20185
20571
|
process.exit(1);
|
|
@@ -20206,7 +20592,7 @@ async function runForget(dir, flags, positionals) {
|
|
|
20206
20592
|
console.log(`${result2.remaining} patches remaining.`);
|
|
20207
20593
|
return;
|
|
20208
20594
|
}
|
|
20209
|
-
const searchResult = pattern ? forget(dir, { pattern }) : forget(dir);
|
|
20595
|
+
const searchResult = pattern ? await forget(dir, { pattern }) : await forget(dir);
|
|
20210
20596
|
if (!searchResult.initialized) {
|
|
20211
20597
|
console.log("Replay is not initialized. Run 'fern replay init' to get started.");
|
|
20212
20598
|
process.exit(1);
|
|
@@ -20254,7 +20640,7 @@ This will remove ${selectedIds.length} patch(es). Affected files will be overwri
|
|
|
20254
20640
|
return;
|
|
20255
20641
|
}
|
|
20256
20642
|
}
|
|
20257
|
-
const result = forget(dir, { patchIds: selectedIds, dryRun });
|
|
20643
|
+
const result = await forget(dir, { patchIds: selectedIds, dryRun });
|
|
20258
20644
|
printForgetWarnings(result.warnings);
|
|
20259
20645
|
const verb = dryRun ? "Would remove" : "Removed";
|
|
20260
20646
|
console.log(`
|
|
@@ -20321,12 +20707,23 @@ Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' agai
|
|
|
20321
20707
|
if (result.patchesResolved) {
|
|
20322
20708
|
console.log(`Resolution complete. Updated ${result.patchesResolved} patch(es) in lockfile.`);
|
|
20323
20709
|
}
|
|
20710
|
+
if (result.patchesDismissed && result.patchesDismissed.length > 0) {
|
|
20711
|
+
console.log(`Permanently dismissed ${result.patchesDismissed.length} customization(s) (you kept the generated code):`);
|
|
20712
|
+
for (const d of result.patchesDismissed) {
|
|
20713
|
+
console.log(` "${d.message}" (${d.id}) \u2014 will not return on future generations`);
|
|
20714
|
+
}
|
|
20715
|
+
}
|
|
20716
|
+
if (result.warnings) {
|
|
20717
|
+
for (const w of result.warnings) {
|
|
20718
|
+
console.log(`Warning: ${w}`);
|
|
20719
|
+
}
|
|
20720
|
+
}
|
|
20324
20721
|
console.log(`Created [fern-replay] commit: ${result.commitSha?.slice(0, 7)}`);
|
|
20325
20722
|
console.log("Push to update the PR.");
|
|
20326
20723
|
}
|
|
20327
20724
|
async function main() {
|
|
20328
20725
|
const { command, dir, flags, positionals } = parseArgs(process.argv);
|
|
20329
|
-
if (!(0,
|
|
20726
|
+
if (!(0, import_node_fs7.existsSync)(dir)) {
|
|
20330
20727
|
console.error(`Directory not found: ${dir}`);
|
|
20331
20728
|
process.exit(1);
|
|
20332
20729
|
}
|