Skip to content

Commit 654f306

Browse files
authored
Merge pull request #44 from Nayjest/0.5
v0.5
2 parents f649f8d + f762e92 commit 654f306

File tree

8 files changed

+120
-32
lines changed

8 files changed

+120
-32
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ venv.bak/
6464
.aico/*
6565
!.aico/project.json
6666
storage
67-
code-review-report.*
67+
code-review-report.*
68+
*.zip

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
uses: actions/setup-python@v5
4343
with: { python-version: "3.13" }
4444
- name: Install AI Code Review tool
45-
run: pip install ai-code-review==0.4.7
45+
run: pip install ai-code-review==0.5.0
4646
- name: Run AI code analysis
4747
env:
4848
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}

ai_code_review/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .cli import main
2+
3+
if __name__ == "__main__":
4+
main()

ai_code_review/cli.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,79 @@
33
import sys
44
import os
55
import shutil
6+
import requests
67

78
import microcore as mc
89
import async_typer
910
import typer
10-
from .core import review
11-
from .report_struct import Report
11+
from ai_code_review.utils import parse_refs_pair
1212
from git import Repo
13-
import requests
1413

14+
from .core import review
15+
from .report_struct import Report
1516
from .constants import ENV_CONFIG_FILE
1617
from .bootstrap import bootstrap
1718
from .project_config import ProjectConfig
18-
19-
app = async_typer.AsyncTyper(
20-
pretty_exceptions_show_locals=False,
21-
)
19+
from .utils import is_app_command_invocation
2220

2321

22+
app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
23+
default_command_app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
2424
if sys.platform == "win32":
2525
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
2626

2727

28+
def main():
29+
if is_app_command_invocation(app):
30+
app()
31+
else:
32+
bootstrap()
33+
default_command_app()
34+
35+
2836
@app.callback(invoke_without_command=True)
29-
def cli(ctx: typer.Context, filters=typer.Option("", "--filter", "-f", "--filters")):
37+
def cli(ctx: typer.Context):
3038
if ctx.invoked_subcommand != "setup":
3139
bootstrap()
32-
if not ctx.invoked_subcommand:
33-
asyncio.run(review(filters=filters))
40+
41+
42+
@default_command_app.async_command(name="review", help="Perform code review")
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+
):
63+
_what, _against = parse_refs_pair(refs)
64+
if _what:
65+
if what:
66+
raise typer.BadParameter(
67+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
68+
)
69+
else:
70+
_what = what
71+
if _against:
72+
if against:
73+
raise typer.BadParameter(
74+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
75+
)
76+
else:
77+
_against = against
78+
await review(what=_what, against=_against, filters=filters)
3479

3580

3681
@app.async_command(help="Configure LLM for local usage interactively")

ai_code_review/core.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@
1111
from .report_struct import Report
1212

1313

14-
def get_diff(repo: Repo = None, against: str = "HEAD") -> PatchSet | list[PatchedFile]:
14+
def get_diff(
15+
repo: Repo = None,
16+
what: str = None,
17+
against: str = None
18+
) -> PatchSet | list[PatchedFile]:
1519
repo = repo or Repo(".")
16-
base = repo.remotes.origin.refs.HEAD.reference.name
17-
logging.info(f"{base}...{against}")
18-
diff_content = repo.git.diff(base, against)
20+
if not against:
21+
against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
22+
if not what:
23+
what = None # working copy
24+
logging.info(f"Reviewing {mc.ui.green(what or 'working copy')} vs {mc.ui.yellow(against)}")
25+
diff_content = repo.git.diff(against, what)
1926
diff = PatchSet.from_string(diff_content)
2027
return diff
2128

@@ -62,10 +69,14 @@ def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
6269
).to_llm() if cfg.summary_prompt else ""
6370

6471

65-
async def review(filters: str | list[str] = ""):
72+
async def review(
73+
what: str = None,
74+
against: str = None,
75+
filters: str | list[str] = ""
76+
):
6677
cfg = ProjectConfig.load()
6778
repo = Repo(".")
68-
diff = get_diff(repo=repo, against="HEAD")
79+
diff = get_diff(repo=repo, what=what, against=against)
6980
diff = filter_diff(diff, filters)
7081
if not diff:
7182
logging.error("Nothing to review")
@@ -78,7 +89,7 @@ async def review(filters: str | list[str] = ""):
7889
cfg.max_code_tokens
7990
- mc.tokenizing.num_tokens_from_string(str(file_diff)),
8091
)
81-
if file_diff.target_file != DEV_NULL
92+
if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
8293
else ""
8394
)
8495
for file_diff in diff

ai_code_review/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import sys
12
import os
23
from pathlib import Path
4+
import typer
5+
36

47
_EXT_TO_HINT: dict[str, str] = {
58
# scripting & languages
@@ -87,3 +90,27 @@ def syntax_hint(file_path: str | Path) -> str:
8790

8891
def is_running_in_github_action():
8992
return os.getenv("GITHUB_ACTIONS") == "true"
93+
94+
95+
def is_app_command_invocation(app: typer.Typer) -> bool:
96+
"""
97+
Checks if the current script is being invoked as a command in a target Typer application.
98+
"""
99+
return (
100+
(first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
101+
and first_arg in (
102+
cmd.name or cmd.callback.__name__.replace('_', '-')
103+
for cmd in app.registered_commands
104+
)
105+
or '--help' in sys.argv
106+
)
107+
108+
109+
def parse_refs_pair(refs: str):
110+
SEPARATOR = '..'
111+
if not refs:
112+
return None, None
113+
if SEPARATOR not in refs:
114+
return refs, None
115+
what, against = refs.split(SEPARATOR)
116+
return what or None, against or None

poetry.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "ai-code-review"
3-
version = "0.4.8"
3+
version = "0.5.0"
44
description = "LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions"
55
authors = ["Nayjest <mail@vitaliy.in>"]
66
readme = "README.md"
@@ -21,11 +21,11 @@ packages = [
2121

2222
[tool.poetry.dependencies]
2323
python = "^3.11"
24-
ai-microcore = "4.0.0.dev16"
24+
ai-microcore = "4.0.0.dev18"
2525
GitPython = "3.1.44"
2626
unidiff = "0.7.5"
2727
google-generativeai = "0.8.5"
28-
anthropic = "0.49.0"
28+
anthropic = "0.52.2"
2929
typer = "0.9.4"
3030
async-typer = "0.1.8"
3131

@@ -49,7 +49,7 @@ requires = ["poetry-core"]
4949
build-backend = "poetry.core.masonry.api"
5050

5151
[tool.poetry.scripts]
52-
ai-code-review = "ai_code_review.cli:app"
52+
ai-code-review = "ai_code_review.cli:main"
5353

5454
[tool.pytest.ini_options]
5555
minversion = "6.0"

0 commit comments

Comments
 (0)