@fern-api/replay 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +90 -125
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +90 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -37
- package/dist/index.d.ts +24 -37
- package/dist/index.js +90 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -12,8 +12,6 @@ interface GenerationRecord {
|
|
|
12
12
|
timestamp: string;
|
|
13
13
|
cli_version: string;
|
|
14
14
|
generator_versions: Record<string, string>;
|
|
15
|
-
/** SHA of the base branch HEAD before replay ran. Always on main's lineage. */
|
|
16
|
-
base_branch_head?: string;
|
|
17
15
|
}
|
|
18
16
|
interface StoredPatch {
|
|
19
17
|
id: string;
|
|
@@ -259,22 +257,24 @@ declare class ReplayDetector {
|
|
|
259
257
|
private sdkOutputDir;
|
|
260
258
|
readonly warnings: string[];
|
|
261
259
|
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
262
|
-
detectNewPatches(): Promise<DetectionResult>;
|
|
263
260
|
/**
|
|
264
|
-
*
|
|
261
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
262
|
+
* `lock.current_generation`.
|
|
263
|
+
*
|
|
264
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
265
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
266
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
265
267
|
*
|
|
266
|
-
*
|
|
267
|
-
* `[fern-
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
272
|
-
* generation) get bundled as customer customizations.
|
|
268
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
269
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
270
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
271
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
272
|
+
* on second-parent — stopping at the merge would hide them).
|
|
273
273
|
*
|
|
274
|
-
*
|
|
275
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
274
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
276
275
|
*/
|
|
277
|
-
|
|
276
|
+
findPreviousGenerationFromHistory(): Promise<string | null>;
|
|
277
|
+
detectNewPatches(): Promise<DetectionResult>;
|
|
278
278
|
/**
|
|
279
279
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
280
280
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -368,6 +368,13 @@ declare class ReplayDetector {
|
|
|
368
368
|
private resolveDiffBase;
|
|
369
369
|
private parseGitLog;
|
|
370
370
|
private getLastGeneration;
|
|
371
|
+
/**
|
|
372
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
373
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
374
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
375
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
376
|
+
*/
|
|
377
|
+
private synthesizeGenerationRecord;
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
/**
|
|
@@ -468,7 +475,6 @@ declare class ReplayApplicator {
|
|
|
468
475
|
interface CommitOptions {
|
|
469
476
|
cliVersion: string;
|
|
470
477
|
generatorVersions: Record<string, string>;
|
|
471
|
-
baseBranchHead?: string;
|
|
472
478
|
}
|
|
473
479
|
declare class ReplayCommitter {
|
|
474
480
|
private git;
|
|
@@ -587,12 +593,10 @@ interface ReplayOptions {
|
|
|
587
593
|
generatorVersions?: Record<string, string>;
|
|
588
594
|
/** Commit generation + update lockfile, skip detection/application */
|
|
589
595
|
skipApplication?: boolean;
|
|
590
|
-
/** SHA of the base branch HEAD before replay runs. Stored in lockfile for squash merge recovery. */
|
|
591
|
-
baseBranchHead?: string;
|
|
592
596
|
}
|
|
593
597
|
/**
|
|
594
598
|
* Options honored by `applyPreparedReplay()`. `cliVersion`, `generatorVersions`,
|
|
595
|
-
* `
|
|
599
|
+
* `dryRun`, and `skipApplication` were all captured in phase 1
|
|
596
600
|
* and re-supplying them here is meaningless.
|
|
597
601
|
*/
|
|
598
602
|
interface ApplyOptions {
|
|
@@ -612,8 +616,8 @@ interface ApplyOptions {
|
|
|
612
616
|
* respected. `rebasePatches` uses `currentGenerationSha` (the pure `[fern-generated]`
|
|
613
617
|
* SHA), NOT HEAD — mid-phase commits do not retarget patches' `base_generation`.
|
|
614
618
|
* - The service instance is single-use across the phase boundary: do not call
|
|
615
|
-
*
|
|
616
|
-
*
|
|
619
|
+
* another `prepareReplay()` between phases. In-memory lockfile and warning
|
|
620
|
+
* state would be corrupted.
|
|
617
621
|
* - If the caller's mid-phase work throws or `applyPreparedReplay` is never called,
|
|
618
622
|
* the repo is left with the new `[fern-generated]` commit but a stale lockfile.
|
|
619
623
|
* Consumers MUST either (a) still call `applyPreparedReplay` so the lockfile
|
|
@@ -628,8 +632,6 @@ interface ReplayPreparation {
|
|
|
628
632
|
previousGenerationSha: string | null;
|
|
629
633
|
/** SHA of the new [fern-generated] commit created by prepareReplay (or HEAD for dry-run terminals) */
|
|
630
634
|
currentGenerationSha: string;
|
|
631
|
-
/** options.baseBranchHead passed through */
|
|
632
|
-
baseBranchHead: string | null;
|
|
633
635
|
/** @internal Opaque state consumed by applyPreparedReplay */
|
|
634
636
|
readonly _prepared: InternalPreparationState;
|
|
635
637
|
}
|
|
@@ -749,21 +751,6 @@ declare class ReplayService {
|
|
|
749
751
|
* Behavior is byte-identical to the pre-split implementation for all existing callers.
|
|
750
752
|
*/
|
|
751
753
|
runReplay(options?: ReplayOptions): Promise<ReplayReport>;
|
|
752
|
-
/**
|
|
753
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
754
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
755
|
-
*
|
|
756
|
-
* After updating the generation record, re-detects customer patches via
|
|
757
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
758
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
759
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
760
|
-
* was restored from the base branch during conflict PR creation.
|
|
761
|
-
*/
|
|
762
|
-
syncFromDivergentMerge(generationCommitSha: string, options?: {
|
|
763
|
-
cliVersion?: string;
|
|
764
|
-
generatorVersions?: Record<string, string>;
|
|
765
|
-
baseBranchHead?: string;
|
|
766
|
-
}): Promise<void>;
|
|
767
754
|
private determineFlow;
|
|
768
755
|
/**
|
|
769
756
|
* Rebase cleanly applied patches so they are relative to the current generation.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ interface GenerationRecord {
|
|
|
12
12
|
timestamp: string;
|
|
13
13
|
cli_version: string;
|
|
14
14
|
generator_versions: Record<string, string>;
|
|
15
|
-
/** SHA of the base branch HEAD before replay ran. Always on main's lineage. */
|
|
16
|
-
base_branch_head?: string;
|
|
17
15
|
}
|
|
18
16
|
interface StoredPatch {
|
|
19
17
|
id: string;
|
|
@@ -259,22 +257,24 @@ declare class ReplayDetector {
|
|
|
259
257
|
private sdkOutputDir;
|
|
260
258
|
readonly warnings: string[];
|
|
261
259
|
constructor(git: GitClient, lockManager: LockfileManager, sdkOutputDir: string);
|
|
262
|
-
detectNewPatches(): Promise<DetectionResult>;
|
|
263
260
|
/**
|
|
264
|
-
*
|
|
261
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
262
|
+
* `lock.current_generation`.
|
|
263
|
+
*
|
|
264
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
265
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
266
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
265
267
|
*
|
|
266
|
-
*
|
|
267
|
-
* `[fern-
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
272
|
-
* generation) get bundled as customer customizations.
|
|
268
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
269
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
270
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
271
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
272
|
+
* on second-parent — stopping at the merge would hide them).
|
|
273
273
|
*
|
|
274
|
-
*
|
|
275
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
274
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
276
275
|
*/
|
|
277
|
-
|
|
276
|
+
findPreviousGenerationFromHistory(): Promise<string | null>;
|
|
277
|
+
detectNewPatches(): Promise<DetectionResult>;
|
|
278
278
|
/**
|
|
279
279
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
280
280
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -368,6 +368,13 @@ declare class ReplayDetector {
|
|
|
368
368
|
private resolveDiffBase;
|
|
369
369
|
private parseGitLog;
|
|
370
370
|
private getLastGeneration;
|
|
371
|
+
/**
|
|
372
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
373
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
374
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
375
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
376
|
+
*/
|
|
377
|
+
private synthesizeGenerationRecord;
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
/**
|
|
@@ -468,7 +475,6 @@ declare class ReplayApplicator {
|
|
|
468
475
|
interface CommitOptions {
|
|
469
476
|
cliVersion: string;
|
|
470
477
|
generatorVersions: Record<string, string>;
|
|
471
|
-
baseBranchHead?: string;
|
|
472
478
|
}
|
|
473
479
|
declare class ReplayCommitter {
|
|
474
480
|
private git;
|
|
@@ -587,12 +593,10 @@ interface ReplayOptions {
|
|
|
587
593
|
generatorVersions?: Record<string, string>;
|
|
588
594
|
/** Commit generation + update lockfile, skip detection/application */
|
|
589
595
|
skipApplication?: boolean;
|
|
590
|
-
/** SHA of the base branch HEAD before replay runs. Stored in lockfile for squash merge recovery. */
|
|
591
|
-
baseBranchHead?: string;
|
|
592
596
|
}
|
|
593
597
|
/**
|
|
594
598
|
* Options honored by `applyPreparedReplay()`. `cliVersion`, `generatorVersions`,
|
|
595
|
-
* `
|
|
599
|
+
* `dryRun`, and `skipApplication` were all captured in phase 1
|
|
596
600
|
* and re-supplying them here is meaningless.
|
|
597
601
|
*/
|
|
598
602
|
interface ApplyOptions {
|
|
@@ -612,8 +616,8 @@ interface ApplyOptions {
|
|
|
612
616
|
* respected. `rebasePatches` uses `currentGenerationSha` (the pure `[fern-generated]`
|
|
613
617
|
* SHA), NOT HEAD — mid-phase commits do not retarget patches' `base_generation`.
|
|
614
618
|
* - The service instance is single-use across the phase boundary: do not call
|
|
615
|
-
*
|
|
616
|
-
*
|
|
619
|
+
* another `prepareReplay()` between phases. In-memory lockfile and warning
|
|
620
|
+
* state would be corrupted.
|
|
617
621
|
* - If the caller's mid-phase work throws or `applyPreparedReplay` is never called,
|
|
618
622
|
* the repo is left with the new `[fern-generated]` commit but a stale lockfile.
|
|
619
623
|
* Consumers MUST either (a) still call `applyPreparedReplay` so the lockfile
|
|
@@ -628,8 +632,6 @@ interface ReplayPreparation {
|
|
|
628
632
|
previousGenerationSha: string | null;
|
|
629
633
|
/** SHA of the new [fern-generated] commit created by prepareReplay (or HEAD for dry-run terminals) */
|
|
630
634
|
currentGenerationSha: string;
|
|
631
|
-
/** options.baseBranchHead passed through */
|
|
632
|
-
baseBranchHead: string | null;
|
|
633
635
|
/** @internal Opaque state consumed by applyPreparedReplay */
|
|
634
636
|
readonly _prepared: InternalPreparationState;
|
|
635
637
|
}
|
|
@@ -749,21 +751,6 @@ declare class ReplayService {
|
|
|
749
751
|
* Behavior is byte-identical to the pre-split implementation for all existing callers.
|
|
750
752
|
*/
|
|
751
753
|
runReplay(options?: ReplayOptions): Promise<ReplayReport>;
|
|
752
|
-
/**
|
|
753
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
754
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
755
|
-
*
|
|
756
|
-
* After updating the generation record, re-detects customer patches via
|
|
757
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
758
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
759
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
760
|
-
* was restored from the base branch during conflict PR creation.
|
|
761
|
-
*/
|
|
762
|
-
syncFromDivergentMerge(generationCommitSha: string, options?: {
|
|
763
|
-
cliVersion?: string;
|
|
764
|
-
generatorVersions?: Record<string, string>;
|
|
765
|
-
baseBranchHead?: string;
|
|
766
|
-
}): Promise<void>;
|
|
767
754
|
private determineFlow;
|
|
768
755
|
/**
|
|
769
756
|
* Rebase cleanly applied patches so they are relative to the current generation.
|
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
|
}
|
|
@@ -829,8 +842,48 @@ var ReplayDetector = class {
|
|
|
829
842
|
this.lockManager = lockManager;
|
|
830
843
|
this.sdkOutputDir = sdkOutputDir;
|
|
831
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
847
|
+
* `lock.current_generation`.
|
|
848
|
+
*
|
|
849
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
850
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
851
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
852
|
+
*
|
|
853
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
854
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
855
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
856
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
857
|
+
* on second-parent — stopping at the merge would hide them).
|
|
858
|
+
*
|
|
859
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
860
|
+
*/
|
|
861
|
+
async findPreviousGenerationFromHistory() {
|
|
862
|
+
let log;
|
|
863
|
+
try {
|
|
864
|
+
log = await this.git.exec([
|
|
865
|
+
"log",
|
|
866
|
+
"--first-parent",
|
|
867
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
868
|
+
"HEAD"
|
|
869
|
+
]);
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
for (const commit of this.parseGitLog(log)) {
|
|
874
|
+
if (isGenerationBoundary(commit)) {
|
|
875
|
+
return commit.sha;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
832
880
|
async detectNewPatches() {
|
|
833
881
|
const lock = this.lockManager.read();
|
|
882
|
+
const derivedSha = await this.findPreviousGenerationFromHistory();
|
|
883
|
+
if (derivedSha !== null) {
|
|
884
|
+
const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
|
|
885
|
+
return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
|
|
886
|
+
}
|
|
834
887
|
const lastGen = this.getLastGeneration(lock);
|
|
835
888
|
if (!lastGen) {
|
|
836
889
|
return { patches: [], revertedPatchIds: [] };
|
|
@@ -838,7 +891,7 @@ var ReplayDetector = class {
|
|
|
838
891
|
const exists = await this.git.commitExists(lastGen.commit_sha);
|
|
839
892
|
if (!exists) {
|
|
840
893
|
this.warnings.push(
|
|
841
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to
|
|
894
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
842
895
|
);
|
|
843
896
|
return this.detectPatchesViaTreeDiff(
|
|
844
897
|
lastGen,
|
|
@@ -846,44 +899,8 @@ var ReplayDetector = class {
|
|
|
846
899
|
true
|
|
847
900
|
);
|
|
848
901
|
}
|
|
849
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
850
|
-
if (!isAncestor) {
|
|
851
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
852
|
-
}
|
|
853
902
|
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
854
903
|
}
|
|
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
904
|
/**
|
|
888
905
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
889
906
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -1319,6 +1336,26 @@ var ReplayDetector = class {
|
|
|
1319
1336
|
getLastGeneration(lock) {
|
|
1320
1337
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
1321
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
1341
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
1342
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
1343
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
1344
|
+
*/
|
|
1345
|
+
async synthesizeGenerationRecord(sha) {
|
|
1346
|
+
let tree_hash = "";
|
|
1347
|
+
try {
|
|
1348
|
+
tree_hash = await this.git.getTreeHash(sha);
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
commit_sha: sha,
|
|
1353
|
+
tree_hash,
|
|
1354
|
+
timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1355
|
+
cli_version: "",
|
|
1356
|
+
generator_versions: {}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1322
1359
|
};
|
|
1323
1360
|
|
|
1324
1361
|
// src/ThreeWayMerge.ts
|
|
@@ -2458,8 +2495,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
2458
2495
|
tree_hash: treeHash,
|
|
2459
2496
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2460
2497
|
cli_version: options?.cliVersion ?? "unknown",
|
|
2461
|
-
generator_versions: options?.generatorVersions ?? {}
|
|
2462
|
-
base_branch_head: options?.baseBranchHead
|
|
2498
|
+
generator_versions: options?.generatorVersions ?? {}
|
|
2463
2499
|
};
|
|
2464
2500
|
}
|
|
2465
2501
|
async stageAll() {
|
|
@@ -2846,7 +2882,6 @@ var FirstGenerationFlow = class {
|
|
|
2846
2882
|
flow: "first-generation",
|
|
2847
2883
|
previousGenerationSha: null,
|
|
2848
2884
|
currentGenerationSha: headSha,
|
|
2849
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
2850
2885
|
_prepared: {
|
|
2851
2886
|
kind: "terminal",
|
|
2852
2887
|
flow: "first-generation",
|
|
@@ -2856,8 +2891,7 @@ var FirstGenerationFlow = class {
|
|
|
2856
2891
|
}
|
|
2857
2892
|
const commitOpts = options ? {
|
|
2858
2893
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2859
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2860
|
-
baseBranchHead: options.baseBranchHead
|
|
2894
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2861
2895
|
} : void 0;
|
|
2862
2896
|
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
2863
2897
|
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
@@ -2866,7 +2900,6 @@ var FirstGenerationFlow = class {
|
|
|
2866
2900
|
flow: "first-generation",
|
|
2867
2901
|
previousGenerationSha: null,
|
|
2868
2902
|
currentGenerationSha: genRecord.commit_sha,
|
|
2869
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2870
2903
|
_prepared: {
|
|
2871
2904
|
kind: "active",
|
|
2872
2905
|
flow: "first-generation",
|
|
@@ -2903,8 +2936,7 @@ var SkipApplicationFlow = class {
|
|
|
2903
2936
|
async prepare(options) {
|
|
2904
2937
|
const commitOpts = options ? {
|
|
2905
2938
|
cliVersion: options.cliVersion ?? "unknown",
|
|
2906
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
2907
|
-
baseBranchHead: options.baseBranchHead
|
|
2939
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
2908
2940
|
} : void 0;
|
|
2909
2941
|
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
2910
2942
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -2926,7 +2958,6 @@ var SkipApplicationFlow = class {
|
|
|
2926
2958
|
flow: "skip-application",
|
|
2927
2959
|
previousGenerationSha,
|
|
2928
2960
|
currentGenerationSha: genRecord.commit_sha,
|
|
2929
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
2930
2961
|
_prepared: {
|
|
2931
2962
|
kind: "active",
|
|
2932
2963
|
flow: "skip-application",
|
|
@@ -2987,7 +3018,6 @@ var NoPatchesFlow = class {
|
|
|
2987
3018
|
flow: "no-patches",
|
|
2988
3019
|
previousGenerationSha,
|
|
2989
3020
|
currentGenerationSha: headSha,
|
|
2990
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
2991
3021
|
_prepared: {
|
|
2992
3022
|
kind: "terminal",
|
|
2993
3023
|
flow: "no-patches",
|
|
@@ -3019,8 +3049,7 @@ var NoPatchesFlow = class {
|
|
|
3019
3049
|
}
|
|
3020
3050
|
const commitOpts = options ? {
|
|
3021
3051
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3022
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3023
|
-
baseBranchHead: options.baseBranchHead
|
|
3052
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3024
3053
|
} : void 0;
|
|
3025
3054
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3026
3055
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3030,7 +3059,6 @@ var NoPatchesFlow = class {
|
|
|
3030
3059
|
flow: "no-patches",
|
|
3031
3060
|
previousGenerationSha,
|
|
3032
3061
|
currentGenerationSha: genRecord.commit_sha,
|
|
3033
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3034
3062
|
_prepared: {
|
|
3035
3063
|
kind: "active",
|
|
3036
3064
|
flow: "no-patches",
|
|
@@ -3120,7 +3148,6 @@ var NormalRegenerationFlow = class {
|
|
|
3120
3148
|
flow: "normal-regeneration",
|
|
3121
3149
|
previousGenerationSha,
|
|
3122
3150
|
currentGenerationSha: headSha,
|
|
3123
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
3124
3151
|
_prepared: {
|
|
3125
3152
|
kind: "terminal",
|
|
3126
3153
|
flow: "normal-regeneration",
|
|
@@ -3138,14 +3165,16 @@ var NormalRegenerationFlow = class {
|
|
|
3138
3165
|
existingPatches = this.service.lockManager.getPatches();
|
|
3139
3166
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
3140
3167
|
for (const p of existingPatches) {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3168
|
+
if (!p.original_commit || p.original_commit === "") {
|
|
3169
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
3170
|
+
if (priorPatchId !== void 0) {
|
|
3171
|
+
this.service.lockManager.removePatch(p.id);
|
|
3172
|
+
this.service.warnings.push(
|
|
3173
|
+
`Consolidated patch ${p.id} into prior patch ${priorPatchId}: byte-identical patch_content. Lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
3174
|
+
);
|
|
3175
|
+
} else {
|
|
3176
|
+
seenHashes.set(p.content_hash, p.id);
|
|
3177
|
+
}
|
|
3149
3178
|
}
|
|
3150
3179
|
}
|
|
3151
3180
|
existingPatches = this.service.lockManager.getPatches();
|
|
@@ -3174,8 +3203,7 @@ var NormalRegenerationFlow = class {
|
|
|
3174
3203
|
const allPatches = [...existingPatches, ...newPatches];
|
|
3175
3204
|
const commitOpts = options ? {
|
|
3176
3205
|
cliVersion: options.cliVersion ?? "unknown",
|
|
3177
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
3178
|
-
baseBranchHead: options.baseBranchHead
|
|
3206
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
3179
3207
|
} : void 0;
|
|
3180
3208
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
3181
3209
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -3185,7 +3213,6 @@ var NormalRegenerationFlow = class {
|
|
|
3185
3213
|
flow: "normal-regeneration",
|
|
3186
3214
|
previousGenerationSha,
|
|
3187
3215
|
currentGenerationSha: genRecord.commit_sha,
|
|
3188
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
3189
3216
|
_prepared: {
|
|
3190
3217
|
kind: "active",
|
|
3191
3218
|
flow: "normal-regeneration",
|
|
@@ -3378,68 +3405,6 @@ var ReplayService = class {
|
|
|
3378
3405
|
const prep = await this.prepareReplay(options);
|
|
3379
3406
|
return this.applyPreparedReplay(prep, options);
|
|
3380
3407
|
}
|
|
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
3408
|
determineFlow() {
|
|
3444
3409
|
try {
|
|
3445
3410
|
const lock = this.lockManager.read();
|