@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.cjs
CHANGED
|
@@ -554,9 +554,22 @@ function isGenerationCommit(commit) {
|
|
|
554
554
|
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.
|
|
555
555
|
// The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
|
|
556
556
|
// GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
|
|
557
|
-
commit.message.startsWith("SDK Generation")
|
|
557
|
+
commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
|
|
558
|
+
// regen PR. First-parent walking lands on this commit, not on the
|
|
559
|
+
// generation commit on the side branch, so the classifier must treat
|
|
560
|
+
// it as a generation boundary. GitHub's default subject is
|
|
561
|
+
// "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
|
|
562
|
+
// named "<owner>/fern-bot/<...>".
|
|
563
|
+
/^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
|
|
558
564
|
return isBotAuthor || hasGenerationMarker;
|
|
559
565
|
}
|
|
566
|
+
function isGenerationBoundary(commit) {
|
|
567
|
+
const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
|
|
568
|
+
const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
|
|
569
|
+
const isAutoversion = commit.message.startsWith("[fern-autoversion]");
|
|
570
|
+
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");
|
|
571
|
+
return isBotAuthor && !isAutoversion || hasBoundaryMarker;
|
|
572
|
+
}
|
|
560
573
|
function isReplayCommit(commit) {
|
|
561
574
|
return commit.message.startsWith("[fern-replay]");
|
|
562
575
|
}
|
|
@@ -575,6 +588,7 @@ function parseRevertedMessage(subject) {
|
|
|
575
588
|
// src/LockfileManager.ts
|
|
576
589
|
var import_node_fs = require("fs");
|
|
577
590
|
var import_node_path = require("path");
|
|
591
|
+
var import_minimatch = require("minimatch");
|
|
578
592
|
var import_yaml = require("yaml");
|
|
579
593
|
var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
|
|
580
594
|
var LockfileNotFoundError = class extends Error {
|
|
@@ -586,10 +600,23 @@ var LockfileNotFoundError = class extends Error {
|
|
|
586
600
|
var LockfileManager = class {
|
|
587
601
|
outputDir;
|
|
588
602
|
lock = null;
|
|
603
|
+
fernignorePatterns = [];
|
|
589
604
|
warnings = [];
|
|
590
605
|
constructor(outputDir) {
|
|
591
606
|
this.outputDir = outputDir;
|
|
592
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Inform the lockfile of the customer's `.fernignore` patterns so that
|
|
610
|
+
* `save()` can suppress its "Patch X removed from lockfile" warning when
|
|
611
|
+
* every file in the stripped patch is already `.fernignore`-protected.
|
|
612
|
+
* The strip itself still happens (credentials never persist); only the
|
|
613
|
+
* customer-visible warning is suppressed in that case, because the
|
|
614
|
+
* protected files survive the generator wipe regardless of whether the
|
|
615
|
+
* lockfile tracks them, so there's nothing for the customer to action.
|
|
616
|
+
*/
|
|
617
|
+
setFernignorePatterns(patterns) {
|
|
618
|
+
this.fernignorePatterns = patterns;
|
|
619
|
+
}
|
|
593
620
|
get lockfilePath() {
|
|
594
621
|
return (0, import_node_path.join)(this.outputDir, ".fern", "replay.lock");
|
|
595
622
|
}
|
|
@@ -658,9 +685,14 @@ var LockfileManager = class {
|
|
|
658
685
|
if (sensitiveFiles.length > 0) {
|
|
659
686
|
reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
|
|
660
687
|
}
|
|
661
|
-
|
|
662
|
-
|
|
688
|
+
const allProtected = patch.files.length > 0 && patch.files.every(
|
|
689
|
+
(f) => this.fernignorePatterns.some((p) => f === p || (0, import_minimatch.minimatch)(f, p))
|
|
663
690
|
);
|
|
691
|
+
if (!allProtected) {
|
|
692
|
+
this.warnings.push(
|
|
693
|
+
`Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
664
696
|
} else {
|
|
665
697
|
cleanPatches.push(patch);
|
|
666
698
|
}
|
|
@@ -884,8 +916,48 @@ var ReplayDetector = class {
|
|
|
884
916
|
this.lockManager = lockManager;
|
|
885
917
|
this.sdkOutputDir = sdkOutputDir;
|
|
886
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
921
|
+
* `lock.current_generation`.
|
|
922
|
+
*
|
|
923
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
924
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
925
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
926
|
+
*
|
|
927
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
928
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
929
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
930
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
931
|
+
* on second-parent — stopping at the merge would hide them).
|
|
932
|
+
*
|
|
933
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
934
|
+
*/
|
|
935
|
+
async findPreviousGenerationFromHistory() {
|
|
936
|
+
let log;
|
|
937
|
+
try {
|
|
938
|
+
log = await this.git.exec([
|
|
939
|
+
"log",
|
|
940
|
+
"--first-parent",
|
|
941
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
942
|
+
"HEAD"
|
|
943
|
+
]);
|
|
944
|
+
} catch {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
for (const commit of this.parseGitLog(log)) {
|
|
948
|
+
if (isGenerationBoundary(commit)) {
|
|
949
|
+
return commit.sha;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
887
954
|
async detectNewPatches() {
|
|
888
955
|
const lock = this.lockManager.read();
|
|
956
|
+
const derivedSha = await this.findPreviousGenerationFromHistory();
|
|
957
|
+
if (derivedSha !== null) {
|
|
958
|
+
const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
|
|
959
|
+
return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
|
|
960
|
+
}
|
|
889
961
|
const lastGen = this.getLastGeneration(lock);
|
|
890
962
|
if (!lastGen) {
|
|
891
963
|
return { patches: [], revertedPatchIds: [] };
|
|
@@ -893,7 +965,7 @@ var ReplayDetector = class {
|
|
|
893
965
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
894
966
|
if (!exists) {
|
|
895
967
|
this.warnings.push(
|
|
896
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to
|
|
968
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
897
969
|
);
|
|
898
970
|
return this.detectPatchesViaTreeDiff(
|
|
899
971
|
lastGen,
|
|
@@ -901,44 +973,8 @@ var ReplayDetector = class {
|
|
|
901
973
|
true
|
|
902
974
|
);
|
|
903
975
|
}
|
|
904
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
905
|
-
if (!isAncestor) {
|
|
906
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
907
|
-
}
|
|
908
976
|
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
909
977
|
}
|
|
910
|
-
/**
|
|
911
|
-
* Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
912
|
-
*
|
|
913
|
-
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
914
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
915
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
916
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
917
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
918
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
919
|
-
* generation) get bundled as customer customizations.
|
|
920
|
-
*
|
|
921
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
922
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
923
|
-
*/
|
|
924
|
-
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
925
|
-
let mergeBase = "";
|
|
926
|
-
try {
|
|
927
|
-
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
928
|
-
} catch {
|
|
929
|
-
}
|
|
930
|
-
if (!mergeBase) {
|
|
931
|
-
this.warnings.push(
|
|
932
|
-
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
933
|
-
);
|
|
934
|
-
return this.detectPatchesViaTreeDiff(
|
|
935
|
-
lastGen,
|
|
936
|
-
/* commitKnownMissing */
|
|
937
|
-
false
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
941
|
-
}
|
|
942
978
|
/**
|
|
943
979
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
944
980
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -1374,6 +1410,26 @@ var ReplayDetector = class {
|
|
|
1374
1410
|
getLastGeneration(lock) {
|
|
1375
1411
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
1376
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
1415
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
1416
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
1417
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
1418
|
+
*/
|
|
1419
|
+
async synthesizeGenerationRecord(sha) {
|
|
1420
|
+
let tree_hash = "";
|
|
1421
|
+
try {
|
|
1422
|
+
tree_hash = await this.git.getTreeHash(sha);
|
|
1423
|
+
} catch {
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
commit_sha: sha,
|
|
1427
|
+
tree_hash,
|
|
1428
|
+
timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1429
|
+
cli_version: "",
|
|
1430
|
+
generator_versions: {}
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1377
1433
|
};
|
|
1378
1434
|
|
|
1379
1435
|
// src/ThreeWayMerge.ts
|
|
@@ -1519,7 +1575,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
|
|
|
1519
1575
|
var import_promises3 = require("fs/promises");
|
|
1520
1576
|
var import_node_os = require("os");
|
|
1521
1577
|
var import_node_path5 = require("path");
|
|
1522
|
-
var
|
|
1578
|
+
var import_minimatch2 = require("minimatch");
|
|
1523
1579
|
|
|
1524
1580
|
// src/accumulator/MergeAccumulator.ts
|
|
1525
1581
|
var MergeAccumulator = class {
|
|
@@ -2235,13 +2291,13 @@ var ReplayApplicator = class {
|
|
|
2235
2291
|
isExcluded(patch) {
|
|
2236
2292
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2237
2293
|
if (!config.exclude) return false;
|
|
2238
|
-
return patch.files.some((file) => config.exclude.some((pattern) => (0,
|
|
2294
|
+
return patch.files.some((file) => config.exclude.some((pattern) => (0, import_minimatch2.minimatch)(file, pattern)));
|
|
2239
2295
|
}
|
|
2240
2296
|
async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
|
|
2241
2297
|
const config = this.lockManager.getCustomizationsConfig();
|
|
2242
2298
|
if (config.moves) {
|
|
2243
2299
|
for (const move of config.moves) {
|
|
2244
|
-
if ((0,
|
|
2300
|
+
if ((0, import_minimatch2.minimatch)(filePath, move.from) || filePath === move.from) {
|
|
2245
2301
|
if (filePath === move.from) {
|
|
2246
2302
|
return move.to;
|
|
2247
2303
|
}
|
|
@@ -2513,8 +2569,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2513
2569
|
tree_hash: treeHash,
|
|
2514
2570
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2515
2571
|
cli_version: options?.cliVersion ?? "unknown",
|
|
2516
|
-
generator_versions: options?.generatorVersions ?? {}
|
|
2517
|
-
base_branch_head: options?.baseBranchHead
|
|
2572
|
+
generator_versions: options?.generatorVersions ?? {}
|
|
2518
2573
|
};
|
|
2519
2574
|
}
|
|
2520
2575
|
async stageAll() {
|
|
@@ -2533,7 +2588,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2533
2588
|
var import_node_fs2 = require("fs");
|
|
2534
2589
|
var import_promises6 = require("fs/promises");
|
|
2535
2590
|
var import_node_path8 = require("path");
|
|
2536
|
-
var
|
|
2591
|
+
var import_minimatch4 = require("minimatch");
|
|
2537
2592
|
init_GitClient();
|
|
2538
2593
|
|
|
2539
2594
|
// src/PatchRegionDiff.ts
|
|
@@ -2644,12 +2699,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
|
|
|
2644
2699
|
unlocatableFiles.push(file);
|
|
2645
2700
|
continue;
|
|
2646
2701
|
}
|
|
2647
|
-
const locInCurrentGen =
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2702
|
+
const locInCurrentGen = [];
|
|
2703
|
+
const locInWorkingTree = [];
|
|
2704
|
+
let spanResolutionFailed = false;
|
|
2705
|
+
for (let i = 0; i < locInCurrentGenRaw.length; i++) {
|
|
2706
|
+
const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
|
|
2707
|
+
const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
|
|
2708
|
+
if (cg === null || wt === null) {
|
|
2709
|
+
spanResolutionFailed = true;
|
|
2710
|
+
break;
|
|
2711
|
+
}
|
|
2712
|
+
locInCurrentGen.push(cg);
|
|
2713
|
+
locInWorkingTree.push(wt);
|
|
2714
|
+
}
|
|
2715
|
+
if (spanResolutionFailed) {
|
|
2716
|
+
unlocatableFiles.push(file);
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2653
2719
|
const overlay = overlayRegions(
|
|
2654
2720
|
currentGenLines,
|
|
2655
2721
|
locInCurrentGen,
|
|
@@ -2711,11 +2777,11 @@ function resolveSpan(loc, oursLines, prefer) {
|
|
|
2711
2777
|
if (prefer === "old") {
|
|
2712
2778
|
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2713
2779
|
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2714
|
-
|
|
2715
|
-
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2716
|
-
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2780
|
+
return loc;
|
|
2717
2781
|
}
|
|
2718
|
-
return loc;
|
|
2782
|
+
if (newFits) return { ...loc, oursSpan: hunk.newCount };
|
|
2783
|
+
if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
|
|
2784
|
+
return null;
|
|
2719
2785
|
}
|
|
2720
2786
|
function sequenceMatches(needle, haystack, offset) {
|
|
2721
2787
|
if (offset + needle.length > haystack.length) return false;
|
|
@@ -2829,7 +2895,7 @@ function synthesizeDeleteFileDiff(file, content) {
|
|
|
2829
2895
|
}
|
|
2830
2896
|
|
|
2831
2897
|
// src/classifier/FileOwnership.ts
|
|
2832
|
-
var
|
|
2898
|
+
var import_minimatch3 = require("minimatch");
|
|
2833
2899
|
var FileOwnership = class {
|
|
2834
2900
|
constructor(git) {
|
|
2835
2901
|
this.git = git;
|
|
@@ -2853,7 +2919,7 @@ var FileOwnership = class {
|
|
|
2853
2919
|
const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
|
|
2854
2920
|
if (patch.user_owned) return true;
|
|
2855
2921
|
if (file === ".fernignore") return true;
|
|
2856
|
-
if (fernignorePatterns.some((p) => file === p || (0,
|
|
2922
|
+
if (fernignorePatterns.some((p) => file === p || (0, import_minimatch3.minimatch)(file, p))) return true;
|
|
2857
2923
|
if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
|
|
2858
2924
|
return true;
|
|
2859
2925
|
}
|
|
@@ -2901,7 +2967,6 @@ var FirstGenerationFlow = class {
|
|
|
2901
2967
|
flow: "first-generation",
|
|
2902
2968
|
previousGenerationSha: null,
|
|
2903
2969
|
currentGenerationSha: headSha,
|
|
2904
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
2905
2970
|
_prepared: {
|
|
2906
2971
|
kind: "terminal",
|
|
2907
2972
|
flow: "first-generation",
|
|
@@ -2911,8 +2976,7 @@ var FirstGenerationFlow = class {
|
|
|
2911
2976
|
}
|
|
2912
2977
|
const commitOpts = options ? {
|
|
2913
2978
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2914
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2915
|
-
baseBranchHead: options.baseBranchHead
|
|
2979
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2916
2980
|
} : void 0;
|
|
2917
2981
|
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
2918
2982
|
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
@@ -2921,7 +2985,6 @@ var FirstGenerationFlow = class {
|
|
|
2921
2985
|
flow: "first-generation",
|
|
2922
2986
|
previousGenerationSha: null,
|
|
2923
2987
|
currentGenerationSha: genRecord.commit_sha,
|
|
2924
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2925
2988
|
_prepared: {
|
|
2926
2989
|
kind: "active",
|
|
2927
2990
|
flow: "first-generation",
|
|
@@ -2935,6 +2998,7 @@ var FirstGenerationFlow = class {
|
|
|
2935
2998
|
};
|
|
2936
2999
|
}
|
|
2937
3000
|
async apply(_prep) {
|
|
3001
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2938
3002
|
this.service.lockManager.save();
|
|
2939
3003
|
return firstGenerationReport();
|
|
2940
3004
|
}
|
|
@@ -2958,8 +3022,7 @@ var SkipApplicationFlow = class {
|
|
|
2958
3022
|
async prepare(options) {
|
|
2959
3023
|
const commitOpts = options ? {
|
|
2960
3024
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2961
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2962
|
-
baseBranchHead: options.baseBranchHead
|
|
3025
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2963
3026
|
} : void 0;
|
|
2964
3027
|
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
2965
3028
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -2981,7 +3044,6 @@ var SkipApplicationFlow = class {
|
|
|
2981
3044
|
flow: "skip-application",
|
|
2982
3045
|
previousGenerationSha,
|
|
2983
3046
|
currentGenerationSha: genRecord.commit_sha,
|
|
2984
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2985
3047
|
_prepared: {
|
|
2986
3048
|
kind: "active",
|
|
2987
3049
|
flow: "skip-application",
|
|
@@ -2995,6 +3057,7 @@ var SkipApplicationFlow = class {
|
|
|
2995
3057
|
};
|
|
2996
3058
|
}
|
|
2997
3059
|
async apply(_prep, options) {
|
|
3060
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
2998
3061
|
this.service.lockManager.save();
|
|
2999
3062
|
const credentialWarnings = [...this.service.lockManager.warnings];
|
|
3000
3063
|
if (!options?.stageOnly) {
|
|
@@ -3042,7 +3105,6 @@ var NoPatchesFlow = class {
|
|
|
3042
3105
|
flow: "no-patches",
|
|
3043
3106
|
previousGenerationSha,
|
|
3044
3107
|
currentGenerationSha: headSha,
|
|
3045
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
3046
3108
|
_prepared: {
|
|
3047
3109
|
kind: "terminal",
|
|
3048
3110
|
flow: "no-patches",
|
|
@@ -3074,8 +3136,7 @@ var NoPatchesFlow = class {
|
|
|
3074
3136
|
}
|
|
3075
3137
|
const commitOpts = options ? {
|
|
3076
3138
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3077
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3078
|
-
baseBranchHead: options.baseBranchHead
|
|
3139
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3079
3140
|
} : void 0;
|
|
3080
3141
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3081
3142
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3085,7 +3146,6 @@ var NoPatchesFlow = class {
|
|
|
3085
3146
|
flow: "no-patches",
|
|
3086
3147
|
previousGenerationSha,
|
|
3087
3148
|
currentGenerationSha: genRecord.commit_sha,
|
|
3088
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3089
3149
|
_prepared: {
|
|
3090
3150
|
kind: "active",
|
|
3091
3151
|
flow: "no-patches",
|
|
@@ -3121,6 +3181,7 @@ var NoPatchesFlow = class {
|
|
|
3121
3181
|
this.service.lockManager.addPatch(patch);
|
|
3122
3182
|
}
|
|
3123
3183
|
}
|
|
3184
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3124
3185
|
this.service.lockManager.save();
|
|
3125
3186
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3126
3187
|
if (newPatches.length > 0) {
|
|
@@ -3175,7 +3236,6 @@ var NormalRegenerationFlow = class {
|
|
|
3175
3236
|
flow: "normal-regeneration",
|
|
3176
3237
|
previousGenerationSha,
|
|
3177
3238
|
currentGenerationSha: headSha,
|
|
3178
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
3179
3239
|
_prepared: {
|
|
3180
3240
|
kind: "terminal",
|
|
3181
3241
|
flow: "normal-regeneration",
|
|
@@ -3193,14 +3253,16 @@ var NormalRegenerationFlow = class {
|
|
|
3193
3253
|
existingPatches = this.service.lockManager.getPatches();
|
|
3194
3254
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
3195
3255
|
for (const p of existingPatches) {
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3256
|
+
if (!p.original_commit || p.original_commit === "") {
|
|
3257
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
3258
|
+
if (priorPatchId !== void 0) {
|
|
3259
|
+
this.service.lockManager.removePatch(p.id);
|
|
3260
|
+
this.service.warnings.push(
|
|
3261
|
+
`Consolidated patch ${p.id} into prior patch ${priorPatchId}: byte-identical patch_content. Lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
3262
|
+
);
|
|
3263
|
+
} else {
|
|
3264
|
+
seenHashes.set(p.content_hash, p.id);
|
|
3265
|
+
}
|
|
3204
3266
|
}
|
|
3205
3267
|
}
|
|
3206
3268
|
existingPatches = this.service.lockManager.getPatches();
|
|
@@ -3229,8 +3291,7 @@ var NormalRegenerationFlow = class {
|
|
|
3229
3291
|
const allPatches = [...existingPatches, ...newPatches];
|
|
3230
3292
|
const commitOpts = options ? {
|
|
3231
3293
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3232
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3233
|
-
baseBranchHead: options.baseBranchHead
|
|
3294
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3234
3295
|
} : void 0;
|
|
3235
3296
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3236
3297
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3240,7 +3301,6 @@ var NormalRegenerationFlow = class {
|
|
|
3240
3301
|
flow: "normal-regeneration",
|
|
3241
3302
|
previousGenerationSha,
|
|
3242
3303
|
currentGenerationSha: genRecord.commit_sha,
|
|
3243
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3244
3304
|
_prepared: {
|
|
3245
3305
|
kind: "active",
|
|
3246
3306
|
flow: "normal-regeneration",
|
|
@@ -3284,6 +3344,7 @@ var NormalRegenerationFlow = class {
|
|
|
3284
3344
|
}
|
|
3285
3345
|
}
|
|
3286
3346
|
}
|
|
3347
|
+
this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
|
|
3287
3348
|
this.service.lockManager.save();
|
|
3288
3349
|
this.service.warnings.push(...this.service.lockManager.warnings);
|
|
3289
3350
|
if (options?.stageOnly) {
|
|
@@ -3433,68 +3494,6 @@ var ReplayService = class {
|
|
|
3433
3494
|
const prep = await this.prepareReplay(options);
|
|
3434
3495
|
return this.applyPreparedReplay(prep, options);
|
|
3435
3496
|
}
|
|
3436
|
-
/**
|
|
3437
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
3438
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
3439
|
-
*
|
|
3440
|
-
* After updating the generation record, re-detects customer patches via
|
|
3441
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
3442
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
3443
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
3444
|
-
* was restored from the base branch during conflict PR creation.
|
|
3445
|
-
*/
|
|
3446
|
-
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
3447
|
-
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
3448
|
-
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
3449
|
-
const record = {
|
|
3450
|
-
commit_sha: generationCommitSha,
|
|
3451
|
-
tree_hash: treeHash,
|
|
3452
|
-
timestamp,
|
|
3453
|
-
cli_version: options?.cliVersion ?? "unknown",
|
|
3454
|
-
generator_versions: options?.generatorVersions ?? {},
|
|
3455
|
-
base_branch_head: options?.baseBranchHead
|
|
3456
|
-
};
|
|
3457
|
-
let resolvedPatches;
|
|
3458
|
-
if (!this.lockManager.exists()) {
|
|
3459
|
-
this.lockManager.initializeInMemory(record);
|
|
3460
|
-
} else {
|
|
3461
|
-
this.lockManager.read();
|
|
3462
|
-
const unresolvedPatches = [
|
|
3463
|
-
...this.lockManager.getUnresolvedPatches(),
|
|
3464
|
-
...this.lockManager.getResolvingPatches()
|
|
3465
|
-
];
|
|
3466
|
-
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
3467
|
-
this.lockManager.addGeneration(record);
|
|
3468
|
-
this.lockManager.clearPatches();
|
|
3469
|
-
for (const patch of unresolvedPatches) {
|
|
3470
|
-
this.lockManager.addPatch(patch);
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
try {
|
|
3474
|
-
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
3475
|
-
if (redetectedPatches.length > 0) {
|
|
3476
|
-
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
3477
|
-
const currentPatches = this.lockManager.getPatches();
|
|
3478
|
-
for (const patch of currentPatches) {
|
|
3479
|
-
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
3480
|
-
this.lockManager.removePatch(patch.id);
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
for (const patch of redetectedPatches) {
|
|
3484
|
-
this.lockManager.addPatch(patch);
|
|
3485
|
-
}
|
|
3486
|
-
}
|
|
3487
|
-
} catch (error) {
|
|
3488
|
-
for (const patch of resolvedPatches ?? []) {
|
|
3489
|
-
this.lockManager.addPatch(patch);
|
|
3490
|
-
}
|
|
3491
|
-
this.detector.warnings.push(
|
|
3492
|
-
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
3493
|
-
);
|
|
3494
|
-
}
|
|
3495
|
-
this.lockManager.save();
|
|
3496
|
-
this.detector.warnings.push(...this.lockManager.warnings);
|
|
3497
|
-
}
|
|
3498
3497
|
determineFlow() {
|
|
3499
3498
|
try {
|
|
3500
3499
|
const lock = this.lockManager.read();
|
|
@@ -3579,7 +3578,7 @@ var ReplayService = class {
|
|
|
3579
3578
|
)
|
|
3580
3579
|
);
|
|
3581
3580
|
const hasProtectedFiles = patch.files.some(
|
|
3582
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
3581
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch4.minimatch)(file, p))
|
|
3583
3582
|
);
|
|
3584
3583
|
const allUserOwned = isUserOwnedPerFile.every(Boolean);
|
|
3585
3584
|
if (allUserOwned && patch.files.length > 0) {
|
|
@@ -4019,7 +4018,7 @@ var ReplayService = class {
|
|
|
4019
4018
|
);
|
|
4020
4019
|
if (snapHasStaleMarkers) continue;
|
|
4021
4020
|
const isProtectedSnap = patch.files.some(
|
|
4022
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
4021
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch4.minimatch)(file, p))
|
|
4023
4022
|
);
|
|
4024
4023
|
if (isProtectedSnap) continue;
|
|
4025
4024
|
const snapHash = this.detector.computeContentHash(snapshotDiff);
|
|
@@ -4039,6 +4038,11 @@ var ReplayService = class {
|
|
|
4039
4038
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4040
4039
|
);
|
|
4041
4040
|
if (ppResult === null) continue;
|
|
4041
|
+
if (ppResult.hadFallback) {
|
|
4042
|
+
this.warnings.push(
|
|
4043
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4044
|
+
);
|
|
4045
|
+
}
|
|
4042
4046
|
const diff = ppResult.diff;
|
|
4043
4047
|
if (!diff.trim()) {
|
|
4044
4048
|
if (await allPatchFilesUserOwned(patch)) continue;
|
|
@@ -4054,7 +4058,7 @@ var ReplayService = class {
|
|
|
4054
4058
|
continue;
|
|
4055
4059
|
}
|
|
4056
4060
|
const isProtected = patch.files.some(
|
|
4057
|
-
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0,
|
|
4061
|
+
(file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || (0, import_minimatch4.minimatch)(file, p))
|
|
4058
4062
|
);
|
|
4059
4063
|
if (isProtected) {
|
|
4060
4064
|
continue;
|
|
@@ -4126,6 +4130,11 @@ var ReplayService = class {
|
|
|
4126
4130
|
(files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
|
|
4127
4131
|
);
|
|
4128
4132
|
if (ppResult === null) continue;
|
|
4133
|
+
if (ppResult.hadFallback) {
|
|
4134
|
+
this.warnings.push(
|
|
4135
|
+
`Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4129
4138
|
const diff = ppResult.diff;
|
|
4130
4139
|
if (!diff.trim()) {
|
|
4131
4140
|
if (await allPatchFilesUserOwned(patch)) {
|
|
@@ -4213,13 +4222,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
|
|
|
4213
4222
|
if (files.length === 0) return;
|
|
4214
4223
|
const fernignorePatterns = this.readFernignorePatterns();
|
|
4215
4224
|
for (const file of files) {
|
|
4216
|
-
if (fernignorePatterns.some((pattern) => (0,
|
|
4225
|
+
if (fernignorePatterns.some((pattern) => (0, import_minimatch4.minimatch)(file, pattern))) continue;
|
|
4217
4226
|
try {
|
|
4218
4227
|
await this.git.exec(["checkout", "HEAD", "--", file]);
|
|
4219
4228
|
} catch {
|
|
4220
4229
|
}
|
|
4221
4230
|
}
|
|
4222
4231
|
}
|
|
4232
|
+
/** @internal Used by Flow implementations in `src/flows/`. */
|
|
4223
4233
|
readFernignorePatterns() {
|
|
4224
4234
|
const fernignorePath = (0, import_node_path8.join)(this.outputDir, ".fernignore");
|
|
4225
4235
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -4302,7 +4312,7 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
|
|
|
4302
4312
|
var import_node_crypto2 = require("crypto");
|
|
4303
4313
|
var import_node_fs3 = require("fs");
|
|
4304
4314
|
var import_node_path9 = require("path");
|
|
4305
|
-
var
|
|
4315
|
+
var import_minimatch5 = require("minimatch");
|
|
4306
4316
|
var import_yaml2 = require("yaml");
|
|
4307
4317
|
var FernignoreMigrator = class {
|
|
4308
4318
|
git;
|
|
@@ -4334,7 +4344,7 @@ var FernignoreMigrator = class {
|
|
|
4334
4344
|
const commitsOnly = [];
|
|
4335
4345
|
const syntheticPatches = [];
|
|
4336
4346
|
for (const pattern of patterns) {
|
|
4337
|
-
const matchingPatch = patches.find((p) => p.files.some((f) => (0,
|
|
4347
|
+
const matchingPatch = patches.find((p) => p.files.some((f) => (0, import_minimatch5.minimatch)(f, pattern) || f === pattern));
|
|
4338
4348
|
if (matchingPatch) {
|
|
4339
4349
|
trackedByBoth.push({
|
|
4340
4350
|
file: pattern,
|
|
@@ -4407,7 +4417,7 @@ var FernignoreMigrator = class {
|
|
|
4407
4417
|
fernignoreOnly.push(...remainingFernignoreOnly);
|
|
4408
4418
|
}
|
|
4409
4419
|
for (const patch of patches) {
|
|
4410
|
-
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => (0,
|
|
4420
|
+
const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => (0, import_minimatch5.minimatch)(f, p) || f === p));
|
|
4411
4421
|
if (hasUnprotectedFiles) {
|
|
4412
4422
|
commitsOnly.push(patch);
|
|
4413
4423
|
}
|
|
@@ -4419,7 +4429,7 @@ var FernignoreMigrator = class {
|
|
|
4419
4429
|
const results = [];
|
|
4420
4430
|
for (const pattern of patterns) {
|
|
4421
4431
|
const matching = allFiles.filter(
|
|
4422
|
-
(f) => (0,
|
|
4432
|
+
(f) => (0, import_minimatch5.minimatch)(f, pattern) || f === pattern || f.startsWith(pattern + "/")
|
|
4423
4433
|
);
|
|
4424
4434
|
results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
|
|
4425
4435
|
}
|
|
@@ -4732,7 +4742,7 @@ function computeContentHash(patchContent) {
|
|
|
4732
4742
|
}
|
|
4733
4743
|
|
|
4734
4744
|
// src/commands/forget.ts
|
|
4735
|
-
var
|
|
4745
|
+
var import_minimatch6 = require("minimatch");
|
|
4736
4746
|
function parseDiffStat(patchContent) {
|
|
4737
4747
|
let additions = 0;
|
|
4738
4748
|
let deletions = 0;
|
|
@@ -4762,7 +4772,7 @@ function toMatchedPatch(patch) {
|
|
|
4762
4772
|
}
|
|
4763
4773
|
function matchesPatch(patch, pattern) {
|
|
4764
4774
|
const fileMatch = patch.files.some(
|
|
4765
|
-
(file) => file === pattern || (0,
|
|
4775
|
+
(file) => file === pattern || (0, import_minimatch6.minimatch)(file, pattern)
|
|
4766
4776
|
);
|
|
4767
4777
|
if (fileMatch) return true;
|
|
4768
4778
|
return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
|