github-code-review 3.3.1__tar.gz → 3.3.2__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.
- {github_code_review-3.3.1 → github_code_review-3.3.2}/PKG-INFO +2 -1
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/cli.py +4 -3
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/core.py +23 -7
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/gh_api.py +3 -3
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/report_struct.py +45 -29
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/utils.py +4 -1
- {github_code_review-3.3.1 → github_code_review-3.3.2}/pyproject.toml +2 -1
- {github_code_review-3.3.1 → github_code_review-3.3.2}/LICENSE +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/README.md +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/__init__.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/__main__.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/bootstrap.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/cli_base.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/__init__.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/deploy.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/fix.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/gh_post_review_comment.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/gh_react_to_comment.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/linear_comment.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/repl.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/version.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/config.toml +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/constants.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/context.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/env.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/issue_trackers.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/__init__.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/jira.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/linear.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/project_config.py +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/answer.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/components/env-vars.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/components/installs.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/gito-code-review.yml.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/partial/aux_files.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/changes_summary.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/release_notes.j2 +0 -0
- {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/test_cases.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: github-code-review
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
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
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
|
|
@@ -21,6 +21,7 @@ Requires-Dist: anthropic (>=0.57.1,<0.58.0)
|
|
|
21
21
|
Requires-Dist: ghapi (>=1.0.6,<1.1.0)
|
|
22
22
|
Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
|
|
23
23
|
Requires-Dist: jira (>=3.8.0,<4.0.0)
|
|
24
|
+
Requires-Dist: pydantic (>=2.12.3,<3.0.0)
|
|
24
25
|
Requires-Dist: typer (>=0.16.0,<0.17.0)
|
|
25
26
|
Requires-Dist: unidiff (>=0.7.5,<0.8.0)
|
|
26
27
|
Project-URL: Homepage, https://github.com/Nayjest/Gito
|
|
@@ -202,8 +202,8 @@ def setup():
|
|
|
202
202
|
mc.interactive_setup(HOME_ENV_PATH)
|
|
203
203
|
|
|
204
204
|
|
|
205
|
-
@app.command(name="
|
|
206
|
-
@app.command(name="
|
|
205
|
+
@app.command(name="report", help="Render and display code review report.")
|
|
206
|
+
@app.command(name="render", hidden=True)
|
|
207
207
|
def render(
|
|
208
208
|
format: str = typer.Argument(default=Report.Format.CLI),
|
|
209
209
|
source: str = typer.Option(
|
|
@@ -238,7 +238,8 @@ def files(
|
|
|
238
238
|
f"Changed files: "
|
|
239
239
|
f"{mc.ui.green(_what or 'INDEX')} vs "
|
|
240
240
|
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
|
241
|
-
f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
|
|
241
|
+
f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''} --> "
|
|
242
|
+
f"{mc.ui.cyan(len(patch_set or []))} file(s)."
|
|
242
243
|
)
|
|
243
244
|
|
|
244
245
|
for patch in patch_set:
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gito core business logic.
|
|
3
|
+
"""
|
|
1
4
|
import os
|
|
2
5
|
import fnmatch
|
|
3
6
|
import logging
|
|
@@ -6,8 +9,6 @@ from pathlib import Path
|
|
|
6
9
|
from functools import partial
|
|
7
10
|
|
|
8
11
|
import microcore as mc
|
|
9
|
-
from gito.constants import REFS_VALUE_ALL
|
|
10
|
-
from gito.gh_api import gh_api
|
|
11
12
|
from microcore import ui
|
|
12
13
|
from git import Repo, Commit
|
|
13
14
|
from git.exc import GitCommandError
|
|
@@ -16,11 +17,12 @@ from unidiff.constants import DEV_NULL
|
|
|
16
17
|
|
|
17
18
|
from .context import Context
|
|
18
19
|
from .project_config import ProjectConfig
|
|
19
|
-
from .report_struct import Report
|
|
20
|
-
from .constants import JSON_REPORT_FILE_NAME
|
|
20
|
+
from .report_struct import Report, RawIssue
|
|
21
|
+
from .constants import JSON_REPORT_FILE_NAME, REFS_VALUE_ALL
|
|
21
22
|
from .utils import make_streaming_function
|
|
22
23
|
from .pipeline import Pipeline
|
|
23
24
|
from .env import Env
|
|
25
|
+
from .gh_api import gh_api
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def review_subject_is_index(what):
|
|
@@ -384,6 +386,19 @@ def provide_affected_code_blocks(issues: dict, repo: Repo):
|
|
|
384
386
|
i["affected_code"] = block
|
|
385
387
|
|
|
386
388
|
|
|
389
|
+
def _llm_response_validator(parsed_response: list[dict]):
|
|
390
|
+
"""
|
|
391
|
+
Validate that the LLM response is a list of dicts that can be converted to RawIssue.
|
|
392
|
+
"""
|
|
393
|
+
if not isinstance(parsed_response, list):
|
|
394
|
+
raise ValueError("Response is not a list")
|
|
395
|
+
for item in parsed_response:
|
|
396
|
+
if not isinstance(item, dict):
|
|
397
|
+
raise ValueError("Response item is not a dict")
|
|
398
|
+
RawIssue(**item)
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
|
|
387
402
|
async def review(
|
|
388
403
|
repo: Repo = None,
|
|
389
404
|
what: str = None,
|
|
@@ -420,14 +435,15 @@ async def review(
|
|
|
420
435
|
for file_diff in diff
|
|
421
436
|
],
|
|
422
437
|
retries=cfg.retries,
|
|
423
|
-
parse_json=
|
|
438
|
+
parse_json={"validator": _llm_response_validator},
|
|
424
439
|
)
|
|
425
440
|
issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
|
|
426
441
|
provide_affected_code_blocks(issues, repo)
|
|
427
442
|
exec(cfg.post_process, {"mc": mc, **locals()})
|
|
428
443
|
out_folder = Path(out_folder or repo.working_tree_dir)
|
|
429
444
|
out_folder.mkdir(parents=True, exist_ok=True)
|
|
430
|
-
report = Report(
|
|
445
|
+
report = Report(number_of_processed_files=len(diff))
|
|
446
|
+
report.register_issues(issues)
|
|
431
447
|
ctx = Context(
|
|
432
448
|
report=report,
|
|
433
449
|
config=cfg,
|
|
@@ -496,7 +512,7 @@ def answer(
|
|
|
496
512
|
config.max_code_tokens // 2
|
|
497
513
|
)
|
|
498
514
|
else:
|
|
499
|
-
aux_files_dict =
|
|
515
|
+
aux_files_dict = {}
|
|
500
516
|
|
|
501
517
|
if not prompt_file and config.answer_prompt.startswith("tpl:"):
|
|
502
518
|
prompt_file = str(config.answer_prompt)[4:]
|
|
@@ -67,9 +67,9 @@ def post_gh_comment(
|
|
|
67
67
|
if 200 <= resp.status_code < 300:
|
|
68
68
|
logging.info(f"Posted review comment to #{pr_or_issue_number} in {gh_repository}")
|
|
69
69
|
return True
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
|
|
71
|
+
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
|
72
|
+
return False
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def hide_gh_comment(
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
from dataclasses import
|
|
3
|
+
from dataclasses import field, asdict, is_dataclass
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
import textwrap
|
|
8
9
|
import microcore as mc
|
|
9
|
-
from colorama import Fore, Style, Back
|
|
10
10
|
from microcore.utils import file_link
|
|
11
|
-
import
|
|
11
|
+
from colorama import Fore, Style, Back
|
|
12
|
+
from pydantic.dataclasses import dataclass
|
|
12
13
|
|
|
13
14
|
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
|
|
14
15
|
from .project_config import ProjectConfig
|
|
@@ -16,33 +17,47 @@ from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comment
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass
|
|
19
|
-
class
|
|
20
|
+
class RawIssue:
|
|
20
21
|
@dataclass
|
|
21
22
|
class AffectedCode:
|
|
22
23
|
start_line: int = field()
|
|
23
24
|
end_line: int | None = field(default=None)
|
|
25
|
+
proposal: str | None = field(default="")
|
|
26
|
+
|
|
27
|
+
title: str = field()
|
|
28
|
+
details: str | None = field(default="")
|
|
29
|
+
severity: int | None = field(default=None)
|
|
30
|
+
confidence: int | None = field(default=None)
|
|
31
|
+
tags: list[str] = field(default_factory=list)
|
|
32
|
+
affected_lines: list[AffectedCode] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Issue(RawIssue):
|
|
37
|
+
@dataclass
|
|
38
|
+
class AffectedCode(RawIssue.AffectedCode):
|
|
24
39
|
file: str = field(default="")
|
|
25
|
-
proposal: str = field(default="")
|
|
26
40
|
affected_code: str = field(default="")
|
|
27
41
|
|
|
28
42
|
@property
|
|
29
43
|
def syntax_hint(self) -> str:
|
|
30
44
|
return syntax_hint(self.file)
|
|
31
45
|
|
|
32
|
-
id: str = field()
|
|
33
|
-
title: str = field()
|
|
34
|
-
details: str = field(default="")
|
|
35
|
-
severity: int | None = field(default=None)
|
|
36
|
-
confidence: int | None = field(default=None)
|
|
37
|
-
tags: list[str] = field(default_factory=list)
|
|
46
|
+
id: int | str = field(kw_only=True)
|
|
38
47
|
file: str = field(default="")
|
|
39
48
|
affected_lines: list[AffectedCode] = field(default_factory=list)
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
@staticmethod
|
|
51
|
+
def from_raw_issue(file: str, raw_issue: RawIssue | dict, issue_id: int | str) -> "Issue":
|
|
52
|
+
if is_dataclass(raw_issue):
|
|
53
|
+
raw_issue = asdict(raw_issue)
|
|
54
|
+
params = filter_kwargs(Issue, raw_issue | {"file": file, "id": issue_id})
|
|
55
|
+
for i, obj in enumerate(params["affected_lines"]):
|
|
56
|
+
d = obj if isinstance(obj, dict) else asdict(obj)
|
|
57
|
+
params["affected_lines"][i] = Issue.AffectedCode(
|
|
58
|
+
**filter_kwargs(Issue.AffectedCode, {"file": file} | d)
|
|
59
|
+
)
|
|
60
|
+
return Issue(**params)
|
|
46
61
|
|
|
47
62
|
def github_code_link(self, github_env: dict) -> str:
|
|
48
63
|
url = (
|
|
@@ -63,7 +78,7 @@ class Report:
|
|
|
63
78
|
MARKDOWN = "md"
|
|
64
79
|
CLI = "cli"
|
|
65
80
|
|
|
66
|
-
issues: dict[str, list[Issue
|
|
81
|
+
issues: dict[str, list[Issue]] = field(default_factory=dict)
|
|
67
82
|
summary: str = field(default="")
|
|
68
83
|
number_of_processed_files: int = field(default=0)
|
|
69
84
|
total_issues: int = field(init=False)
|
|
@@ -79,19 +94,20 @@ class Report:
|
|
|
79
94
|
for issue in issues
|
|
80
95
|
]
|
|
81
96
|
|
|
97
|
+
def register_issues(self, issues: dict[str, list[RawIssue | dict]]):
|
|
98
|
+
for file, file_issues in issues.items():
|
|
99
|
+
for issue in file_issues:
|
|
100
|
+
self.register_issue(file, issue)
|
|
101
|
+
|
|
102
|
+
def register_issue(self, file: str, issue: RawIssue | dict):
|
|
103
|
+
if file not in self.issues:
|
|
104
|
+
self.issues[file] = []
|
|
105
|
+
total = len(self.plain_issues)
|
|
106
|
+
self.issues[file].append(Issue.from_raw_issue(file, issue, issue_id=total + 1))
|
|
107
|
+
self.total_issues = total + 1
|
|
108
|
+
|
|
82
109
|
def __post_init__(self):
|
|
83
|
-
|
|
84
|
-
for file in self.issues.keys():
|
|
85
|
-
self.issues[file] = [
|
|
86
|
-
Issue(
|
|
87
|
-
**filter_kwargs(Issue, {
|
|
88
|
-
"id": (issue_id := issue_id + 1),
|
|
89
|
-
"file": file,
|
|
90
|
-
} | issue)
|
|
91
|
-
)
|
|
92
|
-
for issue in self.issues[file]
|
|
93
|
-
]
|
|
94
|
-
self.total_issues = issue_id
|
|
110
|
+
self.total_issues = len(self.plain_issues)
|
|
95
111
|
|
|
96
112
|
def save(self, file_name: str = ""):
|
|
97
113
|
file_name = file_name or JSON_REPORT_FILE_NAME
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
|
-
from dataclasses import fields
|
|
5
|
+
from dataclasses import fields, is_dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import importlib.metadata
|
|
8
8
|
from typing import Optional
|
|
@@ -254,6 +254,9 @@ def filter_kwargs(cls, kwargs, log_warnings=True):
|
|
|
254
254
|
Returns:
|
|
255
255
|
A dictionary containing only the fields that are defined in the dataclass.
|
|
256
256
|
"""
|
|
257
|
+
if not is_dataclass(cls):
|
|
258
|
+
raise TypeError(f"{cls.__name__} is not a dataclass or pydantic dataclass")
|
|
259
|
+
|
|
257
260
|
cls_fields = {f.name for f in fields(cls)}
|
|
258
261
|
filtered = {}
|
|
259
262
|
for k, v in kwargs.items():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "github-code-review"
|
|
3
|
-
version = "3.3.
|
|
3
|
+
version = "3.3.2"
|
|
4
4
|
description = "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
5
|
authors = ["Nayjest <mail@vitaliy.in>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -29,6 +29,7 @@ anthropic = "^0.57.1"
|
|
|
29
29
|
typer = "^0.16.0"
|
|
30
30
|
ghapi = "~=1.0.6"
|
|
31
31
|
jira = "^3.8.0"
|
|
32
|
+
pydantic = "^2.12.3"
|
|
32
33
|
|
|
33
34
|
[tool.poetry.group.dev.dependencies]
|
|
34
35
|
flake8 = "*"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/gh_post_review_comment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|