gitxplain 0.1.6 → 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/release.yml +1 -1
- package/README.md +362 -133
- package/cli/index.js +189 -278
- package/cli/services/configService.js +75 -3
- package/cli/services/mergeService.js +56 -39
- package/cli/services/pipelineService.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,41 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
const ENV_CONFIG_KEYS = new Set([
|
|
6
|
+
"LLM_PROVIDER",
|
|
7
|
+
"LLM_MODEL",
|
|
8
|
+
"OPENAI_API_KEY",
|
|
9
|
+
"OPENAI_MODEL",
|
|
10
|
+
"OPENAI_BASE_URL",
|
|
11
|
+
"GROQ_API_KEY",
|
|
12
|
+
"GROQ_MODEL",
|
|
13
|
+
"GROQ_BASE_URL",
|
|
14
|
+
"OPENROUTER_API_KEY",
|
|
15
|
+
"OPENROUTER_MODEL",
|
|
16
|
+
"OPENROUTER_BASE_URL",
|
|
17
|
+
"OPENROUTER_SITE_URL",
|
|
18
|
+
"OPENROUTER_APP_NAME",
|
|
19
|
+
"GEMINI_API_KEY",
|
|
20
|
+
"GEMINI_MODEL",
|
|
21
|
+
"GEMINI_BASE_URL",
|
|
22
|
+
"OLLAMA_API_KEY",
|
|
23
|
+
"OLLAMA_MODEL",
|
|
24
|
+
"OLLAMA_BASE_URL",
|
|
25
|
+
"CHUTES_API_KEY",
|
|
26
|
+
"CHUTES_MODEL",
|
|
27
|
+
"CHUTES_BASE_URL"
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const PROVIDER_API_KEY_FIELDS = {
|
|
31
|
+
openai: "OPENAI_API_KEY",
|
|
32
|
+
groq: "GROQ_API_KEY",
|
|
33
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
34
|
+
gemini: "GEMINI_API_KEY",
|
|
35
|
+
ollama: "OLLAMA_API_KEY",
|
|
36
|
+
chutes: "CHUTES_API_KEY"
|
|
37
|
+
};
|
|
38
|
+
|
|
5
39
|
function readJsonConfig(filePath) {
|
|
6
40
|
if (!existsSync(filePath)) {
|
|
7
41
|
return {};
|
|
@@ -14,9 +48,16 @@ function readJsonConfig(filePath) {
|
|
|
14
48
|
}
|
|
15
49
|
}
|
|
16
50
|
|
|
51
|
+
export function getUserConfigPath() {
|
|
52
|
+
return path.join(os.homedir(), ".gitxplain", "config.json");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadUserConfig() {
|
|
56
|
+
return readJsonConfig(getUserConfigPath());
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
export function loadConfig(cwd) {
|
|
18
|
-
const
|
|
19
|
-
const userConfigPath = path.join(homeDir, ".gitxplain", "config.json");
|
|
60
|
+
const userConfigPath = getUserConfigPath();
|
|
20
61
|
const projectConfigPath = path.join(cwd, ".gitxplainrc");
|
|
21
62
|
const projectJsonConfigPath = path.join(cwd, ".gitxplainrc.json");
|
|
22
63
|
|
|
@@ -26,3 +67,34 @@ export function loadConfig(cwd) {
|
|
|
26
67
|
...readJsonConfig(projectJsonConfigPath)
|
|
27
68
|
};
|
|
28
69
|
}
|
|
70
|
+
|
|
71
|
+
export function applyConfigEnvironment(config) {
|
|
72
|
+
for (const [key, value] of Object.entries(config)) {
|
|
73
|
+
if (!ENV_CONFIG_KEYS.has(key)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === "string" && value !== "" && !process.env[key]) {
|
|
78
|
+
process.env[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getProviderApiKeyField(provider) {
|
|
84
|
+
const normalized = provider?.toLowerCase();
|
|
85
|
+
return normalized ? PROVIDER_API_KEY_FIELDS[normalized] ?? null : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeUserConfig(nextConfig) {
|
|
89
|
+
const configPath = getUserConfigPath();
|
|
90
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
91
|
+
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
92
|
+
return configPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function updateUserConfig(updates) {
|
|
96
|
+
const currentConfig = loadUserConfig();
|
|
97
|
+
const nextConfig = { ...currentConfig, ...updates };
|
|
98
|
+
const configPath = writeUserConfig(nextConfig);
|
|
99
|
+
return { configPath, config: nextConfig };
|
|
100
|
+
}
|
|
@@ -306,6 +306,23 @@ export function buildReleaseWindows(sourceCommits) {
|
|
|
306
306
|
return windows;
|
|
307
307
|
}
|
|
308
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
|
+
|
|
309
326
|
export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
310
327
|
const windows = buildReleaseWindows(sourceCommits);
|
|
311
328
|
const releasedVersions = getReleasedVersions(releaseCommits);
|
|
@@ -319,7 +336,7 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
|
|
|
319
336
|
}
|
|
320
337
|
|
|
321
338
|
export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
322
|
-
const windows = buildReleaseWindows(sourceCommits);
|
|
339
|
+
const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
|
|
323
340
|
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
324
341
|
const tags = windows
|
|
325
342
|
.filter((window) => !taggedVersions.has(window.version))
|
|
@@ -342,6 +359,31 @@ export function selectReleaseTags(sourceCommits, existingTagNames = []) {
|
|
|
342
359
|
};
|
|
343
360
|
}
|
|
344
361
|
|
|
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
|
+
|
|
345
387
|
export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagNames = []) {
|
|
346
388
|
const taggedVersions = extractTaggedVersions(existingTagNames);
|
|
347
389
|
const tags = releaseCommits
|
|
@@ -431,26 +473,7 @@ export function buildReleaseTagPlan(cwd) {
|
|
|
431
473
|
throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --tag.`);
|
|
432
474
|
}
|
|
433
475
|
|
|
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
|
-
};
|
|
476
|
+
return buildReleaseTagPlanForSource(sourceBranch, "HEAD", cwd);
|
|
454
477
|
}
|
|
455
478
|
|
|
456
479
|
export function finalizeReleaseMergePlan(plan) {
|
|
@@ -523,7 +546,11 @@ function buildDriftStatus(sourceRef, sourceLabel, releaseExists, cwd) {
|
|
|
523
546
|
|
|
524
547
|
function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount }) {
|
|
525
548
|
if (!releaseExists && mergePlan.windows.length > 0) {
|
|
526
|
-
return `Run \`gitxplain merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
|
|
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.`;
|
|
527
554
|
}
|
|
528
555
|
|
|
529
556
|
if (!releaseExists) {
|
|
@@ -531,15 +558,15 @@ function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount })
|
|
|
531
558
|
}
|
|
532
559
|
|
|
533
560
|
if (mergePlan.windows.length > 0 && missingTagCount > 0) {
|
|
534
|
-
return `Run \`gitxplain merge --execute\`
|
|
561
|
+
return `Run \`gitxplain --merge --execute\` to update ${RELEASE_BRANCH}, and \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
535
562
|
}
|
|
536
563
|
|
|
537
564
|
if (mergePlan.windows.length > 0) {
|
|
538
|
-
return `Run \`gitxplain merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
565
|
+
return `Run \`gitxplain --merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
|
|
539
566
|
}
|
|
540
567
|
|
|
541
568
|
if (missingTagCount > 0) {
|
|
542
|
-
return `Run \`gitxplain tag --execute\` to create ${missingTagCount} missing
|
|
569
|
+
return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
|
|
543
570
|
}
|
|
544
571
|
|
|
545
572
|
return "No action required. Release branch and tags are up to date.";
|
|
@@ -552,11 +579,9 @@ export function buildReleaseStatus(cwd) {
|
|
|
552
579
|
const sourceRef = currentBranch === RELEASE_BRANCH ? sourceBranch : "HEAD";
|
|
553
580
|
const mergePlan = finalizeReleaseMergePlan(buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd));
|
|
554
581
|
const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
|
|
555
|
-
const
|
|
556
|
-
? selectReleaseTagsFromReleaseCommits(releaseCommits, listTags(cwd))
|
|
557
|
-
: { tags: [], taggedVersions: [], latestDetectedVersion: null };
|
|
582
|
+
const tagPlan = finalizeReleaseTagPlan(buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd));
|
|
558
583
|
const drift = buildDriftStatus(sourceRef, sourceBranch, releaseExists, cwd);
|
|
559
|
-
const missingTagVersions =
|
|
584
|
+
const missingTagVersions = tagPlan.tags.map((tag) => tag.tagName);
|
|
560
585
|
const unmergedVersions = mergePlan.windows.map((window) => window.version);
|
|
561
586
|
|
|
562
587
|
return {
|
|
@@ -571,20 +596,12 @@ export function buildReleaseStatus(cwd) {
|
|
|
571
596
|
: "healthy",
|
|
572
597
|
latestSourceVersion: mergePlan.latestDetectedVersion,
|
|
573
598
|
latestReleaseVersion: findLatestReleaseVersion(releaseCommits),
|
|
574
|
-
latestTaggedVersion:
|
|
599
|
+
latestTaggedVersion: tagPlan.latestTaggedVersion,
|
|
575
600
|
unmergedVersions,
|
|
576
601
|
missingTagVersions,
|
|
577
602
|
drift,
|
|
578
603
|
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
|
-
}),
|
|
604
|
+
tagPlan,
|
|
588
605
|
nextRecommendedAction: getNextRecommendedAction({
|
|
589
606
|
releaseExists,
|
|
590
607
|
mergePlan,
|
|
@@ -714,7 +714,7 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
714
714
|
}
|
|
715
715
|
|
|
716
716
|
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.");
|
|
717
|
+
notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
|
|
718
718
|
}
|
|
719
719
|
|
|
720
720
|
return { writtenFiles, notes };
|