gitxplain 0.1.6 → 0.1.9
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/release.yml +1 -1
- package/README.md +543 -128
- package/cli/index.js +609 -385
- package/cli/services/aiService.js +234 -28
- package/cli/services/cacheService.js +92 -1
- package/cli/services/clipboardService.js +6 -1
- package/cli/services/colorSupport.js +31 -0
- package/cli/services/commitService.js +105 -23
- package/cli/services/configService.js +91 -3
- package/cli/services/envLoader.js +2 -2
- package/cli/services/gitService.js +369 -23
- package/cli/services/hookService.js +36 -4
- package/cli/services/mergeService.js +88 -65
- package/cli/services/outputFormatter.js +23 -73
- package/cli/services/pipelineService.js +113 -1
- package/cli/services/promptService.js +8 -1
- package/cli/services/splitService.js +1 -21
- package/cli/services/usageService.js +158 -0
- package/package.json +2 -2
- package/prompts/blame.txt +29 -0
- package/prompts/changelog.txt +36 -0
- package/prompts/conflict.txt +33 -0
- package/prompts/pr-description.txt +40 -0
- package/prompts/refactor.txt +29 -0
- package/prompts/stash.txt +34 -0
- package/prompts/test-suggest.txt +29 -0
- package/IMPLEMENTATION.md +0 -225
- package/cli/services/chatService.js +0 -683
- package/cli/services/gitConnectionService.js +0 -267
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
1
|
import {
|
|
3
2
|
createCommitFromTree,
|
|
4
3
|
getCommitMetadata,
|
|
@@ -15,19 +14,13 @@ import {
|
|
|
15
14
|
listBranchCommits,
|
|
16
15
|
listCommitsAfter,
|
|
17
16
|
listTags,
|
|
17
|
+
listTagTargets,
|
|
18
18
|
localBranchExists,
|
|
19
19
|
resolveTreeSha,
|
|
20
20
|
resolveCommitSha,
|
|
21
21
|
runGitCommand
|
|
22
22
|
} from "./gitService.js";
|
|
23
|
-
|
|
24
|
-
const ANSI = {
|
|
25
|
-
reset: "\u001b[0m",
|
|
26
|
-
bold: "\u001b[1m",
|
|
27
|
-
cyan: "\u001b[36m",
|
|
28
|
-
yellow: "\u001b[33m",
|
|
29
|
-
green: "\u001b[32m"
|
|
30
|
-
};
|
|
23
|
+
import { ANSI, colorize } from "./colorSupport.js";
|
|
31
24
|
|
|
32
25
|
const RELEASE_BRANCH = "release";
|
|
33
26
|
const VERSION_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/g;
|
|
@@ -36,18 +29,6 @@ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$
|
|
|
36
29
|
const INTEGER_PATTERN = /^\d+$/;
|
|
37
30
|
const TAG_VERSION_PATTERN = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?|\d+)$/;
|
|
38
31
|
|
|
39
|
-
function supportsColor() {
|
|
40
|
-
return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function colorize(text, color) {
|
|
44
|
-
if (!supportsColor()) {
|
|
45
|
-
return text;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return `${color}${text}${ANSI.reset}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
32
|
function unique(values) {
|
|
52
33
|
return [...new Set(values)];
|
|
53
34
|
}
|
|
@@ -306,6 +287,23 @@ export function buildReleaseWindows(sourceCommits) {
|
|
|
306
287
|
return windows;
|
|
307
288
|
}
|
|
308
289
|
|
|
290
|
+
function selectLatestWindowsPerVersion(windows) {
|
|
291
|
+
const seenVersions = new Set();
|
|
292
|
+
const latestWindows = [];
|
|
293
|
+
|
|
294
|
+
for (let index = windows.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const window = windows[index];
|
|
296
|
+
if (!window.version || seenVersions.has(window.version)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
seenVersions.add(window.version);
|
|
301
|
+
latestWindows.push(window);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return latestWindows.reverse();
|
|
305
|
+
}
|
|
306
|
+
|
|
309
307
|
export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
310
308
|
const windows = buildReleaseWindows(sourceCommits);
|
|
311
309
|
const releasedVersions = getReleasedVersions(releaseCommits);
|
|
@@ -318,21 +316,38 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
|
318
316
|
};
|
|
319
317
|
}
|
|
320
318
|
|
|
321
|
-
export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
322
|
-
const windows = buildReleaseWindows(sourceCommits);
|
|
319
|
+
export function selectReleaseTags(sourceCommits, existingTagNames = [], existingTagTargets = []) {
|
|
320
|
+
const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
|
|
323
321
|
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
322
|
+
const targetByVersion = new Map(
|
|
323
|
+
existingTagTargets
|
|
324
|
+
.map((tag) => {
|
|
325
|
+
const version = tag.tagName?.match(TAG_VERSION_PATTERN)?.[1] ?? null;
|
|
326
|
+
return version ? [version, tag.targetSha] : null;
|
|
327
|
+
})
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
);
|
|
324
330
|
const tags = windows
|
|
325
|
-
.filter((window) => !taggedVersions.has(window.version))
|
|
326
331
|
.map((window) => {
|
|
327
332
|
const targetCommit = window.commits.at(-1) ?? null;
|
|
333
|
+
const existingTargetSha = targetByVersion.get(window.version) ?? null;
|
|
334
|
+
const windowCommitShas = new Set(window.commits.map((commit) => commit.sha));
|
|
335
|
+
|
|
328
336
|
return {
|
|
329
337
|
...window,
|
|
330
338
|
tagName: window.version,
|
|
339
|
+
existingTargetSha,
|
|
340
|
+
needsMove:
|
|
341
|
+
existingTargetSha != null &&
|
|
342
|
+
targetCommit?.sha != null &&
|
|
343
|
+
windowCommitShas.has(existingTargetSha) &&
|
|
344
|
+
existingTargetSha !== targetCommit.sha,
|
|
331
345
|
targetSha: targetCommit?.sha ?? null,
|
|
332
346
|
targetShortSha: targetCommit?.shortSha ?? null,
|
|
333
347
|
targetSubject: targetCommit?.subject ?? null
|
|
334
348
|
};
|
|
335
349
|
})
|
|
350
|
+
.filter((tag) => !taggedVersions.has(tag.version) || tag.needsMove)
|
|
336
351
|
.filter((tag) => tag.targetSha != null);
|
|
337
352
|
|
|
338
353
|
return {
|
|
@@ -342,6 +357,32 @@ export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
|
342
357
|
};
|
|
343
358
|
}
|
|
344
359
|
|
|
360
|
+
function findLatestTaggedSourceVersion(sourceCommits, taggedVersions) {
|
|
361
|
+
const tagged = new Set(taggedVersions);
|
|
362
|
+
return selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits))
|
|
363
|
+
.map((window) => window.version)
|
|
364
|
+
.filter((version) => version && tagged.has(version))
|
|
365
|
+
.at(-1) ?? null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd) {
|
|
369
|
+
const sourceCommits = listBranchCommits(sourceRef, cwd).map((sha) => inspectCommit(sha, cwd));
|
|
370
|
+
const existingTagNames = listTags(cwd);
|
|
371
|
+
const existingTagTargets = listTagTargets(cwd);
|
|
372
|
+
const selection = selectReleaseTags(sourceCommits, existingTagNames, existingTagTargets);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
sourceBranch,
|
|
376
|
+
baseRef: sourceRef,
|
|
377
|
+
mergeBase: null,
|
|
378
|
+
releaseExists: localBranchExists(RELEASE_BRANCH, cwd),
|
|
379
|
+
taggedVersions: selection.taggedVersions,
|
|
380
|
+
latestDetectedVersion: selection.latestDetectedVersion,
|
|
381
|
+
latestTaggedVersion: findLatestTaggedSourceVersion(sourceCommits, selection.taggedVersions),
|
|
382
|
+
tags: selection.tags
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
345
386
|
export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagNames = []) {
|
|
346
387
|
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
347
388
|
const tags = releaseCommits
|
|
@@ -431,26 +472,7 @@ export function buildReleaseTagPlan(cwd) {
|
|
|
431
472
|
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --tag.`);
|
|
432
473
|
}
|
|
433
474
|
|
|
434
|
-
|
|
435
|
-
const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
|
|
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);
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
sourceBranch,
|
|
447
|
-
baseRef,
|
|
448
|
-
mergeBase,
|
|
449
|
-
releaseExists,
|
|
450
|
-
taggedVersions: selection.taggedVersions,
|
|
451
|
-
latestDetectedVersion: selection.latestDetectedVersion,
|
|
452
|
-
tags: selection.tags
|
|
453
|
-
};
|
|
475
|
+
return buildReleaseTagPlanForSource(sourceBranch, "HEAD", cwd);
|
|
454
476
|
}
|
|
455
477
|
|
|
456
478
|
export function finalizeReleaseMergePlan(plan) {
|
|
@@ -523,7 +545,11 @@ function buildDriftStatus(sourceRef, sourceLabel, releaseExists, cwd) {
|
|
|
523
545
|
|
|
524
546
|
function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount }) {
|
|
525
547
|
if (!releaseExists && mergePlan.windows.length > 0) {
|
|
526
|
-
return `Run \`gitxplain merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
|
|
548
|
+
return `Run \`gitxplain --merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!releaseExists && missingTagCount > 0) {
|
|
552
|
+
return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s) on the current branch.`;
|
|
527
553
|
}
|
|
528
554
|
|
|
529
555
|
if (!releaseExists) {
|
|
@@ -531,15 +557,15 @@ function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount })
|
|
|
531
557
|
}
|
|
532
558
|
|
|
533
559
|
if (mergePlan.windows.length > 0 && missingTagCount > 0) {
|
|
534
|
-
return `Run \`gitxplain merge --execute\`
|
|
560
|
+
return `Run \`gitxplain --merge --execute\` to update ${RELEASE_BRANCH}, and \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
535
561
|
}
|
|
536
562
|
|
|
537
563
|
if (mergePlan.windows.length > 0) {
|
|
538
|
-
return `Run \`gitxplain merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
564
|
+
return `Run \`gitxplain --merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
539
565
|
}
|
|
540
566
|
|
|
541
567
|
if (missingTagCount > 0) {
|
|
542
|
-
return `Run \`gitxplain tag --execute\` to create ${missingTagCount} missing
|
|
568
|
+
return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
543
569
|
}
|
|
544
570
|
|
|
545
571
|
return "No action required. Release branch and tags are up to date.";
|
|
@@ -552,11 +578,9 @@ export function buildReleaseStatus(cwd) {
|
|
|
552
578
|
const sourceRef = currentBranch === RELEASE_BRANCH ? sourceBranch : "HEAD";
|
|
553
579
|
const mergePlan = finalizeReleaseMergePlan(buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd));
|
|
554
580
|
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
555
|
-
const
|
|
556
|
-
? selectReleaseTagsFromReleaseCommits(releaseCommits, listTags(cwd))
|
|
557
|
-
: { tags: [], taggedVersions: [], latestDetectedVersion: null };
|
|
581
|
+
const tagPlan = finalizeReleaseTagPlan(buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd));
|
|
558
582
|
const drift = buildDriftStatus(sourceRef, sourceBranch, releaseExists, cwd);
|
|
559
|
-
const missingTagVersions =
|
|
583
|
+
const missingTagVersions = tagPlan.tags.map((tag) => tag.tagName);
|
|
560
584
|
const unmergedVersions = mergePlan.windows.map((window) => window.version);
|
|
561
585
|
|
|
562
586
|
return {
|
|
@@ -571,20 +595,12 @@ export function buildReleaseStatus(cwd) {
|
|
|
571
595
|
: "healthy",
|
|
572
596
|
latestSourceVersion: mergePlan.latestDetectedVersion,
|
|
573
597
|
latestReleaseVersion: findLatestReleaseVersion(releaseCommits),
|
|
574
|
-
latestTaggedVersion:
|
|
598
|
+
latestTaggedVersion: tagPlan.latestTaggedVersion,
|
|
575
599
|
unmergedVersions,
|
|
576
600
|
missingTagVersions,
|
|
577
601
|
drift,
|
|
578
602
|
mergePlan,
|
|
579
|
-
tagPlan
|
|
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
|
-
}),
|
|
603
|
+
tagPlan,
|
|
588
604
|
nextRecommendedAction: getNextRecommendedAction({
|
|
589
605
|
releaseExists,
|
|
590
606
|
mergePlan,
|
|
@@ -638,15 +654,18 @@ export function formatReleaseTagPlan(plan) {
|
|
|
638
654
|
];
|
|
639
655
|
|
|
640
656
|
if (plan.tags.length === 0) {
|
|
641
|
-
lines.push(colorize("No
|
|
657
|
+
lines.push(colorize("No release tag changes detected. Nothing to tag.", ANSI.green));
|
|
642
658
|
return lines.join("\n");
|
|
643
659
|
}
|
|
644
660
|
|
|
645
661
|
for (const tag of plan.tags) {
|
|
646
662
|
lines.push("");
|
|
647
|
-
lines.push(colorize(
|
|
663
|
+
lines.push(colorize(`${tag.needsMove ? "move tag" : "tag"} ${tag.tagName}`, ANSI.bold + ANSI.yellow));
|
|
648
664
|
lines.push(`${colorize("Commit Range:", ANSI.bold + ANSI.cyan)} ${tag.startRef}..${tag.endRef}`);
|
|
649
665
|
lines.push(`${colorize("Target Commit:", ANSI.bold + ANSI.cyan)} ${tag.targetShortSha} ${tag.targetSubject}`);
|
|
666
|
+
if (tag.needsMove) {
|
|
667
|
+
lines.push(`${colorize("Action:", ANSI.bold + ANSI.cyan)} move existing tag to the latest commit for ${tag.tagName}`);
|
|
668
|
+
}
|
|
650
669
|
|
|
651
670
|
for (const commit of tag.commits) {
|
|
652
671
|
lines.push(`${colorize(commit.shortSha, ANSI.bold + ANSI.cyan)} ${commit.subject}`);
|
|
@@ -800,13 +819,17 @@ export function executeReleaseMerge(plan, cwd) {
|
|
|
800
819
|
|
|
801
820
|
export function executeReleaseTagPlan(plan, cwd) {
|
|
802
821
|
if (plan.tags.length === 0) {
|
|
803
|
-
throw new Error("No
|
|
822
|
+
throw new Error("No release tag changes detected. Nothing to tag.");
|
|
804
823
|
}
|
|
805
824
|
|
|
806
825
|
const createdTags = [];
|
|
807
826
|
|
|
808
827
|
try {
|
|
809
828
|
for (const tag of plan.tags) {
|
|
829
|
+
if (tag.needsMove) {
|
|
830
|
+
gitDeleteTag(tag.tagName, cwd);
|
|
831
|
+
}
|
|
832
|
+
|
|
810
833
|
gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.tagName}`, cwd);
|
|
811
834
|
createdTags.push(tag.tagName);
|
|
812
835
|
}
|
|
@@ -1,34 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const ANSI = {
|
|
4
|
-
reset: "\u001b[0m",
|
|
5
|
-
bold: "\u001b[1m",
|
|
6
|
-
cyan: "\u001b[36m",
|
|
7
|
-
yellow: "\u001b[33m",
|
|
8
|
-
green: "\u001b[32m",
|
|
9
|
-
red: "\u001b[31m",
|
|
10
|
-
gray: "\u001b[90m"
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
function supportsColor() {
|
|
14
|
-
if (process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "0") {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (process.env.NO_COLOR != null) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return Boolean(process.stdout?.isTTY);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function colorize(text, color) {
|
|
26
|
-
if (!supportsColor()) {
|
|
27
|
-
return text;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return `${color}${text}${ANSI.reset}`;
|
|
31
|
-
}
|
|
1
|
+
import { ANSI, colorize } from "./colorSupport.js";
|
|
32
2
|
|
|
33
3
|
function stripInlineMarkdown(text) {
|
|
34
4
|
return text
|
|
@@ -84,61 +54,37 @@ function normalizeMarkdownLine(line, state) {
|
|
|
84
54
|
}
|
|
85
55
|
|
|
86
56
|
function formatTargetLabel(commitData) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
function normalizeHeading(line) {
|
|
91
|
-
const match = line.match(/^([0-9]+\.)?\s*(Summary|Issues? Fixed|Issue|Root Cause|Fix(?: Explanation)?|Impact|Risk Level|Severity|Technical Breakdown|Full Analysis|Line-by-Line Code Walkthrough|Code Review|Security Review|Security Findings|Review Findings|Suggestions|Recommended Mitigations)\s*:?\s*$/i);
|
|
92
|
-
|
|
93
|
-
if (!match) {
|
|
94
|
-
return null;
|
|
57
|
+
if (commitData.analysisType === "range") {
|
|
58
|
+
return "Range";
|
|
95
59
|
}
|
|
96
60
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function isFileHeading(line) {
|
|
101
|
-
return /^(?:File|Path)\s*:/i.test(line) || /^[A-Za-z0-9_./-]+\.[A-Za-z0-9]+:\s*$/.test(line);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function classifyTone(line) {
|
|
105
|
-
if (/^\s*low(?:\b|[.:])/i.test(line)) {
|
|
106
|
-
return "good";
|
|
61
|
+
if (commitData.analysisType === "blame") {
|
|
62
|
+
return "File";
|
|
107
63
|
}
|
|
108
64
|
|
|
109
|
-
if (
|
|
110
|
-
return "
|
|
65
|
+
if (commitData.analysisType === "stash") {
|
|
66
|
+
return "Stash";
|
|
111
67
|
}
|
|
112
68
|
|
|
113
|
-
if (
|
|
114
|
-
return "
|
|
69
|
+
if (commitData.analysisType === "conflict") {
|
|
70
|
+
return "Conflict";
|
|
115
71
|
}
|
|
116
72
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
line
|
|
120
|
-
)
|
|
121
|
-
) {
|
|
122
|
-
return "good";
|
|
123
|
-
}
|
|
73
|
+
return "Commit";
|
|
74
|
+
}
|
|
124
75
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
line
|
|
128
|
-
)
|
|
129
|
-
) {
|
|
130
|
-
return "bad";
|
|
131
|
-
}
|
|
76
|
+
function normalizeHeading(line) {
|
|
77
|
+
const match = line.match(/^([0-9]+\.)?\s*(Summary|Issues? Fixed|Issue|Root Cause|Fix(?: Explanation)?|Impact|Risk Level|Severity|Technical Breakdown|Full Analysis|Line-by-Line Code Walkthrough|Code Review|Security Review|Security Findings|Review Findings|Suggestions|Recommended Mitigations)\s*:?\s*$/i);
|
|
132
78
|
|
|
133
|
-
if (
|
|
134
|
-
return
|
|
79
|
+
if (!match) {
|
|
80
|
+
return null;
|
|
135
81
|
}
|
|
136
82
|
|
|
137
|
-
return
|
|
83
|
+
return `${match[2]}:`;
|
|
138
84
|
}
|
|
139
85
|
|
|
140
|
-
function
|
|
141
|
-
return line;
|
|
86
|
+
function isFileHeading(line) {
|
|
87
|
+
return /^(?:File|Path)\s*:/i.test(line) || /^[A-Za-z0-9_./-]+\.[A-Za-z0-9]+:\s*$/.test(line);
|
|
142
88
|
}
|
|
143
89
|
|
|
144
90
|
function formatBulletLine(line) {
|
|
@@ -189,7 +135,7 @@ function formatLine(line) {
|
|
|
189
135
|
return severityLine;
|
|
190
136
|
}
|
|
191
137
|
|
|
192
|
-
return
|
|
138
|
+
return line;
|
|
193
139
|
}
|
|
194
140
|
|
|
195
141
|
function formatExplanation(explanation) {
|
|
@@ -266,6 +212,10 @@ export function formatFooter({ responseMeta, promptMeta, options }) {
|
|
|
266
212
|
lines.push(`Usage: ${JSON.stringify(responseMeta.usage)}`);
|
|
267
213
|
}
|
|
268
214
|
|
|
215
|
+
if (responseMeta.estimatedCostUsd != null) {
|
|
216
|
+
lines.push(`Estimated Cost: $${responseMeta.estimatedCostUsd.toFixed(6)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
269
219
|
if (promptMeta?.warnings?.length) {
|
|
270
220
|
lines.push(...promptMeta.warnings);
|
|
271
221
|
}
|
|
@@ -414,6 +414,24 @@ export function inspectRepositoryForPipeline(cwd) {
|
|
|
414
414
|
label: "GitHub Actions CI verification",
|
|
415
415
|
description: "Runs install, lint, test, build, and package checks when supported.",
|
|
416
416
|
files: [".github/workflows/ci.yml"]
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: "gitlab-ci",
|
|
420
|
+
label: "GitLab CI verification",
|
|
421
|
+
description: "Creates a .gitlab-ci.yml pipeline with install, lint, test, and build stages.",
|
|
422
|
+
files: [".gitlab-ci.yml"]
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: "circleci",
|
|
426
|
+
label: "CircleCI verification",
|
|
427
|
+
description: "Creates a .circleci/config.yml pipeline for verification jobs.",
|
|
428
|
+
files: [".circleci/config.yml"]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "bitbucket-pipelines",
|
|
432
|
+
label: "Bitbucket Pipelines verification",
|
|
433
|
+
description: "Creates bitbucket-pipelines.yml with install, test, and build steps.",
|
|
434
|
+
files: ["bitbucket-pipelines.yml"]
|
|
417
435
|
}
|
|
418
436
|
];
|
|
419
437
|
|
|
@@ -676,6 +694,87 @@ export function buildContainerWorkflow() {
|
|
|
676
694
|
].join("\n").concat("\n");
|
|
677
695
|
}
|
|
678
696
|
|
|
697
|
+
function buildPipelineCommands(context) {
|
|
698
|
+
return [context.commands.install, context.commands.lint, context.commands.test, context.commands.build, context.commands.pack]
|
|
699
|
+
.filter(Boolean);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function buildGitLabCiWorkflow(context) {
|
|
703
|
+
const commands = buildPipelineCommands(context);
|
|
704
|
+
const image =
|
|
705
|
+
context.type === "python"
|
|
706
|
+
? "python:3.12"
|
|
707
|
+
: context.type === "go"
|
|
708
|
+
? "golang:1.22"
|
|
709
|
+
: context.type === "rust"
|
|
710
|
+
? "rust:latest"
|
|
711
|
+
: "node:20";
|
|
712
|
+
|
|
713
|
+
return [
|
|
714
|
+
`image: ${image}`,
|
|
715
|
+
"",
|
|
716
|
+
"stages:",
|
|
717
|
+
" - verify",
|
|
718
|
+
"",
|
|
719
|
+
"verify:",
|
|
720
|
+
" stage: verify",
|
|
721
|
+
" script:",
|
|
722
|
+
...commands.map((command) => ` - ${command}`)
|
|
723
|
+
].join("\n").concat("\n");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function buildCircleCiWorkflow(context) {
|
|
727
|
+
const image =
|
|
728
|
+
context.type === "python"
|
|
729
|
+
? "cimg/python:3.12"
|
|
730
|
+
: context.type === "go"
|
|
731
|
+
? "cimg/go:1.22"
|
|
732
|
+
: context.type === "rust"
|
|
733
|
+
? "cimg/rust:1.83"
|
|
734
|
+
: "cimg/node:20.10";
|
|
735
|
+
const commands = buildPipelineCommands(context);
|
|
736
|
+
|
|
737
|
+
return [
|
|
738
|
+
"version: 2.1",
|
|
739
|
+
"",
|
|
740
|
+
"jobs:",
|
|
741
|
+
" verify:",
|
|
742
|
+
" docker:",
|
|
743
|
+
` - image: ${image}`,
|
|
744
|
+
" steps:",
|
|
745
|
+
" - checkout",
|
|
746
|
+
...commands.map((command) => ` - run: ${command}`),
|
|
747
|
+
"",
|
|
748
|
+
"workflows:",
|
|
749
|
+
" verify:",
|
|
750
|
+
" jobs:",
|
|
751
|
+
" - verify"
|
|
752
|
+
].join("\n").concat("\n");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function buildBitbucketPipelinesWorkflow(context) {
|
|
756
|
+
const image =
|
|
757
|
+
context.type === "python"
|
|
758
|
+
? "python:3.12"
|
|
759
|
+
: context.type === "go"
|
|
760
|
+
? "golang:1.22"
|
|
761
|
+
: context.type === "rust"
|
|
762
|
+
? "rust:latest"
|
|
763
|
+
: "node:20";
|
|
764
|
+
const commands = buildPipelineCommands(context);
|
|
765
|
+
|
|
766
|
+
return [
|
|
767
|
+
`image: ${image}`,
|
|
768
|
+
"",
|
|
769
|
+
"pipelines:",
|
|
770
|
+
" default:",
|
|
771
|
+
" - step:",
|
|
772
|
+
" name: Verify",
|
|
773
|
+
" script:",
|
|
774
|
+
...commands.map((command) => ` - ${command}`)
|
|
775
|
+
].join("\n").concat("\n");
|
|
776
|
+
}
|
|
777
|
+
|
|
679
778
|
export function writePipelineFiles(cwd, analysis, selection) {
|
|
680
779
|
if (!analysis.supported) {
|
|
681
780
|
throw new Error(analysis.reason);
|
|
@@ -689,6 +788,7 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
689
788
|
|
|
690
789
|
const writeWorkflow = (relativePath, contents) => {
|
|
691
790
|
const absolutePath = path.join(cwd, relativePath);
|
|
791
|
+
mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
692
792
|
writeFileSync(absolutePath, contents, "utf8");
|
|
693
793
|
writtenFiles.push(relativePath);
|
|
694
794
|
};
|
|
@@ -713,8 +813,20 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
713
813
|
writeWorkflow(".github/workflows/container.yml", buildContainerWorkflow());
|
|
714
814
|
}
|
|
715
815
|
|
|
816
|
+
if (selection.id === "gitlab-ci") {
|
|
817
|
+
writeWorkflow(".gitlab-ci.yml", buildGitLabCiWorkflow(analysis.primary));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (selection.id === "circleci") {
|
|
821
|
+
writeWorkflow(".circleci/config.yml", buildCircleCiWorkflow(analysis.primary));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (selection.id === "bitbucket-pipelines") {
|
|
825
|
+
writeWorkflow("bitbucket-pipelines.yml", buildBitbucketPipelinesWorkflow(analysis.primary));
|
|
826
|
+
}
|
|
827
|
+
|
|
716
828
|
if (selection.id === "container" && !selection.files.includes(".github/workflows/ci.yml")) {
|
|
717
|
-
notes.push("This option only creates the container workflow. Run `gitxplain pipeline` again if you also want CI verification.");
|
|
829
|
+
notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
|
|
718
830
|
}
|
|
719
831
|
|
|
720
832
|
return { writtenFiles, notes };
|
|
@@ -16,7 +16,14 @@ const PROMPT_FILES = {
|
|
|
16
16
|
review: "review.txt",
|
|
17
17
|
security: "security.txt",
|
|
18
18
|
split: "split.txt",
|
|
19
|
-
commit: "commit.txt"
|
|
19
|
+
commit: "commit.txt",
|
|
20
|
+
changelog: "changelog.txt",
|
|
21
|
+
refactor: "refactor.txt",
|
|
22
|
+
"test-suggest": "test-suggest.txt",
|
|
23
|
+
"pr-description": "pr-description.txt",
|
|
24
|
+
blame: "blame.txt",
|
|
25
|
+
stash: "stash.txt",
|
|
26
|
+
conflict: "conflict.txt"
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
function fillTemplate(template, values) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
1
|
import {
|
|
3
2
|
createCommitFromTree,
|
|
4
3
|
deletePaths,
|
|
@@ -32,26 +31,7 @@ import {
|
|
|
32
31
|
runGitCommandUnchecked,
|
|
33
32
|
resolveCommitSha
|
|
34
33
|
} from "./gitService.js";
|
|
35
|
-
|
|
36
|
-
const ANSI = {
|
|
37
|
-
reset: "\u001b[0m",
|
|
38
|
-
bold: "\u001b[1m",
|
|
39
|
-
cyan: "\u001b[36m",
|
|
40
|
-
yellow: "\u001b[33m",
|
|
41
|
-
green: "\u001b[32m"
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function supportsColor() {
|
|
45
|
-
return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function colorize(text, color) {
|
|
49
|
-
if (!supportsColor()) {
|
|
50
|
-
return text;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return `${color}${text}${ANSI.reset}`;
|
|
54
|
-
}
|
|
34
|
+
import { ANSI, colorize } from "./colorSupport.js";
|
|
55
35
|
|
|
56
36
|
function extractJsonPayload(explanation) {
|
|
57
37
|
const fencedMatch = explanation.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|