@fern-api/replay 0.15.2 → 0.16.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 +2340 -2331
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +163 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -38
- package/dist/index.d.ts +37 -38
- package/dist/index.js +163 -153
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -499,9 +499,22 @@ function isGenerationCommit(commit) {
|
|
|
499
499
|
const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
|
|
500
500
|
// The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
|
|
501
501
|
// GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
|
|
502
|
-
commit.message.startsWith("SDK Generation")
|
|
502
|
+
commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
|
|
503
|
+
// regen PR. First-parent walking lands on this commit, not on the
|
|
504
|
+
// generation commit on the side branch, so the classifier must treat
|
|
505
|
+
// it as a generation boundary. GitHub's default subject is
|
|
506
|
+
// "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
|
|
507
|
+
// named "<owner>/fern-bot/<...>".
|
|
508
|
+
/^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
|
|
503
509
|
return isBotAuthor || hasGenerationMarker;
|
|
504
510
|
}
|
|
511
|
+
function isGenerationBoundary(commit) {
|
|
512
|
+
const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
|
|
513
|
+
const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
|
|
514
|
+
const isAutoversion = commit.message.startsWith("[fern-autoversion]");
|
|
515
|
+
const hasBoundaryMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || commit.message.startsWith("SDK Generation");
|
|
516
|
+
return isBotAuthor && !isAutoversion || hasBoundaryMarker;
|
|
517
|
+
}
|
|
505
518
|
function isReplayCommit(commit) {
|
|
506
519
|
return commit.message.startsWith("[fern-replay]");
|
|
507
520
|
}
|
|
@@ -520,6 +533,7 @@ function parseRevertedMessage(subject) {
|
|
|
520
533
|
// src/LockfileManager.ts
|
|
521
534
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
|
|
522
535
|
import { join, dirname } from "path";
|
|
536
|
+
import { minimatch } from "minimatch";
|
|
523
537
|
import { stringify, parse } from "yaml";
|
|
524
538
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
525
539
|
var LockfileNotFoundError = class extends Error {
|
|
@@ -531,10 +545,23 @@ var LockfileNotFoundError = class extends Error {
|
|
|
531
545
|
var LockfileManager = class {
|
|
532
546
|
outputDir;
|
|
533
547
|
lock = null;
|
|
548
|
+
fernignorePatterns = [];
|
|
534
549
|
warnings = [];
|
|
535
550
|
constructor(outputDir) {
|
|
536
551
|
this.outputDir = outputDir;
|
|
537
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Inform the lockfile of the customer's `.fernignore` patterns so that
|
|
555
|
+
* `save()` can suppress its "Patch X removed from lockfile" warning when
|
|
556
|
+
* every file in the stripped patch is already `.fernignore`-protected.
|
|
557
|
+
* The strip itself still happens (credentials never persist); only the
|
|
558
|
+
* customer-visible warning is suppressed in that case, because the
|
|
559
|
+
* protected files survive the generator wipe regardless of whether the
|
|
560
|
+
* lockfile tracks them, so there's nothing for the customer to action.
|
|
561
|
+
*/
|
|
562
|
+
setFernignorePatterns(patterns) {
|
|
563
|
+
this.fernignorePatterns = patterns;
|
|
564
|
+
}
|
|
538
565
|
get lockfilePath() {
|
|
539
566
|
return join(this.outputDir, ".fern", "replay.lock");
|
|
540
567
|
}
|
|
@@ -603,9 +630,14 @@ var LockfileManager = class {
|
|
|
603
630
|
if (sensitiveFiles.length > 0) {
|
|
604
631
|
reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
|
|
605
632
|
}
|
|
606
|
-
|
|
607
|
-
|
|
633
|
+
const allProtected = patch.files.length > 0 && patch.files.every(
|
|
634
|
+
(f) => this.fernignorePatterns.some((p) => f === p || minimatch(f, p))
|
|
608
635
|
);
|
|
636
|
+
if (!allProtected) {
|
|
637
|
+
this.warnings.push(
|
|
638
|
+
`Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
609
641
|
} else {
|
|
610
642
|
cleanPatches.push(patch);
|
|
611
643
|
}
|
|
@@ -829,8 +861,48 @@ var ReplayDetector = class {
|
|
|
829
861
|
this.lockManager = lockManager;
|
|
830
862
|
this.sdkOutputDir = sdkOutputDir;
|
|
831
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
866
|
+
* `lock.current_generation`.
|
|
867
|
+
*
|
|
868
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
869
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
870
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
871
|
+
*
|
|
872
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
873
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
874
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
875
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
876
|
+
* on second-parent — stopping at the merge would hide them).
|
|
877
|
+
*
|
|
878
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
879
|
+
*/
|
|
880
|
+
async findPreviousGenerationFromHistory() {
|
|
881
|
+
let log;
|
|
882
|
+
try {
|
|
883
|
+
log = await this.git.exec([
|
|
884
|
+
"log",
|
|
885
|
+
"--first-parent",
|
|
886
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
887
|
+
"HEAD"
|
|
888
|
+
]);
|
|
889
|
+
} catch {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
for (const commit of this.parseGitLog(log)) {
|
|
893
|
+
if (isGenerationBoundary(commit)) {
|
|
894
|
+
return commit.sha;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
832
899
|
async detectNewPatches() {
|
|
833
900
|
const lock = this.lockManager.read();
|
|
901
|
+
const derivedSha = await this.findPreviousGenerationFromHistory();
|
|
902
|
+
if (derivedSha !== null) {
|
|
903
|
+
const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
|
|
904
|
+
return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
|
|
905
|
+
}
|
|
834
906
|
const lastGen = this.getLastGeneration(lock);
|
|
835
907
|
if (!lastGen) {
|
|
836
908
|
return { patches: [], revertedPatchIds: [] };
|
|
@@ -838,7 +910,7 @@ var ReplayDetector = class {
|
|
|
838
910
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
839
911
|
if (!exists) {
|
|
840
912
|
this.warnings.push(
|
|
841
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to
|
|
913
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
842
914
|
);
|
|
843
915
|
return this.detectPatchesViaTreeDiff(
|
|
844
916
|
lastGen,
|
|
@@ -846,44 +918,8 @@ var ReplayDetector = class {
|
|
|
846
918
|
true
|
|
847
919
|
);
|
|
848
920
|
}
|
|
849
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
850
|
-
if (!isAncestor) {
|
|
851
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
852
|
-
}
|
|
853
921
|
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
854
922
|
}
|
|
855
|
-
/**
|
|
856
|
-
* Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
857
|
-
*
|
|
858
|
-
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
859
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
860
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
861
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
862
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
863
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
864
|
-
* generation) get bundled as customer customizations.
|
|
865
|
-
*
|
|
866
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
867
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
868
|
-
*/
|
|
869
|
-
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
870
|
-
let mergeBase = "";
|
|
871
|
-
try {
|
|
872
|
-
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
873
|
-
} catch {
|
|
874
|
-
}
|
|
875
|
-
if (!mergeBase) {
|
|
876
|
-
this.warnings.push(
|
|
877
|
-
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
878
|
-
);
|
|
879
|
-
return this.detectPatchesViaTreeDiff(
|
|
880
|
-
lastGen,
|
|
881
|
-
/* commitKnownMissing */
|
|
882
|
-
false
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
886
|
-
}
|
|
887
923
|
/**
|
|
888
924
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
889
925
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -1319,6 +1355,26 @@ var ReplayDetector = class {
|
|
|
1319
1355
|
getLastGeneration(lock) {
|
|
1320
1356
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
1321
1357
|
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
1360
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
1361
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
1362
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
1363
|
+
*/
|
|
1364
|
+
async synthesizeGenerationRecord(sha) {
|
|
1365
|
+
let tree_hash = "";
|
|
1366
|
+
try {
|
|
1367
|
+
tree_hash = await this.git.getTreeHash(sha);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
commit_sha: sha,
|
|
1372
|
+
tree_hash,
|
|
1373
|
+
timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1374
|
+
cli_version: "",
|
|
1375
|
+
generator_versions: {}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1322
1378
|
};
|
|
1323
1379
|
|
|
1324
1380
|
// src/ThreeWayMerge.ts
|
|
@@ -1464,7 +1520,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
1464
1520
|
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1465
1521
|
import { tmpdir } from "os";
|
|
1466
1522
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1467
|
-
import { minimatch } from "minimatch";
|
|
1523
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
1468
1524
|
|
|
1469
1525
|
// src/accumulator/MergeAccumulator.ts
|
|
1470
1526
|
var MergeAccumulator = class {
|
|
@@ -2180,13 +2236,13 @@ var ReplayApplicator = class {
|
|
|
2180
2236
|
isExcluded(patch) {
|
|
2181
2237
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2182
2238
|
if (!config.exclude) return false;
|
|
2183
|
-
return patch.files.some((file) => config.exclude.some((pattern) =>
|
|
2239
|
+
return patch.files.some((file) => config.exclude.some((pattern) => minimatch2(file, pattern)));
|
|
2184
2240
|
}
|
|
2185
2241
|
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
2186
2242
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2187
2243
|
if (config.moves) {
|
|
2188
2244
|
for (const move of config.moves) {
|
|
2189
|
-
if (
|
|
2245
|
+
if (minimatch2(filePath, move.from) || filePath === move.from) {
|
|
2190
2246
|
if (filePath === move.from) {
|
|
2191
2247
|
return move.to;
|
|
2192
2248
|
}
|
|
@@ -2458,8 +2514,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2458
2514
|
tree_hash: treeHash,
|
|
2459
2515
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2460
2516
|
cli_version: options?.cliVersion ?? "unknown",
|
|
2461
|
-
generator_versions: options?.generatorVersions ?? {}
|
|
2462
|
-
base_branch_head: options?.baseBranchHead
|
|
2517
|
+
generator_versions: options?.generatorVersions ?? {}
|
|
2463
2518
|
};
|
|
2464
2519
|
}
|
|
2465
2520
|
async stageAll() {
|
|
@@ -2478,7 +2533,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2478
2533
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2479
2534
|
import { readFile as readFileAsync } from "fs/promises";
|
|
2480
2535
|
import { join as join6 } from "path";
|
|
2481
|
-
import { minimatch as
|
|
2536
|
+
import { minimatch as minimatch4 } from "minimatch";
|
|
2482
2537
|
init_GitClient();
|
|
2483
2538
|
|
|
2484
2539
|
// src/PatchRegionDiff.ts
|
|
@@ -2589,12 +2644,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
|
|
|
2589
2644
|
unlocatableFiles.push(file);
|
|
2590
2645
|
continue;
|
|
2591
2646
|
}
|
|
2592
|
-
const locInCurrentGen =
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2647
|
+
const locInCurrentGen = [];
|
|
2648
|
+
const locInWorkingTree = [];
|
|
2649
|
+
let spanResolutionFailed = false;
|
|
2650
|
+
for (let i = 0; i < locInCurrentGenRaw.length; i++) {
|
|
2651
|
+
const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
|
|
2652
|
+
const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
|
|
2653
|
+
if (cg === null || wt === null) {
|
|
2654
|
+
spanResolutionFailed = true;
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
locInCurrentGen.push(cg);
|
|
2658
|
+
locInWorkingTree.push(wt);
|
|
2659
|
+
}
|
|
2660
|
+
if (spanResolutionFailed) {
|
|
2661
|
+
unlocatableFiles.push(file);
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2598
2664
|
const overlay = overlayRegions(
|
|
2599
2665
|
currentGenLines,
|
|
2600
2666
|
locInCurrentGen,
|
|
@@ -2656,11 +2722,11 @@ function resolveSpan(loc, oursLines, prefer) {
|
|
|
2656
2722
|
if (prefer === "old") {
|
|
2657
2723
|
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2658
2724
|
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2659
|
-
|
|
2660
|
-
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2661
|
-
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2725
|
+
return loc;
|
|
2662
2726
|
}
|
|
2663
|
-
return loc;
|
|
2727
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2728
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2729
|
+
return null;
|
|
2664
2730
|
}
|
|
2665
2731
|
function sequenceMatches(needle, haystack, offset) {
|
|
2666
2732
|
if (offset + needle.length > haystack.length) return false;
|
|
@@ -2774,7 +2840,7 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
2774
2840
|
}
|
|
2775
2841
|
|
|
2776
2842
|
// src/classifier/FileOwnership.ts
|
|
2777
|
-
import { minimatch as
|
|
2843
|
+
import { minimatch as minimatch3 } from "minimatch";
|
|
2778
2844
|
var FileOwnership = class {
|
|
2779
2845
|
constructor(git) {
|
|
2780
2846
|
this.git = git;
|
|
@@ -2798,7 +2864,7 @@ var FileOwnership = class {
|
|
|
2798
2864
|
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
2799
2865
|
if (patch.user_owned) return true;
|
|
2800
2866
|
if (file === ".fernignore") return true;
|
|
2801
|
-
if (fernignorePatterns.some((p) => file === p ||
|
|
2867
|
+
if (fernignorePatterns.some((p) => file === p || minimatch3(file, p))) return true;
|
|
2802
2868
|
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
2803
2869
|
return true;
|
|
2804
2870
|
}
|
|
@@ -2846,7 +2912,6 @@ var FirstGenerationFlow = class {
|
|
|
2846
2912
|
flow: "first-generation",
|
|
2847
2913
|
previousGenerationSha: null,
|
|
2848
2914
|
currentGenerationSha: headSha,
|
|
2849
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
2850
2915
|
_prepared: {
|
|
2851
2916
|
kind: "terminal",
|
|
2852
2917
|
flow: "first-generation",
|
|
@@ -2856,8 +2921,7 @@ var FirstGenerationFlow = class {
|
|
|
2856
2921
|
}
|
|
2857
2922
|
const commitOpts = options ? {
|
|
2858
2923
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2859
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2860
|
-
baseBranchHead: options.baseBranchHead
|
|
2924
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2861
2925
|
} : void 0;
|
|
2862
2926
|
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
2863
2927
|
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
@@ -2866,7 +2930,6 @@ var FirstGenerationFlow = class {
|
|
|
2866
2930
|
flow: "first-generation",
|
|
2867
2931
|
previousGenerationSha: null,
|
|
2868
2932
|
currentGenerationSha: genRecord.commit_sha,
|
|
2869
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2870
2933
|
_prepared: {
|
|
2871
2934
|
kind: "active",
|
|
2872
2935
|
flow: "first-generation",
|
|
@@ -2880,6 +2943,7 @@ var FirstGenerationFlow = class {
|
|
|
2880
2943
|
};
|
|
2881
2944
|
}
|
|
2882
2945
|
async apply(_prep) {
|
|
2946
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2883
2947
|
this.service.lockManager.save();
|
|
2884
2948
|
return firstGenerationReport();
|
|
2885
2949
|
}
|
|
@@ -2903,8 +2967,7 @@ var SkipApplicationFlow = class {
|
|
|
2903
2967
|
async prepare(options) {
|
|
2904
2968
|
const commitOpts = options ? {
|
|
2905
2969
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2906
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2907
|
-
baseBranchHead: options.baseBranchHead
|
|
2970
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2908
2971
|
} : void 0;
|
|
2909
2972
|
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
2910
2973
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -2926,7 +2989,6 @@ var SkipApplicationFlow = class {
|
|
|
2926
2989
|
flow: "skip-application",
|
|
2927
2990
|
previousGenerationSha,
|
|
2928
2991
|
currentGenerationSha: genRecord.commit_sha,
|
|
2929
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2930
2992
|
_prepared: {
|
|
2931
2993
|
kind: "active",
|
|
2932
2994
|
flow: "skip-application",
|
|
@@ -2940,6 +3002,7 @@ var SkipApplicationFlow = class {
|
|
|
2940
3002
|
};
|
|
2941
3003
|
}
|
|
2942
3004
|
async apply(_prep, options) {
|
|
3005
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2943
3006
|
this.service.lockManager.save();
|
|
2944
3007
|
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
2945
3008
|
if (!options?.stageOnly) {
|
|
@@ -2987,7 +3050,6 @@ var NoPatchesFlow = class {
|
|
|
2987
3050
|
flow: "no-patches",
|
|
2988
3051
|
previousGenerationSha,
|
|
2989
3052
|
currentGenerationSha: headSha,
|
|
2990
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
2991
3053
|
_prepared: {
|
|
2992
3054
|
kind: "terminal",
|
|
2993
3055
|
flow: "no-patches",
|
|
@@ -3019,8 +3081,7 @@ var NoPatchesFlow = class {
|
|
|
3019
3081
|
}
|
|
3020
3082
|
const commitOpts = options ? {
|
|
3021
3083
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3022
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3023
|
-
baseBranchHead: options.baseBranchHead
|
|
3084
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3024
3085
|
} : void 0;
|
|
3025
3086
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3026
3087
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3030,7 +3091,6 @@ var NoPatchesFlow = class {
|
|
|
3030
3091
|
flow: "no-patches",
|
|
3031
3092
|
previousGenerationSha,
|
|
3032
3093
|
currentGenerationSha: genRecord.commit_sha,
|
|
3033
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3034
3094
|
_prepared: {
|
|
3035
3095
|
kind: "active",
|
|
3036
3096
|
flow: "no-patches",
|
|
@@ -3066,6 +3126,7 @@ var NoPatchesFlow = class {
|
|
|
3066
3126
|
this.service.lockManager.addPatch(patch);
|
|
3067
3127
|
}
|
|
3068
3128
|
}
|
|
3129
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3069
3130
|
this.service.lockManager.save();
|
|
3070
3131
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3071
3132
|
if (newPatches.length > 0) {
|
|
@@ -3120,7 +3181,6 @@ var NormalRegenerationFlow = class {
|
|
|
3120
3181
|
flow: "normal-regeneration",
|
|
3121
3182
|
previousGenerationSha,
|
|
3122
3183
|
currentGenerationSha: headSha,
|
|
3123
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
3124
3184
|
_prepared: {
|
|
3125
3185
|
kind: "terminal",
|
|
3126
3186
|
flow: "normal-regeneration",
|
|
@@ -3138,14 +3198,16 @@ var NormalRegenerationFlow = class {
|
|
|
3138
3198
|
existingPatches = this.service.lockManager.getPatches();
|
|
3139
3199
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
3140
3200
|
for (const p of existingPatches) {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3201
|
+
if (!p.original_commit || p.original_commit === "") {
|
|
3202
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
3203
|
+
if (priorPatchId !== void 0) {
|
|
3204
|
+
this.service.lockManager.removePatch(p.id);
|
|
3205
|
+
this.service.warnings.push(
|
|
3206
|
+
`Consolidated patch ${p.id} into prior patch ${priorPatchId}: byte-identical patch_content. Lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
3207
|
+
);
|
|
3208
|
+
} else {
|
|
3209
|
+
seenHashes.set(p.content_hash, p.id);
|
|
3210
|
+
}
|
|
3149
3211
|
}
|
|
3150
3212
|
}
|
|
3151
3213
|
existingPatches = this.service.lockManager.getPatches();
|
|
@@ -3174,8 +3236,7 @@ var NormalRegenerationFlow = class {
|
|
|
3174
3236
|
const allPatches = [...existingPatches, ...newPatches];
|
|
3175
3237
|
const commitOpts = options ? {
|
|
3176
3238
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3177
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3178
|
-
baseBranchHead: options.baseBranchHead
|
|
3239
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3179
3240
|
} : void 0;
|
|
3180
3241
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3181
3242
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3185,7 +3246,6 @@ var NormalRegenerationFlow = class {
|
|
|
3185
3246
|
flow: "normal-regeneration",
|
|
3186
3247
|
previousGenerationSha,
|
|
3187
3248
|
currentGenerationSha: genRecord.commit_sha,
|
|
3188
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3189
3249
|
_prepared: {
|
|
3190
3250
|
kind: "active",
|
|
3191
3251
|
flow: "normal-regeneration",
|
|
@@ -3229,6 +3289,7 @@ var NormalRegenerationFlow = class {
|
|
|
3229
3289
|
}
|
|
3230
3290
|
}
|
|
3231
3291
|
}
|
|
3292
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3232
3293
|
this.service.lockManager.save();
|
|
3233
3294
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3234
3295
|
if (options?.stageOnly) {
|
|
@@ -3378,68 +3439,6 @@ var ReplayService = class {
|
|
|
3378
3439
|
const prep = await this.prepareReplay(options);
|
|
3379
3440
|
return this.applyPreparedReplay(prep, options);
|
|
3380
3441
|
}
|
|
3381
|
-
/**
|
|
3382
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
3383
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
3384
|
-
*
|
|
3385
|
-
* After updating the generation record, re-detects customer patches via
|
|
3386
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
3387
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
3388
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
3389
|
-
* was restored from the base branch during conflict PR creation.
|
|
3390
|
-
*/
|
|
3391
|
-
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
3392
|
-
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
3393
|
-
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
3394
|
-
const record = {
|
|
3395
|
-
commit_sha: generationCommitSha,
|
|
3396
|
-
tree_hash: treeHash,
|
|
3397
|
-
timestamp,
|
|
3398
|
-
cli_version: options?.cliVersion ?? "unknown",
|
|
3399
|
-
generator_versions: options?.generatorVersions ?? {},
|
|
3400
|
-
base_branch_head: options?.baseBranchHead
|
|
3401
|
-
};
|
|
3402
|
-
let resolvedPatches;
|
|
3403
|
-
if (!this.lockManager.exists()) {
|
|
3404
|
-
this.lockManager.initializeInMemory(record);
|
|
3405
|
-
} else {
|
|
3406
|
-
this.lockManager.read();
|
|
3407
|
-
const unresolvedPatches = [
|
|
3408
|
-
...this.lockManager.getUnresolvedPatches(),
|
|
3409
|
-
...this.lockManager.getResolvingPatches()
|
|
3410
|
-
];
|
|
3411
|
-
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
3412
|
-
this.lockManager.addGeneration(record);
|
|
3413
|
-
this.lockManager.clearPatches();
|
|
3414
|
-
for (const patch of unresolvedPatches) {
|
|
3415
|
-
this.lockManager.addPatch(patch);
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
try {
|
|
3419
|
-
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
3420
|
-
if (redetectedPatches.length > 0) {
|
|
3421
|
-
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
3422
|
-
const currentPatches = this.lockManager.getPatches();
|
|
3423
|
-
for (const patch of currentPatches) {
|
|
3424
|
-
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
3425
|
-
this.lockManager.removePatch(patch.id);
|
|
3426
|
-
}
|
|
3427
|
-
}
|
|
3428
|
-
for (const patch of redetectedPatches) {
|
|
3429
|
-
this.lockManager.addPatch(patch);
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
} catch (error) {
|
|
3433
|
-
for (const patch of resolvedPatches ?? []) {
|
|
3434
|
-
this.lockManager.addPatch(patch);
|
|
3435
|
-
}
|
|
3436
|
-
this.detector.warnings.push(
|
|
3437
|
-
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
3438
|
-
);
|
|
3439
|
-
}
|
|
3440
|
-
this.lockManager.save();
|
|
3441
|
-
this.detector.warnings.push(...this.lockManager.warnings);
|
|
3442
|
-
}
|
|
3443
3442
|
determineFlow() {
|
|
3444
3443
|
try {
|
|
3445
3444
|
const lock = this.lockManager.read();
|
|
@@ -3524,7 +3523,7 @@ var ReplayService = class {
|
|
|
3524
3523
|
)
|
|
3525
3524
|
);
|
|
3526
3525
|
const hasProtectedFiles = patch.files.some(
|
|
3527
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
3526
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
|
|
3528
3527
|
);
|
|
3529
3528
|
const allUserOwned = isUserOwnedPerFile.every(Boolean);
|
|
3530
3529
|
if (allUserOwned && patch.files.length > 0) {
|
|
@@ -3964,7 +3963,7 @@ var ReplayService = class {
|
|
|
3964
3963
|
);
|
|
3965
3964
|
if (snapHasStaleMarkers) continue;
|
|
3966
3965
|
const isProtectedSnap = patch.files.some(
|
|
3967
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
3966
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
|
|
3968
3967
|
);
|
|
3969
3968
|
if (isProtectedSnap) continue;
|
|
3970
3969
|
const snapHash = this.detector.computeContentHash(snapshotDiff);
|
|
@@ -3984,6 +3983,11 @@ var ReplayService = class {
|
|
|
3984
3983
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
3985
3984
|
);
|
|
3986
3985
|
if (ppResult === null) continue;
|
|
3986
|
+
if (ppResult.hadFallback) {
|
|
3987
|
+
this.warnings.push(
|
|
3988
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3987
3991
|
const diff = ppResult.diff;
|
|
3988
3992
|
if (!diff.trim()) {
|
|
3989
3993
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
@@ -3999,7 +4003,7 @@ var ReplayService = class {
|
|
|
3999
4003
|
continue;
|
|
4000
4004
|
}
|
|
4001
4005
|
const isProtected = patch.files.some(
|
|
4002
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p ||
|
|
4006
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
|
|
4003
4007
|
);
|
|
4004
4008
|
if (isProtected) {
|
|
4005
4009
|
continue;
|
|
@@ -4071,6 +4075,11 @@ var ReplayService = class {
|
|
|
4071
4075
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4072
4076
|
);
|
|
4073
4077
|
if (ppResult === null) continue;
|
|
4078
|
+
if (ppResult.hadFallback) {
|
|
4079
|
+
this.warnings.push(
|
|
4080
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4081
|
+
);
|
|
4082
|
+
}
|
|
4074
4083
|
const diff = ppResult.diff;
|
|
4075
4084
|
if (!diff.trim()) {
|
|
4076
4085
|
if (await allPatchFilesUserOwned(patch)) {
|
|
@@ -4158,13 +4167,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4158
4167
|
if (files.length === 0) return;
|
|
4159
4168
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
4160
4169
|
for (const file of files) {
|
|
4161
|
-
if (fernignorePatterns.some((pattern) =>
|
|
4170
|
+
if (fernignorePatterns.some((pattern) => minimatch4(file, pattern))) continue;
|
|
4162
4171
|
try {
|
|
4163
4172
|
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
4164
4173
|
} catch {
|
|
4165
4174
|
}
|
|
4166
4175
|
}
|
|
4167
4176
|
}
|
|
4177
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4168
4178
|
readFernignorePatterns() {
|
|
4169
4179
|
const fernignorePath = join6(this.outputDir, ".fernignore");
|
|
4170
4180
|
if (!existsSync2(fernignorePath)) return [];
|
|
@@ -4247,7 +4257,7 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
4247
4257
|
import { createHash as createHash2 } from "crypto";
|
|
4248
4258
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4249
4259
|
import { dirname as dirname5, join as join7 } from "path";
|
|
4250
|
-
import { minimatch as
|
|
4260
|
+
import { minimatch as minimatch5 } from "minimatch";
|
|
4251
4261
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
4252
4262
|
var FernignoreMigrator = class {
|
|
4253
4263
|
git;
|
|
@@ -4279,7 +4289,7 @@ var FernignoreMigrator = class {
|
|
|
4279
4289
|
const commitsOnly = [];
|
|
4280
4290
|
const syntheticPatches = [];
|
|
4281
4291
|
for (const pattern of patterns) {
|
|
4282
|
-
const matchingPatch = patches.find((p) => p.files.some((f) =>
|
|
4292
|
+
const matchingPatch = patches.find((p) => p.files.some((f) => minimatch5(f, pattern) || f === pattern));
|
|
4283
4293
|
if (matchingPatch) {
|
|
4284
4294
|
trackedByBoth.push({
|
|
4285
4295
|
file: pattern,
|
|
@@ -4352,7 +4362,7 @@ var FernignoreMigrator = class {
|
|
|
4352
4362
|
fernignoreOnly.push(...remainingFernignoreOnly);
|
|
4353
4363
|
}
|
|
4354
4364
|
for (const patch of patches) {
|
|
4355
|
-
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) =>
|
|
4365
|
+
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch5(f, p) || f === p));
|
|
4356
4366
|
if (hasUnprotectedFiles) {
|
|
4357
4367
|
commitsOnly.push(patch);
|
|
4358
4368
|
}
|
|
@@ -4364,7 +4374,7 @@ var FernignoreMigrator = class {
|
|
|
4364
4374
|
const results = [];
|
|
4365
4375
|
for (const pattern of patterns) {
|
|
4366
4376
|
const matching = allFiles.filter(
|
|
4367
|
-
(f) =>
|
|
4377
|
+
(f) => minimatch5(f, pattern) || f === pattern || f.startsWith(pattern + "/")
|
|
4368
4378
|
);
|
|
4369
4379
|
results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
|
|
4370
4380
|
}
|
|
@@ -4677,7 +4687,7 @@ function computeContentHash(patchContent) {
|
|
|
4677
4687
|
}
|
|
4678
4688
|
|
|
4679
4689
|
// src/commands/forget.ts
|
|
4680
|
-
import { minimatch as
|
|
4690
|
+
import { minimatch as minimatch6 } from "minimatch";
|
|
4681
4691
|
function parseDiffStat(patchContent) {
|
|
4682
4692
|
let additions = 0;
|
|
4683
4693
|
let deletions = 0;
|
|
@@ -4707,7 +4717,7 @@ function toMatchedPatch(patch) {
|
|
|
4707
4717
|
}
|
|
4708
4718
|
function matchesPatch(patch, pattern) {
|
|
4709
4719
|
const fileMatch = patch.files.some(
|
|
4710
|
-
(file) => file === pattern ||
|
|
4720
|
+
(file) => file === pattern || minimatch6(file, pattern)
|
|
4711
4721
|
);
|
|
4712
4722
|
if (fileMatch) return true;
|
|
4713
4723
|
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|