ai-cr 2.0.2__py3-none-any.whl → 3.0.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.
- {ai_cr-2.0.2.dist-info → ai_cr-3.0.0.dist-info}/METADATA +4 -4
- ai_cr-3.0.0.dist-info/RECORD +36 -0
- gito/bootstrap.py +34 -13
- gito/cli.py +110 -117
- gito/cli_base.py +90 -0
- gito/commands/deploy.py +101 -0
- gito/commands/fix.py +1 -1
- gito/commands/gh_post_review_comment.py +76 -28
- gito/commands/gh_react_to_comment.py +3 -5
- gito/commands/linear_comment.py +53 -0
- gito/commands/repl.py +2 -1
- gito/config.toml +31 -12
- gito/constants.py +1 -0
- gito/context.py +19 -0
- gito/core.py +158 -31
- gito/env.py +3 -0
- gito/gh_api.py +50 -0
- gito/issue_trackers.py +3 -2
- gito/pipeline.py +8 -7
- gito/pipeline_steps/jira.py +6 -1
- gito/project_config.py +11 -0
- gito/report_struct.py +4 -2
- gito/tpl/github_workflows/components/env-vars.j2 +10 -0
- gito/tpl/github_workflows/components/installs.j2 +23 -0
- gito/tpl/github_workflows/gito-code-review.yml.j2 +33 -0
- gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +70 -0
- gito/tpl/release_notes.j2 +24 -0
- gito/utils.py +19 -3
- ai_cr-2.0.2.dist-info/RECORD +0 -26
- {ai_cr-2.0.2.dist-info → ai_cr-3.0.0.dist-info}/LICENSE +0 -0
- {ai_cr-2.0.2.dist-info → ai_cr-3.0.0.dist-info}/WHEEL +0 -0
- {ai_cr-2.0.2.dist-info → ai_cr-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ai-cr
|
3
|
-
Version:
|
3
|
+
Version: 3.0.0
|
4
4
|
Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
|
5
5
|
License: MIT
|
6
6
|
Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
|
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
17
17
|
Classifier: Topic :: Software Development
|
18
18
|
Requires-Dist: GitPython (>=3.1.44,<4.0.0)
|
19
|
-
Requires-Dist: ai-microcore (==4.
|
20
|
-
Requires-Dist: anthropic (>=0.
|
19
|
+
Requires-Dist: ai-microcore (==4.2.1)
|
20
|
+
Requires-Dist: anthropic (>=0.57.1,<0.58.0)
|
21
21
|
Requires-Dist: ghapi (>=1.0.6,<1.1.0)
|
22
22
|
Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
|
23
23
|
Requires-Dist: jira (>=3.8.0,<4.0.0)
|
@@ -87,7 +87,7 @@ jobs:
|
|
87
87
|
uses: actions/setup-python@v5
|
88
88
|
with: { python-version: "3.13" }
|
89
89
|
- name: Install AI Code Review tool
|
90
|
-
run: pip install gito.bot~=
|
90
|
+
run: pip install gito.bot~=3.0
|
91
91
|
- name: Run AI code analysis
|
92
92
|
env:
|
93
93
|
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
3
|
+
gito/bootstrap.py,sha256=9MCV80dMs8QFaB0tmuSlrpPbnRu2ACPHAWKnOSQV-xI,3168
|
4
|
+
gito/cli.py,sha256=H6LpoUfw-R4BdJcG4WGcktaIlq2C1kgKWIZ7IEwFljI,6808
|
5
|
+
gito/cli_base.py,sha256=h0CvkfOaIYGArqNuKyAuDcJDx-hOB7H7uyfJV3afINg,2390
|
6
|
+
gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
|
7
|
+
gito/commands/deploy.py,sha256=PbNcw7Ccau2Bncpi121YDRz9gg9xvkrviytrAEFfpmw,3749
|
8
|
+
gito/commands/fix.py,sha256=C4imdS776yl7g-KmH9Wu2PqeN4WNJh8NvSeV3pV-63g,5304
|
9
|
+
gito/commands/gh_post_review_comment.py,sha256=xrCauuifrUEufBjx43sw5FMtWP9seHiGYzwAq5SFnvQ,3795
|
10
|
+
gito/commands/gh_react_to_comment.py,sha256=3R0Ke-Met06epooImI6ZC2TuLQ2Qpv276OWMSelVcNY,6395
|
11
|
+
gito/commands/linear_comment.py,sha256=8kCudz0cogWm8LsgLRWN-BybDq13zEqwV456JB3_7xo,1411
|
12
|
+
gito/commands/repl.py,sha256=jJCRO-O7r_fFXZrk9f1_oAcYPOAwPi3cGI3bMoRyer8,549
|
13
|
+
gito/config.toml,sha256=kd2fbztnr2LGaNE2BE81yt1Ed0PTF5xe_8WtTgDxxkw,18417
|
14
|
+
gito/constants.py,sha256=_G40SAMfuYjYsBzEVUIxKT8Vo0dAWIQqCWWxdaBAltY,766
|
15
|
+
gito/context.py,sha256=OBfcQOREsNx8WHANsplNrnrKYrXz1PyZyne11lSfZjw,446
|
16
|
+
gito/core.py,sha256=Huz0WnnuuojwFWKD_zPLIjB6wzcoGw3r7a4zzDiQLBI,14484
|
17
|
+
gito/env.py,sha256=HWNQb2q1mzyTg6w1FmDIxUheNSuoIUEsIEPiC1SYaYY,61
|
18
|
+
gito/gh_api.py,sha256=FbJ7w6dtuAFUVbZNyNHn8RRDGBvJyQlrIX82m46cTac,2958
|
19
|
+
gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
|
20
|
+
gito/pipeline.py,sha256=H6eiyDHUg_p3jxNVhSnyB7EVVNbMIZkA35WMymgPnh0,2610
|
21
|
+
gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
+
gito/pipeline_steps/jira.py,sha256=NjFgpGFAkT5PSXi6jQ9Yr8CY9M8sa3_rMb7RFhdpoNg,1862
|
23
|
+
gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
|
24
|
+
gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
|
25
|
+
gito/report_struct.py,sha256=96gDYnw0MXhOZrrGaNOTyWstqpjjS7_cuWufd0XJzR4,4320
|
26
|
+
gito/tpl/github_workflows/components/env-vars.j2,sha256=ouZY4vpKUkAAM5qUxA3BKAh9BpJMfwsrn8qqahhrh7U,356
|
27
|
+
gito/tpl/github_workflows/components/installs.j2,sha256=j5wl0yVEIrXZDpAgzqBwmhXQA9End3xFspPxr2ZzHR0,693
|
28
|
+
gito/tpl/github_workflows/gito-code-review.yml.j2,sha256=PcxXMTblWX2ANK-8bkxFPI3H-xby5Do_yaufhxGzw0Y,1159
|
29
|
+
gito/tpl/github_workflows/gito-react-to-comments.yml.j2,sha256=zVNkJRgje75AejXAePZugruDu3pOuDaPcujrDfth8K4,2165
|
30
|
+
gito/tpl/release_notes.j2,sha256=20QtQwdZYcFEjmfYxTysenY-njTPl2nmHpv9077WFdg,849
|
31
|
+
gito/utils.py,sha256=OSBn7IeWKjoLJoGOcdgQcJHBA69UiHUChtaYMInUeko,6852
|
32
|
+
ai_cr-3.0.0.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
|
33
|
+
ai_cr-3.0.0.dist-info/METADATA,sha256=HzNuuuJhSDq9LwSRuzx0QUOHQRhC7-p3KX6XmAoYH_c,7989
|
34
|
+
ai_cr-3.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
35
|
+
ai_cr-3.0.0.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
|
36
|
+
ai_cr-3.0.0.dist-info/RECORD,,
|
gito/bootstrap.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
-
import logging
|
2
1
|
import os
|
2
|
+
import sys
|
3
|
+
import io
|
4
|
+
import logging
|
3
5
|
from datetime import datetime
|
6
|
+
from pathlib import Path
|
4
7
|
|
5
8
|
import microcore as mc
|
6
|
-
import typer
|
7
9
|
|
8
10
|
from .utils import is_running_in_github_action
|
9
|
-
from .constants import HOME_ENV_PATH, EXECUTABLE
|
11
|
+
from .constants import HOME_ENV_PATH, EXECUTABLE, PROJECT_GITO_FOLDER
|
12
|
+
from .env import Env
|
10
13
|
|
11
14
|
|
12
|
-
def setup_logging():
|
15
|
+
def setup_logging(log_level: int = logging.INFO):
|
13
16
|
class CustomFormatter(logging.Formatter):
|
14
17
|
def format(self, record):
|
15
18
|
dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
|
@@ -24,19 +27,41 @@ def setup_logging():
|
|
24
27
|
|
25
28
|
handler = logging.StreamHandler()
|
26
29
|
handler.setFormatter(CustomFormatter())
|
27
|
-
logging.basicConfig(level=
|
30
|
+
logging.basicConfig(level=log_level, handlers=[handler])
|
28
31
|
|
29
32
|
|
30
|
-
def bootstrap():
|
33
|
+
def bootstrap(verbosity: int = 1):
|
31
34
|
"""Bootstrap the application with the environment configuration."""
|
32
|
-
|
33
|
-
|
35
|
+
log_levels_by_verbosity = {
|
36
|
+
0: logging.CRITICAL,
|
37
|
+
1: logging.INFO,
|
38
|
+
2: logging.DEBUG,
|
39
|
+
3: logging.DEBUG,
|
40
|
+
}
|
41
|
+
Env.verbosity = verbosity
|
42
|
+
Env.logging_level = log_levels_by_verbosity.get(verbosity, logging.INFO)
|
43
|
+
setup_logging(Env.logging_level)
|
44
|
+
logging.info("Bootstrapping... "+mc.ui.gray(f"[verbosity={verbosity}]"))
|
45
|
+
|
46
|
+
# cp1251 is used on Windows when redirecting output
|
47
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
48
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
49
|
+
|
34
50
|
try:
|
35
51
|
mc.configure(
|
36
52
|
DOT_ENV_FILE=HOME_ENV_PATH,
|
37
|
-
USE_LOGGING=
|
53
|
+
USE_LOGGING=verbosity >= 1,
|
38
54
|
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
55
|
+
PROMPT_TEMPLATES_PATH=[
|
56
|
+
PROJECT_GITO_FOLDER,
|
57
|
+
Path(__file__).parent / "tpl"
|
58
|
+
],
|
39
59
|
)
|
60
|
+
if verbosity > 1:
|
61
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
|
62
|
+
else:
|
63
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
64
|
+
|
40
65
|
except mc.LLMConfigError as e:
|
41
66
|
msg = str(e)
|
42
67
|
if is_running_in_github_action():
|
@@ -60,7 +85,3 @@ def bootstrap():
|
|
60
85
|
except Exception as e:
|
61
86
|
logging.error(f"Unexpected configuration error: {e}")
|
62
87
|
raise SystemExit(3)
|
63
|
-
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
64
|
-
|
65
|
-
|
66
|
-
app = typer.Typer(pretty_exceptions_show_locals=False)
|
gito/cli.py
CHANGED
@@ -1,22 +1,34 @@
|
|
1
|
+
import os
|
1
2
|
import asyncio
|
2
3
|
import logging
|
3
4
|
import sys
|
4
5
|
import textwrap
|
5
|
-
import tempfile
|
6
6
|
|
7
7
|
import microcore as mc
|
8
8
|
import typer
|
9
9
|
from git import Repo
|
10
10
|
|
11
11
|
from .core import review, get_diff, filter_diff, answer
|
12
|
+
from .cli_base import (
|
13
|
+
app,
|
14
|
+
args_to_target,
|
15
|
+
arg_refs,
|
16
|
+
arg_what,
|
17
|
+
arg_filters,
|
18
|
+
arg_out,
|
19
|
+
arg_against,
|
20
|
+
get_repo_context,
|
21
|
+
)
|
12
22
|
from .report_struct import Report
|
13
|
-
from .constants import HOME_ENV_PATH
|
14
|
-
from .bootstrap import bootstrap
|
15
|
-
from .utils import no_subcommand,
|
23
|
+
from .constants import HOME_ENV_PATH, GITHUB_MD_REPORT_FILE_NAME
|
24
|
+
from .bootstrap import bootstrap
|
25
|
+
from .utils import no_subcommand, extract_gh_owner_repo, remove_html_comments
|
26
|
+
from .gh_api import resolve_gh_token
|
16
27
|
|
17
28
|
# Import fix command to register it
|
18
|
-
from .commands import fix,
|
19
|
-
|
29
|
+
from .commands import fix, gh_react_to_comment, repl, deploy # noqa
|
30
|
+
from .commands.gh_post_review_comment import post_github_cr_comment
|
31
|
+
from .commands.linear_comment import linear_comment
|
20
32
|
|
21
33
|
app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
|
22
34
|
|
@@ -36,67 +48,26 @@ def main():
|
|
36
48
|
|
37
49
|
|
38
50
|
@app.callback(invoke_without_command=True)
|
39
|
-
def cli(
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
|
44
|
-
|
45
|
-
|
46
|
-
def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
|
47
|
-
_what, _against = parse_refs_pair(refs)
|
48
|
-
if _what:
|
49
|
-
if what:
|
50
|
-
raise typer.BadParameter(
|
51
|
-
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
|
52
|
-
)
|
53
|
-
else:
|
54
|
-
_what = what
|
55
|
-
if _against:
|
56
|
-
if against:
|
57
|
-
raise typer.BadParameter(
|
58
|
-
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
|
59
|
-
)
|
60
|
-
else:
|
61
|
-
_against = against
|
62
|
-
return _what, _against
|
63
|
-
|
64
|
-
|
65
|
-
def arg_refs() -> typer.Argument:
|
66
|
-
return typer.Argument(
|
67
|
-
default=None,
|
68
|
-
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
69
|
-
)
|
70
|
-
|
71
|
-
|
72
|
-
def arg_what() -> typer.Option:
|
73
|
-
return typer.Option(None, "--what", "-w", help="Git ref to review")
|
74
|
-
|
75
|
-
|
76
|
-
def arg_filters() -> typer.Option:
|
77
|
-
return typer.Option(
|
78
|
-
"", "--filter", "-f", "--filters",
|
79
|
-
help="""
|
80
|
-
filter reviewed files by glob / fnmatch pattern(s),
|
81
|
-
e.g. 'src/**/*.py', may be comma-separated
|
82
|
-
""",
|
83
|
-
)
|
84
|
-
|
85
|
-
|
86
|
-
def arg_out() -> typer.Option:
|
87
|
-
return typer.Option(
|
51
|
+
def cli(
|
52
|
+
ctx: typer.Context,
|
53
|
+
verbose: bool = typer.Option(default=None),
|
54
|
+
verbosity: int = typer.Option(
|
88
55
|
None,
|
89
|
-
|
90
|
-
help="
|
91
|
-
)
|
92
|
-
|
56
|
+
'--verbosity', '-v',
|
57
|
+
help="Set verbosity level (0-3, default 1)"
|
58
|
+
),
|
59
|
+
):
|
60
|
+
if verbose is not None and verbosity is not None:
|
61
|
+
raise typer.BadParameter(
|
62
|
+
"Please specify either --verbose or --verbosity, not both."
|
63
|
+
)
|
64
|
+
if verbose is not None:
|
65
|
+
verbosity = 2 if verbose else 0
|
66
|
+
if verbosity is None:
|
67
|
+
verbosity = 1
|
93
68
|
|
94
|
-
|
95
|
-
|
96
|
-
None,
|
97
|
-
"--against", "-vs", "--vs",
|
98
|
-
help="Git ref to compare against"
|
99
|
-
)
|
69
|
+
if ctx.invoked_subcommand != "setup":
|
70
|
+
bootstrap(verbosity)
|
100
71
|
|
101
72
|
|
102
73
|
@app_no_subcommand.command(name="review", help="Perform code review")
|
@@ -108,16 +79,43 @@ def cmd_review(
|
|
108
79
|
against: str = arg_against(),
|
109
80
|
filters: str = arg_filters(),
|
110
81
|
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
82
|
+
url: str = typer.Option("", "--url", help="Git repository URL"),
|
83
|
+
post_comment: bool = typer.Option(default=False, help="Post review comment to GitHub"),
|
84
|
+
pr: int = typer.Option(
|
85
|
+
default=None,
|
86
|
+
help=textwrap.dedent("""\n
|
87
|
+
GitHub Pull Request number to post the comment to
|
88
|
+
(for local usage together with --post-comment,
|
89
|
+
in the github actions PR is resolved from the environment)
|
90
|
+
""")
|
91
|
+
),
|
111
92
|
out: str = arg_out()
|
112
93
|
):
|
113
94
|
_what, _against = args_to_target(refs, what, against)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
95
|
+
with get_repo_context(url, _what) as (repo, out_folder):
|
96
|
+
asyncio.run(review(
|
97
|
+
repo=repo,
|
98
|
+
what=_what,
|
99
|
+
against=_against,
|
100
|
+
filters=filters,
|
101
|
+
use_merge_base=merge_base,
|
102
|
+
out_folder=out or out_folder,
|
103
|
+
))
|
104
|
+
if post_comment:
|
105
|
+
try:
|
106
|
+
owner, repo_name = extract_gh_owner_repo(repo)
|
107
|
+
except ValueError as e:
|
108
|
+
logging.error(
|
109
|
+
"Error posting comment:\n"
|
110
|
+
"Could not extract GitHub owner and repository name from the local repository."
|
111
|
+
)
|
112
|
+
raise typer.Exit(code=1) from e
|
113
|
+
post_github_cr_comment(
|
114
|
+
md_report_file=os.path.join(out or out_folder, GITHUB_MD_REPORT_FILE_NAME),
|
115
|
+
pr=pr,
|
116
|
+
gh_repo=f"{owner}/{repo_name}",
|
117
|
+
token=resolve_gh_token()
|
118
|
+
)
|
121
119
|
|
122
120
|
|
123
121
|
@app.command(name="ask", help="Answer questions about codebase changes")
|
@@ -130,15 +128,32 @@ def cmd_answer(
|
|
130
128
|
against: str = arg_against(),
|
131
129
|
filters: str = arg_filters(),
|
132
130
|
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
131
|
+
use_pipeline: bool = typer.Option(default=True),
|
132
|
+
post_to: str = typer.Option(
|
133
|
+
help="Post answer to ... Supported values: linear",
|
134
|
+
default=None,
|
135
|
+
show_default=False
|
136
|
+
),
|
133
137
|
):
|
134
138
|
_what, _against = args_to_target(refs, what, against)
|
135
|
-
|
139
|
+
if str(question).startswith("tpl:"):
|
140
|
+
prompt_file = str(question)[4:]
|
141
|
+
question = ""
|
142
|
+
else:
|
143
|
+
prompt_file = None
|
144
|
+
out = answer(
|
136
145
|
question=question,
|
137
146
|
what=_what,
|
138
147
|
against=_against,
|
139
148
|
filters=filters,
|
140
149
|
use_merge_base=merge_base,
|
150
|
+
prompt_file=prompt_file,
|
151
|
+
use_pipeline=use_pipeline,
|
141
152
|
)
|
153
|
+
if post_to == 'linear':
|
154
|
+
logging.info("Posting answer to Linear...")
|
155
|
+
linear_comment(remove_html_comments(out))
|
156
|
+
return out
|
142
157
|
|
143
158
|
|
144
159
|
@app.command(help="Configure LLM for local usage interactively")
|
@@ -160,31 +175,6 @@ def render(
|
|
160
175
|
Report.load(file_name=source).to_cli(report_format=format)
|
161
176
|
|
162
177
|
|
163
|
-
@app.command(help="Review remote code")
|
164
|
-
def remote(
|
165
|
-
url: str = typer.Argument(..., help="Git repository URL"),
|
166
|
-
refs: str = arg_refs(),
|
167
|
-
what: str = arg_what(),
|
168
|
-
against: str = arg_against(),
|
169
|
-
filters: str = arg_filters(),
|
170
|
-
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
171
|
-
out: str = arg_out()
|
172
|
-
):
|
173
|
-
_what, _against = args_to_target(refs, what, against)
|
174
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
175
|
-
logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
|
176
|
-
repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
|
177
|
-
asyncio.run(review(
|
178
|
-
repo=repo,
|
179
|
-
what=_what,
|
180
|
-
against=_against,
|
181
|
-
filters=filters,
|
182
|
-
use_merge_base=merge_base,
|
183
|
-
out_folder=out or '.',
|
184
|
-
))
|
185
|
-
repo.close()
|
186
|
-
|
187
|
-
|
188
178
|
@app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
|
189
179
|
def files(
|
190
180
|
refs: str = arg_refs(),
|
@@ -196,22 +186,25 @@ def files(
|
|
196
186
|
):
|
197
187
|
_what, _against = args_to_target(refs, what, against)
|
198
188
|
repo = Repo(".")
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
189
|
+
try:
|
190
|
+
patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
|
191
|
+
patch_set = filter_diff(patch_set, filters)
|
192
|
+
print(
|
193
|
+
f"Changed files: "
|
194
|
+
f"{mc.ui.green(_what or 'INDEX')} vs "
|
195
|
+
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
196
|
+
f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
|
197
|
+
)
|
198
|
+
|
199
|
+
for patch in patch_set:
|
200
|
+
if patch.is_added_file:
|
201
|
+
color = mc.ui.green
|
202
|
+
elif patch.is_removed_file:
|
203
|
+
color = mc.ui.red
|
204
|
+
else:
|
205
|
+
color = mc.ui.blue
|
206
|
+
print(f"- {color(patch.path)}")
|
207
|
+
if diff:
|
208
|
+
print(mc.ui.gray(textwrap.indent(str(patch), " ")))
|
209
|
+
finally:
|
210
|
+
repo.close()
|
gito/cli_base.py
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
import contextlib
|
2
|
+
import logging
|
3
|
+
import tempfile
|
4
|
+
|
5
|
+
import microcore as mc
|
6
|
+
import typer
|
7
|
+
from git import Repo
|
8
|
+
from gito.utils import parse_refs_pair
|
9
|
+
|
10
|
+
|
11
|
+
def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
|
12
|
+
_what, _against = parse_refs_pair(refs)
|
13
|
+
if _what:
|
14
|
+
if what:
|
15
|
+
raise typer.BadParameter(
|
16
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
|
17
|
+
)
|
18
|
+
else:
|
19
|
+
_what = what
|
20
|
+
if _against:
|
21
|
+
if against:
|
22
|
+
raise typer.BadParameter(
|
23
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
|
24
|
+
)
|
25
|
+
else:
|
26
|
+
_against = against
|
27
|
+
return _what, _against
|
28
|
+
|
29
|
+
|
30
|
+
def arg_refs() -> typer.Argument:
|
31
|
+
return typer.Argument(
|
32
|
+
default=None,
|
33
|
+
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
def arg_what() -> typer.Option:
|
38
|
+
return typer.Option(None, "--what", "-w", help="Git ref to review")
|
39
|
+
|
40
|
+
|
41
|
+
def arg_filters() -> typer.Option:
|
42
|
+
return typer.Option(
|
43
|
+
"", "--filter", "-f", "--filters",
|
44
|
+
help="""
|
45
|
+
filter reviewed files by glob / fnmatch pattern(s),
|
46
|
+
e.g. 'src/**/*.py', may be comma-separated
|
47
|
+
""",
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
def arg_out() -> typer.Option:
|
52
|
+
return typer.Option(
|
53
|
+
None,
|
54
|
+
"--out", "-o", "--output",
|
55
|
+
help="Output folder for the code review report"
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
def arg_against() -> typer.Option:
|
60
|
+
return typer.Option(
|
61
|
+
None,
|
62
|
+
"--against", "-vs", "--vs",
|
63
|
+
help="Git ref to compare against"
|
64
|
+
)
|
65
|
+
|
66
|
+
|
67
|
+
app = typer.Typer(pretty_exceptions_show_locals=False)
|
68
|
+
|
69
|
+
|
70
|
+
@contextlib.contextmanager
|
71
|
+
def get_repo_context(url: str, branch: str):
|
72
|
+
"""Context manager for handling both local and remote repositories."""
|
73
|
+
if url:
|
74
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
75
|
+
logging.info(
|
76
|
+
f"get_repo_context: "
|
77
|
+
f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ..."
|
78
|
+
)
|
79
|
+
repo = Repo.clone_from(url, branch=branch, to_path=temp_dir)
|
80
|
+
try:
|
81
|
+
yield repo, temp_dir
|
82
|
+
finally:
|
83
|
+
repo.close()
|
84
|
+
else:
|
85
|
+
logging.info("get_repo_context: Using local repo...")
|
86
|
+
repo = Repo(".")
|
87
|
+
try:
|
88
|
+
yield repo, "."
|
89
|
+
finally:
|
90
|
+
repo.close()
|
gito/commands/deploy.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
import microcore as mc
|
4
|
+
from microcore import ApiType, ui, utils
|
5
|
+
from git import Repo
|
6
|
+
|
7
|
+
from ..utils import version, extract_gh_owner_repo
|
8
|
+
from ..cli_base import app
|
9
|
+
|
10
|
+
|
11
|
+
@app.command(name="deploy", help="Deploy Gito workflows to GitHub Actions")
|
12
|
+
@app.command(name="init", hidden=True)
|
13
|
+
def deploy(api_type: ApiType = None, commit: bool = None, rewrite: bool = False):
|
14
|
+
repo = Repo(".")
|
15
|
+
workflow_files = dict(
|
16
|
+
code_review=Path(".github/workflows/gito-code-review.yml"),
|
17
|
+
react_to_comments=Path(".github/workflows/gito-react-to-comments.yml")
|
18
|
+
)
|
19
|
+
for file in workflow_files.values():
|
20
|
+
if file.exists():
|
21
|
+
message = f"Gito workflow already exists at {utils.file_link(file)}."
|
22
|
+
if rewrite:
|
23
|
+
ui.warning(message)
|
24
|
+
else:
|
25
|
+
message += "\nUse --rewrite to overwrite it."
|
26
|
+
ui.error(message)
|
27
|
+
return False
|
28
|
+
|
29
|
+
api_types = [ApiType.ANTHROPIC, ApiType.OPEN_AI, ApiType.GOOGLE_AI_STUDIO]
|
30
|
+
default_models = {
|
31
|
+
ApiType.ANTHROPIC: "claude-sonnet-4-20250514",
|
32
|
+
ApiType.OPEN_AI: "gpt-4.1",
|
33
|
+
ApiType.GOOGLE_AI_STUDIO: "gemini-2.5-pro",
|
34
|
+
}
|
35
|
+
secret_names = {
|
36
|
+
ApiType.ANTHROPIC: "ANTHROPIC_API_KEY",
|
37
|
+
ApiType.OPEN_AI: "OPENAI_API_KEY",
|
38
|
+
ApiType.GOOGLE_AI_STUDIO: "GOOGLE_AI_API_KEY",
|
39
|
+
}
|
40
|
+
if not api_type:
|
41
|
+
api_type = mc.ui.ask_choose(
|
42
|
+
"Choose your LLM API type",
|
43
|
+
api_types,
|
44
|
+
)
|
45
|
+
elif api_type not in api_types:
|
46
|
+
mc.ui.error(f"Unsupported API type: {api_type}")
|
47
|
+
return False
|
48
|
+
major, minor, *_ = version().split(".")
|
49
|
+
template_vars = dict(
|
50
|
+
model=default_models[api_type],
|
51
|
+
api_type=api_type,
|
52
|
+
secret_name=secret_names[api_type],
|
53
|
+
major=major,
|
54
|
+
minor=minor,
|
55
|
+
ApiType=ApiType,
|
56
|
+
remove_indent=True,
|
57
|
+
)
|
58
|
+
gito_code_review_yml = mc.tpl(
|
59
|
+
"github_workflows/gito-code-review.yml.j2",
|
60
|
+
**template_vars
|
61
|
+
)
|
62
|
+
gito_react_to_comments_yml = mc.tpl(
|
63
|
+
"github_workflows/gito-react-to-comments.yml.j2",
|
64
|
+
**template_vars
|
65
|
+
)
|
66
|
+
|
67
|
+
workflow_files["code_review"].parent.mkdir(parents=True, exist_ok=True)
|
68
|
+
workflow_files["code_review"].write_text(gito_code_review_yml)
|
69
|
+
workflow_files["react_to_comments"].write_text(gito_react_to_comments_yml)
|
70
|
+
print(
|
71
|
+
mc.ui.green("Gito workflows have been created.\n")
|
72
|
+
+ f" - {mc.utils.file_link(workflow_files['code_review'])}\n"
|
73
|
+
+ f" - {mc.utils.file_link(workflow_files['react_to_comments'])}\n"
|
74
|
+
)
|
75
|
+
owner, repo_name = extract_gh_owner_repo(repo)
|
76
|
+
if commit is True or commit is None and mc.ui.ask_yn(
|
77
|
+
"Do you want to commit and push created GitHub workflows to a new branch?"
|
78
|
+
):
|
79
|
+
repo.git.add([str(file) for file in workflow_files.values()])
|
80
|
+
branch_name = "gito_deploy"
|
81
|
+
if not repo.active_branch.name.startswith(branch_name):
|
82
|
+
repo.git.checkout("-b", branch_name)
|
83
|
+
repo.git.commit("-m", "Deploy Gito workflows")
|
84
|
+
repo.git.push("origin", branch_name)
|
85
|
+
print(f"Changes pushed to {branch_name} branch.")
|
86
|
+
print(
|
87
|
+
f"Please create a PR from {branch_name} to your main branch and merge it:\n"
|
88
|
+
f"https://github.com/{owner}/{repo_name}/compare/gito_deploy?expand=1"
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
print(
|
92
|
+
"Now you can commit and push created GitHub workflows to your main repository branch.\n"
|
93
|
+
)
|
94
|
+
|
95
|
+
print(
|
96
|
+
"(!IMPORTANT):\n"
|
97
|
+
f"Add {mc.ui.cyan(secret_names[api_type])} with actual API_KEY "
|
98
|
+
"to your repository secrets here:\n"
|
99
|
+
f"https://github.com/{owner}/{repo_name}/settings/secrets/actions"
|
100
|
+
)
|
101
|
+
return True
|