ai-cr 2.0.3__py3-none-any.whl → 3.1.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.3.dist-info → ai_cr-3.1.0.dist-info}/METADATA +4 -4
- ai_cr-3.1.0.dist-info/RECORD +37 -0
- gito/bootstrap.py +31 -12
- gito/cli.py +110 -117
- gito/cli_base.py +90 -0
- gito/commands/deploy.py +3 -3
- gito/commands/fix.py +1 -1
- gito/commands/gh_post_review_comment.py +23 -20
- gito/commands/gh_react_to_comment.py +1 -1
- gito/commands/linear_comment.py +53 -0
- gito/commands/repl.py +1 -1
- gito/commands/version.py +8 -0
- gito/config.toml +18 -4
- gito/constants.py +2 -1
- gito/context.py +19 -0
- gito/core.py +63 -19
- gito/env.py +7 -0
- gito/gh_api.py +1 -1
- gito/pipeline.py +8 -7
- gito/pipeline_steps/jira.py +6 -1
- gito/report_struct.py +2 -1
- gito/tpl/github_workflows/components/{env-vars.jinja2 → env-vars.j2} +1 -1
- gito/tpl/github_workflows/components/installs.j2 +23 -0
- gito/tpl/github_workflows/{gito-code-review.yml.jinja2 → gito-code-review.yml.j2} +5 -5
- gito/tpl/github_workflows/{gito-react-to-comments.yml.jinja2 → gito-react-to-comments.yml.j2} +2 -2
- gito/tpl/release_notes.j2 +24 -0
- gito/utils.py +14 -3
- ai_cr-2.0.3.dist-info/RECORD +0 -31
- gito/tpl/github_workflows/components/installs.jinja2 +0 -8
- {ai_cr-2.0.3.dist-info → ai_cr-3.1.0.dist-info}/LICENSE +0 -0
- {ai_cr-2.0.3.dist-info → ai_cr-3.1.0.dist-info}/WHEEL +0 -0
- {ai_cr-2.0.3.dist-info → ai_cr-3.1.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.1.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,37 @@
|
|
1
|
+
gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
3
|
+
gito/bootstrap.py,sha256=m8g7e4sacTe7SsRKmDms1_L2ESF8TAas1lMaH3hvrl8,3218
|
4
|
+
gito/cli.py,sha256=BSLbE2Nbe5-HNCcjXJZrFhgTpUhOhbmlO7g-gFSNoBA,6817
|
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/commands/version.py,sha256=u3khraY95jpIoIo-ndBxXYIEvSy_uyCmiHicAXk-V28,188
|
14
|
+
gito/config.toml,sha256=kd2fbztnr2LGaNE2BE81yt1Ed0PTF5xe_8WtTgDxxkw,18417
|
15
|
+
gito/constants.py,sha256=1ElhE4RH0EPEq3xlhwyYRUcgr38X8wXduos0gV9yPy8,819
|
16
|
+
gito/context.py,sha256=OBfcQOREsNx8WHANsplNrnrKYrXz1PyZyne11lSfZjw,446
|
17
|
+
gito/core.py,sha256=dRPDvfJfQnHV3aequ0L9ayMvUdT1seUKFEis9YSYwDA,14505
|
18
|
+
gito/env.py,sha256=TVNxqrnLefqfZt5sSg495p8UenSgHaMgTImK1rRMIlY,146
|
19
|
+
gito/gh_api.py,sha256=FbJ7w6dtuAFUVbZNyNHn8RRDGBvJyQlrIX82m46cTac,2958
|
20
|
+
gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
|
21
|
+
gito/pipeline.py,sha256=H6eiyDHUg_p3jxNVhSnyB7EVVNbMIZkA35WMymgPnh0,2610
|
22
|
+
gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
+
gito/pipeline_steps/jira.py,sha256=NjFgpGFAkT5PSXi6jQ9Yr8CY9M8sa3_rMb7RFhdpoNg,1862
|
24
|
+
gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
|
25
|
+
gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
|
26
|
+
gito/report_struct.py,sha256=96gDYnw0MXhOZrrGaNOTyWstqpjjS7_cuWufd0XJzR4,4320
|
27
|
+
gito/tpl/github_workflows/components/env-vars.j2,sha256=ouZY4vpKUkAAM5qUxA3BKAh9BpJMfwsrn8qqahhrh7U,356
|
28
|
+
gito/tpl/github_workflows/components/installs.j2,sha256=j5wl0yVEIrXZDpAgzqBwmhXQA9End3xFspPxr2ZzHR0,693
|
29
|
+
gito/tpl/github_workflows/gito-code-review.yml.j2,sha256=PcxXMTblWX2ANK-8bkxFPI3H-xby5Do_yaufhxGzw0Y,1159
|
30
|
+
gito/tpl/github_workflows/gito-react-to-comments.yml.j2,sha256=zVNkJRgje75AejXAePZugruDu3pOuDaPcujrDfth8K4,2165
|
31
|
+
gito/tpl/release_notes.j2,sha256=20QtQwdZYcFEjmfYxTysenY-njTPl2nmHpv9077WFdg,849
|
32
|
+
gito/utils.py,sha256=OSBn7IeWKjoLJoGOcdgQcJHBA69UiHUChtaYMInUeko,6852
|
33
|
+
ai_cr-3.1.0.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
|
34
|
+
ai_cr-3.1.0.dist-info/METADATA,sha256=yGLpvMwx55qR2mIzKvrIqMPqaG48JUAI48e54weEBhQ,7989
|
35
|
+
ai_cr-3.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
36
|
+
ai_cr-3.1.0.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
|
37
|
+
ai_cr-3.1.0.dist-info/RECORD,,
|
gito/bootstrap.py
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
import logging
|
2
1
|
import os
|
2
|
+
import sys
|
3
|
+
import io
|
4
|
+
import logging
|
3
5
|
from datetime import datetime
|
4
6
|
from pathlib import Path
|
5
7
|
|
6
8
|
import microcore as mc
|
7
|
-
import typer
|
8
9
|
|
9
10
|
from .utils import is_running_in_github_action
|
10
11
|
from .constants import HOME_ENV_PATH, EXECUTABLE, PROJECT_GITO_FOLDER
|
12
|
+
from .env import Env
|
11
13
|
|
12
14
|
|
13
|
-
def setup_logging():
|
15
|
+
def setup_logging(log_level: int = logging.INFO):
|
14
16
|
class CustomFormatter(logging.Formatter):
|
15
17
|
def format(self, record):
|
16
18
|
dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
|
@@ -25,23 +27,44 @@ def setup_logging():
|
|
25
27
|
|
26
28
|
handler = logging.StreamHandler()
|
27
29
|
handler.setFormatter(CustomFormatter())
|
28
|
-
logging.basicConfig(level=
|
30
|
+
logging.basicConfig(level=log_level, handlers=[handler])
|
29
31
|
|
30
32
|
|
31
|
-
def bootstrap():
|
33
|
+
def bootstrap(verbosity: int = 1):
|
32
34
|
"""Bootstrap the application with the environment configuration."""
|
33
|
-
|
34
|
-
|
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(
|
45
|
+
f"Bootstrapping Gito v{Env.gito_version}... "
|
46
|
+
+ mc.ui.gray(f"[verbosity={verbosity}]")
|
47
|
+
)
|
48
|
+
|
49
|
+
# cp1251 is used on Windows when redirecting output
|
50
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
51
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
52
|
+
|
35
53
|
try:
|
36
54
|
mc.configure(
|
37
55
|
DOT_ENV_FILE=HOME_ENV_PATH,
|
38
|
-
USE_LOGGING=
|
56
|
+
USE_LOGGING=verbosity >= 1,
|
39
57
|
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
40
58
|
PROMPT_TEMPLATES_PATH=[
|
41
59
|
PROJECT_GITO_FOLDER,
|
42
60
|
Path(__file__).parent / "tpl"
|
43
61
|
],
|
44
62
|
)
|
63
|
+
if verbosity > 1:
|
64
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
|
65
|
+
else:
|
66
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
67
|
+
|
45
68
|
except mc.LLMConfigError as e:
|
46
69
|
msg = str(e)
|
47
70
|
if is_running_in_github_action():
|
@@ -65,7 +88,3 @@ def bootstrap():
|
|
65
88
|
except Exception as e:
|
66
89
|
logging.error(f"Unexpected configuration error: {e}")
|
67
90
|
raise SystemExit(3)
|
68
|
-
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
69
|
-
|
70
|
-
|
71
|
-
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, version # 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
CHANGED
@@ -5,7 +5,7 @@ from microcore import ApiType, ui, utils
|
|
5
5
|
from git import Repo
|
6
6
|
|
7
7
|
from ..utils import version, extract_gh_owner_repo
|
8
|
-
from ..
|
8
|
+
from ..cli_base import app
|
9
9
|
|
10
10
|
|
11
11
|
@app.command(name="deploy", help="Deploy Gito workflows to GitHub Actions")
|
@@ -56,11 +56,11 @@ def deploy(api_type: ApiType = None, commit: bool = None, rewrite: bool = False)
|
|
56
56
|
remove_indent=True,
|
57
57
|
)
|
58
58
|
gito_code_review_yml = mc.tpl(
|
59
|
-
"github_workflows/gito-code-review.yml.
|
59
|
+
"github_workflows/gito-code-review.yml.j2",
|
60
60
|
**template_vars
|
61
61
|
)
|
62
62
|
gito_react_to_comments_yml = mc.tpl(
|
63
|
-
"github_workflows/gito-react-to-comments.yml.
|
63
|
+
"github_workflows/gito-react-to-comments.yml.j2",
|
64
64
|
**template_vars
|
65
65
|
)
|
66
66
|
|
gito/commands/fix.py
CHANGED
@@ -5,7 +5,7 @@ from time import sleep
|
|
5
5
|
import typer
|
6
6
|
from ghapi.core import GhApi
|
7
7
|
|
8
|
-
from ..
|
8
|
+
from ..cli_base import app
|
9
9
|
from ..constants import GITHUB_MD_REPORT_FILE_NAME, HTML_CR_COMMENT_MARKER
|
10
10
|
from ..gh_api import (
|
11
11
|
post_gh_comment,
|
@@ -15,8 +15,11 @@ from ..gh_api import (
|
|
15
15
|
from ..project_config import ProjectConfig
|
16
16
|
|
17
17
|
|
18
|
-
@app.command(help="Leave a GitHub PR comment with the review.")
|
19
|
-
def
|
18
|
+
@app.command(name="github-comment", help="Leave a GitHub PR comment with the review.")
|
19
|
+
def post_github_cr_comment(
|
20
|
+
md_report_file: str = typer.Option(default=None),
|
21
|
+
pr: int = typer.Option(default=None),
|
22
|
+
gh_repo: str = typer.Option(default=None, help="owner/repo"),
|
20
23
|
token: str = typer.Option(
|
21
24
|
"", help="GitHub token (or set GITHUB_TOKEN env var)"
|
22
25
|
),
|
@@ -24,7 +27,7 @@ def github_comment(
|
|
24
27
|
"""
|
25
28
|
Leaves a comment with the review on the current GitHub pull request.
|
26
29
|
"""
|
27
|
-
file = GITHUB_MD_REPORT_FILE_NAME
|
30
|
+
file = md_report_file or GITHUB_MD_REPORT_FILE_NAME
|
28
31
|
if not os.path.exists(file):
|
29
32
|
logging.error(f"Review file not found: {file}, comment will not be posted.")
|
30
33
|
raise typer.Exit(4)
|
@@ -38,25 +41,25 @@ def github_comment(
|
|
38
41
|
raise typer.Exit(1)
|
39
42
|
config = ProjectConfig.load()
|
40
43
|
gh_env = config.prompt_vars["github_env"]
|
41
|
-
gh_repo = gh_env.get("github_repo", "")
|
44
|
+
gh_repo = gh_repo or gh_env.get("github_repo", "")
|
42
45
|
pr_env_val = gh_env.get("github_pr_number", "")
|
43
46
|
logging.info(f"github_pr_number = {pr_env_val}")
|
44
47
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
48
|
+
if not pr:
|
49
|
+
# e.g. could be "refs/pull/123/merge" or a direct number
|
50
|
+
if "/" in pr_env_val and "pull" in pr_env_val:
|
51
|
+
# refs/pull/123/merge
|
52
|
+
try:
|
53
|
+
pr_num_candidate = pr_env_val.strip("/").split("/")
|
54
|
+
idx = pr_num_candidate.index("pull")
|
55
|
+
pr = int(pr_num_candidate[idx + 1])
|
56
|
+
except Exception:
|
57
|
+
pass
|
58
|
+
else:
|
59
|
+
try:
|
60
|
+
pr = int(pr_env_val)
|
61
|
+
except ValueError:
|
62
|
+
pass
|
60
63
|
if not pr:
|
61
64
|
if pr_str := os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH"):
|
62
65
|
try:
|
@@ -16,7 +16,7 @@ from microcore import ui
|
|
16
16
|
from ghapi.all import GhApi
|
17
17
|
import git
|
18
18
|
|
19
|
-
from ..
|
19
|
+
from ..cli_base import app
|
20
20
|
from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
|
21
21
|
from ..core import answer
|
22
22
|
from ..gh_api import post_gh_comment, resolve_gh_token
|
@@ -0,0 +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()
|
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
gito/commands/version.py
ADDED
gito/config.toml
CHANGED
@@ -50,7 +50,7 @@ report_template_cli = """
|
|
50
50
|
{%- if report.summary -%}
|
51
51
|
{{- "\n" }}
|
52
52
|
{{- "\n" }}{{- Style.BRIGHT }}✨ SUMMARY {{ Style.RESET_ALL -}}
|
53
|
-
{{- "\n" }}{{- report.summary -}}
|
53
|
+
{{- "\n" }}{{- remove_html_comments(report.summary) -}}
|
54
54
|
{%- endif %}
|
55
55
|
{% for issue in report.plain_issues -%}
|
56
56
|
{{"\n"}}{{ Style.BRIGHT }}{{Back.RED}}[ {{ issue.id}} ]{{Back.RESET}} {{ issue.title -}}{{ Style.RESET_ALL -}}
|
@@ -205,14 +205,16 @@ Summarize the code review in one sentence.
|
|
205
205
|
{{ issues | tojson(indent=2) }}
|
206
206
|
---
|
207
207
|
If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
|
208
|
-
|
208
|
+
- (!) Only give awards to initial codebase authors, NOT to reviewers.
|
209
|
+
- (!) If you give an award, place the hidden <!-- award --> HTML comment on its own line immediately before the award text.
|
209
210
|
--Available Awards--
|
210
211
|
{{ awards }}
|
211
212
|
---
|
212
213
|
{% if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
|
213
214
|
----SUBTASK----
|
214
215
|
Include one sentence about how the code changes address the requirements of the associated issue listed below.
|
215
|
-
|
216
|
+
- (!) Place the hidden <!-- issue_alignment --> comment on its own line immediately before the related text.
|
217
|
+
- Use ✅ or ⚠️ to indicate whether the implementation fully satisfies the issue requirements.
|
216
218
|
--Associated Issue--
|
217
219
|
# {{ pipeline_out.associated_issue.title }}
|
218
220
|
{{ pipeline_out.associated_issue.description }}
|
@@ -223,10 +225,12 @@ Examples:
|
|
223
225
|
|
224
226
|
If the implementation fully delivers the requested functionality:
|
225
227
|
```
|
228
|
+
<!-- issue_alignment -->
|
226
229
|
✅ Implementation Satisfies [<ISSUE_KEY>](<ISSUE_URL>).
|
227
230
|
```
|
228
231
|
If there are concerns about how thoroughly the code covers the requirements and technical description from the associated issue:
|
229
232
|
```
|
233
|
+
<!-- issue_alignment -->
|
230
234
|
⚠️ <Describe specific gap or concern>.
|
231
235
|
⚠️ <Describe additional limitation or missing feature>.
|
232
236
|
```
|
@@ -253,6 +257,14 @@ Answer the following user question:
|
|
253
257
|
--FILE: {{ file }}--
|
254
258
|
{{ file_lines }}
|
255
259
|
{% endfor %}
|
260
|
+
|
261
|
+
{%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
|
262
|
+
----ASSOCIATED ISSUE----
|
263
|
+
# {{ pipeline_out.associated_issue.title }}
|
264
|
+
{{ pipeline_out.associated_issue.description }}
|
265
|
+
URL: {{ pipeline_out.associated_issue.url }}
|
266
|
+
{%- endif -%}{{ '\n' }}
|
267
|
+
|
256
268
|
----ANSWERING INSTRUCTIONS----
|
257
269
|
{{ answering_instructions }}
|
258
270
|
"""
|
@@ -438,7 +450,9 @@ decorators add depth and texture, and observer masterfully completes the composi
|
|
438
450
|
The Gang of Four gives a standing ovation from the stalls."
|
439
451
|
```
|
440
452
|
"""
|
441
|
-
requirements = ""
|
453
|
+
requirements = """
|
454
|
+
- (!) Never report issues related to software versions, model names, or similar details that you believe have not yet been released—you cannot reliably determine this.
|
455
|
+
"""
|
442
456
|
summary_requirements = ""
|
443
457
|
answering_instructions = """
|
444
458
|
- (!) Provide a concise, direct answer in engaging speech.
|
gito/constants.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from pathlib import Path
|
2
|
+
from .env import Env
|
2
3
|
|
3
4
|
PROJECT_GITO_FOLDER = ".gito"
|
4
5
|
PROJECT_CONFIG_FILE_NAME = "config.toml"
|
@@ -9,5 +10,5 @@ JSON_REPORT_FILE_NAME = "code-review-report.json"
|
|
9
10
|
GITHUB_MD_REPORT_FILE_NAME = "code-review-report.md"
|
10
11
|
EXECUTABLE = "gito"
|
11
12
|
TEXT_ICON_URL = 'https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png' # noqa: E501
|
12
|
-
HTML_TEXT_ICON = f'<a href="https://github.com/Nayjest/Gito"><img src="{TEXT_ICON_URL}" align="left" width=64 height=50 /></a>' # noqa: E501
|
13
|
+
HTML_TEXT_ICON = f'<a href="https://github.com/Nayjest/Gito"><img src="{TEXT_ICON_URL}" align="left" width=64 height=50 title="Gito v{Env.gito_version}"/></a>' # noqa: E501
|
13
14
|
HTML_CR_COMMENT_MARKER = '<!-- GITO_COMMENT:CODE_REVIEW_REPORT -->'
|
gito/context.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Iterable, TYPE_CHECKING
|
3
|
+
|
4
|
+
from unidiff.patch import PatchSet, PatchedFile
|
5
|
+
from git import Repo
|
6
|
+
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from .project_config import ProjectConfig
|
10
|
+
from .report_struct import Report
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class Context:
|
15
|
+
report: "Report"
|
16
|
+
config: "ProjectConfig"
|
17
|
+
diff: PatchSet | Iterable[PatchedFile]
|
18
|
+
repo: Repo
|
19
|
+
pipeline_out: dict = field(default_factory=dict)
|
gito/core.py
CHANGED
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
from os import PathLike
|
4
4
|
from typing import Iterable
|
5
5
|
from pathlib import Path
|
6
|
+
from functools import partial
|
6
7
|
|
7
8
|
import microcore as mc
|
8
9
|
from microcore import ui
|
@@ -11,11 +12,13 @@ from git.exc import GitCommandError
|
|
11
12
|
from unidiff import PatchSet, PatchedFile
|
12
13
|
from unidiff.constants import DEV_NULL
|
13
14
|
|
15
|
+
from .context import Context
|
14
16
|
from .project_config import ProjectConfig
|
15
17
|
from .report_struct import Report
|
16
18
|
from .constants import JSON_REPORT_FILE_NAME
|
17
|
-
from .utils import
|
19
|
+
from .utils import make_streaming_function
|
18
20
|
from .pipeline import Pipeline
|
21
|
+
from .env import Env
|
19
22
|
|
20
23
|
|
21
24
|
def review_subject_is_index(what):
|
@@ -88,7 +91,10 @@ def get_diff(
|
|
88
91
|
else:
|
89
92
|
current_ref = what
|
90
93
|
merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
|
91
|
-
|
94
|
+
logging.info(
|
95
|
+
f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
|
96
|
+
f" --> {ui.cyan(merge_base.hexsha)}"
|
97
|
+
)
|
92
98
|
# if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
|
93
99
|
if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
|
94
100
|
# @todo: check case: reviewing working copy index in main branch #103
|
@@ -104,19 +110,35 @@ def get_diff(
|
|
104
110
|
'--pretty=format:%H'
|
105
111
|
).strip()
|
106
112
|
if merge_sha:
|
113
|
+
logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
|
107
114
|
merge_commit = repo.commit(merge_sha)
|
108
115
|
|
109
116
|
other_merge_parent = None
|
110
117
|
for parent in merge_commit.parents:
|
118
|
+
logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
|
111
119
|
if parent.hexsha == merge_base.hexsha:
|
120
|
+
logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
|
112
121
|
continue
|
113
122
|
if not commit_in_branch(repo, parent, against):
|
114
123
|
logging.warning(f"merge parent is not in {against}, skipping")
|
115
124
|
continue
|
125
|
+
logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
|
116
126
|
other_merge_parent = parent
|
117
127
|
break
|
118
128
|
if other_merge_parent:
|
119
129
|
first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
|
130
|
+
# for gito remote (feature_branch vs origin/main)
|
131
|
+
# the same merge base appears in first_common_ancestor again
|
132
|
+
if first_common_ancestor.hexsha == merge_base.hexsha:
|
133
|
+
if merge_base.parents:
|
134
|
+
first_common_ancestor = repo.merge_base(
|
135
|
+
other_merge_parent, merge_base.parents[0]
|
136
|
+
)[0]
|
137
|
+
else:
|
138
|
+
logging.error(
|
139
|
+
"merge_base has no parents, "
|
140
|
+
"using merge_base as first_common_ancestor"
|
141
|
+
)
|
120
142
|
logging.info(
|
121
143
|
f"{what} will be compared to "
|
122
144
|
f"first common ancestor of {what} and {against}: "
|
@@ -201,16 +223,18 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: b
|
|
201
223
|
return "".join(lines)
|
202
224
|
|
203
225
|
|
204
|
-
def make_cr_summary(
|
226
|
+
def make_cr_summary(ctx: Context, **kwargs) -> str:
|
205
227
|
return (
|
206
228
|
mc.prompt(
|
207
|
-
config.summary_prompt,
|
208
|
-
diff=mc.tokenizing.fit_to_token_size(diff, config.max_code_tokens)[0],
|
209
|
-
issues=report.issues,
|
210
|
-
|
229
|
+
ctx.config.summary_prompt,
|
230
|
+
diff=mc.tokenizing.fit_to_token_size(ctx.diff, ctx.config.max_code_tokens)[0],
|
231
|
+
issues=ctx.report.issues,
|
232
|
+
pipeline_out=ctx.pipeline_out,
|
233
|
+
env=Env,
|
234
|
+
**ctx.config.prompt_vars,
|
211
235
|
**kwargs,
|
212
236
|
).to_llm()
|
213
|
-
if config.summary_prompt
|
237
|
+
if ctx.config.summary_prompt
|
214
238
|
else ""
|
215
239
|
)
|
216
240
|
|
@@ -322,12 +346,11 @@ async def review(
|
|
322
346
|
out_folder = Path(out_folder or repo.working_tree_dir)
|
323
347
|
out_folder.mkdir(parents=True, exist_ok=True)
|
324
348
|
report = Report(issues=issues, number_of_processed_files=len(diff))
|
325
|
-
ctx =
|
349
|
+
ctx = Context(
|
326
350
|
report=report,
|
327
351
|
config=cfg,
|
328
352
|
diff=diff,
|
329
353
|
repo=repo,
|
330
|
-
pipeline_out={},
|
331
354
|
)
|
332
355
|
if cfg.pipeline_steps:
|
333
356
|
pipe = Pipeline(
|
@@ -338,7 +361,7 @@ async def review(
|
|
338
361
|
else:
|
339
362
|
logging.info("No pipeline steps defined, skipping pipeline execution")
|
340
363
|
|
341
|
-
report.summary = make_cr_summary(
|
364
|
+
report.summary = make_cr_summary(ctx)
|
342
365
|
report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
|
343
366
|
report_text = report.render(cfg, Report.Format.MARKDOWN)
|
344
367
|
text_report_path = out_folder / "code-review-report.md"
|
@@ -353,20 +376,41 @@ def answer(
|
|
353
376
|
against: str = None,
|
354
377
|
filters: str | list[str] = "",
|
355
378
|
use_merge_base: bool = True,
|
379
|
+
use_pipeline: bool = True,
|
380
|
+
prompt_file: str = None,
|
356
381
|
) -> str | None:
|
357
382
|
try:
|
358
|
-
repo,
|
383
|
+
repo, config, diff, lines = _prepare(
|
359
384
|
repo=repo, what=what, against=against, filters=filters, use_merge_base=use_merge_base
|
360
385
|
)
|
361
386
|
except NoChangesInContextError:
|
362
387
|
logging.error("No changes to review")
|
363
388
|
return
|
364
|
-
|
365
|
-
|
366
|
-
|
389
|
+
|
390
|
+
ctx = Context(
|
391
|
+
repo=repo,
|
367
392
|
diff=diff,
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
393
|
+
config=config,
|
394
|
+
report=Report()
|
395
|
+
)
|
396
|
+
if use_pipeline:
|
397
|
+
pipe = Pipeline(
|
398
|
+
ctx=ctx,
|
399
|
+
steps=config.pipeline_steps
|
400
|
+
)
|
401
|
+
pipe.run()
|
402
|
+
if prompt_file:
|
403
|
+
prompt_func = partial(mc.tpl, prompt_file)
|
404
|
+
else:
|
405
|
+
prompt_func = partial(mc.prompt, config.answer_prompt)
|
406
|
+
response = mc.llm(
|
407
|
+
prompt_func(
|
408
|
+
question=question,
|
409
|
+
diff=diff,
|
410
|
+
all_file_lines=lines,
|
411
|
+
pipeline_out=ctx.pipeline_out,
|
412
|
+
**config.prompt_vars,
|
413
|
+
),
|
414
|
+
callback=make_streaming_function() if Env.verbosity == 0 else None,
|
415
|
+
)
|
372
416
|
return response
|
gito/env.py
ADDED
gito/gh_api.py
CHANGED
@@ -5,7 +5,7 @@ import requests
|
|
5
5
|
from fastcore.basics import AttrDict # objects returned by ghapi
|
6
6
|
|
7
7
|
|
8
|
-
def resolve_gh_token(token_or_none):
|
8
|
+
def resolve_gh_token(token_or_none: str | None = None) -> str | None:
|
9
9
|
return token_or_none or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
|
10
10
|
|
11
11
|
|
gito/pipeline.py
CHANGED
@@ -2,10 +2,12 @@ import logging
|
|
2
2
|
from enum import StrEnum
|
3
3
|
from dataclasses import dataclass, field
|
4
4
|
|
5
|
-
from gito.utils import is_running_in_github_action
|
6
5
|
from microcore import ui
|
7
6
|
from microcore.utils import resolve_callable
|
8
7
|
|
8
|
+
from .context import Context
|
9
|
+
from .utils import is_running_in_github_action
|
10
|
+
|
9
11
|
|
10
12
|
class PipelineEnv(StrEnum):
|
11
13
|
LOCAL = "local"
|
@@ -42,7 +44,7 @@ class PipelineStep:
|
|
42
44
|
|
43
45
|
@dataclass
|
44
46
|
class Pipeline:
|
45
|
-
ctx:
|
47
|
+
ctx: Context = field()
|
46
48
|
steps: dict[str, PipelineStep] = field(default_factory=dict)
|
47
49
|
verbose: bool = False
|
48
50
|
|
@@ -55,15 +57,14 @@ class Pipeline:
|
|
55
57
|
def run(self, *args, **kwargs):
|
56
58
|
cur_env = PipelineEnv.current()
|
57
59
|
logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
|
58
|
-
self.ctx["pipeline_out"] = self.ctx.get("pipeline_out", {})
|
59
60
|
for step_name, step in self.enabled_steps.items():
|
60
61
|
if cur_env in step.envs:
|
61
62
|
logging.info(f"Running pipeline step: {step_name}")
|
62
63
|
try:
|
63
|
-
step_output = step.run(*args, **kwargs, **self.ctx)
|
64
|
+
step_output = step.run(*args, **kwargs, **vars(self.ctx))
|
64
65
|
if isinstance(step_output, dict):
|
65
|
-
self.ctx
|
66
|
-
self.ctx
|
66
|
+
self.ctx.pipeline_out.update(step_output)
|
67
|
+
self.ctx.pipeline_out[step_name] = step_output
|
67
68
|
if self.verbose and step_output:
|
68
69
|
logging.info(
|
69
70
|
f"Pipeline step {step_name} output: {repr(step_output)}"
|
@@ -79,4 +80,4 @@ class Pipeline:
|
|
79
80
|
f"Skipping pipeline step: {step_name}"
|
80
81
|
f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
|
81
82
|
)
|
82
|
-
return self.ctx
|
83
|
+
return self.ctx.pipeline_out
|
gito/pipeline_steps/jira.py
CHANGED
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
import os
|
3
3
|
|
4
4
|
import git
|
5
|
-
from jira import JIRA
|
5
|
+
from jira import JIRA, JIRAError
|
6
6
|
|
7
7
|
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
8
|
|
@@ -16,6 +16,11 @@ def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue |
|
|
16
16
|
description=issue.fields.description or "",
|
17
17
|
url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
|
18
18
|
)
|
19
|
+
except JIRAError as e:
|
20
|
+
logging.error(
|
21
|
+
f"Failed to fetch Jira issue {issue_key}: code {e.status_code} :: {e.text}"
|
22
|
+
)
|
23
|
+
return None
|
19
24
|
except Exception as e:
|
20
25
|
logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
|
21
26
|
return None
|
gito/report_struct.py
CHANGED
@@ -12,7 +12,7 @@ import textwrap
|
|
12
12
|
|
13
13
|
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
|
14
14
|
from .project_config import ProjectConfig
|
15
|
-
from .utils import syntax_hint, block_wrap_lr, max_line_len
|
15
|
+
from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments
|
16
16
|
|
17
17
|
|
18
18
|
@dataclass
|
@@ -126,6 +126,7 @@ class Report:
|
|
126
126
|
max_line_len=max_line_len,
|
127
127
|
HTML_TEXT_ICON=HTML_TEXT_ICON,
|
128
128
|
HTML_CR_COMMENT_MARKER=HTML_CR_COMMENT_MARKER,
|
129
|
+
remove_html_comments=remove_html_comments,
|
129
130
|
**config.prompt_vars
|
130
131
|
)
|
131
132
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
- name: Set up Python
|
4
|
+
uses: actions/setup-python@v5
|
5
|
+
with: { python-version: "3.13" }
|
6
|
+
|
7
|
+
- name: Fetch Latest Gito Version
|
8
|
+
id: gito-version
|
9
|
+
run: pip index versions gito.bot 2>/dev/null | head -1 | sed -n 's/.* (\([^)]*\)).*/version=\1/p' >> $GITHUB_OUTPUT
|
10
|
+
|
11
|
+
{% raw -%}
|
12
|
+
- uses: actions/cache@v4
|
13
|
+
id: cache
|
14
|
+
with:
|
15
|
+
path: |
|
16
|
+
${{ env.pythonLocation }}/lib/python3.13/site-packages
|
17
|
+
${{ env.pythonLocation }}/bin
|
18
|
+
key: gito_v${{ steps.gito-version.outputs.version }}
|
19
|
+
{%- endraw %}
|
20
|
+
|
21
|
+
- name: Install Gito
|
22
|
+
if: steps.cache.outputs.cache-hit != 'true'
|
23
|
+
run: pip install gito.bot~={{ major }}.{{ minor }}
|
@@ -15,15 +15,15 @@ jobs:
|
|
15
15
|
- uses: actions/checkout@v4
|
16
16
|
with: { fetch-depth: 0 }
|
17
17
|
|
18
|
-
{%- include("github_workflows/components/installs.
|
18
|
+
{%- include("github_workflows/components/installs.j2") %}
|
19
19
|
|
20
20
|
- name: Run AI code review
|
21
21
|
env:
|
22
|
-
{%- include("github_workflows/components/env-vars.
|
22
|
+
{%- include("github_workflows/components/env-vars.j2") %}
|
23
23
|
PR_NUMBER_FROM_WORKFLOW_DISPATCH: {% raw %}${{ github.event.inputs.pr_number }}{% endraw %}
|
24
|
-
run: |
|
25
|
-
gito --verbose review
|
26
|
-
gito github-comment --token
|
24
|
+
run: |{% raw %}
|
25
|
+
gito --verbose review ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref && format(' --against="origin/{0}"', github.event.pull_request.base.ref) || '' }}
|
26
|
+
gito github-comment --token ${{ secrets.GITHUB_TOKEN }}{% endraw %}
|
27
27
|
|
28
28
|
- uses: actions/upload-artifact@v4
|
29
29
|
with:
|
gito/tpl/github_workflows/{gito-react-to-comments.yml.jinja2 → gito-react-to-comments.yml.j2}
RENAMED
@@ -59,12 +59,12 @@ jobs:
|
|
59
59
|
fetch-depth: 0
|
60
60
|
{%- endraw %}
|
61
61
|
|
62
|
-
{%- include("github_workflows/components/installs.
|
62
|
+
{%- include("github_workflows/components/installs.j2") %}
|
63
63
|
|
64
64
|
- name: Run Gito react
|
65
65
|
env:
|
66
66
|
# LLM config is needed only if answer_github_comments = true in .gito/config.toml
|
67
67
|
# Otherwise, use LLM_API_TYPE: none
|
68
|
-
{%- include("github_workflows/components/env-vars.
|
68
|
+
{%- include("github_workflows/components/env-vars.j2") %}
|
69
69
|
run: |
|
70
70
|
{% raw %}gito react-to-comment ${{ github.event.comment.id }} --token ${{ secrets.GITHUB_TOKEN }}{%- endraw %}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
{{ self_id }}
|
2
|
+
----TASK----
|
3
|
+
Write release notes for public documentation.
|
4
|
+
Summarize the following changes, focusing on what is new, improved, or fixed for the end user.
|
5
|
+
Do not include internal or technical details.
|
6
|
+
Structure release notes using clear sections: Added, Changed, Fixed.
|
7
|
+
Avoid internal technical jargon or developer-specific details.
|
8
|
+
|
9
|
+
|
10
|
+
----RELATED CODEBASE CHANGES----
|
11
|
+
{% for part in diff %}{{ part }}\n{% endfor %}
|
12
|
+
|
13
|
+
----FULL FILE CONTENT AFTER APPLYING CHANGES----
|
14
|
+
{% for file, file_lines in all_file_lines.items() %}
|
15
|
+
--FILE: {{ file }}--
|
16
|
+
{{ file_lines }}
|
17
|
+
{% endfor %}
|
18
|
+
|
19
|
+
{%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
|
20
|
+
----ASSOCIATED ISSUE----
|
21
|
+
# {{ pipeline_out.associated_issue.title }}
|
22
|
+
{{ pipeline_out.associated_issue.description }}
|
23
|
+
URL: {{ pipeline_out.associated_issue.url }}
|
24
|
+
{%- endif -%}{{ '\n' }}
|
gito/utils.py
CHANGED
@@ -3,11 +3,11 @@ import sys
|
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
5
|
import importlib.metadata
|
6
|
+
from typing import Optional
|
6
7
|
|
7
8
|
import typer
|
8
9
|
import git
|
9
10
|
from git import Repo
|
10
|
-
from microcore import ui
|
11
11
|
|
12
12
|
|
13
13
|
_EXT_TO_HINT: dict[str, str] = {
|
@@ -223,9 +223,20 @@ def detect_github_env() -> dict:
|
|
223
223
|
return d
|
224
224
|
|
225
225
|
|
226
|
-
def
|
227
|
-
|
226
|
+
def make_streaming_function(handler: Optional[callable] = None) -> callable:
|
227
|
+
def stream(text):
|
228
|
+
if handler:
|
229
|
+
text = handler(text)
|
230
|
+
print(text, end='', flush=True)
|
231
|
+
return stream
|
228
232
|
|
229
233
|
|
230
234
|
def version() -> str:
|
231
235
|
return importlib.metadata.version("gito.bot")
|
236
|
+
|
237
|
+
|
238
|
+
def remove_html_comments(text):
|
239
|
+
"""
|
240
|
+
Removes all HTML comments (<!-- ... -->) from the input text.
|
241
|
+
"""
|
242
|
+
return re.sub(r'<!--.*?-->\s*', '', text, flags=re.DOTALL)
|
ai_cr-2.0.3.dist-info/RECORD
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
3
|
-
gito/bootstrap.py,sha256=vGI19asH0-GFj3_w_wqWucSAWPwgCP4-FkTgVuvyIGY,2517
|
4
|
-
gito/cli.py,sha256=_OwUeOQr3seMVp6ZIOfHHvKBJIKWb3-kQic5nZ2tmgE,6563
|
5
|
-
gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
|
6
|
-
gito/commands/deploy.py,sha256=1199AisP3RIpz5v48JTPmQEcA7KIWqYF5lLkU2HEiJ4,3758
|
7
|
-
gito/commands/fix.py,sha256=OS829xVrLMLQqlol9AoxJMZ9qaAWRCSbUkVNszK96ws,5305
|
8
|
-
gito/commands/gh_post_review_comment.py,sha256=BTLcS5-pfmGtVjpQEG77jexFVvzme94_DMU51icaCpQ,3517
|
9
|
-
gito/commands/gh_react_to_comment.py,sha256=Pu7T-rBB-jTJg1oUO34_FfpJsVgEIw-7rZRkTAL72tc,6396
|
10
|
-
gito/commands/repl.py,sha256=wiM5vbGNIgAeyjOKt9QhrRdGYB13Md-QYx41dMIPvaU,544
|
11
|
-
gito/config.toml,sha256=2gYU8R6UvYZg3ZniN4s2-kECmbC9k_K0nrk38-I1bKo,17596
|
12
|
-
gito/constants.py,sha256=_G40SAMfuYjYsBzEVUIxKT8Vo0dAWIQqCWWxdaBAltY,766
|
13
|
-
gito/core.py,sha256=tpPTMtjBCFtXuVshdbmL3fO6gc-bG-zlIPmnXCRdOhc,12648
|
14
|
-
gito/gh_api.py,sha256=FlqENZo0nmdtAozaKYj582w05gfs3PpEevS48hyup3M,2925
|
15
|
-
gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
|
16
|
-
gito/pipeline.py,sha256=Nq5VUVrXVDXVVYX6nVSMX1CyhoWsNtSeTOQyNgQAsdE,2672
|
17
|
-
gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
-
gito/pipeline_steps/jira.py,sha256=cgRSHoU6j0lcK6hQfhg950PEmIu3uWLGMKZbW3-H3mg,1683
|
19
|
-
gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
|
20
|
-
gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
|
21
|
-
gito/report_struct.py,sha256=HuvetI4XWTNEm-afS9Fx2doTP0ybn0P22UbH-55ambs,4243
|
22
|
-
gito/tpl/github_workflows/components/env-vars.jinja2,sha256=GPlIyhspqJFQLLjTAKz8yKeU563bsUs_n-Ao1MGmh_Y,355
|
23
|
-
gito/tpl/github_workflows/components/installs.jinja2,sha256=fQJAici1KiF3C35leB8ovO7zAvwBvcZy9tSCHKyIdkI,186
|
24
|
-
gito/tpl/github_workflows/gito-code-review.yml.jinja2,sha256=KpZdOsjINPT0TuL8FYA9TxK84hRqbGIkFjOI1XNew2g,1007
|
25
|
-
gito/tpl/github_workflows/gito-react-to-comments.yml.jinja2,sha256=yPKa285eQZBHSbmSknmcQjfVqBzgUHR3SvBJg7u1Jsc,2173
|
26
|
-
gito/utils.py,sha256=SOD89s5qpr0fToumUl7hYAPsOKfix_8MjoxeUks5s8I,6518
|
27
|
-
ai_cr-2.0.3.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
|
28
|
-
ai_cr-2.0.3.dist-info/METADATA,sha256=f6L-_hbYBYIdoYdigM_pdw4oUuzrgyMj_IGslT5-A-o,7989
|
29
|
-
ai_cr-2.0.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
30
|
-
ai_cr-2.0.3.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
|
31
|
-
ai_cr-2.0.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|