gitxplain 0.1.8 → 0.2.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.
@@ -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,22 +29,14 @@ 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
  }
54
35
 
36
+ function formatVersionTag(version) {
37
+ return `v${version}`;
38
+ }
39
+
55
40
  function extractVersions(line) {
56
41
  return unique(line.match(VERSION_PATTERN) ?? []);
57
42
  }
@@ -335,21 +320,41 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
335
320
  };
336
321
  }
337
322
 
338
- export function selectReleaseTags(sourceCommits, existingTagNames = []) {
323
+ export function selectReleaseTags(sourceCommits, existingTagNames = [], existingTagTargets = []) {
339
324
  const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
340
325
  const taggedVersions = extractTaggedVersions(existingTagNames);
326
+ const existingTagByVersion = new Map(
327
+ existingTagTargets
328
+ .map((tag) => {
329
+ const version = tag.tagName?.match(TAG_VERSION_PATTERN)?.[1] ?? null;
330
+ return version ? [version, tag] : null;
331
+ })
332
+ .filter(Boolean)
333
+ );
341
334
  const tags = windows
342
- .filter((window) => !taggedVersions.has(window.version))
343
335
  .map((window) => {
344
336
  const targetCommit = window.commits.at(-1) ?? null;
337
+ const existingTag = existingTagByVersion.get(window.version) ?? null;
338
+ const existingTargetSha = existingTag?.targetSha ?? null;
339
+ const windowCommitShas = new Set(window.commits.map((commit) => commit.sha));
340
+
345
341
  return {
346
342
  ...window,
347
- tagName: window.version,
343
+ version: window.version,
344
+ tagName: existingTag?.tagName ?? formatVersionTag(window.version),
345
+ existingTagName: existingTag?.tagName ?? null,
346
+ existingTargetSha,
347
+ needsMove:
348
+ existingTargetSha != null &&
349
+ targetCommit?.sha != null &&
350
+ windowCommitShas.has(existingTargetSha) &&
351
+ existingTargetSha !== targetCommit.sha,
348
352
  targetSha: targetCommit?.sha ?? null,
349
353
  targetShortSha: targetCommit?.shortSha ?? null,
350
354
  targetSubject: targetCommit?.subject ?? null
351
355
  };
352
356
  })
357
+ .filter((tag) => !taggedVersions.has(tag.version) || tag.needsMove)
353
358
  .filter((tag) => tag.targetSha != null);
354
359
 
355
360
  return {
@@ -370,7 +375,8 @@ function findLatestTaggedSourceVersion(sourceCommits, taggedVersions) {
370
375
  function buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd) {
371
376
  const sourceCommits = listBranchCommits(sourceRef, cwd).map((sha) => inspectCommit(sha, cwd));
372
377
  const existingTagNames = listTags(cwd);
373
- const selection = selectReleaseTags(sourceCommits, existingTagNames);
378
+ const existingTagTargets = listTagTargets(cwd);
379
+ const selection = selectReleaseTags(sourceCommits, existingTagNames, existingTagTargets);
374
380
 
375
381
  return {
376
382
  sourceBranch,
@@ -395,7 +401,7 @@ export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagN
395
401
  .filter((entry) => !taggedVersions.has(entry.version))
396
402
  .map(({ commit, version }) => ({
397
403
  version,
398
- tagName: version,
404
+ tagName: formatVersionTag(version),
399
405
  startRef: commit.shortSha,
400
406
  endRef: commit.shortSha,
401
407
  targetSha: commit.sha,
@@ -655,15 +661,18 @@ export function formatReleaseTagPlan(plan) {
655
661
  ];
656
662
 
657
663
  if (plan.tags.length === 0) {
658
- lines.push(colorize("No unreleased release tags detected. Nothing to tag.", ANSI.green));
664
+ lines.push(colorize("No release tag changes detected. Nothing to tag.", ANSI.green));
659
665
  return lines.join("\n");
660
666
  }
661
667
 
662
668
  for (const tag of plan.tags) {
663
669
  lines.push("");
664
- lines.push(colorize(`tag ${tag.tagName}`, ANSI.bold + ANSI.yellow));
670
+ lines.push(colorize(`${tag.needsMove ? "move tag" : "tag"} ${tag.tagName}`, ANSI.bold + ANSI.yellow));
665
671
  lines.push(`${colorize("Commit Range:", ANSI.bold + ANSI.cyan)} ${tag.startRef}..${tag.endRef}`);
666
672
  lines.push(`${colorize("Target Commit:", ANSI.bold + ANSI.cyan)} ${tag.targetShortSha} ${tag.targetSubject}`);
673
+ if (tag.needsMove) {
674
+ lines.push(`${colorize("Action:", ANSI.bold + ANSI.cyan)} move existing tag to the latest commit for ${tag.tagName}`);
675
+ }
667
676
 
668
677
  for (const commit of tag.commits) {
669
678
  lines.push(`${colorize(commit.shortSha, ANSI.bold + ANSI.cyan)} ${commit.subject}`);
@@ -817,14 +826,18 @@ export function executeReleaseMerge(plan, cwd) {
817
826
 
818
827
  export function executeReleaseTagPlan(plan, cwd) {
819
828
  if (plan.tags.length === 0) {
820
- throw new Error("No unreleased release tags detected. Nothing to tag.");
829
+ throw new Error("No release tag changes detected. Nothing to tag.");
821
830
  }
822
831
 
823
832
  const createdTags = [];
824
833
 
825
834
  try {
826
835
  for (const tag of plan.tags) {
827
- gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.tagName}`, cwd);
836
+ if (tag.needsMove) {
837
+ gitDeleteTag(tag.existingTagName ?? tag.tagName, cwd);
838
+ }
839
+
840
+ gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.version ?? tag.tagName.replace(/^v/, "")}`, cwd);
828
841
  createdTags.push(tag.tagName);
829
842
  }
830
843
  } catch (error) {
@@ -1,34 +1,4 @@
1
- import process from "node:process";
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
- return commitData.analysisType === "range" ? "Range" : "Commit";
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
- return `${match[2]}:`;
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 (/^\s*medium(?:\b|[.:])/i.test(line)) {
110
- return "neutral";
65
+ if (commitData.analysisType === "stash") {
66
+ return "Stash";
111
67
  }
112
68
 
113
- if (/^\s*high(?:\b|[.:])/i.test(line)) {
114
- return "bad";
69
+ if (commitData.analysisType === "conflict") {
70
+ return "Conflict";
115
71
  }
116
72
 
117
- if (
118
- /\b(no significant findings|no security findings|none apparent|looks good|safe|improved|improvement|fixed|resolved|successful|passes?|low risk|low severity)\b/i.test(
119
- line
120
- )
121
- ) {
122
- return "good";
123
- }
73
+ return "Commit";
74
+ }
124
75
 
125
- if (
126
- /\b(issue|issues|bug|broken|failure|failing|risk|risky|severity|vulnerability|insecure|regression|warning|problem|bad|missing|error|high risk|high severity)\b/i.test(
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 (/\b(suggestion|consider|follow-up|todo|medium risk|medium severity)\b/i.test(line)) {
134
- return "neutral";
79
+ if (!match) {
80
+ return null;
135
81
  }
136
82
 
137
- return null;
83
+ return `${match[2]}:`;
138
84
  }
139
85
 
140
- function colorizeByTone(line, tone) {
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 colorizeByTone(line, classifyTone(line));
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
  }