gitxplain 0.1.3 → 0.1.8
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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +27 -0
- package/IMPLEMENTATION.md +10 -10
- package/README.md +386 -110
- package/cli/index.js +359 -209
- package/cli/services/chatService.js +28 -8
- package/cli/services/configService.js +75 -3
- package/cli/services/gitService.js +45 -3
- package/cli/services/mergeService.js +303 -69
- package/cli/services/outputFormatter.js +2 -36
- package/cli/services/pipelineService.js +721 -0
- package/package.json +2 -2
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
createCommitFromTree,
|
|
4
|
+
getCommitMetadata,
|
|
4
5
|
getCurrentBranchName,
|
|
5
|
-
getCurrentHeadSha,
|
|
6
6
|
getDefaultBaseRef,
|
|
7
7
|
getMergeBase,
|
|
8
8
|
gitCheckout,
|
|
9
|
-
|
|
10
|
-
gitCherryPickAbort,
|
|
11
|
-
gitCherryPickNoCommit,
|
|
12
|
-
gitCommit,
|
|
9
|
+
gitCreateBranch,
|
|
13
10
|
gitCreateAnnotatedTag,
|
|
14
11
|
gitDeleteBranch,
|
|
12
|
+
gitForceBranch,
|
|
15
13
|
gitDeleteTag,
|
|
16
|
-
gitRemoveCachedAll,
|
|
17
|
-
gitResetHard,
|
|
18
14
|
isWorkingTreeClean,
|
|
19
15
|
listBranchCommits,
|
|
20
16
|
listCommitsAfter,
|
|
21
|
-
listFilesInRef,
|
|
22
17
|
listTags,
|
|
23
18
|
localBranchExists,
|
|
19
|
+
resolveTreeSha,
|
|
24
20
|
resolveCommitSha,
|
|
25
21
|
runGitCommand
|
|
26
22
|
} from "./gitService.js";
|
|
@@ -310,27 +306,37 @@ export function buildReleaseWindows(sourceCommits) {
|
|
|
310
306
|
return windows;
|
|
311
307
|
}
|
|
312
308
|
|
|
309
|
+
function selectLatestWindowsPerVersion(windows) {
|
|
310
|
+
const seenVersions = new Set();
|
|
311
|
+
const latestWindows = [];
|
|
312
|
+
|
|
313
|
+
for (let index = windows.length - 1; index >= 0; index -= 1) {
|
|
314
|
+
const window = windows[index];
|
|
315
|
+
if (!window.version || seenVersions.has(window.version)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
seenVersions.add(window.version);
|
|
320
|
+
latestWindows.push(window);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return latestWindows.reverse();
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
314
327
|
const windows = buildReleaseWindows(sourceCommits);
|
|
315
328
|
const releasedVersions = getReleasedVersions(releaseCommits);
|
|
316
329
|
const unreleasedWindows = windows.filter((window) => !releasedVersions.has(window.version));
|
|
317
330
|
|
|
318
|
-
const selectedWindows =
|
|
319
|
-
releasedVersions.size === 0
|
|
320
|
-
? unreleasedWindows
|
|
321
|
-
: unreleasedWindows.length > 0
|
|
322
|
-
? [unreleasedWindows.at(-1)]
|
|
323
|
-
: [];
|
|
324
|
-
|
|
325
331
|
return {
|
|
326
|
-
windows:
|
|
332
|
+
windows: unreleasedWindows,
|
|
327
333
|
releasedVersions: [...releasedVersions],
|
|
328
334
|
latestDetectedVersion: windows.at(-1)?.version ?? null
|
|
329
335
|
};
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
333
|
-
const windows = buildReleaseWindows(sourceCommits);
|
|
339
|
+
const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
|
|
334
340
|
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
335
341
|
const tags = windows
|
|
336
342
|
.filter((window) => !taggedVersions.has(window.version))
|
|
@@ -353,37 +359,88 @@ export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
|
353
359
|
};
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
function
|
|
362
|
+
function findLatestTaggedSourceVersion(sourceCommits, taggedVersions) {
|
|
363
|
+
const tagged = new Set(taggedVersions);
|
|
364
|
+
return selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits))
|
|
365
|
+
.map((window) => window.version)
|
|
366
|
+
.filter((version) => version && tagged.has(version))
|
|
367
|
+
.at(-1) ?? null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd) {
|
|
371
|
+
const sourceCommits = listBranchCommits(sourceRef, cwd).map((sha) => inspectCommit(sha, cwd));
|
|
372
|
+
const existingTagNames = listTags(cwd);
|
|
373
|
+
const selection = selectReleaseTags(sourceCommits, existingTagNames);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
sourceBranch,
|
|
377
|
+
baseRef: sourceRef,
|
|
378
|
+
mergeBase: null,
|
|
379
|
+
releaseExists: localBranchExists(RELEASE_BRANCH, cwd),
|
|
380
|
+
taggedVersions: selection.taggedVersions,
|
|
381
|
+
latestDetectedVersion: selection.latestDetectedVersion,
|
|
382
|
+
latestTaggedVersion: findLatestTaggedSourceVersion(sourceCommits, selection.taggedVersions),
|
|
383
|
+
tags: selection.tags
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagNames = []) {
|
|
388
|
+
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
389
|
+
const tags = releaseCommits
|
|
390
|
+
.map((commit) => ({
|
|
391
|
+
commit,
|
|
392
|
+
version: commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null
|
|
393
|
+
}))
|
|
394
|
+
.filter((entry) => entry.version)
|
|
395
|
+
.filter((entry) => !taggedVersions.has(entry.version))
|
|
396
|
+
.map(({ commit, version }) => ({
|
|
397
|
+
version,
|
|
398
|
+
tagName: version,
|
|
399
|
+
startRef: commit.shortSha,
|
|
400
|
+
endRef: commit.shortSha,
|
|
401
|
+
targetSha: commit.sha,
|
|
402
|
+
targetShortSha: commit.shortSha,
|
|
403
|
+
targetSubject: commit.subject,
|
|
404
|
+
commits: [commit]
|
|
405
|
+
}));
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
tags,
|
|
409
|
+
taggedVersions: [...taggedVersions],
|
|
410
|
+
latestDetectedVersion:
|
|
411
|
+
releaseCommits
|
|
412
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
413
|
+
.filter(Boolean)
|
|
414
|
+
.at(-1) ?? null
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getReleaseTrackSourceCommitShas(releaseExists, baseRef, sourceRef, cwd) {
|
|
357
419
|
if (!releaseExists) {
|
|
358
420
|
return {
|
|
359
421
|
mergeBase: null,
|
|
360
|
-
sourceCommitShas: listBranchCommits(
|
|
422
|
+
sourceCommitShas: listBranchCommits(sourceRef, cwd)
|
|
361
423
|
};
|
|
362
424
|
}
|
|
363
425
|
|
|
364
426
|
try {
|
|
365
|
-
const mergeBase = getMergeBase(baseRef,
|
|
427
|
+
const mergeBase = getMergeBase(baseRef, sourceRef, cwd);
|
|
366
428
|
return {
|
|
367
429
|
mergeBase,
|
|
368
|
-
sourceCommitShas: listCommitsAfter(mergeBase,
|
|
430
|
+
sourceCommitShas: listCommitsAfter(mergeBase, sourceRef, cwd)
|
|
369
431
|
};
|
|
370
432
|
} catch {
|
|
371
433
|
return {
|
|
372
434
|
mergeBase: null,
|
|
373
|
-
sourceCommitShas: listBranchCommits(
|
|
435
|
+
sourceCommitShas: listBranchCommits(sourceRef, cwd)
|
|
374
436
|
};
|
|
375
437
|
}
|
|
376
438
|
}
|
|
377
439
|
|
|
378
|
-
|
|
379
|
-
const sourceBranch = getCurrentBranchName(cwd);
|
|
380
|
-
if (sourceBranch === RELEASE_BRANCH) {
|
|
381
|
-
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --merge.`);
|
|
382
|
-
}
|
|
383
|
-
|
|
440
|
+
function buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd) {
|
|
384
441
|
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
385
442
|
const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
|
|
386
|
-
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, cwd);
|
|
443
|
+
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, sourceRef, cwd);
|
|
387
444
|
const sourceCommits = sourceCommitShas.map((sha) => inspectCommit(sha, cwd));
|
|
388
445
|
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
389
446
|
const selection = selectReleaseWindows(sourceCommits, releaseCommits);
|
|
@@ -401,27 +458,22 @@ export function buildReleaseMergePlan(cwd) {
|
|
|
401
458
|
};
|
|
402
459
|
}
|
|
403
460
|
|
|
461
|
+
export function buildReleaseMergePlan(cwd) {
|
|
462
|
+
const sourceBranch = getCurrentBranchName(cwd);
|
|
463
|
+
if (sourceBranch === RELEASE_BRANCH) {
|
|
464
|
+
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --merge.`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return buildReleaseMergePlanForSource(sourceBranch, "HEAD", cwd);
|
|
468
|
+
}
|
|
469
|
+
|
|
404
470
|
export function buildReleaseTagPlan(cwd) {
|
|
405
471
|
const sourceBranch = getCurrentBranchName(cwd);
|
|
406
472
|
if (sourceBranch === RELEASE_BRANCH) {
|
|
407
473
|
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --tag.`);
|
|
408
474
|
}
|
|
409
475
|
|
|
410
|
-
|
|
411
|
-
const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
|
|
412
|
-
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, cwd);
|
|
413
|
-
const sourceCommits = sourceCommitShas.map((sha) => inspectCommit(sha, cwd));
|
|
414
|
-
const selection = selectReleaseTags(sourceCommits, listTags(cwd));
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
sourceBranch,
|
|
418
|
-
baseRef,
|
|
419
|
-
mergeBase,
|
|
420
|
-
releaseExists,
|
|
421
|
-
taggedVersions: selection.taggedVersions,
|
|
422
|
-
latestDetectedVersion: selection.latestDetectedVersion,
|
|
423
|
-
tags: selection.tags
|
|
424
|
-
};
|
|
476
|
+
return buildReleaseTagPlanForSource(sourceBranch, "HEAD", cwd);
|
|
425
477
|
}
|
|
426
478
|
|
|
427
479
|
export function finalizeReleaseMergePlan(plan) {
|
|
@@ -438,6 +490,126 @@ export function finalizeReleaseTagPlan(plan) {
|
|
|
438
490
|
};
|
|
439
491
|
}
|
|
440
492
|
|
|
493
|
+
function findLatestReleaseVersion(releaseCommits) {
|
|
494
|
+
return releaseCommits
|
|
495
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
496
|
+
.filter(Boolean)
|
|
497
|
+
.at(-1) ?? null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function findLatestTaggedReleaseVersion(releaseCommits, taggedVersions) {
|
|
501
|
+
const tagged = new Set(taggedVersions);
|
|
502
|
+
return releaseCommits
|
|
503
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
504
|
+
.filter((version) => version && tagged.has(version))
|
|
505
|
+
.at(-1) ?? null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildDriftStatus(sourceRef, sourceLabel, releaseExists, cwd) {
|
|
509
|
+
if (!releaseExists) {
|
|
510
|
+
return {
|
|
511
|
+
hasReleaseBranch: false,
|
|
512
|
+
disconnectedHistory: false,
|
|
513
|
+
sourceOnlyCount: listBranchCommits(sourceRef, cwd).length,
|
|
514
|
+
releaseOnlyCount: 0,
|
|
515
|
+
summary: `Release branch "${RELEASE_BRANCH}" does not exist yet.`
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const mergeBase = getMergeBase(sourceRef, RELEASE_BRANCH, cwd);
|
|
521
|
+
const sourceOnlyCount = listCommitsAfter(mergeBase, sourceRef, cwd).length;
|
|
522
|
+
const releaseOnlyCount = listCommitsAfter(mergeBase, RELEASE_BRANCH, cwd).length;
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
hasReleaseBranch: true,
|
|
526
|
+
disconnectedHistory: false,
|
|
527
|
+
mergeBase,
|
|
528
|
+
sourceOnlyCount,
|
|
529
|
+
releaseOnlyCount,
|
|
530
|
+
summary:
|
|
531
|
+
sourceOnlyCount === 0 && releaseOnlyCount === 0
|
|
532
|
+
? `${sourceLabel} and ${RELEASE_BRANCH} point at the same history.`
|
|
533
|
+
: `${sourceLabel} has ${sourceOnlyCount} unique commit(s); ${RELEASE_BRANCH} has ${releaseOnlyCount} unique commit(s).`
|
|
534
|
+
};
|
|
535
|
+
} catch {
|
|
536
|
+
return {
|
|
537
|
+
hasReleaseBranch: true,
|
|
538
|
+
disconnectedHistory: true,
|
|
539
|
+
mergeBase: null,
|
|
540
|
+
sourceOnlyCount: listBranchCommits(sourceRef, cwd).length,
|
|
541
|
+
releaseOnlyCount: listBranchCommits(RELEASE_BRANCH, cwd).length,
|
|
542
|
+
summary: `${sourceLabel} and ${RELEASE_BRANCH} do not share a merge base. This is expected when the release branch is orphaned.`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount }) {
|
|
548
|
+
if (!releaseExists && mergePlan.windows.length > 0) {
|
|
549
|
+
return `Run \`gitxplain --merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!releaseExists && missingTagCount > 0) {
|
|
553
|
+
return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s) on the current branch.`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!releaseExists) {
|
|
557
|
+
return `No ${RELEASE_BRANCH} branch exists yet, and no releasable version bumps were detected.`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (mergePlan.windows.length > 0 && missingTagCount > 0) {
|
|
561
|
+
return `Run \`gitxplain --merge --execute\` to update ${RELEASE_BRANCH}, and \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (mergePlan.windows.length > 0) {
|
|
565
|
+
return `Run \`gitxplain --merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (missingTagCount > 0) {
|
|
569
|
+
return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return "No action required. Release branch and tags are up to date.";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function buildReleaseStatus(cwd) {
|
|
576
|
+
const currentBranch = getCurrentBranchName(cwd);
|
|
577
|
+
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
578
|
+
const sourceBranch = currentBranch === RELEASE_BRANCH ? getDefaultBaseRef(cwd) : currentBranch;
|
|
579
|
+
const sourceRef = currentBranch === RELEASE_BRANCH ? sourceBranch : "HEAD";
|
|
580
|
+
const mergePlan = finalizeReleaseMergePlan(buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd));
|
|
581
|
+
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
582
|
+
const tagPlan = finalizeReleaseTagPlan(buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd));
|
|
583
|
+
const drift = buildDriftStatus(sourceRef, sourceBranch, releaseExists, cwd);
|
|
584
|
+
const missingTagVersions = tagPlan.tags.map((tag) => tag.tagName);
|
|
585
|
+
const unmergedVersions = mergePlan.windows.map((window) => window.version);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
sourceBranch,
|
|
589
|
+
sourceRef,
|
|
590
|
+
releaseBranch: RELEASE_BRANCH,
|
|
591
|
+
releaseExists,
|
|
592
|
+
currentBranch,
|
|
593
|
+
health:
|
|
594
|
+
!releaseExists || unmergedVersions.length > 0 || missingTagVersions.length > 0
|
|
595
|
+
? "needs attention"
|
|
596
|
+
: "healthy",
|
|
597
|
+
latestSourceVersion: mergePlan.latestDetectedVersion,
|
|
598
|
+
latestReleaseVersion: findLatestReleaseVersion(releaseCommits),
|
|
599
|
+
latestTaggedVersion: tagPlan.latestTaggedVersion,
|
|
600
|
+
unmergedVersions,
|
|
601
|
+
missingTagVersions,
|
|
602
|
+
drift,
|
|
603
|
+
mergePlan,
|
|
604
|
+
tagPlan,
|
|
605
|
+
nextRecommendedAction: getNextRecommendedAction({
|
|
606
|
+
releaseExists,
|
|
607
|
+
mergePlan,
|
|
608
|
+
missingTagCount: missingTagVersions.length
|
|
609
|
+
})
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
441
613
|
export function formatReleaseMergePlan(plan) {
|
|
442
614
|
const lines = [
|
|
443
615
|
colorize("Release Merge Plan", ANSI.bold + ANSI.cyan),
|
|
@@ -504,6 +676,50 @@ export function formatReleaseTagPlan(plan) {
|
|
|
504
676
|
return lines.join("\n");
|
|
505
677
|
}
|
|
506
678
|
|
|
679
|
+
export function formatReleaseStatus(status) {
|
|
680
|
+
const lines = [
|
|
681
|
+
colorize("Release Status", ANSI.bold + ANSI.cyan),
|
|
682
|
+
`${colorize("Source Branch:", ANSI.bold + ANSI.cyan)} ${status.sourceBranch}`,
|
|
683
|
+
`${colorize("Release Branch:", ANSI.bold + ANSI.cyan)} ${status.releaseBranch}`,
|
|
684
|
+
`${colorize("Current Branch:", ANSI.bold + ANSI.cyan)} ${status.currentBranch}`,
|
|
685
|
+
`${colorize("Overall:", ANSI.bold + ANSI.cyan)} ${status.health}`,
|
|
686
|
+
`${colorize("Latest Source Version:", ANSI.bold + ANSI.cyan)} ${status.latestSourceVersion ?? "none"}`,
|
|
687
|
+
`${colorize("Latest Release Version:", ANSI.bold + ANSI.cyan)} ${status.latestReleaseVersion ?? "none"}`,
|
|
688
|
+
`${colorize("Latest Tagged Version:", ANSI.bold + ANSI.cyan)} ${status.latestTaggedVersion ?? "none"}`
|
|
689
|
+
];
|
|
690
|
+
|
|
691
|
+
lines.push("");
|
|
692
|
+
lines.push(colorize("Unmerged Version Bumps", ANSI.bold + ANSI.yellow));
|
|
693
|
+
if (status.unmergedVersions.length === 0) {
|
|
694
|
+
lines.push("none");
|
|
695
|
+
} else {
|
|
696
|
+
for (const window of status.mergePlan.windows) {
|
|
697
|
+
lines.push(`- ${window.version} (${window.startRef}..${window.endRef})`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
lines.push("");
|
|
702
|
+
lines.push(colorize("Missing Release Tags", ANSI.bold + ANSI.yellow));
|
|
703
|
+
if (status.missingTagVersions.length === 0) {
|
|
704
|
+
lines.push("none");
|
|
705
|
+
} else {
|
|
706
|
+
for (const tag of status.tagPlan.tags) {
|
|
707
|
+
lines.push(`- ${tag.tagName} -> ${tag.targetShortSha} ${tag.targetSubject}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
lines.push("");
|
|
712
|
+
lines.push(colorize("Branch Drift", ANSI.bold + ANSI.yellow));
|
|
713
|
+
lines.push(status.drift.summary);
|
|
714
|
+
lines.push(`- Commits only on ${status.sourceBranch}: ${status.drift.sourceOnlyCount}`);
|
|
715
|
+
lines.push(`- Commits only on ${status.releaseBranch}: ${status.drift.releaseOnlyCount}`);
|
|
716
|
+
|
|
717
|
+
lines.push("");
|
|
718
|
+
lines.push(`${colorize("Next Recommended Action:", ANSI.bold + ANSI.cyan)} ${status.nextRecommendedAction}`);
|
|
719
|
+
|
|
720
|
+
return lines.join("\n");
|
|
721
|
+
}
|
|
722
|
+
|
|
507
723
|
function buildRecoveryMessage({ originalBranch, originalReleaseSha, createdReleaseBranch }) {
|
|
508
724
|
const lines = ["Release promotion failed. Recovery steps:"];
|
|
509
725
|
|
|
@@ -518,6 +734,15 @@ function buildRecoveryMessage({ originalBranch, originalReleaseSha, createdRelea
|
|
|
518
734
|
return lines.join("\n");
|
|
519
735
|
}
|
|
520
736
|
|
|
737
|
+
function buildReleaseCommitMetadata(ref, version, cwd) {
|
|
738
|
+
const metadata = getCommitMetadata(ref, cwd);
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
...metadata,
|
|
742
|
+
message: `release ${version}`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
521
746
|
export function executeReleaseMerge(plan, cwd) {
|
|
522
747
|
if (plan.windows.length === 0) {
|
|
523
748
|
throw new Error("No unreleased release commits detected. Nothing to merge.");
|
|
@@ -530,36 +755,50 @@ export function executeReleaseMerge(plan, cwd) {
|
|
|
530
755
|
const originalBranch = getCurrentBranchName(cwd);
|
|
531
756
|
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
532
757
|
const originalReleaseSha = releaseExists ? resolveCommitSha(RELEASE_BRANCH, cwd) : null;
|
|
533
|
-
|
|
534
|
-
const originalHeadFiles = releaseExists ? [] : listFilesInRef("HEAD", cwd);
|
|
758
|
+
let updatedReleaseSha = originalReleaseSha;
|
|
535
759
|
|
|
536
760
|
try {
|
|
537
|
-
if (releaseExists) {
|
|
538
|
-
gitCheckout(RELEASE_BRANCH, cwd);
|
|
539
|
-
} else {
|
|
540
|
-
gitCheckoutOrphan(RELEASE_BRANCH, cwd);
|
|
541
|
-
gitRemoveCachedAll(cwd);
|
|
542
|
-
deletePaths(originalHeadFiles, cwd);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
761
|
for (const window of plan.windows) {
|
|
546
|
-
|
|
547
|
-
|
|
762
|
+
const targetCommit = window.commits.at(-1);
|
|
763
|
+
if (targetCommit?.sha == null) {
|
|
764
|
+
throw new Error(`Unable to determine the source commit for release ${window.version}.`);
|
|
548
765
|
}
|
|
549
766
|
|
|
550
|
-
|
|
767
|
+
const treeSha = resolveTreeSha(targetCommit.sha, cwd);
|
|
768
|
+
const metadata = buildReleaseCommitMetadata(targetCommit.sha, window.version, cwd);
|
|
769
|
+
updatedReleaseSha = createCommitFromTree(
|
|
770
|
+
treeSha,
|
|
771
|
+
updatedReleaseSha == null ? [] : [updatedReleaseSha],
|
|
772
|
+
metadata,
|
|
773
|
+
cwd
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (updatedReleaseSha == null || updatedReleaseSha === originalReleaseSha) {
|
|
778
|
+
throw new Error("Release merge did not create any new commits.");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (releaseExists) {
|
|
782
|
+
gitForceBranch(RELEASE_BRANCH, updatedReleaseSha, cwd);
|
|
783
|
+
} else {
|
|
784
|
+
gitCreateBranch(RELEASE_BRANCH, updatedReleaseSha, cwd);
|
|
551
785
|
}
|
|
552
|
-
} catch (error) {
|
|
553
|
-
gitCherryPickAbort(cwd);
|
|
554
786
|
|
|
787
|
+
gitCheckout(RELEASE_BRANCH, cwd);
|
|
788
|
+
} catch (error) {
|
|
555
789
|
try {
|
|
556
790
|
if (releaseExists) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
791
|
+
gitForceBranch(RELEASE_BRANCH, originalReleaseSha, cwd);
|
|
792
|
+
} else if (localBranchExists(RELEASE_BRANCH, cwd)) {
|
|
793
|
+
if (getCurrentBranchName(cwd) === RELEASE_BRANCH) {
|
|
794
|
+
gitCheckout(originalBranch, cwd);
|
|
795
|
+
}
|
|
561
796
|
gitDeleteBranch(RELEASE_BRANCH, cwd);
|
|
562
797
|
}
|
|
798
|
+
|
|
799
|
+
if (getCurrentBranchName(cwd) !== originalBranch) {
|
|
800
|
+
gitCheckout(originalBranch, cwd);
|
|
801
|
+
}
|
|
563
802
|
} catch {
|
|
564
803
|
// Preserve original failure and print recovery guidance below.
|
|
565
804
|
}
|
|
@@ -574,11 +813,6 @@ export function executeReleaseMerge(plan, cwd) {
|
|
|
574
813
|
);
|
|
575
814
|
throw new Error("Release merge aborted.");
|
|
576
815
|
}
|
|
577
|
-
|
|
578
|
-
const updatedReleaseSha = getCurrentHeadSha(cwd);
|
|
579
|
-
if (updatedReleaseSha === originalHeadSha) {
|
|
580
|
-
throw new Error("Release merge did not create any new commits.");
|
|
581
|
-
}
|
|
582
816
|
}
|
|
583
817
|
|
|
584
818
|
export function executeReleaseTagPlan(plan, cwd) {
|
|
@@ -138,18 +138,6 @@ function classifyTone(line) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function colorizeByTone(line, tone) {
|
|
141
|
-
if (tone === "good") {
|
|
142
|
-
return colorize(line, ANSI.green);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (tone === "bad") {
|
|
146
|
-
return colorize(line, ANSI.red);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (tone === "neutral") {
|
|
150
|
-
return colorize(line, ANSI.yellow);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
141
|
return line;
|
|
154
142
|
}
|
|
155
143
|
|
|
@@ -161,17 +149,7 @@ function formatBulletLine(line) {
|
|
|
161
149
|
}
|
|
162
150
|
|
|
163
151
|
const [, indent, marker, content] = match;
|
|
164
|
-
|
|
165
|
-
const coloredMarker =
|
|
166
|
-
tone === "good"
|
|
167
|
-
? colorize(marker, ANSI.green)
|
|
168
|
-
: tone === "bad"
|
|
169
|
-
? colorize(marker, ANSI.red)
|
|
170
|
-
: tone === "neutral"
|
|
171
|
-
? colorize(marker, ANSI.yellow)
|
|
172
|
-
: colorize(marker, ANSI.cyan);
|
|
173
|
-
|
|
174
|
-
return `${indent}${coloredMarker} ${colorizeByTone(content, tone)}`;
|
|
152
|
+
return `${indent}${colorize(marker, ANSI.cyan)} ${content}`;
|
|
175
153
|
}
|
|
176
154
|
|
|
177
155
|
function formatSeverityLine(line) {
|
|
@@ -179,19 +157,7 @@ function formatSeverityLine(line) {
|
|
|
179
157
|
return null;
|
|
180
158
|
}
|
|
181
159
|
|
|
182
|
-
|
|
183
|
-
return colorize(line, ANSI.green);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (/\bmedium\b/i.test(line)) {
|
|
187
|
-
return colorize(line, ANSI.yellow);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (/\bhigh\b/i.test(line)) {
|
|
191
|
-
return colorize(line, ANSI.red);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return colorize(line, ANSI.bold + ANSI.yellow);
|
|
160
|
+
return line;
|
|
195
161
|
}
|
|
196
162
|
|
|
197
163
|
function formatLine(line) {
|