ai-cr 2.0.2__py3-none-any.whl → 2.0.3__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-2.0.3.dist-info}/METADATA +1 -1
- ai_cr-2.0.3.dist-info/RECORD +31 -0
- gito/bootstrap.py +6 -1
- gito/cli.py +1 -1
- gito/commands/deploy.py +101 -0
- gito/commands/gh_post_review_comment.py +55 -10
- gito/commands/gh_react_to_comment.py +2 -4
- gito/commands/repl.py +1 -0
- gito/config.toml +13 -8
- gito/constants.py +1 -0
- gito/core.py +99 -15
- gito/gh_api.py +50 -0
- gito/issue_trackers.py +3 -2
- gito/project_config.py +11 -0
- gito/report_struct.py +2 -1
- gito/tpl/github_workflows/components/env-vars.jinja2 +10 -0
- gito/tpl/github_workflows/components/installs.jinja2 +8 -0
- gito/tpl/github_workflows/gito-code-review.yml.jinja2 +33 -0
- gito/tpl/github_workflows/gito-react-to-comments.yml.jinja2 +70 -0
- gito/utils.py +5 -0
- ai_cr-2.0.2.dist-info/RECORD +0 -26
- {ai_cr-2.0.2.dist-info → ai_cr-2.0.3.dist-info}/LICENSE +0 -0
- {ai_cr-2.0.2.dist-info → ai_cr-2.0.3.dist-info}/WHEEL +0 -0
- {ai_cr-2.0.2.dist-info → ai_cr-2.0.3.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ai-cr
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.3
|
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
|
@@ -0,0 +1,31 @@
|
|
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,,
|
gito/bootstrap.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
from datetime import datetime
|
4
|
+
from pathlib import Path
|
4
5
|
|
5
6
|
import microcore as mc
|
6
7
|
import typer
|
7
8
|
|
8
9
|
from .utils import is_running_in_github_action
|
9
|
-
from .constants import HOME_ENV_PATH, EXECUTABLE
|
10
|
+
from .constants import HOME_ENV_PATH, EXECUTABLE, PROJECT_GITO_FOLDER
|
10
11
|
|
11
12
|
|
12
13
|
def setup_logging():
|
@@ -36,6 +37,10 @@ def bootstrap():
|
|
36
37
|
DOT_ENV_FILE=HOME_ENV_PATH,
|
37
38
|
USE_LOGGING=True,
|
38
39
|
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
40
|
+
PROMPT_TEMPLATES_PATH=[
|
41
|
+
PROJECT_GITO_FOLDER,
|
42
|
+
Path(__file__).parent / "tpl"
|
43
|
+
],
|
39
44
|
)
|
40
45
|
except mc.LLMConfigError as e:
|
41
46
|
msg = str(e)
|
gito/cli.py
CHANGED
@@ -15,7 +15,7 @@ from .bootstrap import bootstrap, app
|
|
15
15
|
from .utils import no_subcommand, parse_refs_pair
|
16
16
|
|
17
17
|
# Import fix command to register it
|
18
|
-
from .commands import fix, gh_post_review_comment, gh_react_to_comment, repl # noqa
|
18
|
+
from .commands import fix, gh_post_review_comment, gh_react_to_comment, repl, deploy # noqa
|
19
19
|
|
20
20
|
|
21
21
|
app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
|
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 ..bootstrap 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.jinja2",
|
60
|
+
**template_vars
|
61
|
+
)
|
62
|
+
gito_react_to_comments_yml = mc.tpl(
|
63
|
+
"github_workflows/gito-react-to-comments.yml.jinja2",
|
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
|
@@ -1,17 +1,24 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
|
+
from time import sleep
|
3
4
|
|
4
5
|
import typer
|
5
|
-
from
|
6
|
-
|
7
|
-
from
|
8
|
-
from
|
6
|
+
from ghapi.core import GhApi
|
7
|
+
|
8
|
+
from ..bootstrap import app
|
9
|
+
from ..constants import GITHUB_MD_REPORT_FILE_NAME, HTML_CR_COMMENT_MARKER
|
10
|
+
from ..gh_api import (
|
11
|
+
post_gh_comment,
|
12
|
+
resolve_gh_token,
|
13
|
+
hide_gh_comment,
|
14
|
+
)
|
15
|
+
from ..project_config import ProjectConfig
|
9
16
|
|
10
17
|
|
11
18
|
@app.command(help="Leave a GitHub PR comment with the review.")
|
12
19
|
def github_comment(
|
13
20
|
token: str = typer.Option(
|
14
|
-
|
21
|
+
"", help="GitHub token (or set GITHUB_TOKEN env var)"
|
15
22
|
),
|
16
23
|
):
|
17
24
|
"""
|
@@ -25,13 +32,14 @@ def github_comment(
|
|
25
32
|
with open(file, "r", encoding="utf-8") as f:
|
26
33
|
body = f.read()
|
27
34
|
|
35
|
+
token = resolve_gh_token(token)
|
28
36
|
if not token:
|
29
37
|
print("GitHub token is required (--token or GITHUB_TOKEN env var).")
|
30
38
|
raise typer.Exit(1)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
pr_env_val =
|
39
|
+
config = ProjectConfig.load()
|
40
|
+
gh_env = config.prompt_vars["github_env"]
|
41
|
+
gh_repo = gh_env.get("github_repo", "")
|
42
|
+
pr_env_val = gh_env.get("github_pr_number", "")
|
35
43
|
logging.info(f"github_pr_number = {pr_env_val}")
|
36
44
|
|
37
45
|
pr = None
|
@@ -59,5 +67,42 @@ def github_comment(
|
|
59
67
|
logging.error("Could not resolve PR number from environment variables.")
|
60
68
|
raise typer.Exit(3)
|
61
69
|
|
62
|
-
if not post_gh_comment(
|
70
|
+
if not post_gh_comment(gh_repo, pr, token, body):
|
63
71
|
raise typer.Exit(5)
|
72
|
+
|
73
|
+
if config.collapse_previous_code_review_comments:
|
74
|
+
sleep(1)
|
75
|
+
collapse_gh_outdated_cr_comments(gh_repo, pr, token)
|
76
|
+
|
77
|
+
|
78
|
+
def collapse_gh_outdated_cr_comments(
|
79
|
+
gh_repository: str,
|
80
|
+
pr_or_issue_number: int,
|
81
|
+
token: str = None
|
82
|
+
):
|
83
|
+
"""
|
84
|
+
Collapse outdated code review comments in a GitHub pull request or issue.
|
85
|
+
"""
|
86
|
+
logging.info(f"Collapsing outdated comments in {gh_repository} #{pr_or_issue_number}...")
|
87
|
+
|
88
|
+
token = resolve_gh_token(token)
|
89
|
+
owner, repo = gh_repository.split('/')
|
90
|
+
api = GhApi(owner, repo, token=token)
|
91
|
+
|
92
|
+
comments = api.issues.list_comments(pr_or_issue_number)
|
93
|
+
review_marker = HTML_CR_COMMENT_MARKER
|
94
|
+
collapsed_title = "🗑️ Outdated Code Review by Gito"
|
95
|
+
collapsed_marker = f"<summary>{collapsed_title}</summary>"
|
96
|
+
outdated_comments = [
|
97
|
+
c for c in comments
|
98
|
+
if c.body and review_marker in c.body and collapsed_marker not in c.body
|
99
|
+
][:-1]
|
100
|
+
if not outdated_comments:
|
101
|
+
logging.info("No outdated comments found")
|
102
|
+
return
|
103
|
+
for comment in outdated_comments:
|
104
|
+
logging.info(f"Collapsing comment {comment.id}...")
|
105
|
+
new_body = f"<details>\n<summary>{collapsed_title}</summary>\n\n{comment.body}\n</details>"
|
106
|
+
api.issues.update_comment(comment.id, new_body)
|
107
|
+
hide_gh_comment(comment.node_id, token)
|
108
|
+
logging.info("All outdated comments collapsed successfully.")
|
@@ -19,7 +19,7 @@ import git
|
|
19
19
|
from ..bootstrap import app
|
20
20
|
from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
|
21
21
|
from ..core import answer
|
22
|
-
from ..gh_api import post_gh_comment
|
22
|
+
from ..gh_api import post_gh_comment, resolve_gh_token
|
23
23
|
from ..project_config import ProjectConfig
|
24
24
|
from ..utils import extract_gh_owner_repo
|
25
25
|
from .fix import fix
|
@@ -51,9 +51,7 @@ def react_to_comment(
|
|
51
51
|
repo = git.Repo(".") # Current directory
|
52
52
|
owner, repo_name = extract_gh_owner_repo(repo)
|
53
53
|
logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
|
54
|
-
gh_token = (
|
55
|
-
gh_token or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
|
56
|
-
)
|
54
|
+
gh_token = resolve_gh_token(gh_token)
|
57
55
|
api = GhApi(owner=owner, repo=repo_name, token=gh_token)
|
58
56
|
comment = api.issues.get_comment(comment_id=comment_id)
|
59
57
|
logging.info(
|
gito/commands/repl.py
CHANGED
gito/config.toml
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
# Defines the keyword or mention tag that triggers bot actions when referenced in code review comments.
|
4
4
|
# list of strings, case-insensitive
|
5
5
|
mention_triggers = ["gito", "bot", "ai", "/fix"]
|
6
|
+
collapse_previous_code_review_comments = true
|
6
7
|
report_template_md = """
|
7
8
|
<h2>{{ HTML_TEXT_ICON }}I've Reviewed the Code</h2>
|
8
9
|
|
@@ -36,7 +37,7 @@ report_template_md = """
|
|
36
37
|
{%- endfor -%}
|
37
38
|
{{ "\n" }}
|
38
39
|
{%- endfor -%}
|
39
|
-
|
40
|
+
{{- HTML_CR_COMMENT_MARKER -}}
|
40
41
|
"""
|
41
42
|
report_template_cli = """
|
42
43
|
{{ Back.BLUE }} + + + ---==<<[ CODE REVIEW{{Style.NORMAL}} ]>>==--- + + + {{Style.RESET_ALL}}
|
@@ -134,18 +135,18 @@ Review the provided code diff carefully and identify *only* highly confident iss
|
|
134
135
|
{{ requirements -}}
|
135
136
|
{{ json_requirements }}
|
136
137
|
|
137
|
-
Respond with a valid JSON array of issues
|
138
|
+
Respond with a valid JSON array of issues following this schema:
|
138
139
|
[
|
139
140
|
{
|
140
141
|
"title": "<issue_title>",
|
141
142
|
"details": "<issue_description>",
|
142
|
-
"tags": ["<issue_tag1>", "<issue_tag2>"],
|
143
|
-
"severity": <issue_severity>,
|
144
|
-
"confidence": <confidence_score>,
|
145
|
-
"affected_lines": [ // optional;
|
143
|
+
"tags": ["<issue_tag1>", "<issue_tag2>", ...],
|
144
|
+
"severity": <issue_severity:int>,
|
145
|
+
"confidence": <confidence_score:int>,
|
146
|
+
"affected_lines": [ // optional;
|
146
147
|
{
|
147
|
-
"start_line": <
|
148
|
-
"end_line": <
|
148
|
+
"start_line": <int>,
|
149
|
+
"end_line": <int>,
|
149
150
|
"proposal": "<proposed code to replace the affected lines (optional)>"
|
150
151
|
},
|
151
152
|
...
|
@@ -153,6 +154,10 @@ Respond with a valid JSON array of issues in the following format:
|
|
153
154
|
},
|
154
155
|
...
|
155
156
|
]
|
157
|
+
|
158
|
+
- if present, `proposal` blocks must match the indentation of the original code
|
159
|
+
and apply cleanly to lines `start_line`..`end_line`. It is designed for programmatical substitution.
|
160
|
+
|
156
161
|
Available issue tags:
|
157
162
|
- bug
|
158
163
|
- security
|
gito/constants.py
CHANGED
@@ -10,3 +10,4 @@ GITHUB_MD_REPORT_FILE_NAME = "code-review-report.md"
|
|
10
10
|
EXECUTABLE = "gito"
|
11
11
|
TEXT_ICON_URL = 'https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png' # noqa: E501
|
12
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_CR_COMMENT_MARKER = '<!-- GITO_COMMENT:CODE_REVIEW_REPORT -->'
|
gito/core.py
CHANGED
@@ -5,8 +5,9 @@ from typing import Iterable
|
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
7
|
import microcore as mc
|
8
|
-
from
|
9
|
-
from
|
8
|
+
from microcore import ui
|
9
|
+
from git import Repo, Commit
|
10
|
+
from git.exc import GitCommandError
|
10
11
|
from unidiff import PatchSet, PatchedFile
|
11
12
|
from unidiff.constants import DEV_NULL
|
12
13
|
|
@@ -14,6 +15,7 @@ from .project_config import ProjectConfig
|
|
14
15
|
from .report_struct import Report
|
15
16
|
from .constants import JSON_REPORT_FILE_NAME
|
16
17
|
from .utils import stream_to_cli
|
18
|
+
from .pipeline import Pipeline
|
17
19
|
|
18
20
|
|
19
21
|
def review_subject_is_index(what):
|
@@ -51,6 +53,16 @@ def is_binary_file(repo: Repo, file_path: str) -> bool:
|
|
51
53
|
return True # Conservatively treat errors as binary to avoid issues
|
52
54
|
|
53
55
|
|
56
|
+
def commit_in_branch(repo: Repo, commit: Commit, target_branch: str) -> bool:
|
57
|
+
try:
|
58
|
+
# exit code 0 if commit is ancestor of branch
|
59
|
+
repo.git.merge_base('--is-ancestor', commit.hexsha, target_branch)
|
60
|
+
return True
|
61
|
+
except GitCommandError:
|
62
|
+
pass
|
63
|
+
return False
|
64
|
+
|
65
|
+
|
54
66
|
def get_diff(
|
55
67
|
repo: Repo = None,
|
56
68
|
what: str = None,
|
@@ -76,12 +88,56 @@ def get_diff(
|
|
76
88
|
else:
|
77
89
|
current_ref = what
|
78
90
|
merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
91
|
+
|
92
|
+
# if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
|
93
|
+
if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
|
94
|
+
# @todo: check case: reviewing working copy index in main branch #103
|
95
|
+
logging.info(
|
96
|
+
f"Branch is already merged. ({ui.green(current_ref)} vs {ui.yellow(against)})"
|
97
|
+
)
|
98
|
+
merge_sha = repo.git.log(
|
99
|
+
'--merges',
|
100
|
+
'--ancestry-path',
|
101
|
+
f'{current_ref}..{against}',
|
102
|
+
'-n',
|
103
|
+
'1',
|
104
|
+
'--pretty=format:%H'
|
105
|
+
).strip()
|
106
|
+
if merge_sha:
|
107
|
+
merge_commit = repo.commit(merge_sha)
|
108
|
+
|
109
|
+
other_merge_parent = None
|
110
|
+
for parent in merge_commit.parents:
|
111
|
+
if parent.hexsha == merge_base.hexsha:
|
112
|
+
continue
|
113
|
+
if not commit_in_branch(repo, parent, against):
|
114
|
+
logging.warning(f"merge parent is not in {against}, skipping")
|
115
|
+
continue
|
116
|
+
other_merge_parent = parent
|
117
|
+
break
|
118
|
+
if other_merge_parent:
|
119
|
+
first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
|
120
|
+
logging.info(
|
121
|
+
f"{what} will be compared to "
|
122
|
+
f"first common ancestor of {what} and {against}: "
|
123
|
+
f"{ui.cyan(first_common_ancestor.hexsha[:8])}"
|
124
|
+
)
|
125
|
+
against = first_common_ancestor.hexsha
|
126
|
+
else:
|
127
|
+
logging.error(f"Can't find other merge parent for {merge_sha}")
|
128
|
+
else:
|
129
|
+
logging.error(
|
130
|
+
f"No merge‐commit found for {current_ref!r}→{against!r}; "
|
131
|
+
"falling back to merge‐base diff"
|
132
|
+
)
|
133
|
+
else:
|
134
|
+
# normal case: branch not yet merged
|
135
|
+
against = merge_base.hexsha
|
136
|
+
logging.info(
|
137
|
+
f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
|
138
|
+
)
|
83
139
|
logging.info(
|
84
|
-
f"Making diff: {
|
140
|
+
f"Making diff: {ui.green(what or 'INDEX')} vs {ui.yellow(against)}"
|
85
141
|
)
|
86
142
|
diff_content = repo.git.diff(against, what)
|
87
143
|
diff = PatchSet.from_string(diff_content)
|
@@ -197,6 +253,41 @@ def _prepare(
|
|
197
253
|
return repo, cfg, diff, lines
|
198
254
|
|
199
255
|
|
256
|
+
def get_affected_code_block(repo: Repo, file: str, start_line: int, end_line: int) -> str | None:
|
257
|
+
if not start_line or not end_line:
|
258
|
+
return None
|
259
|
+
try:
|
260
|
+
if isinstance(start_line, str):
|
261
|
+
start_line = int(start_line)
|
262
|
+
if isinstance(end_line, str):
|
263
|
+
end_line = int(end_line)
|
264
|
+
lines = file_lines(repo, file, max_tokens=None, use_local_files=True)
|
265
|
+
if lines:
|
266
|
+
lines = [""] + lines.splitlines()
|
267
|
+
return "\n".join(
|
268
|
+
lines[start_line: end_line + 1]
|
269
|
+
)
|
270
|
+
except Exception as e:
|
271
|
+
logging.error(
|
272
|
+
f"Error getting affected code block for {file} from {start_line} to {end_line}: {e}"
|
273
|
+
)
|
274
|
+
return None
|
275
|
+
|
276
|
+
|
277
|
+
def provide_affected_code_blocks(issues: dict, repo: Repo):
|
278
|
+
for file, file_issues in issues.items():
|
279
|
+
for issue in file_issues:
|
280
|
+
for i in issue.get("affected_lines", []):
|
281
|
+
file_name = i.get("file", issue.get("file", file))
|
282
|
+
if block := get_affected_code_block(
|
283
|
+
repo,
|
284
|
+
file_name,
|
285
|
+
i.get("start_line"),
|
286
|
+
i.get("end_line")
|
287
|
+
):
|
288
|
+
i["affected_code"] = block
|
289
|
+
|
290
|
+
|
200
291
|
async def review(
|
201
292
|
repo: Repo = None,
|
202
293
|
what: str = None,
|
@@ -226,14 +317,7 @@ async def review(
|
|
226
317
|
parse_json=True,
|
227
318
|
)
|
228
319
|
issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
|
229
|
-
|
230
|
-
for issue in file_issues:
|
231
|
-
for i in issue.get("affected_lines", []):
|
232
|
-
if lines[file]:
|
233
|
-
f_lines = [""] + lines[file].splitlines()
|
234
|
-
i["affected_code"] = "\n".join(
|
235
|
-
f_lines[i["start_line"]: i["end_line"] + 1]
|
236
|
-
)
|
320
|
+
provide_affected_code_blocks(issues, repo)
|
237
321
|
exec(cfg.post_process, {"mc": mc, **locals()})
|
238
322
|
out_folder = Path(out_folder or repo.working_tree_dir)
|
239
323
|
out_folder.mkdir(parents=True, exist_ok=True)
|
gito/gh_api.py
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
import os
|
1
2
|
import logging
|
2
3
|
|
3
4
|
import requests
|
5
|
+
from fastcore.basics import AttrDict # objects returned by ghapi
|
6
|
+
|
7
|
+
|
8
|
+
def resolve_gh_token(token_or_none):
|
9
|
+
return token_or_none or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
|
4
10
|
|
5
11
|
|
6
12
|
def post_gh_comment(
|
@@ -33,3 +39,47 @@ def post_gh_comment(
|
|
33
39
|
else:
|
34
40
|
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
35
41
|
return False
|
42
|
+
|
43
|
+
|
44
|
+
def hide_gh_comment(
|
45
|
+
comment: dict | str,
|
46
|
+
token: str = None,
|
47
|
+
reason: str = "OUTDATED"
|
48
|
+
) -> bool:
|
49
|
+
"""
|
50
|
+
Hide a GitHub comment using GraphQL API with specified reason.
|
51
|
+
Args:
|
52
|
+
comment (dict | str):
|
53
|
+
The comment to hide,
|
54
|
+
either as a object returned from ghapi or a string node ID.
|
55
|
+
note: comment.id is not the same as node_id.
|
56
|
+
token (str): GitHub personal access token with permissions to minimize comments.
|
57
|
+
reason (str): The reason for hiding the comment, e.g., "OUTDATED".
|
58
|
+
"""
|
59
|
+
comment_node_id = comment.node_id if isinstance(comment, AttrDict) else comment
|
60
|
+
token = resolve_gh_token(token)
|
61
|
+
mutation = """
|
62
|
+
mutation($commentId: ID!, $reason: ReportedContentClassifiers!) {
|
63
|
+
minimizeComment(input: {subjectId: $commentId, classifier: $reason}) {
|
64
|
+
minimizedComment { isMinimized }
|
65
|
+
}
|
66
|
+
}"""
|
67
|
+
|
68
|
+
response = requests.post(
|
69
|
+
"https://api.github.com/graphql",
|
70
|
+
headers={"Authorization": f"Bearer {token}"},
|
71
|
+
json={
|
72
|
+
"query": mutation,
|
73
|
+
"variables": {"commentId": comment_node_id, "reason": reason}
|
74
|
+
}
|
75
|
+
)
|
76
|
+
success = (
|
77
|
+
response.status_code == 200
|
78
|
+
and response.json().get("data", {}).get("minimizeComment") is not None
|
79
|
+
)
|
80
|
+
if not success:
|
81
|
+
logging.error(
|
82
|
+
f"Failed to hide comment {comment_node_id}: "
|
83
|
+
f"{response.status_code} {response.reason}\n{response.text}"
|
84
|
+
)
|
85
|
+
return success
|
gito/issue_trackers.py
CHANGED
@@ -15,9 +15,10 @@ class IssueTrackerIssue:
|
|
15
15
|
|
16
16
|
|
17
17
|
def extract_issue_key(branch_name: str, min_len=2, max_len=10) -> str | None:
|
18
|
-
|
18
|
+
boundary = r'\b|_|-|/|\\'
|
19
|
+
pattern = fr"(?:{boundary})([A-Z][A-Z0-9]{{{min_len - 1},{max_len - 1}}}-\d+)(?:{boundary})"
|
19
20
|
match = re.search(pattern, branch_name)
|
20
|
-
return match.group(
|
21
|
+
return match.group(1) if match else None
|
21
22
|
|
22
23
|
|
23
24
|
def get_branch(repo: git.Repo):
|
gito/project_config.py
CHANGED
@@ -33,6 +33,11 @@ class ProjectConfig:
|
|
33
33
|
when referenced in code review comments.
|
34
34
|
"""
|
35
35
|
pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
|
36
|
+
collapse_previous_code_review_comments: bool = field(default=True)
|
37
|
+
"""
|
38
|
+
If True, previously added code review comments in the pull request
|
39
|
+
will be collapsed automatically when a new comment is added.
|
40
|
+
"""
|
36
41
|
|
37
42
|
def __post_init__(self):
|
38
43
|
self.pipeline_steps = {
|
@@ -61,10 +66,16 @@ class ProjectConfig:
|
|
61
66
|
logging.info(
|
62
67
|
f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
|
63
68
|
default_prompt_vars = config["prompt_vars"]
|
69
|
+
default_pipeline_steps = config["pipeline_steps"]
|
64
70
|
with open(config_path, "rb") as f:
|
65
71
|
config.update(tomllib.load(f))
|
66
72
|
# overriding prompt_vars config section will not empty default values
|
67
73
|
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
74
|
+
# merge individual pipeline steps
|
75
|
+
for k, v in config["pipeline_steps"].items():
|
76
|
+
config["pipeline_steps"][k] = default_pipeline_steps.get(k, {}) | v
|
77
|
+
# merge pipeline steps dict
|
78
|
+
config["pipeline_steps"] = default_pipeline_steps | config["pipeline_steps"]
|
68
79
|
else:
|
69
80
|
logging.info(
|
70
81
|
f"No project config found at {ui.blue(config_path)}, using defaults"
|
gito/report_struct.py
CHANGED
@@ -10,7 +10,7 @@ from colorama import Fore, Style, Back
|
|
10
10
|
from microcore.utils import file_link
|
11
11
|
import textwrap
|
12
12
|
|
13
|
-
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
|
13
|
+
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
|
14
14
|
from .project_config import ProjectConfig
|
15
15
|
from .utils import syntax_hint, block_wrap_lr, max_line_len
|
16
16
|
|
@@ -125,6 +125,7 @@ class Report:
|
|
125
125
|
block_wrap_lr=block_wrap_lr,
|
126
126
|
max_line_len=max_line_len,
|
127
127
|
HTML_TEXT_ICON=HTML_TEXT_ICON,
|
128
|
+
HTML_CR_COMMENT_MARKER=HTML_CR_COMMENT_MARKER,
|
128
129
|
**config.prompt_vars
|
129
130
|
)
|
130
131
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
LLM_API_TYPE: {{ api_type }}
|
3
|
+
LLM_API_KEY: {{ "${{ secrets." + secret_name + "}}" }}
|
4
|
+
MODEL: {{ model }}
|
5
|
+
{% raw -%}
|
6
|
+
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
|
7
|
+
JIRA_URL: ${{ secrets.JIRA_URL }}
|
8
|
+
JIRA_USER: ${{ secrets.JIRA_USER }}
|
9
|
+
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
10
|
+
{%- endraw %}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
name: "Gito: AI Code Reviewer"
|
2
|
+
on:
|
3
|
+
pull_request:
|
4
|
+
types: [opened, synchronize, reopened]
|
5
|
+
workflow_dispatch:
|
6
|
+
inputs:
|
7
|
+
pr_number:
|
8
|
+
description: "Pull Request number"
|
9
|
+
required: true
|
10
|
+
jobs:
|
11
|
+
review:
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v4
|
16
|
+
with: { fetch-depth: 0 }
|
17
|
+
|
18
|
+
{%- include("github_workflows/components/installs.jinja2") %}
|
19
|
+
|
20
|
+
- name: Run AI code review
|
21
|
+
env:
|
22
|
+
{%- include("github_workflows/components/env-vars.jinja2") %}
|
23
|
+
PR_NUMBER_FROM_WORKFLOW_DISPATCH: {% raw %}${{ github.event.inputs.pr_number }}{% endraw %}
|
24
|
+
run: |
|
25
|
+
gito --verbose review
|
26
|
+
gito github-comment --token {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
|
27
|
+
|
28
|
+
- uses: actions/upload-artifact@v4
|
29
|
+
with:
|
30
|
+
name: gito-code-review-results
|
31
|
+
path: |
|
32
|
+
code-review-report.md
|
33
|
+
code-review-report.json
|
@@ -0,0 +1,70 @@
|
|
1
|
+
name: "Gito: React to GitHub comment"
|
2
|
+
|
3
|
+
on:
|
4
|
+
issue_comment:
|
5
|
+
types: [created]
|
6
|
+
|
7
|
+
permissions:
|
8
|
+
contents: write # to make PR
|
9
|
+
issues: write
|
10
|
+
pull-requests: write
|
11
|
+
# read: to download the code review artifact
|
12
|
+
# write: to trigger other actions
|
13
|
+
actions: write
|
14
|
+
|
15
|
+
jobs:
|
16
|
+
process-comment:
|
17
|
+
if: |
|
18
|
+
github.event.issue.pull_request &&
|
19
|
+
(
|
20
|
+
github.event.comment.author_association == 'OWNER' ||
|
21
|
+
github.event.comment.author_association == 'MEMBER' ||
|
22
|
+
github.event.comment.author_association == 'COLLABORATOR'
|
23
|
+
) &&
|
24
|
+
(
|
25
|
+
startsWith(github.event.comment.body, '/') ||
|
26
|
+
startsWith(github.event.comment.body, 'gito') ||
|
27
|
+
startsWith(github.event.comment.body, 'ai') ||
|
28
|
+
startsWith(github.event.comment.body, 'bot') ||
|
29
|
+
contains(github.event.comment.body, '@gito') ||
|
30
|
+
contains(github.event.comment.body, '@ai') ||
|
31
|
+
contains(github.event.comment.body, '@bot')
|
32
|
+
)
|
33
|
+
runs-on: ubuntu-latest
|
34
|
+
|
35
|
+
steps:
|
36
|
+
- name: Get PR details
|
37
|
+
id: pr
|
38
|
+
uses: actions/github-script@v7
|
39
|
+
with:
|
40
|
+
script: |
|
41
|
+
const pr = await github.rest.pulls.get({
|
42
|
+
owner: context.repo.owner,
|
43
|
+
repo: context.repo.repo,
|
44
|
+
pull_number: context.issue.number
|
45
|
+
});
|
46
|
+
return {
|
47
|
+
head_ref: pr.data.head.ref,
|
48
|
+
head_sha: pr.data.head.sha,
|
49
|
+
base_ref: pr.data.base.ref
|
50
|
+
};
|
51
|
+
|
52
|
+
- name: Checkout repository
|
53
|
+
uses: actions/checkout@v4
|
54
|
+
with:
|
55
|
+
{% raw -%}
|
56
|
+
repository: ${{ github.repository }}
|
57
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
58
|
+
ref: ${{ fromJson(steps.pr.outputs.result).head_ref }}
|
59
|
+
fetch-depth: 0
|
60
|
+
{%- endraw %}
|
61
|
+
|
62
|
+
{%- include("github_workflows/components/installs.jinja2") %}
|
63
|
+
|
64
|
+
- name: Run Gito react
|
65
|
+
env:
|
66
|
+
# LLM config is needed only if answer_github_comments = true in .gito/config.toml
|
67
|
+
# Otherwise, use LLM_API_TYPE: none
|
68
|
+
{%- include("github_workflows/components/env-vars.jinja2") %}
|
69
|
+
run: |
|
70
|
+
{% raw %}gito react-to-comment ${{ github.event.comment.id }} --token ${{ secrets.GITHUB_TOKEN }}{%- endraw %}
|
gito/utils.py
CHANGED
@@ -2,6 +2,7 @@ import re
|
|
2
2
|
import sys
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
|
+
import importlib.metadata
|
5
6
|
|
6
7
|
import typer
|
7
8
|
import git
|
@@ -224,3 +225,7 @@ def detect_github_env() -> dict:
|
|
224
225
|
|
225
226
|
def stream_to_cli(text):
|
226
227
|
print(ui.blue(text), end='')
|
228
|
+
|
229
|
+
|
230
|
+
def version() -> str:
|
231
|
+
return importlib.metadata.version("gito.bot")
|
ai_cr-2.0.2.dist-info/RECORD
DELETED
@@ -1,26 +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=ceeoSCD8RX4xKxWri_n0xx00aTFS6xPUHhAs3rKVP5o,2337
|
4
|
-
gito/cli.py,sha256=qFY1ObA0-Ep19bNSEcwjn6KIu2OOSWDj6QGe_o7Ujoc,6555
|
5
|
-
gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
|
6
|
-
gito/commands/fix.py,sha256=OS829xVrLMLQqlol9AoxJMZ9qaAWRCSbUkVNszK96ws,5305
|
7
|
-
gito/commands/gh_post_review_comment.py,sha256=oZh3kSRBx3cSMozI2llDEOVZ2WeT_5msXMLLCiLina4,1984
|
8
|
-
gito/commands/gh_react_to_comment.py,sha256=4uUb39FnM45DO-wESQq6pUlO5AFvh0FSluCCTxkl8_g,6442
|
9
|
-
gito/commands/repl.py,sha256=wDHWl2VyRK32hLZHImrhqpQNkWV3u3358fwDiA6D3iA,521
|
10
|
-
gito/config.toml,sha256=iK0CeP3mynTnWIEsCi_jA9Gtb_UdAqwLC5nWHSTc-zc,17367
|
11
|
-
gito/constants.py,sha256=9G4sZbqLBg13FYieJqkq_hzek4-smmL4KKYF64tovQQ,698
|
12
|
-
gito/core.py,sha256=tp-eB_FIsZ2wffTiMrRLc_9MaFaGIky_ExWHWTf1DTk,9341
|
13
|
-
gito/gh_api.py,sha256=syhM8sbs_lLNG53bfRHQpusO818nwXCl4jDxVY__IK0,1204
|
14
|
-
gito/issue_trackers.py,sha256=nfib6zhvmL_zjRDCdZ0d6rMT4ZFJ3PxO5UORaJzb6gk,1495
|
15
|
-
gito/pipeline.py,sha256=Nq5VUVrXVDXVVYX6nVSMX1CyhoWsNtSeTOQyNgQAsdE,2672
|
16
|
-
gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
gito/pipeline_steps/jira.py,sha256=cgRSHoU6j0lcK6hQfhg950PEmIu3uWLGMKZbW3-H3mg,1683
|
18
|
-
gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
|
19
|
-
gito/project_config.py,sha256=XdpXsRDFWBNNpktbZDp9p-nbijyKjG_oD6UWzGZSLjM,2580
|
20
|
-
gito/report_struct.py,sha256=tLhdmCPse3Jbo56y752vuGlndWY2f2g_mNFQ-BmJZGw,4160
|
21
|
-
gito/utils.py,sha256=HpterYtjceVhklJe0w1n7PIQJk8MJFUYi4FiG-CzMqg,6418
|
22
|
-
ai_cr-2.0.2.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
|
23
|
-
ai_cr-2.0.2.dist-info/METADATA,sha256=K8YJPk8Mv3GwYKbGe3gpE4qJgoWfv2wQBemoXOe-awc,7989
|
24
|
-
ai_cr-2.0.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
25
|
-
ai_cr-2.0.2.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
|
26
|
-
ai_cr-2.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|