ai-cr 2.0.0.dev2__py3-none-any.whl → 2.0.2__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.0.dev2.dist-info → ai_cr-2.0.2.dist-info}/LICENSE +21 -21
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.2.dist-info}/METADATA +13 -5
- ai_cr-2.0.2.dist-info/RECORD +26 -0
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.2.dist-info}/WHEEL +1 -1
- gito/__main__.py +4 -4
- gito/bootstrap.py +66 -66
- gito/cli.py +217 -255
- gito/commands/__init__.py +1 -1
- gito/commands/fix.py +157 -157
- gito/commands/gh_post_review_comment.py +63 -0
- gito/commands/{gh_comment.py → gh_react_to_comment.py} +194 -157
- gito/commands/repl.py +5 -2
- gito/config.toml +453 -415
- gito/constants.py +12 -9
- gito/core.py +288 -239
- gito/gh_api.py +35 -0
- gito/issue_trackers.py +49 -15
- gito/pipeline.py +82 -70
- gito/pipeline_steps/jira.py +57 -83
- gito/pipeline_steps/linear.py +85 -0
- gito/project_config.py +73 -71
- gito/report_struct.py +134 -133
- gito/utils.py +226 -214
- ai_cr-2.0.0.dev2.dist-info/RECORD +0 -23
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.2.dist-info}/entry_points.txt +0 -0
@@ -1,157 +1,194 @@
|
|
1
|
-
"""
|
2
|
-
Fix issues from code review report
|
3
|
-
"""
|
4
|
-
|
5
|
-
import logging
|
6
|
-
import os
|
7
|
-
import re
|
8
|
-
from pathlib import Path
|
9
|
-
from typing import Optional
|
10
|
-
import zipfile
|
11
|
-
|
12
|
-
import requests
|
13
|
-
import typer
|
14
|
-
from fastcore.basics import AttrDict
|
15
|
-
from
|
16
|
-
from
|
17
|
-
|
18
|
-
|
19
|
-
import
|
20
|
-
|
21
|
-
from ..
|
22
|
-
from ..
|
23
|
-
from
|
24
|
-
from ..utils import
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
"
|
33
|
-
"--token",
|
34
|
-
"
|
35
|
-
"
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
ui.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
1
|
+
"""
|
2
|
+
Fix issues from code review report
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import re
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Optional
|
10
|
+
import zipfile
|
11
|
+
|
12
|
+
import requests
|
13
|
+
import typer
|
14
|
+
from fastcore.basics import AttrDict
|
15
|
+
from microcore import ui
|
16
|
+
from ghapi.all import GhApi
|
17
|
+
import git
|
18
|
+
|
19
|
+
from ..bootstrap import app
|
20
|
+
from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
|
21
|
+
from ..core import answer
|
22
|
+
from ..gh_api import post_gh_comment
|
23
|
+
from ..project_config import ProjectConfig
|
24
|
+
from ..utils import extract_gh_owner_repo
|
25
|
+
from .fix import fix
|
26
|
+
|
27
|
+
|
28
|
+
@app.command(hidden=True)
|
29
|
+
def react_to_comment(
|
30
|
+
comment_id: int = typer.Argument(),
|
31
|
+
gh_token: str = typer.Option(
|
32
|
+
"",
|
33
|
+
"--gh-token",
|
34
|
+
"--token",
|
35
|
+
"-t",
|
36
|
+
"--github-token",
|
37
|
+
help="GitHub token for authentication",
|
38
|
+
),
|
39
|
+
dry_run: bool = typer.Option(
|
40
|
+
False, "--dry-run", "-d", help="Only print changes without applying them"
|
41
|
+
),
|
42
|
+
):
|
43
|
+
"""
|
44
|
+
Handles direct agent instructions from pull request comments.
|
45
|
+
|
46
|
+
Note: Not for local usage. Designed for execution within GitHub Actions workflows.
|
47
|
+
|
48
|
+
Fetches the PR comment by ID, parses agent directives, and executes the requested
|
49
|
+
actions automatically to enable seamless code review workflow integration.
|
50
|
+
"""
|
51
|
+
repo = git.Repo(".") # Current directory
|
52
|
+
owner, repo_name = extract_gh_owner_repo(repo)
|
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
|
+
)
|
57
|
+
api = GhApi(owner=owner, repo=repo_name, token=gh_token)
|
58
|
+
comment = api.issues.get_comment(comment_id=comment_id)
|
59
|
+
logging.info(
|
60
|
+
f"Comment by {ui.yellow('@' + comment.user.login)}: "
|
61
|
+
f"{ui.green(comment.body)}\n"
|
62
|
+
f"url: {comment.html_url}"
|
63
|
+
)
|
64
|
+
|
65
|
+
cfg = ProjectConfig.load_for_repo(repo)
|
66
|
+
if not any(
|
67
|
+
trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
|
68
|
+
):
|
69
|
+
ui.error("No mention trigger found in comment, no reaction added.")
|
70
|
+
return
|
71
|
+
try:
|
72
|
+
logging.info("Comment contains mention trigger, reacting with 'eyes'.")
|
73
|
+
api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
|
74
|
+
except Exception as e:
|
75
|
+
logging.error("Error reacting to comment with emoji: %s", str(e))
|
76
|
+
pr = int(comment.issue_url.split("/")[-1])
|
77
|
+
print(f"Processing comment for PR #{pr}...")
|
78
|
+
|
79
|
+
issue_ids = extract_fix_args(comment.body)
|
80
|
+
if issue_ids:
|
81
|
+
logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
|
82
|
+
out_folder = "artifact"
|
83
|
+
download_latest_code_review_artifact(
|
84
|
+
api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
|
85
|
+
)
|
86
|
+
fix(
|
87
|
+
issue_ids[0], # @todo: support multiple IDs
|
88
|
+
report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
|
89
|
+
dry_run=dry_run,
|
90
|
+
commit=not dry_run,
|
91
|
+
push=not dry_run,
|
92
|
+
)
|
93
|
+
logging.info("Fix applied successfully.")
|
94
|
+
elif is_review_request(comment.body):
|
95
|
+
ref = repo.active_branch.name
|
96
|
+
logging.info(f"Triggering code-review workflow, ref='{ref}'")
|
97
|
+
api.actions.create_workflow_dispatch(
|
98
|
+
workflow_id="gito-code-review.yml",
|
99
|
+
ref=ref,
|
100
|
+
inputs={"pr_number": str(pr)},
|
101
|
+
)
|
102
|
+
else:
|
103
|
+
if cfg.answer_github_comments:
|
104
|
+
response = answer(comment.body, repo=repo)
|
105
|
+
post_gh_comment(
|
106
|
+
gh_repository=f"{owner}/{repo_name}",
|
107
|
+
pr_or_issue_number=pr,
|
108
|
+
gh_token=gh_token,
|
109
|
+
text=HTML_TEXT_ICON+response,
|
110
|
+
)
|
111
|
+
else:
|
112
|
+
ui.error("Can't identify target command in the text.")
|
113
|
+
return
|
114
|
+
|
115
|
+
|
116
|
+
def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
|
117
|
+
pr = api.pulls.get(pr_number)
|
118
|
+
sha = pr["head"]["sha"] # noqa
|
119
|
+
branch = pr["head"]["ref"]
|
120
|
+
|
121
|
+
runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
|
122
|
+
# Find the run for this SHA
|
123
|
+
run = next(
|
124
|
+
(
|
125
|
+
r
|
126
|
+
for r in runs # r['head_sha'] == sha and
|
127
|
+
if (
|
128
|
+
any(
|
129
|
+
marker in r["path"].lower()
|
130
|
+
for marker in ["code-review", "code_review", "cr"]
|
131
|
+
)
|
132
|
+
or "gito.yml" in r["name"].lower()
|
133
|
+
)
|
134
|
+
and r["status"] == "completed"
|
135
|
+
),
|
136
|
+
None,
|
137
|
+
)
|
138
|
+
return run
|
139
|
+
|
140
|
+
|
141
|
+
def download_latest_code_review_artifact(
|
142
|
+
api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
|
143
|
+
) -> tuple[str, dict] | None:
|
144
|
+
run = last_code_review_run(api, pr_number)
|
145
|
+
if not run:
|
146
|
+
raise Exception("No workflow run found for this PR/SHA")
|
147
|
+
|
148
|
+
artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
|
149
|
+
if not artifacts:
|
150
|
+
raise Exception("No artifacts found for this workflow run")
|
151
|
+
|
152
|
+
latest_artifact = artifacts[0]
|
153
|
+
url = latest_artifact["archive_download_url"]
|
154
|
+
print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
|
155
|
+
headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
|
156
|
+
zip_path = "artifact.zip"
|
157
|
+
try:
|
158
|
+
with requests.get(url, headers=headers, stream=True) as r:
|
159
|
+
r.raise_for_status()
|
160
|
+
with open(zip_path, "wb") as f:
|
161
|
+
for chunk in r.iter_content(chunk_size=8192):
|
162
|
+
f.write(chunk)
|
163
|
+
|
164
|
+
# Unpack to ./artifact
|
165
|
+
os.makedirs("artifact", exist_ok=True)
|
166
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
167
|
+
zip_ref.extractall("artifact")
|
168
|
+
finally:
|
169
|
+
if os.path.exists(zip_path):
|
170
|
+
os.remove(zip_path)
|
171
|
+
|
172
|
+
print("Artifact unpacked to ./artifact")
|
173
|
+
|
174
|
+
|
175
|
+
def extract_fix_args(text: str) -> list[int]:
|
176
|
+
pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
|
177
|
+
match = re.search(pattern1, text)
|
178
|
+
if match:
|
179
|
+
numbers_str = match.group(1)
|
180
|
+
numbers = re.findall(r"\d+", numbers_str)
|
181
|
+
issue_numbers = [int(num) for num in numbers]
|
182
|
+
return issue_numbers
|
183
|
+
return []
|
184
|
+
|
185
|
+
|
186
|
+
def is_review_request(text: str) -> bool:
|
187
|
+
text = text.lower().strip()
|
188
|
+
trigger_words = ['review', 'run', 'code-review']
|
189
|
+
if any(f"/{word}" in text for word in trigger_words):
|
190
|
+
return True
|
191
|
+
parts = text.split()
|
192
|
+
if len(parts) == 2 and parts[1] in trigger_words:
|
193
|
+
return True
|
194
|
+
return False
|
gito/commands/repl.py
CHANGED
@@ -4,8 +4,7 @@ Python REPL
|
|
4
4
|
# flake8: noqa: F401
|
5
5
|
import code
|
6
6
|
|
7
|
-
|
8
|
-
# Imports for usage in REPL
|
7
|
+
# Wildcard imports are preferred to capture most of functionality for usage in REPL
|
9
8
|
import os
|
10
9
|
import sys
|
11
10
|
from dataclasses import dataclass
|
@@ -18,6 +17,10 @@ import microcore as mc
|
|
18
17
|
from microcore import ui
|
19
18
|
|
20
19
|
from ..cli import app
|
20
|
+
from ..constants import *
|
21
|
+
from ..core import *
|
22
|
+
from ..utils import *
|
23
|
+
|
21
24
|
|
22
25
|
@app.command(help="python REPL")
|
23
26
|
def repl():
|