gitxplain 0.1.3 → 0.1.6
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 +47 -0
- package/cli/index.js +283 -44
- package/cli/services/chatService.js +28 -8
- package/cli/services/gitService.js +45 -3
- package/cli/services/mergeService.js +273 -56
- package/cli/services/outputFormatter.js +2 -36
- package/cli/services/pipelineService.js +721 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -87,6 +87,33 @@ export function runGitCommandUnchecked(args, cwd) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export function listGitSubcommands() {
|
|
91
|
+
const output = execFileSync("git", ["help", "-a"], {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return new Set(
|
|
97
|
+
output
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((line) => line.match(/^\s{3}([a-z0-9][a-z0-9-]*)\s{2,}/i)?.[1] ?? null)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function runNativeGitPassthrough(args, cwd) {
|
|
105
|
+
const result = spawnSync("git", args, {
|
|
106
|
+
cwd,
|
|
107
|
+
stdio: "inherit"
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.error) {
|
|
111
|
+
throw result.error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result.status ?? 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
export function isGitRepository(cwd) {
|
|
91
118
|
try {
|
|
92
119
|
return runGitCommand(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
|
|
@@ -231,6 +258,21 @@ export function gitPush(cwd, remote = null, branch = null, runner = runGitComman
|
|
|
231
258
|
return runner(args, cwd);
|
|
232
259
|
}
|
|
233
260
|
|
|
261
|
+
export function gitPull(cwd, remote = null, branch = null, runner = runGitCommand) {
|
|
262
|
+
const args = ["pull"];
|
|
263
|
+
|
|
264
|
+
if (remote) {
|
|
265
|
+
args.push(remote);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (branch) {
|
|
269
|
+
args.push(branch);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return runner(args, cwd);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
234
276
|
export function gitCreateAnnotatedTag(tagName, ref, message, cwd) {
|
|
235
277
|
return runGitCommand(["tag", "-a", tagName, ref, "-m", message], cwd);
|
|
236
278
|
}
|
|
@@ -438,8 +480,8 @@ export function isAncestorCommit(ancestorRef, descendantRef, cwd) {
|
|
|
438
480
|
throw new Error(result.stderr || "Unable to determine commit ancestry.");
|
|
439
481
|
}
|
|
440
482
|
|
|
441
|
-
export function gitResetHard(ref, cwd) {
|
|
442
|
-
return
|
|
483
|
+
export function gitResetHard(ref, cwd, runner = runGitCommand) {
|
|
484
|
+
return runner(["reset", "--hard", ref], cwd);
|
|
443
485
|
}
|
|
444
486
|
|
|
445
487
|
export function gitCherryPickNoCommit(ref, cwd) {
|
|
@@ -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";
|
|
@@ -315,15 +311,8 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
|
315
311
|
const releasedVersions = getReleasedVersions(releaseCommits);
|
|
316
312
|
const unreleasedWindows = windows.filter((window) => !releasedVersions.has(window.version));
|
|
317
313
|
|
|
318
|
-
const selectedWindows =
|
|
319
|
-
releasedVersions.size === 0
|
|
320
|
-
? unreleasedWindows
|
|
321
|
-
: unreleasedWindows.length > 0
|
|
322
|
-
? [unreleasedWindows.at(-1)]
|
|
323
|
-
: [];
|
|
324
|
-
|
|
325
314
|
return {
|
|
326
|
-
windows:
|
|
315
|
+
windows: unreleasedWindows,
|
|
327
316
|
releasedVersions: [...releasedVersions],
|
|
328
317
|
latestDetectedVersion: windows.at(-1)?.version ?? null
|
|
329
318
|
};
|
|
@@ -353,37 +342,63 @@ export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
|
353
342
|
};
|
|
354
343
|
}
|
|
355
344
|
|
|
356
|
-
function
|
|
345
|
+
export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagNames = []) {
|
|
346
|
+
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
347
|
+
const tags = releaseCommits
|
|
348
|
+
.map((commit) => ({
|
|
349
|
+
commit,
|
|
350
|
+
version: commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null
|
|
351
|
+
}))
|
|
352
|
+
.filter((entry) => entry.version)
|
|
353
|
+
.filter((entry) => !taggedVersions.has(entry.version))
|
|
354
|
+
.map(({ commit, version }) => ({
|
|
355
|
+
version,
|
|
356
|
+
tagName: version,
|
|
357
|
+
startRef: commit.shortSha,
|
|
358
|
+
endRef: commit.shortSha,
|
|
359
|
+
targetSha: commit.sha,
|
|
360
|
+
targetShortSha: commit.shortSha,
|
|
361
|
+
targetSubject: commit.subject,
|
|
362
|
+
commits: [commit]
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
tags,
|
|
367
|
+
taggedVersions: [...taggedVersions],
|
|
368
|
+
latestDetectedVersion:
|
|
369
|
+
releaseCommits
|
|
370
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
371
|
+
.filter(Boolean)
|
|
372
|
+
.at(-1) ?? null
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getReleaseTrackSourceCommitShas(releaseExists, baseRef, sourceRef, cwd) {
|
|
357
377
|
if (!releaseExists) {
|
|
358
378
|
return {
|
|
359
379
|
mergeBase: null,
|
|
360
|
-
sourceCommitShas: listBranchCommits(
|
|
380
|
+
sourceCommitShas: listBranchCommits(sourceRef, cwd)
|
|
361
381
|
};
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
try {
|
|
365
|
-
const mergeBase = getMergeBase(baseRef,
|
|
385
|
+
const mergeBase = getMergeBase(baseRef, sourceRef, cwd);
|
|
366
386
|
return {
|
|
367
387
|
mergeBase,
|
|
368
|
-
sourceCommitShas: listCommitsAfter(mergeBase,
|
|
388
|
+
sourceCommitShas: listCommitsAfter(mergeBase, sourceRef, cwd)
|
|
369
389
|
};
|
|
370
390
|
} catch {
|
|
371
391
|
return {
|
|
372
392
|
mergeBase: null,
|
|
373
|
-
sourceCommitShas: listBranchCommits(
|
|
393
|
+
sourceCommitShas: listBranchCommits(sourceRef, cwd)
|
|
374
394
|
};
|
|
375
395
|
}
|
|
376
396
|
}
|
|
377
397
|
|
|
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
|
-
|
|
398
|
+
function buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd) {
|
|
384
399
|
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
385
400
|
const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
|
|
386
|
-
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, cwd);
|
|
401
|
+
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, sourceRef, cwd);
|
|
387
402
|
const sourceCommits = sourceCommitShas.map((sha) => inspectCommit(sha, cwd));
|
|
388
403
|
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
389
404
|
const selection = selectReleaseWindows(sourceCommits, releaseCommits);
|
|
@@ -401,6 +416,15 @@ export function buildReleaseMergePlan(cwd) {
|
|
|
401
416
|
};
|
|
402
417
|
}
|
|
403
418
|
|
|
419
|
+
export function buildReleaseMergePlan(cwd) {
|
|
420
|
+
const sourceBranch = getCurrentBranchName(cwd);
|
|
421
|
+
if (sourceBranch === RELEASE_BRANCH) {
|
|
422
|
+
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --merge.`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return buildReleaseMergePlanForSource(sourceBranch, "HEAD", cwd);
|
|
426
|
+
}
|
|
427
|
+
|
|
404
428
|
export function buildReleaseTagPlan(cwd) {
|
|
405
429
|
const sourceBranch = getCurrentBranchName(cwd);
|
|
406
430
|
if (sourceBranch === RELEASE_BRANCH) {
|
|
@@ -409,9 +433,14 @@ export function buildReleaseTagPlan(cwd) {
|
|
|
409
433
|
|
|
410
434
|
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
411
435
|
const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
|
|
412
|
-
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, cwd);
|
|
413
|
-
const
|
|
414
|
-
const selection =
|
|
436
|
+
const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, "HEAD", cwd);
|
|
437
|
+
const existingTagNames = listTags(cwd);
|
|
438
|
+
const selection = releaseExists
|
|
439
|
+
? selectReleaseTagsFromReleaseCommits(
|
|
440
|
+
listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)),
|
|
441
|
+
existingTagNames
|
|
442
|
+
)
|
|
443
|
+
: selectReleaseTags(sourceCommits.map((sha) => inspectCommit(sha, cwd)), existingTagNames);
|
|
415
444
|
|
|
416
445
|
return {
|
|
417
446
|
sourceBranch,
|
|
@@ -438,6 +467,132 @@ export function finalizeReleaseTagPlan(plan) {
|
|
|
438
467
|
};
|
|
439
468
|
}
|
|
440
469
|
|
|
470
|
+
function findLatestReleaseVersion(releaseCommits) {
|
|
471
|
+
return releaseCommits
|
|
472
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
473
|
+
.filter(Boolean)
|
|
474
|
+
.at(-1) ?? null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function findLatestTaggedReleaseVersion(releaseCommits, taggedVersions) {
|
|
478
|
+
const tagged = new Set(taggedVersions);
|
|
479
|
+
return releaseCommits
|
|
480
|
+
.map((commit) => commit.subject.match(RELEASE_SUBJECT_PATTERN)?.[1]?.trim() ?? null)
|
|
481
|
+
.filter((version) => version && tagged.has(version))
|
|
482
|
+
.at(-1) ?? null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildDriftStatus(sourceRef, sourceLabel, releaseExists, cwd) {
|
|
486
|
+
if (!releaseExists) {
|
|
487
|
+
return {
|
|
488
|
+
hasReleaseBranch: false,
|
|
489
|
+
disconnectedHistory: false,
|
|
490
|
+
sourceOnlyCount: listBranchCommits(sourceRef, cwd).length,
|
|
491
|
+
releaseOnlyCount: 0,
|
|
492
|
+
summary: `Release branch "${RELEASE_BRANCH}" does not exist yet.`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const mergeBase = getMergeBase(sourceRef, RELEASE_BRANCH, cwd);
|
|
498
|
+
const sourceOnlyCount = listCommitsAfter(mergeBase, sourceRef, cwd).length;
|
|
499
|
+
const releaseOnlyCount = listCommitsAfter(mergeBase, RELEASE_BRANCH, cwd).length;
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
hasReleaseBranch: true,
|
|
503
|
+
disconnectedHistory: false,
|
|
504
|
+
mergeBase,
|
|
505
|
+
sourceOnlyCount,
|
|
506
|
+
releaseOnlyCount,
|
|
507
|
+
summary:
|
|
508
|
+
sourceOnlyCount === 0 && releaseOnlyCount === 0
|
|
509
|
+
? `${sourceLabel} and ${RELEASE_BRANCH} point at the same history.`
|
|
510
|
+
: `${sourceLabel} has ${sourceOnlyCount} unique commit(s); ${RELEASE_BRANCH} has ${releaseOnlyCount} unique commit(s).`
|
|
511
|
+
};
|
|
512
|
+
} catch {
|
|
513
|
+
return {
|
|
514
|
+
hasReleaseBranch: true,
|
|
515
|
+
disconnectedHistory: true,
|
|
516
|
+
mergeBase: null,
|
|
517
|
+
sourceOnlyCount: listBranchCommits(sourceRef, cwd).length,
|
|
518
|
+
releaseOnlyCount: listBranchCommits(RELEASE_BRANCH, cwd).length,
|
|
519
|
+
summary: `${sourceLabel} and ${RELEASE_BRANCH} do not share a merge base. This is expected when the release branch is orphaned.`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount }) {
|
|
525
|
+
if (!releaseExists && mergePlan.windows.length > 0) {
|
|
526
|
+
return `Run \`gitxplain merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!releaseExists) {
|
|
530
|
+
return `No ${RELEASE_BRANCH} branch exists yet, and no releasable version bumps were detected.`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (mergePlan.windows.length > 0 && missingTagCount > 0) {
|
|
534
|
+
return `Run \`gitxplain merge --execute\` first, then \`gitxplain tag --execute\` to finish tagging release commits.`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (mergePlan.windows.length > 0) {
|
|
538
|
+
return `Run \`gitxplain merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (missingTagCount > 0) {
|
|
542
|
+
return `Run \`gitxplain tag --execute\` to create ${missingTagCount} missing release tag(s).`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return "No action required. Release branch and tags are up to date.";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function buildReleaseStatus(cwd) {
|
|
549
|
+
const currentBranch = getCurrentBranchName(cwd);
|
|
550
|
+
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
551
|
+
const sourceBranch = currentBranch === RELEASE_BRANCH ? getDefaultBaseRef(cwd) : currentBranch;
|
|
552
|
+
const sourceRef = currentBranch === RELEASE_BRANCH ? sourceBranch : "HEAD";
|
|
553
|
+
const mergePlan = finalizeReleaseMergePlan(buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd));
|
|
554
|
+
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
555
|
+
const tagSelection = releaseExists
|
|
556
|
+
? selectReleaseTagsFromReleaseCommits(releaseCommits, listTags(cwd))
|
|
557
|
+
: { tags: [], taggedVersions: [], latestDetectedVersion: null };
|
|
558
|
+
const drift = buildDriftStatus(sourceRef, sourceBranch, releaseExists, cwd);
|
|
559
|
+
const missingTagVersions = tagSelection.tags.map((tag) => tag.tagName);
|
|
560
|
+
const unmergedVersions = mergePlan.windows.map((window) => window.version);
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
sourceBranch,
|
|
564
|
+
sourceRef,
|
|
565
|
+
releaseBranch: RELEASE_BRANCH,
|
|
566
|
+
releaseExists,
|
|
567
|
+
currentBranch,
|
|
568
|
+
health:
|
|
569
|
+
!releaseExists || unmergedVersions.length > 0 || missingTagVersions.length > 0
|
|
570
|
+
? "needs attention"
|
|
571
|
+
: "healthy",
|
|
572
|
+
latestSourceVersion: mergePlan.latestDetectedVersion,
|
|
573
|
+
latestReleaseVersion: findLatestReleaseVersion(releaseCommits),
|
|
574
|
+
latestTaggedVersion: findLatestTaggedReleaseVersion(releaseCommits, tagSelection.taggedVersions),
|
|
575
|
+
unmergedVersions,
|
|
576
|
+
missingTagVersions,
|
|
577
|
+
drift,
|
|
578
|
+
mergePlan,
|
|
579
|
+
tagPlan: finalizeReleaseTagPlan({
|
|
580
|
+
sourceBranch,
|
|
581
|
+
baseRef: mergePlan.baseRef,
|
|
582
|
+
mergeBase: mergePlan.mergeBase,
|
|
583
|
+
releaseExists,
|
|
584
|
+
taggedVersions: tagSelection.taggedVersions,
|
|
585
|
+
latestDetectedVersion: tagSelection.latestDetectedVersion,
|
|
586
|
+
tags: tagSelection.tags
|
|
587
|
+
}),
|
|
588
|
+
nextRecommendedAction: getNextRecommendedAction({
|
|
589
|
+
releaseExists,
|
|
590
|
+
mergePlan,
|
|
591
|
+
missingTagCount: missingTagVersions.length
|
|
592
|
+
})
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
441
596
|
export function formatReleaseMergePlan(plan) {
|
|
442
597
|
const lines = [
|
|
443
598
|
colorize("Release Merge Plan", ANSI.bold + ANSI.cyan),
|
|
@@ -504,6 +659,50 @@ export function formatReleaseTagPlan(plan) {
|
|
|
504
659
|
return lines.join("\n");
|
|
505
660
|
}
|
|
506
661
|
|
|
662
|
+
export function formatReleaseStatus(status) {
|
|
663
|
+
const lines = [
|
|
664
|
+
colorize("Release Status", ANSI.bold + ANSI.cyan),
|
|
665
|
+
`${colorize("Source Branch:", ANSI.bold + ANSI.cyan)} ${status.sourceBranch}`,
|
|
666
|
+
`${colorize("Release Branch:", ANSI.bold + ANSI.cyan)} ${status.releaseBranch}`,
|
|
667
|
+
`${colorize("Current Branch:", ANSI.bold + ANSI.cyan)} ${status.currentBranch}`,
|
|
668
|
+
`${colorize("Overall:", ANSI.bold + ANSI.cyan)} ${status.health}`,
|
|
669
|
+
`${colorize("Latest Source Version:", ANSI.bold + ANSI.cyan)} ${status.latestSourceVersion ?? "none"}`,
|
|
670
|
+
`${colorize("Latest Release Version:", ANSI.bold + ANSI.cyan)} ${status.latestReleaseVersion ?? "none"}`,
|
|
671
|
+
`${colorize("Latest Tagged Version:", ANSI.bold + ANSI.cyan)} ${status.latestTaggedVersion ?? "none"}`
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
lines.push("");
|
|
675
|
+
lines.push(colorize("Unmerged Version Bumps", ANSI.bold + ANSI.yellow));
|
|
676
|
+
if (status.unmergedVersions.length === 0) {
|
|
677
|
+
lines.push("none");
|
|
678
|
+
} else {
|
|
679
|
+
for (const window of status.mergePlan.windows) {
|
|
680
|
+
lines.push(`- ${window.version} (${window.startRef}..${window.endRef})`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
lines.push("");
|
|
685
|
+
lines.push(colorize("Missing Release Tags", ANSI.bold + ANSI.yellow));
|
|
686
|
+
if (status.missingTagVersions.length === 0) {
|
|
687
|
+
lines.push("none");
|
|
688
|
+
} else {
|
|
689
|
+
for (const tag of status.tagPlan.tags) {
|
|
690
|
+
lines.push(`- ${tag.tagName} -> ${tag.targetShortSha} ${tag.targetSubject}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
lines.push("");
|
|
695
|
+
lines.push(colorize("Branch Drift", ANSI.bold + ANSI.yellow));
|
|
696
|
+
lines.push(status.drift.summary);
|
|
697
|
+
lines.push(`- Commits only on ${status.sourceBranch}: ${status.drift.sourceOnlyCount}`);
|
|
698
|
+
lines.push(`- Commits only on ${status.releaseBranch}: ${status.drift.releaseOnlyCount}`);
|
|
699
|
+
|
|
700
|
+
lines.push("");
|
|
701
|
+
lines.push(`${colorize("Next Recommended Action:", ANSI.bold + ANSI.cyan)} ${status.nextRecommendedAction}`);
|
|
702
|
+
|
|
703
|
+
return lines.join("\n");
|
|
704
|
+
}
|
|
705
|
+
|
|
507
706
|
function buildRecoveryMessage({ originalBranch, originalReleaseSha, createdReleaseBranch }) {
|
|
508
707
|
const lines = ["Release promotion failed. Recovery steps:"];
|
|
509
708
|
|
|
@@ -518,6 +717,15 @@ function buildRecoveryMessage({ originalBranch, originalReleaseSha, createdRelea
|
|
|
518
717
|
return lines.join("\n");
|
|
519
718
|
}
|
|
520
719
|
|
|
720
|
+
function buildReleaseCommitMetadata(ref, version, cwd) {
|
|
721
|
+
const metadata = getCommitMetadata(ref, cwd);
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
...metadata,
|
|
725
|
+
message: `release ${version}`
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
521
729
|
export function executeReleaseMerge(plan, cwd) {
|
|
522
730
|
if (plan.windows.length === 0) {
|
|
523
731
|
throw new Error("No unreleased release commits detected. Nothing to merge.");
|
|
@@ -530,36 +738,50 @@ export function executeReleaseMerge(plan, cwd) {
|
|
|
530
738
|
const originalBranch = getCurrentBranchName(cwd);
|
|
531
739
|
const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
|
|
532
740
|
const originalReleaseSha = releaseExists ? resolveCommitSha(RELEASE_BRANCH, cwd) : null;
|
|
533
|
-
|
|
534
|
-
const originalHeadFiles = releaseExists ? [] : listFilesInRef("HEAD", cwd);
|
|
741
|
+
let updatedReleaseSha = originalReleaseSha;
|
|
535
742
|
|
|
536
743
|
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
744
|
for (const window of plan.windows) {
|
|
546
|
-
|
|
547
|
-
|
|
745
|
+
const targetCommit = window.commits.at(-1);
|
|
746
|
+
if (targetCommit?.sha == null) {
|
|
747
|
+
throw new Error(`Unable to determine the source commit for release ${window.version}.`);
|
|
548
748
|
}
|
|
549
749
|
|
|
550
|
-
|
|
750
|
+
const treeSha = resolveTreeSha(targetCommit.sha, cwd);
|
|
751
|
+
const metadata = buildReleaseCommitMetadata(targetCommit.sha, window.version, cwd);
|
|
752
|
+
updatedReleaseSha = createCommitFromTree(
|
|
753
|
+
treeSha,
|
|
754
|
+
updatedReleaseSha == null ? [] : [updatedReleaseSha],
|
|
755
|
+
metadata,
|
|
756
|
+
cwd
|
|
757
|
+
);
|
|
551
758
|
}
|
|
552
|
-
} catch (error) {
|
|
553
|
-
gitCherryPickAbort(cwd);
|
|
554
759
|
|
|
760
|
+
if (updatedReleaseSha == null || updatedReleaseSha === originalReleaseSha) {
|
|
761
|
+
throw new Error("Release merge did not create any new commits.");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (releaseExists) {
|
|
765
|
+
gitForceBranch(RELEASE_BRANCH, updatedReleaseSha, cwd);
|
|
766
|
+
} else {
|
|
767
|
+
gitCreateBranch(RELEASE_BRANCH, updatedReleaseSha, cwd);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
gitCheckout(RELEASE_BRANCH, cwd);
|
|
771
|
+
} catch (error) {
|
|
555
772
|
try {
|
|
556
773
|
if (releaseExists) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
774
|
+
gitForceBranch(RELEASE_BRANCH, originalReleaseSha, cwd);
|
|
775
|
+
} else if (localBranchExists(RELEASE_BRANCH, cwd)) {
|
|
776
|
+
if (getCurrentBranchName(cwd) === RELEASE_BRANCH) {
|
|
777
|
+
gitCheckout(originalBranch, cwd);
|
|
778
|
+
}
|
|
561
779
|
gitDeleteBranch(RELEASE_BRANCH, cwd);
|
|
562
780
|
}
|
|
781
|
+
|
|
782
|
+
if (getCurrentBranchName(cwd) !== originalBranch) {
|
|
783
|
+
gitCheckout(originalBranch, cwd);
|
|
784
|
+
}
|
|
563
785
|
} catch {
|
|
564
786
|
// Preserve original failure and print recovery guidance below.
|
|
565
787
|
}
|
|
@@ -574,11 +796,6 @@ export function executeReleaseMerge(plan, cwd) {
|
|
|
574
796
|
);
|
|
575
797
|
throw new Error("Release merge aborted.");
|
|
576
798
|
}
|
|
577
|
-
|
|
578
|
-
const updatedReleaseSha = getCurrentHeadSha(cwd);
|
|
579
|
-
if (updatedReleaseSha === originalHeadSha) {
|
|
580
|
-
throw new Error("Release merge did not create any new commits.");
|
|
581
|
-
}
|
|
582
799
|
}
|
|
583
800
|
|
|
584
801
|
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) {
|