ai-cr 0.5.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_code_review/.ai-code-review.toml +11 -6
- ai_code_review/__main__.py +4 -4
- ai_code_review/bootstrap.py +1 -1
- ai_code_review/cli.py +138 -57
- ai_code_review/commands/repl.py +22 -0
- ai_code_review/core.py +83 -17
- ai_code_review/utils.py +3 -3
- {ai_cr-0.5.0.dist-info → ai_cr-1.0.0.dist-info}/METADATA +52 -18
- ai_cr-1.0.0.dist-info/RECORD +16 -0
- ai_cr-0.5.0.dist-info/RECORD +0 -15
- {ai_cr-0.5.0.dist-info → ai_cr-1.0.0.dist-info}/LICENSE +0 -0
- {ai_cr-0.5.0.dist-info → ai_cr-1.0.0.dist-info}/WHEEL +0 -0
- {ai_cr-0.5.0.dist-info → ai_cr-1.0.0.dist-info}/entry_points.txt +0 -0
@@ -38,21 +38,23 @@ prompt = """
|
|
38
38
|
----TASK----
|
39
39
|
Review the provided code diff carefully and identify *only* highly confident issues which are relevant to any code context.
|
40
40
|
|
41
|
-
----
|
41
|
+
----CODEBASE CHANGES TO REVIEW----
|
42
42
|
{{ input }}
|
43
43
|
--------
|
44
44
|
|
45
45
|
{% if file_lines -%}
|
46
|
-
----ADDITIONAL CONTEXT----
|
46
|
+
----ADDITIONAL CONTEXT: FULL FILE CONTENT AFTER APPLYING REVIEWED CHANGES----
|
47
47
|
{{ file_lines }}
|
48
48
|
{%- endif %}
|
49
49
|
|
50
50
|
----TASK GUIDELINES----
|
51
51
|
- Only report issues you are **100% confident** are relevant to any context.
|
52
|
+
- Never report issues about using software versions that have not yet been released.
|
52
53
|
- Only include issues that are **significantly valuable** to the maintainers (e.g., bugs, security flaws, or clear maintainability concerns).
|
53
54
|
- Do **not** report vague, theoretical, or overly generic advice.
|
54
55
|
- Do **not** report anything with medium or lower confidence.
|
55
|
-
- Typographical errors have highest severity
|
56
|
+
- Typographical errors have highest severity.
|
57
|
+
{{ requirements -}}
|
56
58
|
{{ json_requirements }}
|
57
59
|
|
58
60
|
Respond with a valid JSON array of issues in the following format:
|
@@ -124,8 +126,9 @@ If code changes contains exceptional achievements, you may additionally present
|
|
124
126
|
--Available Awards--
|
125
127
|
{{ awards }}
|
126
128
|
---
|
127
|
-
Your response will be parsed programmatically, so do not include any additional text.
|
128
|
-
Use Markdown formatting in your response.
|
129
|
+
- Your response will be parsed programmatically, so do not include any additional text.
|
130
|
+
- Use Markdown formatting in your response.
|
131
|
+
{{ summary_requirements -}}
|
129
132
|
"""
|
130
133
|
|
131
134
|
[prompt_vars]
|
@@ -303,4 +306,6 @@ restored lost knowledge and now bestow it upon a new generation."
|
|
303
306
|
decorators add depth and texture, and observer masterfully completes the composition.
|
304
307
|
The Gang of Four gives a standing ovation from the stalls."
|
305
308
|
```
|
306
|
-
"""
|
309
|
+
"""
|
310
|
+
requirements = ""
|
311
|
+
summary_requirements = ""
|
ai_code_review/__main__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from .cli import main
|
2
|
-
|
3
|
-
if __name__ == "__main__":
|
4
|
-
main()
|
1
|
+
from .cli import main
|
2
|
+
|
3
|
+
if __name__ == "__main__":
|
4
|
+
main()
|
ai_code_review/bootstrap.py
CHANGED
ai_code_review/cli.py
CHANGED
@@ -2,64 +2,44 @@ import asyncio
|
|
2
2
|
import logging
|
3
3
|
import sys
|
4
4
|
import os
|
5
|
-
import
|
5
|
+
import textwrap
|
6
|
+
import tempfile
|
6
7
|
import requests
|
7
8
|
|
8
9
|
import microcore as mc
|
9
|
-
import async_typer
|
10
10
|
import typer
|
11
|
-
from ai_code_review.utils import parse_refs_pair
|
12
11
|
from git import Repo
|
13
12
|
|
14
|
-
from .core import review
|
13
|
+
from .core import review, get_diff, filter_diff
|
15
14
|
from .report_struct import Report
|
16
15
|
from .constants import ENV_CONFIG_FILE
|
17
16
|
from .bootstrap import bootstrap
|
18
17
|
from .project_config import ProjectConfig
|
19
|
-
from .utils import
|
18
|
+
from .utils import no_subcommand, parse_refs_pair
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
default_command_app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
|
24
|
-
if sys.platform == "win32":
|
25
|
-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
20
|
+
app = typer.Typer(pretty_exceptions_show_locals=False)
|
21
|
+
app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
|
26
22
|
|
27
23
|
|
28
24
|
def main():
|
29
|
-
if
|
30
|
-
|
31
|
-
|
25
|
+
if sys.platform == "win32":
|
26
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
27
|
+
if no_subcommand(app):
|
32
28
|
bootstrap()
|
33
|
-
|
29
|
+
app_no_subcommand()
|
30
|
+
else:
|
31
|
+
app()
|
34
32
|
|
35
33
|
|
36
34
|
@app.callback(invoke_without_command=True)
|
37
|
-
def cli(ctx: typer.Context):
|
35
|
+
def cli(ctx: typer.Context, verbose: bool = typer.Option(default=False)):
|
36
|
+
if verbose:
|
37
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
|
38
38
|
if ctx.invoked_subcommand != "setup":
|
39
39
|
bootstrap()
|
40
40
|
|
41
41
|
|
42
|
-
|
43
|
-
@app.async_command(name="review", help="Perform code review")
|
44
|
-
async def cmd_review(
|
45
|
-
refs: str = typer.Argument(
|
46
|
-
default=None,
|
47
|
-
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
48
|
-
),
|
49
|
-
what: str = typer.Option(None, "--what", "-w", help="Git ref to review"),
|
50
|
-
against: str = typer.Option(
|
51
|
-
None,
|
52
|
-
"--against", "-vs", "--vs",
|
53
|
-
help="Git ref to compare against"
|
54
|
-
),
|
55
|
-
filters: str = typer.Option(
|
56
|
-
"", "--filter", "-f", "--filters",
|
57
|
-
help="""
|
58
|
-
filter reviewed files by glob / fnmatch pattern(s),
|
59
|
-
e.g. 'src/**/*.py', may be comma-separated
|
60
|
-
""",
|
61
|
-
)
|
62
|
-
):
|
42
|
+
def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
|
63
43
|
_what, _against = parse_refs_pair(refs)
|
64
44
|
if _what:
|
65
45
|
if what:
|
@@ -75,34 +55,103 @@ async def cmd_review(
|
|
75
55
|
)
|
76
56
|
else:
|
77
57
|
_against = against
|
78
|
-
|
58
|
+
return _what, _against
|
79
59
|
|
80
60
|
|
81
|
-
|
82
|
-
|
83
|
-
|
61
|
+
def arg_refs() -> typer.Argument:
|
62
|
+
return typer.Argument(
|
63
|
+
default=None,
|
64
|
+
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
65
|
+
)
|
84
66
|
|
85
67
|
|
86
|
-
|
87
|
-
|
88
|
-
print(Report.load().render(format=format))
|
68
|
+
def arg_what() -> typer.Option:
|
69
|
+
return typer.Option(None, "--what", "-w", help="Git ref to review")
|
89
70
|
|
90
71
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
await review()
|
100
|
-
finally:
|
101
|
-
os.chdir(prev_dir)
|
72
|
+
def arg_filters() -> typer.Option:
|
73
|
+
return typer.Option(
|
74
|
+
"", "--filter", "-f", "--filters",
|
75
|
+
help="""
|
76
|
+
filter reviewed files by glob / fnmatch pattern(s),
|
77
|
+
e.g. 'src/**/*.py', may be comma-separated
|
78
|
+
""",
|
79
|
+
)
|
102
80
|
|
103
81
|
|
104
|
-
|
105
|
-
|
82
|
+
def arg_out() -> typer.Option:
|
83
|
+
return typer.Option(
|
84
|
+
None,
|
85
|
+
"--out", "-o", "--output",
|
86
|
+
help="Output folder for the code review report"
|
87
|
+
)
|
88
|
+
|
89
|
+
|
90
|
+
def arg_against() -> typer.Option:
|
91
|
+
return typer.Option(
|
92
|
+
None,
|
93
|
+
"--against", "-vs", "--vs",
|
94
|
+
help="Git ref to compare against"
|
95
|
+
)
|
96
|
+
|
97
|
+
|
98
|
+
@app_no_subcommand.command(name="review", help="Perform code review")
|
99
|
+
@app.command(name="review", help="Perform code review")
|
100
|
+
def cmd_review(
|
101
|
+
refs: str = arg_refs(),
|
102
|
+
what: str = arg_what(),
|
103
|
+
against: str = arg_against(),
|
104
|
+
filters: str = arg_filters(),
|
105
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
106
|
+
out: str = arg_out()
|
107
|
+
):
|
108
|
+
_what, _against = args_to_target(refs, what, against)
|
109
|
+
asyncio.run(review(
|
110
|
+
what=_what,
|
111
|
+
against=_against,
|
112
|
+
filters=filters,
|
113
|
+
use_merge_base=merge_base,
|
114
|
+
out_folder=out,
|
115
|
+
))
|
116
|
+
|
117
|
+
|
118
|
+
@app.command(help="Configure LLM for local usage interactively")
|
119
|
+
def setup():
|
120
|
+
mc.interactive_setup(ENV_CONFIG_FILE)
|
121
|
+
|
122
|
+
|
123
|
+
@app.command()
|
124
|
+
def render(format: str = Report.Format.MARKDOWN):
|
125
|
+
print(Report.load().render(format=format))
|
126
|
+
|
127
|
+
|
128
|
+
@app.command(help="Review remote code")
|
129
|
+
def remote(
|
130
|
+
url: str = typer.Argument(..., help="Git repository URL"),
|
131
|
+
refs: str = arg_refs(),
|
132
|
+
what: str = arg_what(),
|
133
|
+
against: str = arg_against(),
|
134
|
+
filters: str = arg_filters(),
|
135
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
136
|
+
out: str = arg_out()
|
137
|
+
):
|
138
|
+
_what, _against = args_to_target(refs, what, against)
|
139
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
140
|
+
logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
|
141
|
+
repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
|
142
|
+
asyncio.run(review(
|
143
|
+
repo=repo,
|
144
|
+
what=_what,
|
145
|
+
against=_against,
|
146
|
+
filters=filters,
|
147
|
+
use_merge_base=merge_base,
|
148
|
+
out_folder=out or '.',
|
149
|
+
))
|
150
|
+
repo.close()
|
151
|
+
|
152
|
+
|
153
|
+
@app.command(help="Leave a GitHub PR comment with the review.")
|
154
|
+
def github_comment(
|
106
155
|
token: str = typer.Option(
|
107
156
|
os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
|
108
157
|
),
|
@@ -110,7 +159,7 @@ async def github_comment(
|
|
110
159
|
"""
|
111
160
|
Leaves a comment with the review on the current GitHub pull request.
|
112
161
|
"""
|
113
|
-
file = "code-review-report.
|
162
|
+
file = "code-review-report.md"
|
114
163
|
if not os.path.exists(file):
|
115
164
|
print(f"Review file not found: {file}")
|
116
165
|
raise typer.Exit(4)
|
@@ -155,3 +204,35 @@ async def github_comment(
|
|
155
204
|
else:
|
156
205
|
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
157
206
|
raise typer.Exit(5)
|
207
|
+
|
208
|
+
|
209
|
+
@app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
|
210
|
+
def files(
|
211
|
+
refs: str = arg_refs(),
|
212
|
+
what: str = arg_what(),
|
213
|
+
against: str = arg_against(),
|
214
|
+
filters: str = arg_filters(),
|
215
|
+
merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
|
216
|
+
diff: bool = typer.Option(default=False, help="Show diff content")
|
217
|
+
):
|
218
|
+
_what, _against = args_to_target(refs, what, against)
|
219
|
+
repo = Repo(".")
|
220
|
+
patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
|
221
|
+
patch_set = filter_diff(patch_set, filters)
|
222
|
+
print(
|
223
|
+
f"Changed files: "
|
224
|
+
f"{mc.ui.green(_what or 'INDEX')} vs "
|
225
|
+
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
226
|
+
f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
|
227
|
+
)
|
228
|
+
|
229
|
+
for patch in patch_set:
|
230
|
+
if patch.is_added_file:
|
231
|
+
color = mc.ui.green
|
232
|
+
elif patch.is_removed_file:
|
233
|
+
color = mc.ui.red
|
234
|
+
else:
|
235
|
+
color = mc.ui.blue
|
236
|
+
print(f"- {color(patch.path)}")
|
237
|
+
if diff:
|
238
|
+
print(mc.ui.gray(textwrap.indent(str(patch), " ")))
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Python REPL
|
3
|
+
"""
|
4
|
+
# flake8: noqa: F401
|
5
|
+
import code
|
6
|
+
from ema.cli import app
|
7
|
+
|
8
|
+
# Imports for usage in REPL
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from datetime import datetime
|
13
|
+
from enum import Enum
|
14
|
+
from time import time
|
15
|
+
from rich.pretty import pprint
|
16
|
+
|
17
|
+
import microcore as mc
|
18
|
+
from microcore import ui
|
19
|
+
|
20
|
+
@app.command(help="python REPL")
|
21
|
+
def repl():
|
22
|
+
code.interact(local=globals())
|
ai_code_review/core.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import fnmatch
|
2
2
|
import logging
|
3
|
+
from os import PathLike
|
3
4
|
from typing import Iterable
|
5
|
+
from pathlib import Path
|
4
6
|
|
5
7
|
import microcore as mc
|
6
8
|
from git import Repo
|
@@ -9,22 +11,76 @@ from unidiff.constants import DEV_NULL
|
|
9
11
|
|
10
12
|
from .project_config import ProjectConfig
|
11
13
|
from .report_struct import Report
|
14
|
+
from .constants import JSON_REPORT_FILE_NAME
|
15
|
+
|
16
|
+
|
17
|
+
def is_binary_file(repo: Repo, file_path: str) -> bool:
|
18
|
+
"""
|
19
|
+
Check if a file is binary by attempting to read it as text.
|
20
|
+
Returns True if the file is binary, False otherwise.
|
21
|
+
"""
|
22
|
+
try:
|
23
|
+
# Attempt to read the file content from the repository tree
|
24
|
+
content = repo.tree()[file_path].data_stream.read()
|
25
|
+
# Try decoding as UTF-8; if it fails, it's likely binary
|
26
|
+
content.decode("utf-8")
|
27
|
+
return False
|
28
|
+
except (UnicodeDecodeError, KeyError):
|
29
|
+
return True
|
30
|
+
except Exception as e:
|
31
|
+
logging.warning(f"Error checking if file {file_path} is binary: {e}")
|
32
|
+
return True # Conservatively treat errors as binary to avoid issues
|
12
33
|
|
13
34
|
|
14
35
|
def get_diff(
|
15
36
|
repo: Repo = None,
|
16
37
|
what: str = None,
|
17
|
-
against: str = None
|
38
|
+
against: str = None,
|
39
|
+
use_merge_base: bool = True,
|
18
40
|
) -> PatchSet | list[PatchedFile]:
|
19
41
|
repo = repo or Repo(".")
|
20
42
|
if not against:
|
21
43
|
against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
|
22
44
|
if not what:
|
23
45
|
what = None # working copy
|
24
|
-
|
46
|
+
if use_merge_base:
|
47
|
+
if what is None:
|
48
|
+
try:
|
49
|
+
current_ref = repo.active_branch.name
|
50
|
+
except TypeError:
|
51
|
+
# In detached HEAD state, use HEAD directly
|
52
|
+
current_ref = "HEAD"
|
53
|
+
logging.info(
|
54
|
+
"Detected detached HEAD state, using HEAD as current reference"
|
55
|
+
)
|
56
|
+
else:
|
57
|
+
current_ref = what
|
58
|
+
merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
|
59
|
+
against = merge_base.hexsha
|
60
|
+
logging.info(
|
61
|
+
f"Using merge base: {mc.ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
|
62
|
+
)
|
63
|
+
logging.info(
|
64
|
+
f"Making diff: {mc.ui.green(what or 'INDEX')} vs {mc.ui.yellow(against)}"
|
65
|
+
)
|
25
66
|
diff_content = repo.git.diff(against, what)
|
26
67
|
diff = PatchSet.from_string(diff_content)
|
27
|
-
|
68
|
+
diff = PatchSet.from_string(diff_content)
|
69
|
+
|
70
|
+
# Filter out binary files
|
71
|
+
non_binary_diff = PatchSet([])
|
72
|
+
for patched_file in diff:
|
73
|
+
# Check if the file is binary using the source or target file path
|
74
|
+
file_path = (
|
75
|
+
patched_file.target_file
|
76
|
+
if patched_file.target_file != DEV_NULL
|
77
|
+
else patched_file.source_file
|
78
|
+
)
|
79
|
+
if file_path == DEV_NULL or is_binary_file(repo, file_path.lstrip("b/")):
|
80
|
+
logging.info(f"Skipping binary file: {patched_file.path}")
|
81
|
+
continue
|
82
|
+
non_binary_diff.append(patched_file)
|
83
|
+
return non_binary_diff
|
28
84
|
|
29
85
|
|
30
86
|
def filter_diff(
|
@@ -33,7 +89,6 @@ def filter_diff(
|
|
33
89
|
"""
|
34
90
|
Filter the diff files by the given fnmatch filters.
|
35
91
|
"""
|
36
|
-
print([f.path for f in patch_set])
|
37
92
|
assert isinstance(filters, (list, str))
|
38
93
|
if not isinstance(filters, list):
|
39
94
|
filters = [f.strip() for f in filters.split(",") if f.strip()]
|
@@ -44,7 +99,6 @@ def filter_diff(
|
|
44
99
|
for file in patch_set
|
45
100
|
if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
|
46
101
|
]
|
47
|
-
print([f.path for f in files])
|
48
102
|
return files
|
49
103
|
|
50
104
|
|
@@ -61,22 +115,32 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
|
|
61
115
|
|
62
116
|
|
63
117
|
def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
|
64
|
-
return
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
118
|
+
return (
|
119
|
+
mc.prompt(
|
120
|
+
cfg.summary_prompt,
|
121
|
+
diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
|
122
|
+
issues=report.issues,
|
123
|
+
**cfg.prompt_vars,
|
124
|
+
).to_llm()
|
125
|
+
if cfg.summary_prompt
|
126
|
+
else ""
|
127
|
+
)
|
70
128
|
|
71
129
|
|
72
130
|
async def review(
|
131
|
+
repo: Repo = None,
|
73
132
|
what: str = None,
|
74
133
|
against: str = None,
|
75
|
-
filters: str | list[str] = ""
|
134
|
+
filters: str | list[str] = "",
|
135
|
+
use_merge_base: bool = True,
|
136
|
+
out_folder: str | PathLike | None = None,
|
76
137
|
):
|
77
138
|
cfg = ProjectConfig.load()
|
78
|
-
repo = Repo(".")
|
79
|
-
|
139
|
+
repo = repo or Repo(".")
|
140
|
+
out_folder = Path(out_folder or repo.working_tree_dir)
|
141
|
+
diff = get_diff(
|
142
|
+
repo=repo, what=what, against=against, use_merge_base=use_merge_base
|
143
|
+
)
|
80
144
|
diff = filter_diff(diff, filters)
|
81
145
|
if not diff:
|
82
146
|
logging.error("Nothing to review")
|
@@ -114,12 +178,14 @@ async def review(
|
|
114
178
|
if lines[file]:
|
115
179
|
f_lines = [""] + lines[file].splitlines()
|
116
180
|
i["affected_code"] = "\n".join(
|
117
|
-
f_lines[i["start_line"]: i["end_line"]+1]
|
181
|
+
f_lines[i["start_line"]: i["end_line"] + 1]
|
118
182
|
)
|
119
183
|
exec(cfg.post_process, {"mc": mc, **locals()})
|
184
|
+
out_folder.mkdir(parents=True, exist_ok=True)
|
120
185
|
report = Report(issues=issues, number_of_processed_files=len(diff))
|
121
186
|
report.summary = make_cr_summary(cfg, report, diff)
|
122
|
-
report.save()
|
187
|
+
report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
|
123
188
|
report_text = report.render(cfg, Report.Format.MARKDOWN)
|
124
189
|
print(mc.ui.yellow(report_text))
|
125
|
-
|
190
|
+
text_report_path = out_folder / "code-review-report.md"
|
191
|
+
text_report_path.write_text(report_text, encoding="utf-8")
|
ai_code_review/utils.py
CHANGED
@@ -92,11 +92,11 @@ def is_running_in_github_action():
|
|
92
92
|
return os.getenv("GITHUB_ACTIONS") == "true"
|
93
93
|
|
94
94
|
|
95
|
-
def
|
95
|
+
def no_subcommand(app: typer.Typer) -> bool:
|
96
96
|
"""
|
97
97
|
Checks if the current script is being invoked as a command in a target Typer application.
|
98
98
|
"""
|
99
|
-
return (
|
99
|
+
return not (
|
100
100
|
(first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
|
101
101
|
and first_arg in (
|
102
102
|
cmd.name or cmd.callback.__name__.replace('_', '-')
|
@@ -106,7 +106,7 @@ def is_app_command_invocation(app: typer.Typer) -> bool:
|
|
106
106
|
)
|
107
107
|
|
108
108
|
|
109
|
-
def parse_refs_pair(refs: str):
|
109
|
+
def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
|
110
110
|
SEPARATOR = '..'
|
111
111
|
if not refs:
|
112
112
|
return None, None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ai-cr
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
|
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
|
@@ -15,13 +15,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
17
17
|
Classifier: Topic :: Software Development
|
18
|
-
Requires-Dist: GitPython (
|
19
|
-
Requires-Dist: ai-microcore (==4.0.0.
|
20
|
-
Requires-Dist: anthropic (
|
21
|
-
Requires-Dist:
|
22
|
-
Requires-Dist:
|
23
|
-
Requires-Dist:
|
24
|
-
Requires-Dist: unidiff (==0.7.5)
|
18
|
+
Requires-Dist: GitPython (>=3.1.44,<4.0.0)
|
19
|
+
Requires-Dist: ai-microcore (==4.0.0.dev19)
|
20
|
+
Requires-Dist: anthropic (>=0.52.2,<0.53.0)
|
21
|
+
Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
|
22
|
+
Requires-Dist: typer (>=0.16.0,<0.17.0)
|
23
|
+
Requires-Dist: unidiff (>=0.7.5,<0.8.0)
|
25
24
|
Project-URL: Homepage, https://github.com/Nayjest/github-ai-code-review
|
26
25
|
Project-URL: Repository, https://github.com/Nayjest/github-ai-code-review
|
27
26
|
Description-Content-Type: text/markdown
|
@@ -30,6 +29,7 @@ Description-Content-Type: text/markdown
|
|
30
29
|
<a href="https://pypi.org/project/ai-code-review/" target="_blank"><img src="https://badge.fury.io/py/ai-code-review.svg" alt="PYPI Release"></a>
|
31
30
|
<a href="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/code-style.yml/badge.svg" alt="Pylint"></a>
|
32
31
|
<a href="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/ai-code-review/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
|
32
|
+
<img src="https://github.com/Nayjest/ai-code-review/blob/main/coverage.svg" alt="Code Coverage">
|
33
33
|
<a href="https://github.com/Nayjest/ai-code-review/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
|
34
34
|
</p>
|
35
35
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
70
70
|
uses: actions/setup-python@v5
|
71
71
|
with: { python-version: "3.13" }
|
72
72
|
- name: Install AI Code Review tool
|
73
|
-
run: pip install ai-code-review
|
73
|
+
run: pip install ai-code-review~=1.0
|
74
74
|
- name: Run AI code analysis
|
75
75
|
env:
|
76
76
|
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
@@ -84,34 +84,66 @@ jobs:
|
|
84
84
|
with:
|
85
85
|
name: ai-code-review-results
|
86
86
|
path: |
|
87
|
-
code-review-report.
|
87
|
+
code-review-report.md
|
88
88
|
code-review-report.json
|
89
89
|
```
|
90
90
|
|
91
91
|
> ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
|
92
92
|
|
93
93
|
💪 Done!
|
94
|
-
PRs to your repository will now receive AI code reviews automatically. ✨
|
94
|
+
PRs to your repository will now receive AI code reviews automatically. ✨
|
95
|
+
See [GitHub Setup Guide](https://github.com/Nayjest/ai-code-review/blob/main/documentation/github_setup.md) for more details.
|
95
96
|
|
96
|
-
### 2.
|
97
|
+
### 2. Running Code Analysis Locally
|
97
98
|
|
98
|
-
|
99
|
+
#### Initial Local Setup
|
99
100
|
|
101
|
+
**Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
|
102
|
+
|
103
|
+
**Step1:** Install [ai-code-review](https://github.com/Nayjest/ai-code-review) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
|
100
104
|
```bash
|
101
|
-
# Prerequisites: Python 3.11+
|
102
105
|
pip install ai-code-review
|
106
|
+
```
|
107
|
+
|
108
|
+
> **Troubleshooting:**
|
109
|
+
> pip may be also available via cli as `pip3` depending on your Python installation.
|
110
|
+
|
111
|
+
**Step2:** Perform initial setup
|
103
112
|
|
104
|
-
|
113
|
+
The following command will perform one-time setup using an interactive wizard.
|
114
|
+
You will be prompted to enter LLM configuration details (API type, API key, etc).
|
115
|
+
Configuration will be saved to ~/.env.ai-code-review.
|
116
|
+
|
117
|
+
```bash
|
105
118
|
ai-code-review setup
|
119
|
+
```
|
120
|
+
|
121
|
+
> **Troubleshooting:**
|
122
|
+
> On some systems, `ai-code-review` command may not became available immediately after installation.
|
123
|
+
> Try restarting your terminal or running `python -m ai_code_review` instead.
|
106
124
|
|
107
|
-
|
125
|
+
|
126
|
+
#### Perform your first AI code review locally
|
127
|
+
|
128
|
+
**Step1:** Navigate to your repository root directory.
|
129
|
+
**Step2:** Switch to the branch you want to review.
|
130
|
+
**Step3:** Run following command
|
131
|
+
```bash
|
108
132
|
ai-code-review
|
109
133
|
```
|
110
134
|
|
111
|
-
|
135
|
+
> **Note:** This will analyze the current branch against the repository main branch by default.
|
136
|
+
> Files that are not staged for commit will be ignored.
|
137
|
+
> See `ai-code-review --help` for more options.
|
138
|
+
|
139
|
+
**Reviewing remote repository**
|
112
140
|
|
113
141
|
```bash
|
114
|
-
ai-code-review remote
|
142
|
+
ai-code-review remote git@github.com:owner/repo.git <FEATURE_BRANCH>..<MAIN_BRANCH>
|
143
|
+
```
|
144
|
+
Use interactive help for details:
|
145
|
+
```bash
|
146
|
+
ai-code-review remote --help
|
115
147
|
```
|
116
148
|
|
117
149
|
## 🔧 Configuration
|
@@ -128,6 +160,8 @@ You can override the default config by placing `.ai-code-review.toml` in your re
|
|
128
160
|
|
129
161
|
See default configuration [here](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml).
|
130
162
|
|
163
|
+
More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/ai-code-review/blob/main/documentation/config_cookbook.md)
|
164
|
+
|
131
165
|
## 💻 Development Setup
|
132
166
|
|
133
167
|
Install dependencies:
|
@@ -0,0 +1,16 @@
|
|
1
|
+
ai_code_review/.ai-code-review.toml,sha256=sIQt7VJWl937VLocF2Vg6bC4qGbHeBgyKF6Bj3XKwIk,10640
|
2
|
+
ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
ai_code_review/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
|
4
|
+
ai_code_review/bootstrap.py,sha256=jqioR_UtTsn5nXezmjMLU3aB8tzlVz73ZRBk33ud5F4,2336
|
5
|
+
ai_code_review/cli.py,sha256=9OWQP2voQOfrhVfJsCzP-nQ9RtLU1cHhMi81QY56vzc,7441
|
6
|
+
ai_code_review/commands/repl.py,sha256=Ms5p6vgcf0EBAUUWKQfJu3X9XFvzJXB018qcvSiJ-oI,396
|
7
|
+
ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
|
8
|
+
ai_code_review/core.py,sha256=BJNs4ZER2-bMulXY2apY6B6hI0nvRCOrLqsJ7L8Bizc,6693
|
9
|
+
ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
|
10
|
+
ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
|
11
|
+
ai_code_review/utils.py,sha256=vlzU3M89qK6_mVkBMnppZaOFsXddVsIBVdfmbN3cxDY,2939
|
12
|
+
ai_cr-1.0.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
|
13
|
+
ai_cr-1.0.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
14
|
+
ai_cr-1.0.0.dist-info/METADATA,sha256=SPJm5aNOWFdwCI7KxtDE2AspsP2Kdn5NLwV6DeBJIeg,7109
|
15
|
+
ai_cr-1.0.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
16
|
+
ai_cr-1.0.0.dist-info/RECORD,,
|
ai_cr-0.5.0.dist-info/RECORD
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
|
2
|
-
ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
ai_code_review/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
4
|
-
ai_code_review/bootstrap.py,sha256=b9BlhRgliwsjmWRCCZcsTivsOw9AbXOf8_AZYjQTx-c,2336
|
5
|
-
ai_code_review/cli.py,sha256=L1utFMHs4D9HVcuJ1VC96PEvP6q26UHsfCz_Lq49VMg,4893
|
6
|
-
ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
|
7
|
-
ai_code_review/core.py,sha256=WMGU2zwZTJ1P6PkUId-RJP7KHQbzB31GKSnI5wsedcA,4177
|
8
|
-
ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
|
9
|
-
ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
|
10
|
-
ai_code_review/utils.py,sha256=UzFp2ShNzQvsydxJ98bdr_j2rBUHcm62pPtl6hBuZD4,2914
|
11
|
-
ai_cr-0.5.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
|
12
|
-
ai_cr-0.5.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
13
|
-
ai_cr-0.5.0.dist-info/METADATA,sha256=k3FaB6nbVV0xbgZYPg_VQ6zMauB65Xy6CLWI4qFOMF0,5609
|
14
|
-
ai_cr-0.5.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
15
|
-
ai_cr-0.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|