ai-cr 3.2.1__py3-none-any.whl → 3.3.0__py3-none-any.whl

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.
Files changed (40) hide show
  1. {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/LICENSE +21 -21
  2. {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/METADATA +1 -1
  3. ai_cr-3.3.0.dist-info/RECORD +41 -0
  4. {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/WHEEL +1 -1
  5. gito/__main__.py +4 -4
  6. gito/bootstrap.py +90 -90
  7. gito/cli.py +255 -244
  8. gito/cli_base.py +104 -94
  9. gito/commands/__init__.py +1 -1
  10. gito/commands/deploy.py +138 -138
  11. gito/commands/fix.py +160 -160
  12. gito/commands/gh_post_review_comment.py +111 -111
  13. gito/commands/gh_react_to_comment.py +217 -217
  14. gito/commands/linear_comment.py +53 -53
  15. gito/commands/repl.py +30 -30
  16. gito/commands/version.py +8 -8
  17. gito/config.toml +450 -448
  18. gito/constants.py +15 -14
  19. gito/context.py +19 -19
  20. gito/core.py +520 -508
  21. gito/env.py +8 -7
  22. gito/gh_api.py +116 -116
  23. gito/issue_trackers.py +50 -50
  24. gito/pipeline.py +83 -83
  25. gito/pipeline_steps/jira.py +62 -62
  26. gito/pipeline_steps/linear.py +85 -85
  27. gito/project_config.py +85 -85
  28. gito/report_struct.py +136 -136
  29. gito/tpl/answer.j2 +25 -25
  30. gito/tpl/github_workflows/components/env-vars.j2 +11 -11
  31. gito/tpl/github_workflows/components/installs.j2 +23 -23
  32. gito/tpl/github_workflows/gito-code-review.yml.j2 +32 -32
  33. gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +70 -70
  34. gito/tpl/partial/aux_files.j2 +8 -8
  35. gito/tpl/questions/changes_summary.j2 +55 -55
  36. gito/tpl/questions/release_notes.j2 +26 -26
  37. gito/tpl/questions/test_cases.j2 +37 -37
  38. gito/utils.py +267 -242
  39. ai_cr-3.2.1.dist-info/RECORD +0 -41
  40. {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,217 +1,217 @@
1
- """
2
- Fix issues from code review report
3
- """
4
-
5
- import logging
6
- import os
7
- import re
8
- from pathlib import Path
9
- from typing import Optional
10
- import zipfile
11
-
12
- import requests
13
- import typer
14
- from fastcore.basics import AttrDict
15
- from microcore import ui
16
- from ghapi.all import GhApi
17
- import git
18
-
19
- from ..cli_base import app
20
- from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
21
- from ..core import answer
22
- from ..gh_api import post_gh_comment, resolve_gh_token
23
- from ..project_config import ProjectConfig
24
- from ..utils import extract_gh_owner_repo
25
- from .fix import fix
26
-
27
-
28
- def cleanup_comment_addressed_to_gito(text):
29
- if not text:
30
- return text
31
- patterns = [
32
- r'^\s*gito\b',
33
- r'^\s*ai\b',
34
- r'^\s*bot\b',
35
- r'^\s*@gito\b',
36
- r'^\s*@ai\b',
37
- r'^\s*@bot\b'
38
- ]
39
- result = text
40
- # Remove each pattern from the beginning
41
- for pattern in patterns:
42
- result = re.sub(pattern, '', result, flags=re.IGNORECASE)
43
-
44
- # Remove leading comma and spaces that may be left after prefix removal
45
- result = re.sub(r'^\s*,\s*', '', result)
46
-
47
- # Clean up extra whitespace
48
- result = re.sub(r'\s+', ' ', result).strip()
49
- return result
50
-
51
-
52
- @app.command(hidden=True)
53
- def react_to_comment(
54
- comment_id: int = typer.Argument(),
55
- gh_token: str = typer.Option(
56
- "",
57
- "--gh-token",
58
- "--token",
59
- "-t",
60
- "--github-token",
61
- help="GitHub token for authentication",
62
- ),
63
- dry_run: bool = typer.Option(
64
- False, "--dry-run", "-d", help="Only print changes without applying them"
65
- ),
66
- ):
67
- """
68
- Handles direct agent instructions from pull request comments.
69
-
70
- Note: Not for local usage. Designed for execution within GitHub Actions workflows.
71
-
72
- Fetches the PR comment by ID, parses agent directives, and executes the requested
73
- actions automatically to enable seamless code review workflow integration.
74
- """
75
- repo = git.Repo(".") # Current directory
76
- owner, repo_name = extract_gh_owner_repo(repo)
77
- logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
78
- gh_token = resolve_gh_token(gh_token)
79
- api = GhApi(owner=owner, repo=repo_name, token=gh_token)
80
- comment = api.issues.get_comment(comment_id=comment_id)
81
- logging.info(
82
- f"Comment by {ui.yellow('@' + comment.user.login)}: "
83
- f"{ui.green(comment.body)}\n"
84
- f"url: {comment.html_url}"
85
- )
86
-
87
- cfg = ProjectConfig.load_for_repo(repo)
88
- if not any(
89
- trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
90
- ):
91
- ui.error("No mention trigger found in comment, no reaction added.")
92
- return
93
- try:
94
- logging.info("Comment contains mention trigger, reacting with 'eyes'.")
95
- api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
96
- except Exception as e:
97
- logging.error("Error reacting to comment with emoji: %s", str(e))
98
- pr = int(comment.issue_url.split("/")[-1])
99
- print(f"Processing comment for PR #{pr}...")
100
-
101
- issue_ids = extract_fix_args(comment.body)
102
- if issue_ids:
103
- logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
104
- out_folder = "artifact"
105
- download_latest_code_review_artifact(
106
- api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
107
- )
108
- fix(
109
- issue_ids[0], # @todo: support multiple IDs
110
- report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
111
- dry_run=dry_run,
112
- commit=not dry_run,
113
- push=not dry_run,
114
- )
115
- logging.info("Fix applied successfully.")
116
- elif is_review_request(comment.body):
117
- ref = repo.active_branch.name
118
- logging.info(f"Triggering code-review workflow, ref='{ref}'")
119
- api.actions.create_workflow_dispatch(
120
- workflow_id="gito-code-review.yml",
121
- ref=ref,
122
- inputs={"pr_number": str(pr)},
123
- )
124
- else:
125
- if cfg.answer_github_comments:
126
- question = cleanup_comment_addressed_to_gito(comment.body)
127
- response = answer(question, repo=repo, pr=pr)
128
- post_gh_comment(
129
- gh_repository=f"{owner}/{repo_name}",
130
- pr_or_issue_number=pr,
131
- gh_token=gh_token,
132
- text=HTML_TEXT_ICON+response,
133
- )
134
- else:
135
- ui.error("Can't identify target command in the text.")
136
- return
137
-
138
-
139
- def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
140
- pr = api.pulls.get(pr_number)
141
- sha = pr["head"]["sha"] # noqa
142
- branch = pr["head"]["ref"]
143
-
144
- runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
145
- # Find the run for this SHA
146
- run = next(
147
- (
148
- r
149
- for r in runs # r['head_sha'] == sha and
150
- if (
151
- any(
152
- marker in r["path"].lower()
153
- for marker in ["code-review", "code_review", "cr"]
154
- )
155
- or "gito.yml" in r["name"].lower()
156
- )
157
- and r["status"] == "completed"
158
- ),
159
- None,
160
- )
161
- return run
162
-
163
-
164
- def download_latest_code_review_artifact(
165
- api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
166
- ) -> tuple[str, dict] | None:
167
- run = last_code_review_run(api, pr_number)
168
- if not run:
169
- raise Exception("No workflow run found for this PR/SHA")
170
-
171
- artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
172
- if not artifacts:
173
- raise Exception("No artifacts found for this workflow run")
174
-
175
- latest_artifact = artifacts[0]
176
- url = latest_artifact["archive_download_url"]
177
- print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
178
- headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
179
- zip_path = "artifact.zip"
180
- try:
181
- with requests.get(url, headers=headers, stream=True) as r:
182
- r.raise_for_status()
183
- with open(zip_path, "wb") as f:
184
- for chunk in r.iter_content(chunk_size=8192):
185
- f.write(chunk)
186
-
187
- # Unpack to ./artifact
188
- os.makedirs("artifact", exist_ok=True)
189
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
190
- zip_ref.extractall("artifact")
191
- finally:
192
- if os.path.exists(zip_path):
193
- os.remove(zip_path)
194
-
195
- print("Artifact unpacked to ./artifact")
196
-
197
-
198
- def extract_fix_args(text: str) -> list[int]:
199
- pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
200
- match = re.search(pattern1, text)
201
- if match:
202
- numbers_str = match.group(1)
203
- numbers = re.findall(r"\d+", numbers_str)
204
- issue_numbers = [int(num) for num in numbers]
205
- return issue_numbers
206
- return []
207
-
208
-
209
- def is_review_request(text: str) -> bool:
210
- text = text.lower().strip()
211
- trigger_words = ['review', 'run', 'code-review']
212
- if any(f"/{word}" in text for word in trigger_words):
213
- return True
214
- parts = text.split()
215
- if len(parts) == 2 and parts[1] in trigger_words:
216
- return True
217
- return False
1
+ """
2
+ Fix issues from code review report
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import zipfile
11
+
12
+ import requests
13
+ import typer
14
+ from fastcore.basics import AttrDict
15
+ from microcore import ui
16
+ from ghapi.all import GhApi
17
+ import git
18
+
19
+ from ..cli_base import app
20
+ from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
21
+ from ..core import answer
22
+ from ..gh_api import post_gh_comment, resolve_gh_token
23
+ from ..project_config import ProjectConfig
24
+ from ..utils import extract_gh_owner_repo
25
+ from .fix import fix
26
+
27
+
28
+ def cleanup_comment_addressed_to_gito(text):
29
+ if not text:
30
+ return text
31
+ patterns = [
32
+ r'^\s*gito\b',
33
+ r'^\s*ai\b',
34
+ r'^\s*bot\b',
35
+ r'^\s*@gito\b',
36
+ r'^\s*@ai\b',
37
+ r'^\s*@bot\b'
38
+ ]
39
+ result = text
40
+ # Remove each pattern from the beginning
41
+ for pattern in patterns:
42
+ result = re.sub(pattern, '', result, flags=re.IGNORECASE)
43
+
44
+ # Remove leading comma and spaces that may be left after prefix removal
45
+ result = re.sub(r'^\s*,\s*', '', result)
46
+
47
+ # Clean up extra whitespace
48
+ result = re.sub(r'\s+', ' ', result).strip()
49
+ return result
50
+
51
+
52
+ @app.command(hidden=True)
53
+ def react_to_comment(
54
+ comment_id: int = typer.Argument(),
55
+ gh_token: str = typer.Option(
56
+ "",
57
+ "--gh-token",
58
+ "--token",
59
+ "-t",
60
+ "--github-token",
61
+ help="GitHub token for authentication",
62
+ ),
63
+ dry_run: bool = typer.Option(
64
+ False, "--dry-run", "-d", help="Only print changes without applying them"
65
+ ),
66
+ ):
67
+ """
68
+ Handles direct agent instructions from pull request comments.
69
+
70
+ Note: Not for local usage. Designed for execution within GitHub Actions workflows.
71
+
72
+ Fetches the PR comment by ID, parses agent directives, and executes the requested
73
+ actions automatically to enable seamless code review workflow integration.
74
+ """
75
+ repo = git.Repo(".") # Current directory
76
+ owner, repo_name = extract_gh_owner_repo(repo)
77
+ logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
78
+ gh_token = resolve_gh_token(gh_token)
79
+ api = GhApi(owner=owner, repo=repo_name, token=gh_token)
80
+ comment = api.issues.get_comment(comment_id=comment_id)
81
+ logging.info(
82
+ f"Comment by {ui.yellow('@' + comment.user.login)}: "
83
+ f"{ui.green(comment.body)}\n"
84
+ f"url: {comment.html_url}"
85
+ )
86
+
87
+ cfg = ProjectConfig.load_for_repo(repo)
88
+ if not any(
89
+ trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
90
+ ):
91
+ ui.error("No mention trigger found in comment, no reaction added.")
92
+ return
93
+ try:
94
+ logging.info("Comment contains mention trigger, reacting with 'eyes'.")
95
+ api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
96
+ except Exception as e:
97
+ logging.error("Error reacting to comment with emoji: %s", str(e))
98
+ pr = int(comment.issue_url.split("/")[-1])
99
+ print(f"Processing comment for PR #{pr}...")
100
+
101
+ issue_ids = extract_fix_args(comment.body)
102
+ if issue_ids:
103
+ logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
104
+ out_folder = "artifact"
105
+ download_latest_code_review_artifact(
106
+ api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
107
+ )
108
+ fix(
109
+ issue_ids[0], # @todo: support multiple IDs
110
+ report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
111
+ dry_run=dry_run,
112
+ commit=not dry_run,
113
+ push=not dry_run,
114
+ )
115
+ logging.info("Fix applied successfully.")
116
+ elif is_review_request(comment.body):
117
+ ref = repo.active_branch.name
118
+ logging.info(f"Triggering code-review workflow, ref='{ref}'")
119
+ api.actions.create_workflow_dispatch(
120
+ workflow_id="gito-code-review.yml",
121
+ ref=ref,
122
+ inputs={"pr_number": str(pr)},
123
+ )
124
+ else:
125
+ if cfg.answer_github_comments:
126
+ question = cleanup_comment_addressed_to_gito(comment.body)
127
+ response = answer(question, repo=repo, pr=pr)
128
+ post_gh_comment(
129
+ gh_repository=f"{owner}/{repo_name}",
130
+ pr_or_issue_number=pr,
131
+ gh_token=gh_token,
132
+ text=HTML_TEXT_ICON+response,
133
+ )
134
+ else:
135
+ ui.error("Can't identify target command in the text.")
136
+ return
137
+
138
+
139
+ def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
140
+ pr = api.pulls.get(pr_number)
141
+ sha = pr["head"]["sha"] # noqa
142
+ branch = pr["head"]["ref"]
143
+
144
+ runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
145
+ # Find the run for this SHA
146
+ run = next(
147
+ (
148
+ r
149
+ for r in runs # r['head_sha'] == sha and
150
+ if (
151
+ any(
152
+ marker in r["path"].lower()
153
+ for marker in ["code-review", "code_review", "cr"]
154
+ )
155
+ or "gito.yml" in r["name"].lower()
156
+ )
157
+ and r["status"] == "completed"
158
+ ),
159
+ None,
160
+ )
161
+ return run
162
+
163
+
164
+ def download_latest_code_review_artifact(
165
+ api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
166
+ ) -> tuple[str, dict] | None:
167
+ run = last_code_review_run(api, pr_number)
168
+ if not run:
169
+ raise Exception("No workflow run found for this PR/SHA")
170
+
171
+ artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
172
+ if not artifacts:
173
+ raise Exception("No artifacts found for this workflow run")
174
+
175
+ latest_artifact = artifacts[0]
176
+ url = latest_artifact["archive_download_url"]
177
+ print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
178
+ headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
179
+ zip_path = "artifact.zip"
180
+ try:
181
+ with requests.get(url, headers=headers, stream=True) as r:
182
+ r.raise_for_status()
183
+ with open(zip_path, "wb") as f:
184
+ for chunk in r.iter_content(chunk_size=8192):
185
+ f.write(chunk)
186
+
187
+ # Unpack to ./artifact
188
+ os.makedirs("artifact", exist_ok=True)
189
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
190
+ zip_ref.extractall("artifact")
191
+ finally:
192
+ if os.path.exists(zip_path):
193
+ os.remove(zip_path)
194
+
195
+ print("Artifact unpacked to ./artifact")
196
+
197
+
198
+ def extract_fix_args(text: str) -> list[int]:
199
+ pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
200
+ match = re.search(pattern1, text)
201
+ if match:
202
+ numbers_str = match.group(1)
203
+ numbers = re.findall(r"\d+", numbers_str)
204
+ issue_numbers = [int(num) for num in numbers]
205
+ return issue_numbers
206
+ return []
207
+
208
+
209
+ def is_review_request(text: str) -> bool:
210
+ text = text.lower().strip()
211
+ trigger_words = ['review', 'run', 'code-review']
212
+ if any(f"/{word}" in text for word in trigger_words):
213
+ return True
214
+ parts = text.split()
215
+ if len(parts) == 2 and parts[1] in trigger_words:
216
+ return True
217
+ return False
@@ -1,53 +1,53 @@
1
- import os
2
- import sys
3
- import logging
4
-
5
- import typer
6
- from git import Repo
7
-
8
- from ..cli_base import app, arg_refs
9
- from ..issue_trackers import resolve_issue_key
10
-
11
- import requests
12
-
13
-
14
- def post_linear_comment(issue_key, text, api_key):
15
- response = requests.post(
16
- 'https://api.linear.app/graphql',
17
- headers={'Authorization': api_key, 'Content-Type': 'application/json'},
18
- json={
19
- 'query': '''
20
- mutation($issueId: String!, $body: String!) {
21
- commentCreate(input: {issueId: $issueId, body: $body}) {
22
- comment { id }
23
- }
24
- }
25
- ''',
26
- 'variables': {'issueId': issue_key, 'body': text}
27
- }
28
- )
29
- return response.json()
30
-
31
-
32
- @app.command(help="Post a comment with specified text to the associated Linear issue.")
33
- def linear_comment(
34
- text: str = typer.Argument(None),
35
- refs: str = arg_refs(),
36
- ):
37
- if text is None or text == "-":
38
- # Read from stdin if no text provided
39
- text = sys.stdin.read()
40
-
41
- if not text or not text.strip():
42
- typer.echo("Error: No comment text provided.", err=True)
43
- raise typer.Exit(code=1)
44
-
45
- api_key = os.getenv("LINEAR_API_KEY")
46
- if not api_key:
47
- logging.error("LINEAR_API_KEY environment variable is not set")
48
- return
49
-
50
- repo = Repo(".")
51
- key = resolve_issue_key(repo)
52
- post_linear_comment(key, text, api_key)
53
- logging.info("Comment posted to Linear issue %s", key)
1
+ import os
2
+ import sys
3
+ import logging
4
+
5
+ import typer
6
+ from git import Repo
7
+
8
+ from ..cli_base import app, arg_refs
9
+ from ..issue_trackers import resolve_issue_key
10
+
11
+ import requests
12
+
13
+
14
+ def post_linear_comment(issue_key, text, api_key):
15
+ response = requests.post(
16
+ 'https://api.linear.app/graphql',
17
+ headers={'Authorization': api_key, 'Content-Type': 'application/json'},
18
+ json={
19
+ 'query': '''
20
+ mutation($issueId: String!, $body: String!) {
21
+ commentCreate(input: {issueId: $issueId, body: $body}) {
22
+ comment { id }
23
+ }
24
+ }
25
+ ''',
26
+ 'variables': {'issueId': issue_key, 'body': text}
27
+ }
28
+ )
29
+ return response.json()
30
+
31
+
32
+ @app.command(help="Post a comment with specified text to the associated Linear issue.")
33
+ def linear_comment(
34
+ text: str = typer.Argument(None),
35
+ refs: str = arg_refs(),
36
+ ):
37
+ if text is None or text == "-":
38
+ # Read from stdin if no text provided
39
+ text = sys.stdin.read()
40
+
41
+ if not text or not text.strip():
42
+ typer.echo("Error: No comment text provided.", err=True)
43
+ raise typer.Exit(code=1)
44
+
45
+ api_key = os.getenv("LINEAR_API_KEY")
46
+ if not api_key:
47
+ logging.error("LINEAR_API_KEY environment variable is not set")
48
+ return
49
+
50
+ repo = Repo(".")
51
+ key = resolve_issue_key(repo)
52
+ post_linear_comment(key, text, api_key)
53
+ logging.info("Comment posted to Linear issue %s", key)
gito/commands/repl.py CHANGED
@@ -1,30 +1,30 @@
1
- """
2
- Python REPL
3
- """
4
- # flake8: noqa: F401
5
- import code
6
-
7
- # Wildcard imports are preferred to capture most of functionality for usage in REPL
8
- import os
9
- import sys
10
- from dataclasses import dataclass
11
- from datetime import datetime
12
- from enum import Enum
13
- from time import time
14
- from rich.pretty import pprint
15
-
16
- import microcore as mc
17
- from microcore import ui
18
-
19
- from ..cli_base import app
20
- from ..constants import *
21
- from ..core import *
22
- from ..utils import *
23
- from ..gh_api import *
24
-
25
-
26
- @app.command(
27
- help="Python REPL with core functionality loaded for quick testing/debugging and exploration."
28
- )
29
- def repl():
30
- code.interact(local=globals())
1
+ """
2
+ Python REPL
3
+ """
4
+ # flake8: noqa: F401
5
+ import code
6
+
7
+ # Wildcard imports are preferred to capture most of functionality for usage in REPL
8
+ import os
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from time import time
14
+ from rich.pretty import pprint
15
+
16
+ import microcore as mc
17
+ from microcore import ui
18
+
19
+ from ..cli_base import app
20
+ from ..constants import *
21
+ from ..core import *
22
+ from ..utils import *
23
+ from ..gh_api import *
24
+
25
+
26
+ @app.command(
27
+ help="Python REPL with core functionality loaded for quick testing/debugging and exploration."
28
+ )
29
+ def repl():
30
+ code.interact(local=globals())
gito/commands/version.py CHANGED
@@ -1,8 +1,8 @@
1
- from ..cli_base import app
2
- from ..env import Env
3
-
4
-
5
- @app.command(name='version', help='Show Gito version.')
6
- def version():
7
- print(Env.gito_version)
8
- return Env.gito_version
1
+ from ..cli_base import app
2
+ from ..env import Env
3
+
4
+
5
+ @app.command(name='version', help='Show Gito version.')
6
+ def version():
7
+ print(Env.gito_version)
8
+ return Env.gito_version