ai-cr 1.0.1__py3-none-any.whl → 2.0.0.dev1__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.dev1.dist-info/METADATA +205 -0
- ai_cr-2.0.0.dev1.dist-info/RECORD +18 -0
- ai_cr-2.0.0.dev1.dist-info/entry_points.txt +3 -0
- {ai_code_review → gito}/bootstrap.py +8 -4
- {ai_code_review → gito}/cli.py +26 -10
- gito/commands/__init__.py +1 -0
- gito/commands/fix.py +124 -0
- {ai_code_review → gito}/commands/repl.py +3 -1
- ai_code_review/.ai-code-review.toml → gito/config.toml +82 -8
- gito/constants.py +9 -0
- {ai_code_review → gito}/core.py +40 -10
- {ai_code_review → gito}/project_config.py +36 -16
- {ai_code_review → gito}/report_struct.py +30 -6
- {ai_code_review → gito}/utils.py +17 -1
- ai_code_review/constants.py +0 -7
- ai_cr-1.0.1.dist-info/METADATA +0 -197
- ai_cr-1.0.1.dist-info/RECORD +0 -16
- ai_cr-1.0.1.dist-info/entry_points.txt +0 -3
- {ai_cr-1.0.1.dist-info → ai_cr-2.0.0.dev1.dist-info}/LICENSE +0 -0
- {ai_cr-1.0.1.dist-info → ai_cr-2.0.0.dev1.dist-info}/WHEEL +0 -0
- {ai_code_review → gito}/__init__.py +0 -0
- {ai_code_review → gito}/__main__.py +0 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: ai-cr
|
3
|
+
Version: 2.0.0.dev1
|
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
|
+
License: MIT
|
6
|
+
Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
|
7
|
+
Author: Nayjest
|
8
|
+
Author-email: mail@vitaliy.in
|
9
|
+
Requires-Python: >=3.11,<4.0
|
10
|
+
Classifier: Environment :: Console
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
17
|
+
Classifier: Topic :: Software Development
|
18
|
+
Requires-Dist: GitPython (>=3.1.44,<4.0.0)
|
19
|
+
Requires-Dist: ai-microcore (==4.0.0)
|
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)
|
24
|
+
Project-URL: Homepage, https://github.com/Nayjest/Gito
|
25
|
+
Project-URL: Repository, https://github.com/Nayjest/Gito
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
<h1 align="center"><a href="#"><img alt="Gito: AI Code Reviewer" src="press-kit/logo/gito-ai-code-reviewer_logo-180.png" align="center" width="180"></a></h1>
|
29
|
+
<p align="center">
|
30
|
+
<a href="https://pypi.org/project/gito.bot/" target="_blank"><img src="https://img.shields.io/pypi/v/gito.bot" alt="PYPI Release"></a>
|
31
|
+
<a href="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml/badge.svg" alt="PyLint"></a>
|
32
|
+
<a href="https://github.com/Nayjest/Gito/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
|
33
|
+
<img src="https://github.com/Nayjest/Gito/blob/main/coverage.svg" alt="Code Coverage">
|
34
|
+
<a href="https://github.com/Nayjest/Gito/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
|
35
|
+
</p>
|
36
|
+
|
37
|
+
**Gito** is an open-source **AI code reviewer** that works with any language model provider.
|
38
|
+
It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
|
39
|
+
|
40
|
+
Get consistent, thorough code reviews in seconds—no waiting for human availability.
|
41
|
+
|
42
|
+
## ✨ Why Gito?
|
43
|
+
|
44
|
+
- [⚡] **Lightning Fast:** Get detailed code reviews in seconds, not days — powered by parallelized LLM processing
|
45
|
+
- [🔧] **Vendor Agnostic:** Works with any language model provider (OpenAI, Anthropic, Google, local models, etc.)
|
46
|
+
- [🌐] **Universal:** Supports all major programming languages and frameworks
|
47
|
+
- [🔍] **Comprehensive Analysis:** Detect issues across security, performance, maintainability, best practices, and much more
|
48
|
+
- [📈] **Consistent Quality:** Never tired, never biased—consistent review quality every time
|
49
|
+
- [🚀] **Easy Integration:** Automatically reviews pull requests via GitHub Actions and posts results as PR comments
|
50
|
+
- [🎛️] **Infinitely Flexible:** Adapt to any project's standards—configure review rules, severity levels, and focus areas, build custom workflows
|
51
|
+
|
52
|
+
## 🎯 Perfect For
|
53
|
+
|
54
|
+
- Solo developers who want expert-level code review without the wait
|
55
|
+
- Teams looking to catch issues before human review
|
56
|
+
- Open source projects maintaining high code quality at scale
|
57
|
+
- CI/CD pipelines requiring automated quality gates
|
58
|
+
|
59
|
+
✨ See [code review in action](https://github.com/Nayjest/Gito/pull/39#issuecomment-2906968729) ✨
|
60
|
+
|
61
|
+
## 🚀 Quickstart
|
62
|
+
|
63
|
+
### 1. Review Pull Requests via GitHub Actions
|
64
|
+
|
65
|
+
Create a `.github/workflows/gito.yml` file:
|
66
|
+
|
67
|
+
```yaml
|
68
|
+
name: "Gito: AI Code Review"
|
69
|
+
on: { pull_request: { types: [opened, synchronize, reopened] } }
|
70
|
+
jobs:
|
71
|
+
review:
|
72
|
+
runs-on: ubuntu-latest
|
73
|
+
permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
|
74
|
+
steps:
|
75
|
+
- uses: actions/checkout@v4
|
76
|
+
with: { fetch-depth: 0 }
|
77
|
+
- name: Set up Python
|
78
|
+
uses: actions/setup-python@v5
|
79
|
+
with: { python-version: "3.13" }
|
80
|
+
- name: Install AI Code Review tool
|
81
|
+
run: pip install gito.bot~=2.0
|
82
|
+
- name: Run AI code analysis
|
83
|
+
env:
|
84
|
+
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
85
|
+
LLM_API_TYPE: openai
|
86
|
+
MODEL: "gpt-4.1"
|
87
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
88
|
+
run: |
|
89
|
+
gito --verbose review
|
90
|
+
gito github-comment --token ${{ secrets.GITHUB_TOKEN }}
|
91
|
+
- uses: actions/upload-artifact@v4
|
92
|
+
with:
|
93
|
+
name: ai-code-review-results
|
94
|
+
path: |
|
95
|
+
code-review-report.md
|
96
|
+
code-review-report.json
|
97
|
+
```
|
98
|
+
|
99
|
+
> ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
|
100
|
+
|
101
|
+
💪 Done!
|
102
|
+
PRs to your repository will now receive AI code reviews automatically. ✨
|
103
|
+
See [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation/github_setup.md) for more details.
|
104
|
+
|
105
|
+
### 2. Running Code Analysis Locally
|
106
|
+
|
107
|
+
#### Initial Local Setup
|
108
|
+
|
109
|
+
**Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
|
110
|
+
|
111
|
+
**Step1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
|
112
|
+
```bash
|
113
|
+
pip install gito.bot
|
114
|
+
```
|
115
|
+
|
116
|
+
> **Troubleshooting:**
|
117
|
+
> pip may be also available via cli as `pip3` depending on your Python installation.
|
118
|
+
|
119
|
+
**Step2:** Perform initial setup
|
120
|
+
|
121
|
+
The following command will perform one-time setup using an interactive wizard.
|
122
|
+
You will be prompted to enter LLM configuration details (API type, API key, etc).
|
123
|
+
Configuration will be saved to `~/.gito/.env`.
|
124
|
+
|
125
|
+
```bash
|
126
|
+
gito setup
|
127
|
+
```
|
128
|
+
|
129
|
+
> **Troubleshooting:**
|
130
|
+
> On some systems, `gito` command may not became available immediately after installation.
|
131
|
+
> Try restarting your terminal or running `python -m gito` instead.
|
132
|
+
|
133
|
+
|
134
|
+
#### Perform your first AI code review locally
|
135
|
+
|
136
|
+
**Step1:** Navigate to your repository root directory.
|
137
|
+
**Step2:** Switch to the branch you want to review.
|
138
|
+
**Step3:** Run following command
|
139
|
+
```bash
|
140
|
+
gito review
|
141
|
+
```
|
142
|
+
|
143
|
+
> **Note:** This will analyze the current branch against the repository main branch by default.
|
144
|
+
> Files that are not staged for commit will be ignored.
|
145
|
+
> See `gito --help` for more options.
|
146
|
+
|
147
|
+
**Reviewing remote repository**
|
148
|
+
|
149
|
+
```bash
|
150
|
+
gito remote git@github.com:owner/repo.git <FEATURE_BRANCH>..<MAIN_BRANCH>
|
151
|
+
```
|
152
|
+
Use interactive help for details:
|
153
|
+
```bash
|
154
|
+
gito remote --help
|
155
|
+
```
|
156
|
+
|
157
|
+
## 🔧 Configuration
|
158
|
+
|
159
|
+
Change behavior via `.gito/config.toml`:
|
160
|
+
|
161
|
+
- Prompt templates, filtering and post-processing using Python code snippets
|
162
|
+
- Tagging, severity, and confidence settings
|
163
|
+
- Custom AI awards for developer brilliance
|
164
|
+
- Output customization
|
165
|
+
|
166
|
+
You can override the default config by placing `.gito/config.toml` in your repo root.
|
167
|
+
|
168
|
+
|
169
|
+
See default configuration [here](https://github.com/Nayjest/Gito/blob/main/gito/config.toml).
|
170
|
+
|
171
|
+
More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
|
172
|
+
|
173
|
+
## 💻 Development Setup
|
174
|
+
|
175
|
+
Install dependencies:
|
176
|
+
|
177
|
+
```bash
|
178
|
+
make install
|
179
|
+
```
|
180
|
+
|
181
|
+
Format code and check style:
|
182
|
+
|
183
|
+
```bash
|
184
|
+
make black
|
185
|
+
make cs
|
186
|
+
```
|
187
|
+
|
188
|
+
Run tests:
|
189
|
+
|
190
|
+
```bash
|
191
|
+
pytest
|
192
|
+
```
|
193
|
+
|
194
|
+
## 🤝 Contributing
|
195
|
+
|
196
|
+
**Looking for a specific feature or having trouble?**
|
197
|
+
Contributions are welcome! ❤️
|
198
|
+
See [CONTRIBUTING.md](https://github.com/Nayjest/Gito/blob/main/CONTRIBUTING.md) for details.
|
199
|
+
|
200
|
+
## 📝 License
|
201
|
+
|
202
|
+
Licensed under the [MIT License](https://github.com/Nayjest/Gito/blob/main/LICENSE).
|
203
|
+
|
204
|
+
© 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
|
205
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
gito/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
|
3
|
+
gito/bootstrap.py,sha256=ETKioiDc2Npc7znd8HJxA5-twd7sZMPCufIGwXFQSbY,2403
|
4
|
+
gito/cli.py,sha256=JASFo1NGUqw9k6-ZnU5KXFuexxUc6EKJNOYs6uPPyjs,7943
|
5
|
+
gito/commands/__init__.py,sha256=NKUUDskR6taZGbHk5qR9msDS22aHpdzAZlUDxzRdMYA,56
|
6
|
+
gito/commands/fix.py,sha256=THEu7vdsK2--hOkz6rha4zESVfRVIDc88mHNNlYfM7Q,4421
|
7
|
+
gito/commands/repl.py,sha256=waN7FJBl98gWmDwZWMa8x157iWbPHIDPJEKmTdzWQ70,396
|
8
|
+
gito/config.toml,sha256=zd6s_UJs7Du1W7ulR2iGu7Grcs4l4qpq--VAS67pj4w,15003
|
9
|
+
gito/constants.py,sha256=DNqh3hOxeLM_KscUH4uFdAc9295o1hiXtwoEw2KxLiU,392
|
10
|
+
gito/core.py,sha256=a98HJyZMPswwbmfzJBJvnPZMWYJU7njs3O8B4Z_UGmc,7848
|
11
|
+
gito/project_config.py,sha256=tbN1mf2FqpUQ9y9hGsGfM2CRX7AmD-ZT0QkXVUdHi4U,4279
|
12
|
+
gito/report_struct.py,sha256=vC9Rs6OZvUspn0Ho4s9gVf0doHFt_dNbdqjdubqoIrY,4162
|
13
|
+
gito/utils.py,sha256=Oh1hyg-YpUZgOlWNJdRTDLzZV-YyjMkgLsC5MiN1uow,3431
|
14
|
+
ai_cr-2.0.0.dev1.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
|
15
|
+
ai_cr-2.0.0.dev1.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
16
|
+
ai_cr-2.0.0.dev1.dist-info/METADATA,sha256=JTgfajLAdhqrWqNw40BfNHh-B3jQRbgJeoiorkuTAaw,7676
|
17
|
+
ai_cr-2.0.0.dev1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
18
|
+
ai_cr-2.0.0.dev1.dist-info/RECORD,,
|
@@ -3,9 +3,10 @@ import os
|
|
3
3
|
from datetime import datetime
|
4
4
|
|
5
5
|
import microcore as mc
|
6
|
-
|
6
|
+
import typer
|
7
7
|
|
8
|
-
from .
|
8
|
+
from .utils import is_running_in_github_action
|
9
|
+
from .constants import HOME_ENV_PATH, EXECUTABLE
|
9
10
|
|
10
11
|
|
11
12
|
def setup_logging():
|
@@ -32,7 +33,7 @@ def bootstrap():
|
|
32
33
|
logging.info("Bootstrapping...")
|
33
34
|
try:
|
34
35
|
mc.configure(
|
35
|
-
DOT_ENV_FILE=
|
36
|
+
DOT_ENV_FILE=HOME_ENV_PATH,
|
36
37
|
USE_LOGGING=True,
|
37
38
|
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
38
39
|
)
|
@@ -51,7 +52,7 @@ def bootstrap():
|
|
51
52
|
)
|
52
53
|
else:
|
53
54
|
msg += (
|
54
|
-
"\nPlease run '
|
55
|
+
f"\nPlease run '{EXECUTABLE} setup' "
|
55
56
|
"to configure LLM API access (API keys, model, etc)."
|
56
57
|
)
|
57
58
|
print(mc.ui.red(msg))
|
@@ -60,3 +61,6 @@ def bootstrap():
|
|
60
61
|
logging.error(f"Unexpected configuration error: {e}")
|
61
62
|
raise SystemExit(3)
|
62
63
|
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
|
64
|
+
|
65
|
+
|
66
|
+
app = typer.Typer(pretty_exceptions_show_locals=False)
|
{ai_code_review → gito}/cli.py
RENAMED
@@ -12,18 +12,25 @@ from git import Repo
|
|
12
12
|
|
13
13
|
from .core import review, get_diff, filter_diff
|
14
14
|
from .report_struct import Report
|
15
|
-
from .constants import
|
16
|
-
from .bootstrap import bootstrap
|
15
|
+
from .constants import HOME_ENV_PATH
|
16
|
+
from .bootstrap import bootstrap, app
|
17
17
|
from .project_config import ProjectConfig
|
18
18
|
from .utils import no_subcommand, parse_refs_pair
|
19
19
|
|
20
|
-
|
20
|
+
# Import fix command to register it
|
21
|
+
from .commands import fix # noqa
|
22
|
+
|
23
|
+
|
21
24
|
app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
|
22
25
|
|
23
26
|
|
24
27
|
def main():
|
25
28
|
if sys.platform == "win32":
|
26
29
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
30
|
+
# Help subcommand alias: if 'help' appears as first non-option arg, replace it with '--help'
|
31
|
+
if len(sys.argv) > 1 and sys.argv[1] == "help":
|
32
|
+
sys.argv = [sys.argv[0]] + sys.argv[2:] + ["--help"]
|
33
|
+
|
27
34
|
if no_subcommand(app):
|
28
35
|
bootstrap()
|
29
36
|
app_no_subcommand()
|
@@ -117,12 +124,21 @@ def cmd_review(
|
|
117
124
|
|
118
125
|
@app.command(help="Configure LLM for local usage interactively")
|
119
126
|
def setup():
|
120
|
-
mc.interactive_setup(
|
121
|
-
|
122
|
-
|
123
|
-
@app.command()
|
124
|
-
|
125
|
-
|
127
|
+
mc.interactive_setup(HOME_ENV_PATH)
|
128
|
+
|
129
|
+
|
130
|
+
@app.command(name="render")
|
131
|
+
@app.command(name="report", hidden=True)
|
132
|
+
def render(
|
133
|
+
format: str = typer.Argument(default=Report.Format.CLI),
|
134
|
+
source: str = typer.Option(
|
135
|
+
"",
|
136
|
+
"--src",
|
137
|
+
"--source",
|
138
|
+
help="Source file (json) to load the report from"
|
139
|
+
)
|
140
|
+
):
|
141
|
+
Report.load(file_name=source).to_cli(report_format=format)
|
126
142
|
|
127
143
|
|
128
144
|
@app.command(help="Review remote code")
|
@@ -225,7 +241,7 @@ def files(
|
|
225
241
|
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
226
242
|
f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
|
227
243
|
)
|
228
|
-
|
244
|
+
repo.close()
|
229
245
|
for patch in patch_set:
|
230
246
|
if patch.is_added_file:
|
231
247
|
color = mc.ui.green
|
@@ -0,0 +1 @@
|
|
1
|
+
# Command modules register themselves with the CLI app
|
gito/commands/fix.py
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
Fix issues from code review report
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
import typer
|
10
|
+
from microcore import ui
|
11
|
+
|
12
|
+
from ..bootstrap import app
|
13
|
+
from ..constants import JSON_REPORT_FILE_NAME
|
14
|
+
from ..report_struct import Report
|
15
|
+
|
16
|
+
|
17
|
+
@app.command(help="Fix an issue from the code review report")
|
18
|
+
def fix(
|
19
|
+
issue_number: int = typer.Argument(..., help="Issue number to fix"),
|
20
|
+
report_path: Optional[str] = typer.Option(
|
21
|
+
None,
|
22
|
+
"--report",
|
23
|
+
"-r",
|
24
|
+
help="Path to the code review report (default: code-review-report.json)"
|
25
|
+
),
|
26
|
+
dry_run: bool = typer.Option(
|
27
|
+
False, "--dry-run", "-d", help="Only print changes without applying them"
|
28
|
+
),
|
29
|
+
):
|
30
|
+
"""
|
31
|
+
Applies the proposed change for the specified issue number from the code review report.
|
32
|
+
"""
|
33
|
+
# Load the report
|
34
|
+
report_path = report_path or JSON_REPORT_FILE_NAME
|
35
|
+
try:
|
36
|
+
report = Report.load(report_path)
|
37
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
38
|
+
logging.error(f"Failed to load report from {report_path}: {e}")
|
39
|
+
raise typer.Exit(code=1)
|
40
|
+
|
41
|
+
# Find the issue by number
|
42
|
+
issue = None
|
43
|
+
for file_issues in report.issues.values():
|
44
|
+
for i in file_issues:
|
45
|
+
if i.id == issue_number:
|
46
|
+
issue = i
|
47
|
+
break
|
48
|
+
if issue:
|
49
|
+
break
|
50
|
+
|
51
|
+
if not issue:
|
52
|
+
logging.error(f"Issue #{issue_number} not found in the report")
|
53
|
+
raise typer.Exit(code=1)
|
54
|
+
|
55
|
+
if not issue.affected_lines:
|
56
|
+
logging.error(f"Issue #{issue_number} has no affected lines specified")
|
57
|
+
raise typer.Exit(code=1)
|
58
|
+
|
59
|
+
if not any(affected_line.proposal for affected_line in issue.affected_lines):
|
60
|
+
logging.error(f"Issue #{issue_number} has no proposal for fixing")
|
61
|
+
raise typer.Exit(code=1)
|
62
|
+
|
63
|
+
# Apply the fix
|
64
|
+
logging.info(f"Fixing issue #{issue_number}: {ui.cyan(issue.title)}")
|
65
|
+
|
66
|
+
for affected_line in issue.affected_lines:
|
67
|
+
if not affected_line.proposal:
|
68
|
+
continue
|
69
|
+
|
70
|
+
file_path = Path(issue.file)
|
71
|
+
if not file_path.exists():
|
72
|
+
logging.error(f"File {file_path} not found")
|
73
|
+
continue
|
74
|
+
|
75
|
+
try:
|
76
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
77
|
+
lines = f.readlines()
|
78
|
+
except Exception as e:
|
79
|
+
logging.error(f"Failed to read file {file_path}: {e}")
|
80
|
+
continue
|
81
|
+
|
82
|
+
# Check if line numbers are valid
|
83
|
+
if affected_line.start_line < 1 or affected_line.end_line > len(lines):
|
84
|
+
logging.error(
|
85
|
+
f"Invalid line range: {affected_line.start_line}-{affected_line.end_line} "
|
86
|
+
f"(file has {len(lines)} lines)"
|
87
|
+
)
|
88
|
+
continue
|
89
|
+
|
90
|
+
# Get the affected line content for display
|
91
|
+
affected_content = "".join(lines[affected_line.start_line - 1:affected_line.end_line])
|
92
|
+
print(f"\nFile: {ui.blue(issue.file)}")
|
93
|
+
print(f"Lines: {affected_line.start_line}-{affected_line.end_line}")
|
94
|
+
print(f"Current content:\n{ui.red(affected_content)}")
|
95
|
+
print(f"Proposed change:\n{ui.green(affected_line.proposal)}")
|
96
|
+
|
97
|
+
if dry_run:
|
98
|
+
print(f"{ui.yellow('Dry run')}: Changes not applied")
|
99
|
+
continue
|
100
|
+
|
101
|
+
# Apply the change
|
102
|
+
proposal_lines = affected_line.proposal.splitlines(keepends=True)
|
103
|
+
if not proposal_lines:
|
104
|
+
proposal_lines = [""]
|
105
|
+
elif not proposal_lines[-1].endswith(("\n", "\r")):
|
106
|
+
# Ensure the last line has a newline if the original does
|
107
|
+
if (
|
108
|
+
affected_line.end_line < len(lines)
|
109
|
+
and lines[affected_line.end_line - 1].endswith(("\n", "\r"))
|
110
|
+
):
|
111
|
+
proposal_lines[-1] += "\n"
|
112
|
+
|
113
|
+
lines[affected_line.start_line - 1:affected_line.end_line] = proposal_lines
|
114
|
+
|
115
|
+
# Write changes back to the file
|
116
|
+
try:
|
117
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
118
|
+
f.writelines(lines)
|
119
|
+
print(f"{ui.green('Success')}: Changes applied to {file_path}")
|
120
|
+
except Exception as e:
|
121
|
+
logging.error(f"Failed to write changes to {file_path}: {e}")
|
122
|
+
raise typer.Exit(code=1)
|
123
|
+
|
124
|
+
print(f"\n{ui.green('✓')} Issue #{issue_number} fixed successfully")
|
@@ -3,7 +3,7 @@ Python REPL
|
|
3
3
|
"""
|
4
4
|
# flake8: noqa: F401
|
5
5
|
import code
|
6
|
-
|
6
|
+
|
7
7
|
|
8
8
|
# Imports for usage in REPL
|
9
9
|
import os
|
@@ -17,6 +17,8 @@ from rich.pretty import pprint
|
|
17
17
|
import microcore as mc
|
18
18
|
from microcore import ui
|
19
19
|
|
20
|
+
from ..cli import app
|
21
|
+
|
20
22
|
@app.command(help="python REPL")
|
21
23
|
def repl():
|
22
24
|
code.interact(local=globals())
|
@@ -1,14 +1,15 @@
|
|
1
1
|
report_template_md = """
|
2
|
-
|
2
|
+
<h2><a href="https://github.com/Nayjest/Gito"><img src="https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png" align="left" width=64 height=50></a>I've Reviewed the Code</h2>
|
3
3
|
|
4
4
|
{% if report.summary -%}
|
5
5
|
{{ report.summary }}
|
6
6
|
{%- endif %}
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
{
|
11
|
-
in
|
8
|
+
{% if report.total_issues > 0 -%}
|
9
|
+
**⚠️ {{ report.total_issues }} issue{{ 's' if report.total_issues != 1 else '' }} found** across {{ report.number_of_processed_files }} file{{ 's' if report.number_of_processed_files != 1 else '' }}
|
10
|
+
{%- else -%}
|
11
|
+
**✅ No issues found** in {{ report.number_of_processed_files }} file{{ 's' if report.number_of_processed_files != 1 else '' }}
|
12
|
+
{%- endif -%}
|
12
13
|
|
13
14
|
{%- for issue in report.plain_issues -%}
|
14
15
|
{{"\n"}}## `#{{ issue.id}}` {{ issue.title -}}
|
@@ -31,6 +32,77 @@ in `{{ report.number_of_processed_files }}` files**
|
|
31
32
|
{{ "\n" }}
|
32
33
|
{%- endfor -%}
|
33
34
|
|
35
|
+
"""
|
36
|
+
report_template_cli = """
|
37
|
+
{{ Back.BLUE }} + + + ---==<<[ CODE REVIEW{{Style.NORMAL}} ]>>==--- + + + {{Style.RESET_ALL}}
|
38
|
+
{% if report.total_issues > 0 -%}
|
39
|
+
{{ Style.BRIGHT }}{{Back.RED}} ⚠️ {{ report.total_issues }} issue{{ 's' if report.total_issues != 1 else '' }} {{Back.RESET}} found across {{Back.BLUE}} {{ report.number_of_processed_files }} {{Back.RESET}} file{{ 's' if report.number_of_processed_files != 1 else '' }}{{ Style.RESET_ALL }}
|
40
|
+
{%- else -%}
|
41
|
+
{{ Style.BRIGHT }}{{Back.GREEN}} ✅ No issues found {{Back.RESET}} in {{Back.BLUE}} {{ report.number_of_processed_files }} {{Back.RESET}} file{{ 's' if report.number_of_processed_files != 1 else '' }}{{ Style.RESET_ALL }}
|
42
|
+
{%- endif -%}
|
43
|
+
|
44
|
+
{%- if report.summary -%}
|
45
|
+
{{- "\n" }}
|
46
|
+
{{- "\n" }}{{- Style.BRIGHT }}✨ SUMMARY {{ Style.RESET_ALL -}}
|
47
|
+
{{- "\n" }}{{- report.summary -}}
|
48
|
+
{%- endif %}
|
49
|
+
{% for issue in report.plain_issues -%}
|
50
|
+
{{"\n"}}{{ Style.BRIGHT }}{{Back.RED}}[ {{ issue.id}} ]{{Back.RESET}} {{ issue.title -}}{{ Style.RESET_ALL -}}
|
51
|
+
{{ "\n"}}{{ file_link(issue.file) -}}
|
52
|
+
{%- if issue.affected_lines -%}:{{issue.affected_lines[0].start_line}}{%- endif -%}
|
53
|
+
{{' '}}
|
54
|
+
|
55
|
+
{%- if issue.affected_lines -%}
|
56
|
+
{% if issue.affected_lines[0].end_line != issue.affected_lines[0].start_line or issue.affected_lines|length > 1 -%}
|
57
|
+
{{ ui.gray }}Lines{{' '}}
|
58
|
+
{{- Fore.RESET -}}
|
59
|
+
{%- for i in issue.affected_lines -%}
|
60
|
+
{{ i.start_line }}{%- if i.end_line != i.start_line -%}{{ ui.gray }}–{{Fore.RESET}}{{ i.end_line }}{%- endif -%}
|
61
|
+
{%- if loop.last == false -%}
|
62
|
+
{{ ui.gray(', ') }}
|
63
|
+
{%- endif -%}
|
64
|
+
{%- endfor -%}
|
65
|
+
{%- endif -%}
|
66
|
+
{%- endif -%}
|
67
|
+
{{-"\n"-}}
|
68
|
+
|
69
|
+
{% if issue.details -%}
|
70
|
+
{{- "\n" -}}
|
71
|
+
{{- issue.details.strip() -}}
|
72
|
+
{{-"\n" -}}
|
73
|
+
{%- endif -%}
|
74
|
+
|
75
|
+
{%- for tag in issue.tags -%}
|
76
|
+
{{Back.YELLOW}}{{Fore.BLACK}} {{tag}} {{Style.RESET_ALL}}{{ ' ' }}
|
77
|
+
{%- endfor -%}
|
78
|
+
{%- if issue.tags %}{{ "\n" }}{% endif -%}
|
79
|
+
|
80
|
+
{%- for i in issue.affected_lines -%}
|
81
|
+
{%- if i.affected_code -%}
|
82
|
+
{{- "\n"+Fore.RED + " ╭─" + "─"*4 + "[ 💥 Affected Code ]" + "─"*4 + " ─── ── ─\n" -}}
|
83
|
+
{{- textwrap.indent(i.affected_code.strip(), Fore.RED+' │ ') -}}
|
84
|
+
{{- "\n ╰─"+"─"*2+Style.RESET_ALL -}}
|
85
|
+
{%- endif -%}
|
86
|
+
{%- if i.proposal -%}
|
87
|
+
{%- set maxlen = 100 -%}
|
88
|
+
{%- if not i.affected_code %}{{ Fore.GREEN }} ╭────{% endif -%}
|
89
|
+
{#- Wrap right for one-liner, doesn't prevent copying code -#}
|
90
|
+
{%- if i.proposal.splitlines() | length == 1 and max_line_len(i.proposal)<80 -%}
|
91
|
+
{{- Fore.GREEN + "─"*2 + "[ 💡 Proposed Change ]" + "─"*(max_line_len(i.proposal)-29) + "─╮" +"\n" -}}
|
92
|
+
{{- block_wrap_lr(i.proposal, '', ' │', 60) -}}
|
93
|
+
{{- "\n" + " ╰──"+"─"*(max_line_len(i.proposal)-5+1)+"─╯" -}}
|
94
|
+
{#- Open right side to not prevent multiline code copying -#}
|
95
|
+
{%- else -%}
|
96
|
+
{{- Fore.GREEN + "─"*2 + "[ 💡 Proposed Change ]" + "─"*([max_line_len(i.proposal)-29+2,maxlen-26-2]|min -2) + "─╮" +"\n" -}}
|
97
|
+
{{- i.proposal -}}
|
98
|
+
{{- "\n" + " ╰───"+"─"*([[max_line_len(i.proposal)-29+2,maxlen-29+1]|min - 2 + 29 - 5,29-7+2]|max)+"─╯" -}}
|
99
|
+
{%- endif -%}
|
100
|
+
|
101
|
+
{{- Style.RESET_ALL -}}
|
102
|
+
{% endif -%}
|
103
|
+
{%- endfor -%}
|
104
|
+
{{ "\n" }}
|
105
|
+
{%- endfor -%}
|
34
106
|
"""
|
35
107
|
retries = 3
|
36
108
|
prompt = """
|
@@ -64,7 +136,7 @@ Respond with a valid JSON array of issues in the following format:
|
|
64
136
|
"details": "<issue_description>",
|
65
137
|
"tags": ["<issue_tag1>", "<issue_tag2>"],
|
66
138
|
"severity": <issue_severity>,
|
67
|
-
"confidence": <confidence_score
|
139
|
+
"confidence": <confidence_score>,
|
68
140
|
"affected_lines": [ // optional; list of affected lines
|
69
141
|
{
|
70
142
|
"start_line": <start_line:int>,
|
@@ -119,14 +191,16 @@ summary_prompt = """
|
|
119
191
|
Summarize the code review in one sentence.
|
120
192
|
--Reviewed Changes--
|
121
193
|
{% for part in diff %}{{ part }}\n{% endfor %}
|
122
|
-
--Detected
|
194
|
+
--Issues Detected by You--
|
123
195
|
{{ issues | tojson(indent=2) }}
|
124
196
|
---
|
125
|
-
If code changes
|
197
|
+
If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
|
198
|
+
Note: Awards should only be given to authors of initial codebase changes, not to code reviewers.
|
126
199
|
--Available Awards--
|
127
200
|
{{ awards }}
|
128
201
|
---
|
129
202
|
- Your response will be parsed programmatically, so do not include any additional text.
|
203
|
+
- Do not include the issues by itself to the summary, they are already provided in the context.
|
130
204
|
- Use Markdown formatting in your response.
|
131
205
|
{{ summary_requirements -}}
|
132
206
|
"""
|
gito/constants.py
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
PROJECT_GITO_FOLDER = ".gito"
|
4
|
+
PROJECT_CONFIG_FILE_NAME = "config.toml"
|
5
|
+
PROJECT_CONFIG_FILE_PATH = Path(".gito") / PROJECT_CONFIG_FILE_NAME
|
6
|
+
PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE_NAME
|
7
|
+
HOME_ENV_PATH = Path("~/.gito/.env").expanduser()
|
8
|
+
JSON_REPORT_FILE_NAME = "code-review-report.json"
|
9
|
+
EXECUTABLE = "gito"
|
{ai_code_review → gito}/core.py
RENAMED
@@ -14,6 +14,10 @@ from .report_struct import Report
|
|
14
14
|
from .constants import JSON_REPORT_FILE_NAME
|
15
15
|
|
16
16
|
|
17
|
+
def review_subject_is_index(what):
|
18
|
+
return not what or what == 'INDEX'
|
19
|
+
|
20
|
+
|
17
21
|
def is_binary_file(repo: Repo, file_path: str) -> bool:
|
18
22
|
"""
|
19
23
|
Check if a file is binary by attempting to read it as text.
|
@@ -25,7 +29,20 @@ def is_binary_file(repo: Repo, file_path: str) -> bool:
|
|
25
29
|
# Try decoding as UTF-8; if it fails, it's likely binary
|
26
30
|
content.decode("utf-8")
|
27
31
|
return False
|
28
|
-
except
|
32
|
+
except KeyError:
|
33
|
+
try:
|
34
|
+
fs_path = Path(repo.working_tree_dir) / file_path
|
35
|
+
fs_path.read_text(encoding='utf-8')
|
36
|
+
return False
|
37
|
+
except FileNotFoundError:
|
38
|
+
logging.error(f"File {file_path} not found in the repository.")
|
39
|
+
return True
|
40
|
+
except UnicodeDecodeError:
|
41
|
+
return True
|
42
|
+
except Exception as e:
|
43
|
+
logging.error(f"Error reading file {file_path}: {e}")
|
44
|
+
return True
|
45
|
+
except UnicodeDecodeError:
|
29
46
|
return True
|
30
47
|
except Exception as e:
|
31
48
|
logging.warning(f"Error checking if file {file_path} is binary: {e}")
|
@@ -40,11 +57,12 @@ def get_diff(
|
|
40
57
|
) -> PatchSet | list[PatchedFile]:
|
41
58
|
repo = repo or Repo(".")
|
42
59
|
if not against:
|
43
|
-
|
44
|
-
|
60
|
+
# 'origin/main', 'origin/master', etc
|
61
|
+
against = repo.remotes.origin.refs.HEAD.reference.name
|
62
|
+
if review_subject_is_index(what):
|
45
63
|
what = None # working copy
|
46
64
|
if use_merge_base:
|
47
|
-
if what
|
65
|
+
if review_subject_is_index(what):
|
48
66
|
try:
|
49
67
|
current_ref = repo.active_branch.name
|
50
68
|
except TypeError:
|
@@ -65,7 +83,6 @@ def get_diff(
|
|
65
83
|
)
|
66
84
|
diff_content = repo.git.diff(against, what)
|
67
85
|
diff = PatchSet.from_string(diff_content)
|
68
|
-
diff = PatchSet.from_string(diff_content)
|
69
86
|
|
70
87
|
# Filter out binary files
|
71
88
|
non_binary_diff = PatchSet([])
|
@@ -76,7 +93,9 @@ def get_diff(
|
|
76
93
|
if patched_file.target_file != DEV_NULL
|
77
94
|
else patched_file.source_file
|
78
95
|
)
|
79
|
-
if file_path == DEV_NULL
|
96
|
+
if file_path == DEV_NULL:
|
97
|
+
continue
|
98
|
+
if is_binary_file(repo, file_path.lstrip("b/")):
|
80
99
|
logging.info(f"Skipping binary file: {patched_file.path}")
|
81
100
|
continue
|
82
101
|
non_binary_diff.append(patched_file)
|
@@ -102,8 +121,18 @@ def filter_diff(
|
|
102
121
|
return files
|
103
122
|
|
104
123
|
|
105
|
-
def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
|
106
|
-
|
124
|
+
def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: bool = False) -> str:
|
125
|
+
if use_local_files:
|
126
|
+
file_path = Path(repo.working_tree_dir) / file
|
127
|
+
try:
|
128
|
+
text = file_path.read_text(encoding='utf-8')
|
129
|
+
except (FileNotFoundError, UnicodeDecodeError) as e:
|
130
|
+
logging.warning(f"Could not read file {file} from working directory: {e}")
|
131
|
+
text = repo.tree()[file].data_stream.read().decode('utf-8')
|
132
|
+
else:
|
133
|
+
# Read from HEAD (committed version)
|
134
|
+
text = repo.tree()[file].data_stream.read().decode('utf-8')
|
135
|
+
|
107
136
|
lines = [f"{i + 1}: {line}\n" for i, line in enumerate(text.splitlines())]
|
108
137
|
if max_tokens:
|
109
138
|
lines, removed_qty = mc.tokenizing.fit_to_token_size(lines, max_tokens)
|
@@ -135,8 +164,8 @@ async def review(
|
|
135
164
|
use_merge_base: bool = True,
|
136
165
|
out_folder: str | PathLike | None = None,
|
137
166
|
):
|
138
|
-
cfg = ProjectConfig.load()
|
139
167
|
repo = repo or Repo(".")
|
168
|
+
cfg = ProjectConfig.load_for_repo(repo)
|
140
169
|
out_folder = Path(out_folder or repo.working_tree_dir)
|
141
170
|
diff = get_diff(
|
142
171
|
repo=repo, what=what, against=against, use_merge_base=use_merge_base
|
@@ -152,6 +181,7 @@ async def review(
|
|
152
181
|
file_diff.path,
|
153
182
|
cfg.max_code_tokens
|
154
183
|
- mc.tokenizing.num_tokens_from_string(str(file_diff)),
|
184
|
+
use_local_files=review_subject_is_index(what)
|
155
185
|
)
|
156
186
|
if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
|
157
187
|
else ""
|
@@ -186,6 +216,6 @@ async def review(
|
|
186
216
|
report.summary = make_cr_summary(cfg, report, diff)
|
187
217
|
report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
|
188
218
|
report_text = report.render(cfg, Report.Format.MARKDOWN)
|
189
|
-
print(mc.ui.yellow(report_text))
|
190
219
|
text_report_path = out_folder / "code-review-report.md"
|
191
220
|
text_report_path.write_text(report_text, encoding="utf-8")
|
221
|
+
report.to_cli()
|
@@ -1,11 +1,14 @@
|
|
1
|
+
import re
|
1
2
|
import logging
|
2
3
|
import tomllib
|
3
4
|
from dataclasses import dataclass, field
|
4
5
|
from pathlib import Path
|
5
6
|
|
6
7
|
import microcore as mc
|
8
|
+
from microcore import ui
|
9
|
+
from git import Repo
|
7
10
|
|
8
|
-
from .constants import
|
11
|
+
from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
|
9
12
|
|
10
13
|
|
11
14
|
def _detect_github_env() -> dict:
|
@@ -39,23 +42,26 @@ def _detect_github_env() -> dict:
|
|
39
42
|
}
|
40
43
|
# Fallback for local usage: try to get from git
|
41
44
|
if not repo:
|
45
|
+
git_repo = None
|
42
46
|
try:
|
43
|
-
|
44
|
-
|
45
|
-
git = GitRepo(".", search_parent_directories=True)
|
46
|
-
origin = git.remotes.origin.url
|
47
|
+
git_repo = Repo(".", search_parent_directories=True)
|
48
|
+
origin = git_repo.remotes.origin.url
|
47
49
|
# e.g. git@github.com:Nayjest/ai-code-review.git -> Nayjest/ai-code-review
|
48
|
-
import re
|
49
|
-
|
50
50
|
match = re.search(r"[:/]([\w\-]+)/([\w\-\.]+?)(\.git)?$", origin)
|
51
51
|
if match:
|
52
52
|
d["github_repo"] = f"{match.group(1)}/{match.group(2)}"
|
53
|
-
d["github_pr_sha"] =
|
53
|
+
d["github_pr_sha"] = git_repo.head.commit.hexsha
|
54
54
|
d["github_branch"] = (
|
55
|
-
|
55
|
+
git_repo.active_branch.name if hasattr(git_repo, "active_branch") else ""
|
56
56
|
)
|
57
57
|
except Exception:
|
58
58
|
pass
|
59
|
+
finally:
|
60
|
+
if git_repo:
|
61
|
+
try:
|
62
|
+
git_repo.close()
|
63
|
+
except Exception:
|
64
|
+
pass
|
59
65
|
# If branch is not a commit SHA, prefer branch for links
|
60
66
|
if d["github_branch"]:
|
61
67
|
d["github_pr_sha_or_branch"] = d["github_branch"]
|
@@ -72,6 +78,8 @@ class ProjectConfig:
|
|
72
78
|
summary_prompt: str = ""
|
73
79
|
report_template_md: str = ""
|
74
80
|
"""Markdown report template"""
|
81
|
+
report_template_cli: str = ""
|
82
|
+
"""Report template for CLI output"""
|
75
83
|
post_process: str = ""
|
76
84
|
retries: int = 3
|
77
85
|
"""LLM retries for one request"""
|
@@ -79,21 +87,33 @@ class ProjectConfig:
|
|
79
87
|
prompt_vars: dict = field(default_factory=dict)
|
80
88
|
|
81
89
|
@staticmethod
|
82
|
-
def
|
83
|
-
|
84
|
-
with open(PROJECT_CONFIG_DEFAULTS_FILE, "rb") as f:
|
90
|
+
def _read_bundled_defaults() -> dict:
|
91
|
+
with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
|
85
92
|
config = tomllib.load(f)
|
93
|
+
return config
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def load_for_repo(repo: Repo):
|
97
|
+
return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def load(config_path: str | Path | None = None) -> "ProjectConfig":
|
101
|
+
config = ProjectConfig._read_bundled_defaults()
|
86
102
|
github_env = _detect_github_env()
|
87
103
|
config["prompt_vars"] |= github_env | dict(github_env=github_env)
|
88
|
-
|
104
|
+
|
105
|
+
config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
|
106
|
+
if config_path.exists():
|
89
107
|
logging.info(
|
90
|
-
f"Loading project-specific configuration from {mc.utils.file_link(
|
108
|
+
f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
|
91
109
|
default_prompt_vars = config["prompt_vars"]
|
92
|
-
with open(
|
110
|
+
with open(config_path, "rb") as f:
|
93
111
|
config.update(tomllib.load(f))
|
94
112
|
# overriding prompt_vars config section will not empty default values
|
95
113
|
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
96
114
|
else:
|
97
|
-
logging.info(
|
115
|
+
logging.info(
|
116
|
+
f"No project config found at {ui.blue(config_path)}, using defaults"
|
117
|
+
)
|
98
118
|
|
99
119
|
return ProjectConfig(**config)
|
@@ -3,12 +3,16 @@ import logging
|
|
3
3
|
from dataclasses import dataclass, field, asdict
|
4
4
|
from datetime import datetime
|
5
5
|
from enum import StrEnum
|
6
|
+
from pathlib import Path
|
6
7
|
|
7
8
|
import microcore as mc
|
9
|
+
from colorama import Fore, Style, Back
|
10
|
+
from microcore.utils import file_link
|
11
|
+
import textwrap
|
8
12
|
|
9
13
|
from .constants import JSON_REPORT_FILE_NAME
|
10
14
|
from .project_config import ProjectConfig
|
11
|
-
from .utils import syntax_hint
|
15
|
+
from .utils import syntax_hint, block_wrap_lr, max_line_len
|
12
16
|
|
13
17
|
|
14
18
|
@dataclass
|
@@ -57,6 +61,7 @@ class Issue:
|
|
57
61
|
class Report:
|
58
62
|
class Format(StrEnum):
|
59
63
|
MARKDOWN = "md"
|
64
|
+
CLI = "cli"
|
60
65
|
|
61
66
|
issues: dict = field(default_factory=dict)
|
62
67
|
summary: str = field(default="")
|
@@ -94,15 +99,34 @@ class Report:
|
|
94
99
|
logging.info(f"Report saved to {mc.utils.file_link(file_name)}")
|
95
100
|
|
96
101
|
@staticmethod
|
97
|
-
def load(file_name: str = ""):
|
102
|
+
def load(file_name: str | Path = ""):
|
98
103
|
with open(file_name or JSON_REPORT_FILE_NAME, "r") as f:
|
99
104
|
data = json.load(f)
|
100
105
|
data.pop("total_issues", None)
|
101
106
|
return Report(**data)
|
102
107
|
|
103
108
|
def render(
|
104
|
-
self,
|
109
|
+
self,
|
110
|
+
config: ProjectConfig = None,
|
111
|
+
report_format: Format = Format.MARKDOWN,
|
105
112
|
) -> str:
|
106
|
-
|
107
|
-
template = getattr(
|
108
|
-
return mc.prompt(
|
113
|
+
config = config or ProjectConfig.load()
|
114
|
+
template = getattr(config, f"report_template_{report_format}")
|
115
|
+
return mc.prompt(
|
116
|
+
template,
|
117
|
+
report=self,
|
118
|
+
ui=mc.ui,
|
119
|
+
Fore=Fore,
|
120
|
+
Style=Style,
|
121
|
+
Back=Back,
|
122
|
+
file_link=file_link,
|
123
|
+
textwrap=textwrap,
|
124
|
+
block_wrap_lr=block_wrap_lr,
|
125
|
+
max_line_len=max_line_len,
|
126
|
+
**config.prompt_vars
|
127
|
+
)
|
128
|
+
|
129
|
+
def to_cli(self, report_format=Format.CLI):
|
130
|
+
output = self.render(report_format=report_format)
|
131
|
+
print("")
|
132
|
+
print(output)
|
{ai_code_review → gito}/utils.py
RENAMED
@@ -112,5 +112,21 @@ def parse_refs_pair(refs: str) -> tuple[str | None, str | None]:
|
|
112
112
|
return None, None
|
113
113
|
if SEPARATOR not in refs:
|
114
114
|
return refs, None
|
115
|
-
what, against = refs.split(SEPARATOR)
|
115
|
+
what, against = refs.split(SEPARATOR, 1)
|
116
116
|
return what or None, against or None
|
117
|
+
|
118
|
+
|
119
|
+
def max_line_len(text: str) -> int:
|
120
|
+
return max((len(line) for line in text.splitlines()), default=0)
|
121
|
+
|
122
|
+
|
123
|
+
def block_wrap_lr(text: str, left: str = "", right: str = "", max_rwrap: int = 60) -> str:
|
124
|
+
ml = max_line_len(text)
|
125
|
+
lines = text.splitlines()
|
126
|
+
wrapped_lines = []
|
127
|
+
for line in lines:
|
128
|
+
ln = left+line
|
129
|
+
if ml <= max_rwrap:
|
130
|
+
ln += ' ' * (ml - len(line)) + right
|
131
|
+
wrapped_lines.append(ln)
|
132
|
+
return "\n".join(wrapped_lines)
|
ai_code_review/constants.py
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
|
3
|
-
|
4
|
-
PROJECT_CONFIG_FILE = Path(".ai-code-review.toml")
|
5
|
-
PROJECT_CONFIG_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE
|
6
|
-
ENV_CONFIG_FILE = Path("~/.env.ai-code-review").expanduser()
|
7
|
-
JSON_REPORT_FILE_NAME = "code-review-report.json"
|
ai_cr-1.0.1.dist-info/METADATA
DELETED
@@ -1,197 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.3
|
2
|
-
Name: ai-cr
|
3
|
-
Version: 1.0.1
|
4
|
-
Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
|
5
|
-
License: MIT
|
6
|
-
Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
|
7
|
-
Author: Nayjest
|
8
|
-
Author-email: mail@vitaliy.in
|
9
|
-
Requires-Python: >=3.11,<4.0
|
10
|
-
Classifier: Environment :: Console
|
11
|
-
Classifier: Intended Audience :: Developers
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
17
|
-
Classifier: Topic :: Software Development
|
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)
|
24
|
-
Project-URL: Homepage, https://github.com/Nayjest/github-ai-code-review
|
25
|
-
Project-URL: Repository, https://github.com/Nayjest/github-ai-code-review
|
26
|
-
Description-Content-Type: text/markdown
|
27
|
-
|
28
|
-
<p align="right">
|
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>
|
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>
|
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
|
-
<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
|
-
</p>
|
35
|
-
|
36
|
-
# 🤖 AI Code Review Tool
|
37
|
-
|
38
|
-
An AI-powered GitHub code review tool that uses LLMs to detect high-confidence, high-impact issues—such as security vulnerabilities, bugs, and maintainability concerns.
|
39
|
-
|
40
|
-
## ✨ Features
|
41
|
-
|
42
|
-
- Automatically reviews pull requests via GitHub Actions
|
43
|
-
- Focuses on critical issues (e.g., bugs, security risks, design flaws)
|
44
|
-
- Posts review results as a comment on your PR
|
45
|
-
- Can be used locally; works with both local and remote Git repositories
|
46
|
-
- Optional, fun AI-generated code awards 🏆
|
47
|
-
- Easily configurable via [`.ai-code-review.toml`](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml) in your repository root
|
48
|
-
- Extremely fast, parallel LLM usage
|
49
|
-
- Model-agnostic (OpenAI, Anthropic, Google, local PyTorch inference, etc.)
|
50
|
-
|
51
|
-
See code review in action: [example](https://github.com/Nayjest/ai-code-review/pull/39#issuecomment-2906968729)
|
52
|
-
|
53
|
-
## 🚀 Quickstart
|
54
|
-
|
55
|
-
### 1. Review Pull Requests via GitHub Actions
|
56
|
-
|
57
|
-
Create a `.github/workflows/ai-code-review.yml` file:
|
58
|
-
|
59
|
-
```yaml
|
60
|
-
name: AI Code Review
|
61
|
-
on: { pull_request: { types: [opened, synchronize, reopened] } }
|
62
|
-
jobs:
|
63
|
-
review:
|
64
|
-
runs-on: ubuntu-latest
|
65
|
-
permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
|
66
|
-
steps:
|
67
|
-
- uses: actions/checkout@v4
|
68
|
-
with: { fetch-depth: 0 }
|
69
|
-
- name: Set up Python
|
70
|
-
uses: actions/setup-python@v5
|
71
|
-
with: { python-version: "3.13" }
|
72
|
-
- name: Install AI Code Review tool
|
73
|
-
run: pip install ai-code-review~=1.0
|
74
|
-
- name: Run AI code analysis
|
75
|
-
env:
|
76
|
-
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
77
|
-
LLM_API_TYPE: openai
|
78
|
-
MODEL: "gpt-4.1"
|
79
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
80
|
-
run: |
|
81
|
-
ai-code-review
|
82
|
-
ai-code-review github-comment --token ${{ secrets.GITHUB_TOKEN }}
|
83
|
-
- uses: actions/upload-artifact@v4
|
84
|
-
with:
|
85
|
-
name: ai-code-review-results
|
86
|
-
path: |
|
87
|
-
code-review-report.md
|
88
|
-
code-review-report.json
|
89
|
-
```
|
90
|
-
|
91
|
-
> ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
|
92
|
-
|
93
|
-
💪 Done!
|
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.
|
96
|
-
|
97
|
-
### 2. Running Code Analysis Locally
|
98
|
-
|
99
|
-
#### Initial Local Setup
|
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)).
|
104
|
-
```bash
|
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
|
112
|
-
|
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
|
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.
|
124
|
-
|
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
|
132
|
-
ai-code-review
|
133
|
-
```
|
134
|
-
|
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**
|
140
|
-
|
141
|
-
```bash
|
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
|
147
|
-
```
|
148
|
-
|
149
|
-
## 🔧 Configuration
|
150
|
-
|
151
|
-
Change behavior via `.ai-code-review.toml`:
|
152
|
-
|
153
|
-
- Prompt templates, filtering and post-processing using Python code snippets
|
154
|
-
- Tagging, severity, and confidence settings
|
155
|
-
- Custom AI awards for developer brilliance
|
156
|
-
- Output customization
|
157
|
-
|
158
|
-
You can override the default config by placing `.ai-code-review.toml` in your repo root.
|
159
|
-
|
160
|
-
|
161
|
-
See default configuration [here](https://github.com/Nayjest/ai-code-review/blob/main/ai_code_review/.ai-code-review.toml).
|
162
|
-
|
163
|
-
More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/ai-code-review/blob/main/documentation/config_cookbook.md)
|
164
|
-
|
165
|
-
## 💻 Development Setup
|
166
|
-
|
167
|
-
Install dependencies:
|
168
|
-
|
169
|
-
```bash
|
170
|
-
make install
|
171
|
-
```
|
172
|
-
|
173
|
-
Format code and check style:
|
174
|
-
|
175
|
-
```bash
|
176
|
-
make black
|
177
|
-
make cs
|
178
|
-
```
|
179
|
-
|
180
|
-
Run tests:
|
181
|
-
|
182
|
-
```bash
|
183
|
-
pytest
|
184
|
-
```
|
185
|
-
|
186
|
-
## 🤝 Contributing
|
187
|
-
|
188
|
-
**Looking for a specific feature or having trouble?**
|
189
|
-
Contributions are welcome! ❤️
|
190
|
-
See [CONTRIBUTING.md](https://github.com/Nayjest/ai-code-review/blob/main/CONTRIBUTING.md) for details.
|
191
|
-
|
192
|
-
## 📝 License
|
193
|
-
|
194
|
-
Licensed under the [MIT License](https://github.com/Nayjest/ai-code-review/blob/main/LICENSE).
|
195
|
-
|
196
|
-
© 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
|
197
|
-
|
ai_cr-1.0.1.dist-info/RECORD
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
ai_code_review/.ai-code-review.toml,sha256=t5Q7oRzc-qsmOOYdXIx5sR6ss9OEertoF-ttG2pJ6Z4,10722
|
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.1.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
|
13
|
-
ai_cr-1.0.1.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
14
|
-
ai_cr-1.0.1.dist-info/METADATA,sha256=Agf_IHJQ-bRJ545lfta0htTSoBivCGH7pw7f6qfv1xM,7109
|
15
|
-
ai_cr-1.0.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
16
|
-
ai_cr-1.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|