code-review-ai-cli 2.2.5__tar.gz → 2.3.0__tar.gz
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.
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/PKG-INFO +4 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/README.md +1 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/PKG-INFO +4 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/requires.txt +2 -4
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/pyproject.toml +3 -3
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/ai_review.py +266 -3
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/config.py +2 -2
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/context_extractor.py +96 -190
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/formatter.py +26 -0
- code_review_ai_cli-2.3.0/src/git_utils.py +628 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/llm_client.py +77 -76
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/prompts/config.yaml.template +3 -3
- code_review_ai_cli-2.3.0/src/prompts/review_prompt.md.template +21 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/tfs_client.py +9 -13
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_ai_review.py +97 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_context_extractor.py +94 -88
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_formatter.py +24 -1
- code_review_ai_cli-2.3.0/tests/test_git_utils.py +318 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_llm_client.py +87 -17
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_tfs_client.py +36 -5
- code_review_ai_cli-2.2.5/src/git_utils.py +0 -282
- code_review_ai_cli-2.2.5/src/prompts/review_prompt.md.template +0 -85
- code_review_ai_cli-2.2.5/tests/test_git_utils.py +0 -159
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/top_level.txt +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/setup.cfg +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/__init__.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/prompt_utils.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/src/token_estimator.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_config.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_prompt_utils.py +0 -0
- {code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/tests/test_token_estimator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -19,6 +19,8 @@ Requires-Dist: requests>=2.28.0
|
|
|
19
19
|
Requires-Dist: PyYAML>=6.0
|
|
20
20
|
Requires-Dist: litellm>=1.40.0
|
|
21
21
|
Requires-Dist: tiktoken>=0.7.0
|
|
22
|
+
Requires-Dist: tree-sitter<0.22,>=0.20
|
|
23
|
+
Requires-Dist: tree-sitter-languages>=1.10.2
|
|
22
24
|
Provides-Extra: bedrock
|
|
23
25
|
Requires-Dist: boto3>=1.34.0; extra == "bedrock"
|
|
24
26
|
Provides-Extra: openai
|
|
@@ -27,8 +29,6 @@ Provides-Extra: gemini
|
|
|
27
29
|
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
30
|
Provides-Extra: claude
|
|
29
31
|
Requires-Dist: anthropic>=0.18.0; extra == "claude"
|
|
30
|
-
Provides-Extra: tree-sitter
|
|
31
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "tree-sitter"
|
|
32
32
|
Provides-Extra: all
|
|
33
33
|
Requires-Dist: boto3>=1.34.0; extra == "all"
|
|
34
34
|
Requires-Dist: litellm>=1.40.0; extra == "all"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
|
|
|
36
36
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
37
37
|
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: anthropic>=0.18.0; extra == "all"
|
|
39
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "all"
|
|
40
39
|
Provides-Extra: dev
|
|
41
40
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
41
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
@@ -49,6 +48,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
|
|
|
49
48
|
## Features
|
|
50
49
|
|
|
51
50
|
- AI Pull Request review (`pr-review`)
|
|
51
|
+
- Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
|
|
52
52
|
- Structured PR comments (inline + general summary)
|
|
53
53
|
- `dry-run` mode to validate without posting
|
|
54
54
|
- PR listing with filters (`list-prs`)
|
|
@@ -5,6 +5,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- AI Pull Request review (`pr-review`)
|
|
8
|
+
- Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
|
|
8
9
|
- Structured PR comments (inline + general summary)
|
|
9
10
|
- `dry-run` mode to validate without posting
|
|
10
11
|
- PR listing with filters (`list-prs`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -19,6 +19,8 @@ Requires-Dist: requests>=2.28.0
|
|
|
19
19
|
Requires-Dist: PyYAML>=6.0
|
|
20
20
|
Requires-Dist: litellm>=1.40.0
|
|
21
21
|
Requires-Dist: tiktoken>=0.7.0
|
|
22
|
+
Requires-Dist: tree-sitter<0.22,>=0.20
|
|
23
|
+
Requires-Dist: tree-sitter-languages>=1.10.2
|
|
22
24
|
Provides-Extra: bedrock
|
|
23
25
|
Requires-Dist: boto3>=1.34.0; extra == "bedrock"
|
|
24
26
|
Provides-Extra: openai
|
|
@@ -27,8 +29,6 @@ Provides-Extra: gemini
|
|
|
27
29
|
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
30
|
Provides-Extra: claude
|
|
29
31
|
Requires-Dist: anthropic>=0.18.0; extra == "claude"
|
|
30
|
-
Provides-Extra: tree-sitter
|
|
31
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "tree-sitter"
|
|
32
32
|
Provides-Extra: all
|
|
33
33
|
Requires-Dist: boto3>=1.34.0; extra == "all"
|
|
34
34
|
Requires-Dist: litellm>=1.40.0; extra == "all"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
|
|
|
36
36
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
37
37
|
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: anthropic>=0.18.0; extra == "all"
|
|
39
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "all"
|
|
40
39
|
Provides-Extra: dev
|
|
41
40
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
41
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
@@ -49,6 +48,7 @@ AI Code Review is an automated AI-powered code review CLI, compatible with vario
|
|
|
49
48
|
## Features
|
|
50
49
|
|
|
51
50
|
- AI Pull Request review (`pr-review`)
|
|
51
|
+
- Local Git review without a PR (`local` / `local-review` — working tree, `--staged`, `--from <branch>`, `--commit <sha>`)
|
|
52
52
|
- Structured PR comments (inline + general summary)
|
|
53
53
|
- `dry-run` mode to validate without posting
|
|
54
54
|
- PR listing with filters (`list-prs`)
|
{code_review_ai_cli-2.2.5 → code_review_ai_cli-2.3.0}/code_review_ai_cli.egg-info/requires.txt
RENAMED
|
@@ -2,6 +2,8 @@ requests>=2.28.0
|
|
|
2
2
|
PyYAML>=6.0
|
|
3
3
|
litellm>=1.40.0
|
|
4
4
|
tiktoken>=0.7.0
|
|
5
|
+
tree-sitter<0.22,>=0.20
|
|
6
|
+
tree-sitter-languages>=1.10.2
|
|
5
7
|
|
|
6
8
|
[all]
|
|
7
9
|
boto3>=1.34.0
|
|
@@ -10,7 +12,6 @@ tiktoken>=0.7.0
|
|
|
10
12
|
openai>=1.0.0
|
|
11
13
|
google-generativeai>=0.3.0
|
|
12
14
|
anthropic>=0.18.0
|
|
13
|
-
tree-sitter-languages>=1.10.2
|
|
14
15
|
|
|
15
16
|
[bedrock]
|
|
16
17
|
boto3>=1.34.0
|
|
@@ -28,6 +29,3 @@ google-generativeai>=0.3.0
|
|
|
28
29
|
|
|
29
30
|
[openai]
|
|
30
31
|
openai>=1.0.0
|
|
31
|
-
|
|
32
|
-
[tree-sitter]
|
|
33
|
-
tree-sitter-languages>=1.10.2
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-review-ai-cli"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,8 @@ dependencies = [
|
|
|
26
26
|
"PyYAML>=6.0",
|
|
27
27
|
"litellm>=1.40.0",
|
|
28
28
|
"tiktoken>=0.7.0",
|
|
29
|
+
"tree-sitter>=0.20,<0.22",
|
|
30
|
+
"tree-sitter-languages>=1.10.2",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
@@ -33,7 +35,6 @@ bedrock = ["boto3>=1.34.0"]
|
|
|
33
35
|
openai = ["openai>=1.0.0"]
|
|
34
36
|
gemini = ["google-generativeai>=0.3.0"]
|
|
35
37
|
claude = ["anthropic>=0.18.0"]
|
|
36
|
-
tree-sitter = ["tree-sitter-languages>=1.10.2"]
|
|
37
38
|
all = [
|
|
38
39
|
"boto3>=1.34.0",
|
|
39
40
|
"litellm>=1.40.0",
|
|
@@ -41,7 +42,6 @@ all = [
|
|
|
41
42
|
"openai>=1.0.0",
|
|
42
43
|
"google-generativeai>=0.3.0",
|
|
43
44
|
"anthropic>=0.18.0",
|
|
44
|
-
"tree-sitter-languages>=1.10.2",
|
|
45
45
|
]
|
|
46
46
|
dev = [
|
|
47
47
|
"pytest>=8.3.0",
|
|
@@ -13,6 +13,7 @@ Usage:
|
|
|
13
13
|
python ai_review.py # Interactive mode (main menu)
|
|
14
14
|
python ai_review.py pr-review # List PRs and do interactive review
|
|
15
15
|
python ai_review.py pr-review <id> # Direct review of a specific PR
|
|
16
|
+
python ai_review.py local # Review local Git changes (uncommitted/staged/branch)
|
|
16
17
|
python ai_review.py list-prs # List active Pull Requests
|
|
17
18
|
|
|
18
19
|
Options:
|
|
@@ -82,7 +83,8 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
82
83
|
PROJECT_ROOT = _ensure_project_root_on_path(__file__)
|
|
83
84
|
|
|
84
85
|
from src.config import ReviewConfig, VALID_PROVIDERS # noqa: E402
|
|
85
|
-
from src.
|
|
86
|
+
from src.context_extractor import ContextExtractor # noqa: E402
|
|
87
|
+
from src.git_utils import GitError, GitUtils # noqa: E402
|
|
86
88
|
from src.llm_client import LLMClient, LLMError # noqa: E402
|
|
87
89
|
from src.formatter import ReviewFormatter, Colors, save_output # noqa: E402
|
|
88
90
|
from src import __version__ as VERSION # noqa: E402
|
|
@@ -162,6 +164,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
162
164
|
" %(prog)s pr-review # List PRs and select\n"
|
|
163
165
|
" %(prog)s pr-review 42 --dry-run # Review PR #42 without posting\n"
|
|
164
166
|
" %(prog)s pr-review 42 --provider bedrock # Review PR #42 with Bedrock\n"
|
|
167
|
+
" %(prog)s local # Review local uncommitted changes\n"
|
|
168
|
+
" %(prog)s local --staged # Review staged changes\n"
|
|
169
|
+
" %(prog)s local --from main # Review branch diff against main\n"
|
|
165
170
|
" %(prog)s list-prs --status active # List active PRs\n"
|
|
166
171
|
),
|
|
167
172
|
)
|
|
@@ -186,6 +191,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
186
191
|
sub_pr_review.add_argument("--target-branch", default=None,
|
|
187
192
|
help="Filter PRs by target branch")
|
|
188
193
|
|
|
194
|
+
# --- local / local-review (LOCAL GIT MODE) ---
|
|
195
|
+
sub_local = subparsers.add_parser(
|
|
196
|
+
"local",
|
|
197
|
+
aliases=["local-review"],
|
|
198
|
+
help="💻 Local Git Review (uncommitted, staged, branch range, or commit)",
|
|
199
|
+
)
|
|
200
|
+
sub_local.add_argument(
|
|
201
|
+
"--staged", "-s", action="store_true",
|
|
202
|
+
help="Review only staged changes (git diff --cached)",
|
|
203
|
+
)
|
|
204
|
+
sub_local.add_argument(
|
|
205
|
+
"--from", "--base", dest="from_ref", default=None, metavar="REF",
|
|
206
|
+
help="Base branch or commit to compare from (e.g., main, origin/main)",
|
|
207
|
+
)
|
|
208
|
+
sub_local.add_argument(
|
|
209
|
+
"--to", dest="to_ref", default=None, metavar="REF",
|
|
210
|
+
help="Target branch or commit to compare to (default: HEAD when --from is used)",
|
|
211
|
+
)
|
|
212
|
+
sub_local.add_argument(
|
|
213
|
+
"--commit", default=None, metavar="SHA",
|
|
214
|
+
help="Review a specific commit (compares <commit>~1 to <commit>)",
|
|
215
|
+
)
|
|
216
|
+
sub_local.add_argument(
|
|
217
|
+
"--repo-path", default=None, metavar="PATH",
|
|
218
|
+
help="Path to the local Git repository (default: current directory)",
|
|
219
|
+
)
|
|
220
|
+
|
|
189
221
|
# --- init ---
|
|
190
222
|
subparsers.add_parser(
|
|
191
223
|
"init", help="Create a config.yaml template in the current directory"
|
|
@@ -203,7 +235,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
203
235
|
help="Filter by author")
|
|
204
236
|
|
|
205
237
|
# --- Global options ---
|
|
206
|
-
all_subs = [sub_pr_review, sub_list_prs]
|
|
238
|
+
all_subs = [sub_pr_review, sub_local, sub_list_prs]
|
|
207
239
|
for sub in all_subs:
|
|
208
240
|
_add_global_options(sub)
|
|
209
241
|
|
|
@@ -294,6 +326,26 @@ def _save_pr_review_output(output_file: str, pr_id: int, repo_name: str,
|
|
|
294
326
|
save_output(md_output, output_file)
|
|
295
327
|
|
|
296
328
|
|
|
329
|
+
def _save_local_review_output(output_file: str, repo_name: str,
|
|
330
|
+
branch_label: str, review_text: str,
|
|
331
|
+
was_truncated: bool) -> None:
|
|
332
|
+
"""Saves the local Git review output if a target file was provided."""
|
|
333
|
+
if not output_file:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
md_formatter = ReviewFormatter(color=False, output_format="markdown")
|
|
337
|
+
md_output = "\n".join([
|
|
338
|
+
md_formatter.format_header(
|
|
339
|
+
review_type="Local Git Review",
|
|
340
|
+
repo_name=repo_name,
|
|
341
|
+
branch=branch_label,
|
|
342
|
+
),
|
|
343
|
+
review_text,
|
|
344
|
+
md_formatter.format_footer(truncated=was_truncated),
|
|
345
|
+
])
|
|
346
|
+
save_output(md_output, output_file)
|
|
347
|
+
|
|
348
|
+
|
|
297
349
|
# ---------------------------------------------------------------------------
|
|
298
350
|
# Command execution functions
|
|
299
351
|
# ---------------------------------------------------------------------------
|
|
@@ -348,6 +400,8 @@ def run_review(args: argparse.Namespace) -> int:
|
|
|
348
400
|
|
|
349
401
|
if command == "pr-review":
|
|
350
402
|
return run_pr_review_workflow(args, config, formatter)
|
|
403
|
+
elif command in ("local", "local-review"):
|
|
404
|
+
return run_local_review_workflow(args, config, formatter)
|
|
351
405
|
elif command == "list-prs":
|
|
352
406
|
return run_list_prs(args, config, formatter)
|
|
353
407
|
|
|
@@ -713,6 +767,174 @@ def _select_comments_to_post(comments: list[dict],
|
|
|
713
767
|
return []
|
|
714
768
|
|
|
715
769
|
|
|
770
|
+
# ---------------------------------------------------------------------------
|
|
771
|
+
# Local Git Review
|
|
772
|
+
# ---------------------------------------------------------------------------
|
|
773
|
+
def run_local_review_workflow(args: argparse.Namespace, config: ReviewConfig,
|
|
774
|
+
formatter: ReviewFormatter) -> int:
|
|
775
|
+
"""
|
|
776
|
+
Local Git review workflow (working tree, staged, branch range, or commit).
|
|
777
|
+
Does not require TFS/Azure DevOps configuration.
|
|
778
|
+
"""
|
|
779
|
+
repo_path = getattr(args, "repo_path", None)
|
|
780
|
+
try:
|
|
781
|
+
git_utils = GitUtils(repo_path=repo_path)
|
|
782
|
+
except GitError as exc:
|
|
783
|
+
print(formatter.format_error(str(exc)))
|
|
784
|
+
return 1
|
|
785
|
+
|
|
786
|
+
staged = bool(getattr(args, "staged", False))
|
|
787
|
+
from_ref = getattr(args, "from_ref", None)
|
|
788
|
+
to_ref = getattr(args, "to_ref", None)
|
|
789
|
+
commit = getattr(args, "commit", None)
|
|
790
|
+
|
|
791
|
+
repo_name = git_utils.get_repo_name()
|
|
792
|
+
branch = git_utils.get_current_branch()
|
|
793
|
+
|
|
794
|
+
if commit:
|
|
795
|
+
target_label = f"Commit {commit}"
|
|
796
|
+
elif from_ref:
|
|
797
|
+
target_label = f"{from_ref} → {to_ref or 'HEAD'}"
|
|
798
|
+
elif staged:
|
|
799
|
+
target_label = "Staged changes (--cached)"
|
|
800
|
+
else:
|
|
801
|
+
target_label = "Uncommitted changes (working tree)"
|
|
802
|
+
|
|
803
|
+
print(formatter.format_progress(f"Inspecting local Git changes ({target_label})"))
|
|
804
|
+
|
|
805
|
+
try:
|
|
806
|
+
changed_files = git_utils.get_local_changed_files(
|
|
807
|
+
staged=staged,
|
|
808
|
+
from_ref=from_ref,
|
|
809
|
+
to_ref=to_ref,
|
|
810
|
+
commit=commit,
|
|
811
|
+
)
|
|
812
|
+
except GitError as exc:
|
|
813
|
+
print(formatter.format_error(str(exc)))
|
|
814
|
+
return 1
|
|
815
|
+
|
|
816
|
+
if not changed_files:
|
|
817
|
+
print(formatter.format_warning("No local code changes found to review."))
|
|
818
|
+
return 0
|
|
819
|
+
|
|
820
|
+
local_details = {
|
|
821
|
+
"repository": repo_name,
|
|
822
|
+
"branch": branch,
|
|
823
|
+
"target": target_label,
|
|
824
|
+
"changed_files": changed_files,
|
|
825
|
+
}
|
|
826
|
+
print(formatter.format_local_details(local_details))
|
|
827
|
+
|
|
828
|
+
if config.debug_dump and not config.debug_dump_file:
|
|
829
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
830
|
+
config.debug_dump_file = os.path.join("logs", f"local_{timestamp}_debug.log")
|
|
831
|
+
os.makedirs(os.path.dirname(config.debug_dump_file), exist_ok=True)
|
|
832
|
+
print(formatter.format_info(f"🐛 Debug dump enabled: {config.debug_dump_file}"))
|
|
833
|
+
|
|
834
|
+
print(formatter.format_progress("Building local diff and AST context"))
|
|
835
|
+
|
|
836
|
+
extractor = ContextExtractor(
|
|
837
|
+
max_scope_blocks=config.max_scope_blocks,
|
|
838
|
+
max_scope_lines=config.max_scope_lines,
|
|
839
|
+
adaptive_context_lines=config.adaptive_context_lines,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
diff = git_utils.get_local_diff(
|
|
844
|
+
review_scope=config.review_scope,
|
|
845
|
+
excluded_paths=config.excluded_paths or None,
|
|
846
|
+
file_extensions_filter=config.file_extensions_filter or None,
|
|
847
|
+
staged=staged,
|
|
848
|
+
from_ref=from_ref,
|
|
849
|
+
to_ref=to_ref,
|
|
850
|
+
commit=commit,
|
|
851
|
+
context_extractor=extractor,
|
|
852
|
+
)
|
|
853
|
+
except GitError as exc:
|
|
854
|
+
print(formatter.format_error(str(exc)))
|
|
855
|
+
return 1
|
|
856
|
+
|
|
857
|
+
if not diff.strip():
|
|
858
|
+
print(formatter.format_warning("No eligible code changes found after filtering."))
|
|
859
|
+
return 0
|
|
860
|
+
|
|
861
|
+
print(formatter.format_info(
|
|
862
|
+
f"Provider: {config.llm_provider} | Model: {config.model} | "
|
|
863
|
+
f"Mode: {config.verbosity} | Scope: {config.review_scope}"
|
|
864
|
+
))
|
|
865
|
+
|
|
866
|
+
diff = git_utils.filter_diff_additions_only(diff)
|
|
867
|
+
if not diff.strip():
|
|
868
|
+
print(formatter.format_warning(
|
|
869
|
+
"After filtering additions only, the diff is empty. No new code to review."
|
|
870
|
+
))
|
|
871
|
+
return 0
|
|
872
|
+
|
|
873
|
+
diff_limited, files_limited, omitted_files = git_utils.limit_diff_files(
|
|
874
|
+
diff,
|
|
875
|
+
config.max_diff_files,
|
|
876
|
+
)
|
|
877
|
+
if files_limited:
|
|
878
|
+
print(formatter.format_warning(
|
|
879
|
+
f"Diff truncated to {config.max_diff_files} files. "
|
|
880
|
+
f"{omitted_files} file(s) omitted."
|
|
881
|
+
))
|
|
882
|
+
|
|
883
|
+
files_summary = git_utils.get_changed_files_summary(diff_limited)
|
|
884
|
+
|
|
885
|
+
diff_truncated, was_truncated = git_utils.truncate_diff(
|
|
886
|
+
diff_limited,
|
|
887
|
+
config.max_diff_lines,
|
|
888
|
+
)
|
|
889
|
+
if was_truncated:
|
|
890
|
+
print(formatter.format_warning(
|
|
891
|
+
f"Diff truncated to {config.max_diff_lines} lines per file."
|
|
892
|
+
))
|
|
893
|
+
|
|
894
|
+
progress = ProgressIndicator("Analyzing local code with AI (may take 30-60s)")
|
|
895
|
+
progress.start()
|
|
896
|
+
start_time = time.time()
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
llm = LLMClient(config)
|
|
900
|
+
review_text, structured_comments = llm.review_pr(
|
|
901
|
+
diff=diff_truncated,
|
|
902
|
+
files_summary=files_summary,
|
|
903
|
+
context=getattr(args, "context", ""),
|
|
904
|
+
review_scope=config.review_scope,
|
|
905
|
+
)
|
|
906
|
+
except LLMError as exc:
|
|
907
|
+
progress.stop()
|
|
908
|
+
print(formatter.format_error(str(exc)))
|
|
909
|
+
return 1
|
|
910
|
+
|
|
911
|
+
elapsed = time.time() - start_time
|
|
912
|
+
progress.stop(formatter.format_success(f"Review completed in {elapsed:.1f}s"))
|
|
913
|
+
|
|
914
|
+
token_estimate = getattr(llm, "last_token_estimate", None)
|
|
915
|
+
if token_estimate is not None and token_estimate.total_prompt_tokens is not None:
|
|
916
|
+
print(formatter.format_info(
|
|
917
|
+
f"Total Prompt: {token_estimate.total_prompt_tokens} tokens"
|
|
918
|
+
))
|
|
919
|
+
else:
|
|
920
|
+
print(formatter.format_warning(
|
|
921
|
+
"Total Prompt: unavailable (LiteLLM and local tiktoken could not count the model)"
|
|
922
|
+
))
|
|
923
|
+
|
|
924
|
+
print(formatter.format_structured_comments(structured_comments))
|
|
925
|
+
|
|
926
|
+
output_file = config.output_file or getattr(args, "output", "")
|
|
927
|
+
_save_local_review_output(
|
|
928
|
+
output_file=output_file,
|
|
929
|
+
repo_name=repo_name,
|
|
930
|
+
branch_label=f"{branch} ({target_label})",
|
|
931
|
+
review_text=review_text,
|
|
932
|
+
was_truncated=was_truncated,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
return 0
|
|
936
|
+
|
|
937
|
+
|
|
716
938
|
# ---------------------------------------------------------------------------
|
|
717
939
|
# List PRs
|
|
718
940
|
# ---------------------------------------------------------------------------
|
|
@@ -768,12 +990,15 @@ def interactive_mode() -> int:
|
|
|
768
990
|
print(f" {c.CYAN}1{c.RESET}) 🌟 Pull Request Review (list PRs and select)")
|
|
769
991
|
print(f" {c.CYAN}2{c.RESET}) 📋 List active Pull Requests")
|
|
770
992
|
print("")
|
|
993
|
+
print(f" {c.CYAN}{c.BOLD}── Local Git ──{c.RESET}")
|
|
994
|
+
print(f" {c.CYAN}4{c.RESET}) 💻 Local Git Review (working tree, staged, or branch diff)")
|
|
995
|
+
print("")
|
|
771
996
|
print(f" {c.CYAN}{c.BOLD}── Other ──{c.RESET}")
|
|
772
997
|
print(f" {c.CYAN}3{c.RESET}) Current configuration")
|
|
773
998
|
print(f" {c.CYAN}0{c.RESET}) Exit")
|
|
774
999
|
|
|
775
1000
|
try:
|
|
776
|
-
choice = input(f"\n{c.BOLD}Choose [0-
|
|
1001
|
+
choice = input(f"\n{c.BOLD}Choose [0-4]: {c.RESET}").strip()
|
|
777
1002
|
except (KeyboardInterrupt, EOFError):
|
|
778
1003
|
print("\n")
|
|
779
1004
|
return 0
|
|
@@ -792,6 +1017,9 @@ def interactive_mode() -> int:
|
|
|
792
1017
|
_show_config(config)
|
|
793
1018
|
return 0
|
|
794
1019
|
|
|
1020
|
+
if choice == "4":
|
|
1021
|
+
return _interactive_local_review(config)
|
|
1022
|
+
|
|
795
1023
|
print(f"{c.RED}Invalid option.{c.RESET}")
|
|
796
1024
|
return 1
|
|
797
1025
|
|
|
@@ -836,6 +1064,41 @@ def _interactive_list_prs(config: ReviewConfig) -> int:
|
|
|
836
1064
|
return run_review(parsed)
|
|
837
1065
|
|
|
838
1066
|
|
|
1067
|
+
def _interactive_local_review(config: ReviewConfig) -> int:
|
|
1068
|
+
"""Interactive local Git review workflow."""
|
|
1069
|
+
c = Colors
|
|
1070
|
+
|
|
1071
|
+
print(f"\n{c.BOLD}Local review target:{c.RESET}")
|
|
1072
|
+
print(f" {c.CYAN}1{c.RESET}) Uncommitted changes (working tree + staged)")
|
|
1073
|
+
print(f" {c.CYAN}2{c.RESET}) Staged changes only (--staged)")
|
|
1074
|
+
print(f" {c.CYAN}3{c.RESET}) Compare against base branch (--from)")
|
|
1075
|
+
|
|
1076
|
+
try:
|
|
1077
|
+
target_choice = input(f"{c.BOLD}Choose [1-3, default=1]: {c.RESET}").strip()
|
|
1078
|
+
except (KeyboardInterrupt, EOFError):
|
|
1079
|
+
print("\n")
|
|
1080
|
+
return 0
|
|
1081
|
+
|
|
1082
|
+
argv = ["local"]
|
|
1083
|
+
if target_choice == "2":
|
|
1084
|
+
argv.append("--staged")
|
|
1085
|
+
elif target_choice == "3":
|
|
1086
|
+
try:
|
|
1087
|
+
base_ref = input(f"{c.BOLD}Base branch/ref (default=main): {c.RESET}").strip() or "main"
|
|
1088
|
+
except (KeyboardInterrupt, EOFError):
|
|
1089
|
+
print("\n")
|
|
1090
|
+
return 0
|
|
1091
|
+
argv.extend(["--from", base_ref])
|
|
1092
|
+
|
|
1093
|
+
verbosity = _ask_verbosity()
|
|
1094
|
+
if verbosity:
|
|
1095
|
+
argv.append(f"--{verbosity}")
|
|
1096
|
+
|
|
1097
|
+
parser = build_parser()
|
|
1098
|
+
parsed = parser.parse_args(argv)
|
|
1099
|
+
return run_review(parsed)
|
|
1100
|
+
|
|
1101
|
+
|
|
839
1102
|
def _ask_verbosity() -> str:
|
|
840
1103
|
"""Asks for the verbosity mode."""
|
|
841
1104
|
c = Colors
|
|
@@ -92,8 +92,8 @@ class ReviewConfig:
|
|
|
92
92
|
review_scope: str = "diff_only" # "diff_only" | "full_code"
|
|
93
93
|
max_diff_files: int = 50 # Max diff files sent to LLM
|
|
94
94
|
max_diff_lines: int = 2000 # Max diff lines
|
|
95
|
-
max_scope_blocks: int =
|
|
96
|
-
max_scope_lines: int = 250 # Max lines in a
|
|
95
|
+
max_scope_blocks: int = 8 # Max changed blocks for Enclosing Scopes strategy
|
|
96
|
+
max_scope_lines: int = 250 # Max lines in a scope before Adaptive Diff fallback
|
|
97
97
|
adaptive_context_lines: int = 10 # Context lines for Adaptive Diff fallback
|
|
98
98
|
custom_prompt_file: str = "review_prompt.md" # Markdown file with extra rules/context
|
|
99
99
|
file_extensions_filter: list = field(default_factory=list) # File extensions to include (empty = all)
|