xai-review 0.29.0__py3-none-any.whl → 0.31.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xai-review might be problematic. Click here for more details.
- ai_review/clients/gitea/__init__.py +0 -0
- ai_review/clients/gitea/client.py +31 -0
- ai_review/clients/gitea/pr/__init__.py +0 -0
- ai_review/clients/gitea/pr/client.py +118 -0
- ai_review/clients/gitea/pr/schema/__init__.py +0 -0
- ai_review/clients/gitea/pr/schema/comments.py +37 -0
- ai_review/clients/gitea/pr/schema/files.py +16 -0
- ai_review/clients/gitea/pr/schema/pull_request.py +18 -0
- ai_review/clients/gitea/pr/schema/user.py +6 -0
- ai_review/clients/gitea/pr/types.py +25 -0
- ai_review/clients/gitea/tools.py +6 -0
- ai_review/libs/config/review.py +1 -0
- ai_review/libs/config/vcs/base.py +8 -1
- ai_review/libs/config/vcs/gitea.py +13 -0
- ai_review/libs/constants/vcs_provider.py +1 -0
- ai_review/services/review/gateway/review_dry_run_comment_gateway.py +42 -0
- ai_review/services/review/service.py +9 -3
- ai_review/services/vcs/factory.py +3 -0
- ai_review/services/vcs/gitea/__init__.py +0 -0
- ai_review/services/vcs/gitea/adapter.py +22 -0
- ai_review/services/vcs/gitea/client.py +151 -0
- ai_review/tests/fixtures/clients/gitea.py +141 -0
- ai_review/tests/fixtures/services/review/gateway/{comment.py → review_comment_gateway.py} +1 -1
- ai_review/tests/fixtures/services/review/gateway/review_dry_run_comment_gateway.py +103 -0
- ai_review/tests/fixtures/services/review/gateway/review_llm_gateway.py +34 -0
- ai_review/tests/suites/clients/gitea/__init__.py +0 -0
- ai_review/tests/suites/clients/gitea/test_client.py +14 -0
- ai_review/tests/suites/clients/gitea/test_tools.py +26 -0
- ai_review/tests/suites/services/review/gateway/{test_comment.py → test_review_comment_gateway.py} +1 -1
- ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py +93 -0
- ai_review/tests/suites/services/review/gateway/{test_llm.py → test_review_llm_gateway.py} +1 -15
- ai_review/tests/suites/services/review/runner/test_context.py +2 -2
- ai_review/tests/suites/services/review/runner/test_inline.py +2 -2
- ai_review/tests/suites/services/review/runner/test_inline_reply.py +2 -2
- ai_review/tests/suites/services/review/runner/test_summary.py +2 -2
- ai_review/tests/suites/services/review/runner/test_summary_reply.py +2 -2
- ai_review/tests/suites/services/review/test_service.py +18 -0
- ai_review/tests/suites/services/vcs/gitea/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/gitea/test_adapter.py +52 -0
- ai_review/tests/suites/services/vcs/gitea/test_client.py +86 -0
- ai_review/tests/suites/services/vcs/test_factory.py +7 -0
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/METADATA +21 -12
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/RECORD +49 -24
- ai_review/tests/fixtures/services/review/gateway/llm.py +0 -17
- /ai_review/services/review/gateway/{comment.py → review_comment_gateway.py} +0 -0
- /ai_review/services/review/gateway/{llm.py → review_llm_gateway.py} +0 -0
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/WHEEL +0 -0
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.29.0.dist-info → xai_review-0.31.0.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ai_review.clients.gitea.pr.client import GiteaPullRequestsHTTPClient
|
|
2
|
+
from httpx import AsyncClient, AsyncHTTPTransport
|
|
3
|
+
|
|
4
|
+
from ai_review.config import settings
|
|
5
|
+
from ai_review.libs.http.event_hooks.logger import LoggerEventHook
|
|
6
|
+
from ai_review.libs.http.transports.retry import RetryTransport
|
|
7
|
+
from ai_review.libs.logger import get_logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GiteaHTTPClient:
|
|
11
|
+
def __init__(self, client: AsyncClient):
|
|
12
|
+
self.pr = GiteaPullRequestsHTTPClient(client)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_gitea_http_client() -> GiteaHTTPClient:
|
|
16
|
+
logger = get_logger("GITEA_HTTP_CLIENT")
|
|
17
|
+
logger_event_hook = LoggerEventHook(logger=logger)
|
|
18
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
19
|
+
|
|
20
|
+
client = AsyncClient(
|
|
21
|
+
timeout=settings.llm.http_client.timeout,
|
|
22
|
+
headers={"Authorization": f"token {settings.vcs.http_client.api_token_value}"},
|
|
23
|
+
base_url=settings.vcs.http_client.api_url_value.rstrip("/"),
|
|
24
|
+
transport=retry_transport,
|
|
25
|
+
event_hooks={
|
|
26
|
+
"request": [logger_event_hook.request],
|
|
27
|
+
"response": [logger_event_hook.response]
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return GiteaHTTPClient(client=client)
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from httpx import Response, QueryParams
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.gitea.pr.schema.comments import (
|
|
4
|
+
GiteaPRCommentSchema,
|
|
5
|
+
GiteaGetPRCommentsQuerySchema,
|
|
6
|
+
GiteaGetPRCommentsResponseSchema,
|
|
7
|
+
GiteaCreateCommentRequestSchema,
|
|
8
|
+
GiteaCreateCommentResponseSchema
|
|
9
|
+
)
|
|
10
|
+
from ai_review.clients.gitea.pr.schema.files import (
|
|
11
|
+
GiteaPRFileSchema,
|
|
12
|
+
GiteaGetPRFilesQuerySchema,
|
|
13
|
+
GiteaGetPRFilesResponseSchema
|
|
14
|
+
)
|
|
15
|
+
from ai_review.clients.gitea.pr.schema.pull_request import GiteaGetPRResponseSchema
|
|
16
|
+
from ai_review.clients.gitea.pr.types import GiteaPullRequestsHTTPClientProtocol
|
|
17
|
+
from ai_review.clients.gitea.tools import gitea_has_next_page
|
|
18
|
+
from ai_review.config import settings
|
|
19
|
+
from ai_review.libs.http.client import HTTPClient
|
|
20
|
+
from ai_review.libs.http.handlers import HTTPClientError, handle_http_error
|
|
21
|
+
from ai_review.libs.http.paginate import paginate
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GiteaPullRequestsHTTPClientError(HTTPClientError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GiteaPullRequestsHTTPClient(HTTPClient, GiteaPullRequestsHTTPClientProtocol):
|
|
29
|
+
@handle_http_error(client="GiteaPullRequestsHTTPClient", exception=GiteaPullRequestsHTTPClientError)
|
|
30
|
+
async def get_pull_request_api(self, owner: str, repo: str, pull_number: str) -> Response:
|
|
31
|
+
return await self.get(f"/repos/{owner}/{repo}/pulls/{pull_number}")
|
|
32
|
+
|
|
33
|
+
@handle_http_error(client="GiteaPullRequestsHTTPClient", exception=GiteaPullRequestsHTTPClientError)
|
|
34
|
+
async def get_files_api(
|
|
35
|
+
self,
|
|
36
|
+
owner: str,
|
|
37
|
+
repo: str,
|
|
38
|
+
pull_number: str,
|
|
39
|
+
query: GiteaGetPRFilesQuerySchema
|
|
40
|
+
) -> Response:
|
|
41
|
+
return await self.get(
|
|
42
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}/files",
|
|
43
|
+
query=QueryParams(**query.model_dump())
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@handle_http_error(client="GiteaPullRequestsHTTPClient", exception=GiteaPullRequestsHTTPClientError)
|
|
47
|
+
async def get_comments_api(
|
|
48
|
+
self,
|
|
49
|
+
owner: str,
|
|
50
|
+
repo: str,
|
|
51
|
+
pull_number: str,
|
|
52
|
+
query: GiteaGetPRCommentsQuerySchema
|
|
53
|
+
) -> Response:
|
|
54
|
+
return await self.get(
|
|
55
|
+
f"/repos/{owner}/{repo}/issues/{pull_number}/comments",
|
|
56
|
+
query=QueryParams(**query.model_dump())
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@handle_http_error(client="GiteaPullRequestsHTTPClient", exception=GiteaPullRequestsHTTPClientError)
|
|
60
|
+
async def create_comment_api(
|
|
61
|
+
self,
|
|
62
|
+
owner: str,
|
|
63
|
+
repo: str,
|
|
64
|
+
pull_number: str,
|
|
65
|
+
request: GiteaCreateCommentRequestSchema
|
|
66
|
+
) -> Response:
|
|
67
|
+
return await self.post(
|
|
68
|
+
f"/repos/{owner}/{repo}/issues/{pull_number}/comments",
|
|
69
|
+
json=request.model_dump(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRResponseSchema:
|
|
73
|
+
response = await self.get_pull_request_api(owner, repo, pull_number)
|
|
74
|
+
return GiteaGetPRResponseSchema.model_validate_json(response.text)
|
|
75
|
+
|
|
76
|
+
async def get_files(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRFilesResponseSchema:
|
|
77
|
+
async def fetch_page(page: int) -> Response:
|
|
78
|
+
query = GiteaGetPRFilesQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
|
|
79
|
+
return await self.get_files_api(owner, repo, pull_number, query)
|
|
80
|
+
|
|
81
|
+
def extract_items(response: Response) -> list[GiteaPRFileSchema]:
|
|
82
|
+
result = GiteaGetPRFilesResponseSchema.model_validate_json(response.text)
|
|
83
|
+
return result.root
|
|
84
|
+
|
|
85
|
+
items = await paginate(
|
|
86
|
+
max_pages=settings.vcs.pagination.max_pages,
|
|
87
|
+
fetch_page=fetch_page,
|
|
88
|
+
extract_items=extract_items,
|
|
89
|
+
has_next_page=gitea_has_next_page
|
|
90
|
+
)
|
|
91
|
+
return GiteaGetPRFilesResponseSchema(root=items)
|
|
92
|
+
|
|
93
|
+
async def get_comments(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRCommentsResponseSchema:
|
|
94
|
+
async def fetch_page(page: int) -> Response:
|
|
95
|
+
query = GiteaGetPRCommentsQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
|
|
96
|
+
return await self.get_comments_api(owner, repo, pull_number, query)
|
|
97
|
+
|
|
98
|
+
def extract_items(response: Response) -> list[GiteaPRCommentSchema]:
|
|
99
|
+
result = GiteaGetPRCommentsResponseSchema.model_validate_json(response.text)
|
|
100
|
+
return result.root
|
|
101
|
+
|
|
102
|
+
items = await paginate(
|
|
103
|
+
max_pages=settings.vcs.pagination.max_pages,
|
|
104
|
+
fetch_page=fetch_page,
|
|
105
|
+
extract_items=extract_items,
|
|
106
|
+
has_next_page=gitea_has_next_page
|
|
107
|
+
)
|
|
108
|
+
return GiteaGetPRCommentsResponseSchema(root=items)
|
|
109
|
+
|
|
110
|
+
async def create_comment(
|
|
111
|
+
self,
|
|
112
|
+
owner: str,
|
|
113
|
+
repo: str,
|
|
114
|
+
pull_number: str,
|
|
115
|
+
request: GiteaCreateCommentRequestSchema
|
|
116
|
+
) -> GiteaCreateCommentResponseSchema:
|
|
117
|
+
response = await self.create_comment_api(owner, repo, pull_number, request)
|
|
118
|
+
return GiteaCreateCommentResponseSchema.model_validate_json(response.text)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pydantic import BaseModel, RootModel
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GiteaPRCommentSchema(BaseModel):
|
|
7
|
+
id: int
|
|
8
|
+
body: str
|
|
9
|
+
path: str | None = None
|
|
10
|
+
line: int | None = None
|
|
11
|
+
user: GiteaUserSchema | None = None
|
|
12
|
+
resolver: GiteaUserSchema | None = None
|
|
13
|
+
position: int | None = None
|
|
14
|
+
commit_id: str | None = None
|
|
15
|
+
original_position: int | None = None
|
|
16
|
+
original_commit_id: str | None = None
|
|
17
|
+
pull_request_review_id: int | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GiteaGetPRCommentsQuerySchema(BaseModel):
|
|
21
|
+
page: int = 1
|
|
22
|
+
per_page: int = 100
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GiteaGetPRCommentsResponseSchema(RootModel[list[GiteaPRCommentSchema]]):
|
|
26
|
+
root: list[GiteaPRCommentSchema]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GiteaCreateCommentRequestSchema(BaseModel):
|
|
30
|
+
body: str
|
|
31
|
+
path: str | None = None
|
|
32
|
+
line: int | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GiteaCreateCommentResponseSchema(BaseModel):
|
|
36
|
+
id: int
|
|
37
|
+
body: str
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel, RootModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GiteaPRFileSchema(BaseModel):
|
|
5
|
+
patch: str | None = None
|
|
6
|
+
status: str
|
|
7
|
+
filename: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GiteaGetPRFilesQuerySchema(BaseModel):
|
|
11
|
+
page: int = 1
|
|
12
|
+
per_page: int = 100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GiteaGetPRFilesResponseSchema(RootModel[list[GiteaPRFileSchema]]):
|
|
16
|
+
root: list[GiteaPRFileSchema]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GiteaBranchSchema(BaseModel):
|
|
7
|
+
ref: str
|
|
8
|
+
sha: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GiteaGetPRResponseSchema(BaseModel):
|
|
12
|
+
id: int
|
|
13
|
+
number: int
|
|
14
|
+
title: str
|
|
15
|
+
body: str | None = None
|
|
16
|
+
user: GiteaUserSchema
|
|
17
|
+
base: GiteaBranchSchema
|
|
18
|
+
head: GiteaBranchSchema
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.gitea.pr.schema.comments import (
|
|
4
|
+
GiteaCreateCommentRequestSchema,
|
|
5
|
+
GiteaCreateCommentResponseSchema,
|
|
6
|
+
GiteaGetPRCommentsResponseSchema
|
|
7
|
+
)
|
|
8
|
+
from ai_review.clients.gitea.pr.schema.files import GiteaGetPRFilesResponseSchema
|
|
9
|
+
from ai_review.clients.gitea.pr.schema.pull_request import GiteaGetPRResponseSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GiteaPullRequestsHTTPClientProtocol(Protocol):
|
|
13
|
+
async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRResponseSchema: ...
|
|
14
|
+
|
|
15
|
+
async def get_files(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRFilesResponseSchema: ...
|
|
16
|
+
|
|
17
|
+
async def get_comments(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRCommentsResponseSchema: ...
|
|
18
|
+
|
|
19
|
+
async def create_comment(
|
|
20
|
+
self,
|
|
21
|
+
owner: str,
|
|
22
|
+
repo: str,
|
|
23
|
+
pull_number: str,
|
|
24
|
+
request: GiteaCreateCommentRequestSchema
|
|
25
|
+
) -> GiteaCreateCommentResponseSchema: ...
|
ai_review/libs/config/review.py
CHANGED
|
@@ -19,6 +19,7 @@ class ReviewMode(StrEnum):
|
|
|
19
19
|
|
|
20
20
|
class ReviewConfig(BaseModel):
|
|
21
21
|
mode: ReviewMode = ReviewMode.FULL_FILE_DIFF
|
|
22
|
+
dry_run: bool = False
|
|
22
23
|
inline_tag: str = Field(default="#ai-review-inline")
|
|
23
24
|
inline_reply_tag: str = Field(default="#ai-review-inline-reply")
|
|
24
25
|
summary_tag: str = Field(default="#ai-review-summary")
|
|
@@ -3,6 +3,7 @@ from typing import Annotated, Literal
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
5
|
from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
|
|
6
|
+
from ai_review.libs.config.vcs.gitea import GiteaPipelineConfig, GiteaHTTPClientConfig
|
|
6
7
|
from ai_review.libs.config.vcs.github import GitHubPipelineConfig, GitHubHTTPClientConfig
|
|
7
8
|
from ai_review.libs.config.vcs.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
|
|
8
9
|
from ai_review.libs.config.vcs.pagination import VCSPaginationConfig
|
|
@@ -14,6 +15,12 @@ class VCSConfigBase(BaseModel):
|
|
|
14
15
|
pagination: VCSPaginationConfig = VCSPaginationConfig()
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
class GiteaVCSConfig(VCSConfigBase):
|
|
19
|
+
provider: Literal[VCSProvider.GITEA]
|
|
20
|
+
pipeline: GiteaPipelineConfig
|
|
21
|
+
http_client: GiteaHTTPClientConfig
|
|
22
|
+
|
|
23
|
+
|
|
17
24
|
class GitLabVCSConfig(VCSConfigBase):
|
|
18
25
|
provider: Literal[VCSProvider.GITLAB]
|
|
19
26
|
pipeline: GitLabPipelineConfig
|
|
@@ -33,6 +40,6 @@ class BitbucketVCSConfig(VCSConfigBase):
|
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
VCSConfig = Annotated[
|
|
36
|
-
GitLabVCSConfig | GitHubVCSConfig | BitbucketVCSConfig,
|
|
43
|
+
GiteaVCSConfig | GitLabVCSConfig | GitHubVCSConfig | BitbucketVCSConfig,
|
|
37
44
|
Field(discriminator="provider")
|
|
38
45
|
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.config.http import HTTPClientWithTokenConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GiteaPipelineConfig(BaseModel):
|
|
7
|
+
repo: str
|
|
8
|
+
owner: str
|
|
9
|
+
pull_number: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GiteaHTTPClientConfig(HTTPClientWithTokenConfig):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from ai_review.libs.asynchronous.gather import bounded_gather
|
|
2
|
+
from ai_review.libs.logger import get_logger
|
|
3
|
+
from ai_review.services.hook import hook
|
|
4
|
+
from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
|
|
5
|
+
from ai_review.services.review.internal.inline.schema import InlineCommentListSchema, InlineCommentSchema
|
|
6
|
+
from ai_review.services.review.internal.inline_reply.schema import InlineCommentReplySchema
|
|
7
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
8
|
+
from ai_review.services.review.internal.summary_reply.schema import SummaryCommentReplySchema
|
|
9
|
+
from ai_review.services.vcs.types import VCSClientProtocol
|
|
10
|
+
|
|
11
|
+
logger = get_logger("REVIEW_DRY_RUN_COMMENT_GATEWAY")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReviewDryRunCommentGateway(ReviewCommentGateway):
|
|
15
|
+
def __init__(self, vcs: VCSClientProtocol):
|
|
16
|
+
super().__init__(vcs)
|
|
17
|
+
logger.warning("Running in DRY RUN mode — no comments will be posted to VCS")
|
|
18
|
+
|
|
19
|
+
async def process_inline_reply(self, thread_id: str, reply: InlineCommentReplySchema) -> None:
|
|
20
|
+
await hook.emit_inline_comment_reply_start(reply)
|
|
21
|
+
logger.info(f"[dry-run] Would create inline reply for thread {thread_id}:\n{reply.body_with_tag}")
|
|
22
|
+
await hook.emit_inline_comment_reply_complete(reply)
|
|
23
|
+
|
|
24
|
+
async def process_summary_reply(self, thread_id: str, reply: SummaryCommentReplySchema) -> None:
|
|
25
|
+
await hook.emit_summary_comment_reply_start(reply)
|
|
26
|
+
logger.info(f"[dry-run] Would create summary reply for thread {thread_id}:\n{reply.body_with_tag}")
|
|
27
|
+
await hook.emit_summary_comment_reply_complete(reply)
|
|
28
|
+
|
|
29
|
+
async def process_inline_comment(self, comment: InlineCommentSchema) -> None:
|
|
30
|
+
await hook.emit_inline_comment_start(comment)
|
|
31
|
+
logger.info(
|
|
32
|
+
f"[dry-run] Would create inline comment for {comment.file}:{comment.line}:\n{comment.body_with_tag}"
|
|
33
|
+
)
|
|
34
|
+
await hook.emit_inline_comment_complete(comment)
|
|
35
|
+
|
|
36
|
+
async def process_summary_comment(self, comment: SummaryCommentSchema) -> None:
|
|
37
|
+
await hook.emit_summary_comment_start(comment)
|
|
38
|
+
logger.info(f"[dry-run] Would create summary comment:\n{comment.body_with_tag}")
|
|
39
|
+
await hook.emit_summary_comment_complete(comment)
|
|
40
|
+
|
|
41
|
+
async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
|
|
42
|
+
await bounded_gather([self.process_inline_comment(comment) for comment in comments.root])
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ai_review.config import settings
|
|
1
2
|
from ai_review.libs.logger import get_logger
|
|
2
3
|
from ai_review.services.artifacts.service import ArtifactsService
|
|
3
4
|
from ai_review.services.cost.service import CostService
|
|
@@ -5,8 +6,9 @@ from ai_review.services.diff.service import DiffService
|
|
|
5
6
|
from ai_review.services.git.service import GitService
|
|
6
7
|
from ai_review.services.llm.factory import get_llm_client
|
|
7
8
|
from ai_review.services.prompt.service import PromptService
|
|
8
|
-
from ai_review.services.review.gateway.
|
|
9
|
-
from ai_review.services.review.gateway.
|
|
9
|
+
from ai_review.services.review.gateway.review_comment_gateway import ReviewCommentGateway
|
|
10
|
+
from ai_review.services.review.gateway.review_dry_run_comment_gateway import ReviewDryRunCommentGateway
|
|
11
|
+
from ai_review.services.review.gateway.review_llm_gateway import ReviewLLMGateway
|
|
10
12
|
from ai_review.services.review.internal.inline.service import InlineCommentService
|
|
11
13
|
from ai_review.services.review.internal.inline_reply.service import InlineCommentReplyService
|
|
12
14
|
from ai_review.services.review.internal.policy.service import ReviewPolicyService
|
|
@@ -42,7 +44,11 @@ class ReviewService:
|
|
|
42
44
|
cost=self.cost,
|
|
43
45
|
artifacts=self.artifacts
|
|
44
46
|
)
|
|
45
|
-
self.review_comment_gateway =
|
|
47
|
+
self.review_comment_gateway = (
|
|
48
|
+
ReviewDryRunCommentGateway(vcs=self.vcs)
|
|
49
|
+
if settings.review.dry_run
|
|
50
|
+
else ReviewCommentGateway(vcs=self.vcs)
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
self.inline_review_runner = InlineReviewRunner(
|
|
48
54
|
vcs=self.vcs,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from ai_review.config import settings
|
|
2
2
|
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
3
3
|
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
4
|
+
from ai_review.services.vcs.gitea.client import GiteaVCSClient
|
|
4
5
|
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
5
6
|
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
6
7
|
from ai_review.services.vcs.types import VCSClientProtocol
|
|
@@ -8,6 +9,8 @@ from ai_review.services.vcs.types import VCSClientProtocol
|
|
|
8
9
|
|
|
9
10
|
def get_vcs_client() -> VCSClientProtocol:
|
|
10
11
|
match settings.vcs.provider:
|
|
12
|
+
case VCSProvider.GITEA:
|
|
13
|
+
return GiteaVCSClient()
|
|
11
14
|
case VCSProvider.GITLAB:
|
|
12
15
|
return GitLabVCSClient()
|
|
13
16
|
case VCSProvider.GITHUB:
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from ai_review.clients.gitea.pr.schema.comments import GiteaPRCommentSchema
|
|
2
|
+
from ai_review.clients.gitea.pr.schema.pull_request import GiteaUserSchema
|
|
3
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_user_from_gitea_user(user: GiteaUserSchema | None) -> UserSchema:
|
|
7
|
+
return UserSchema(
|
|
8
|
+
id=user.id if user else None,
|
|
9
|
+
name=user.login if user else "",
|
|
10
|
+
username=user.login if user else "",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_review_comment_from_gitea_comment(comment: GiteaPRCommentSchema) -> ReviewCommentSchema:
|
|
15
|
+
return ReviewCommentSchema(
|
|
16
|
+
id=comment.id,
|
|
17
|
+
body=comment.body or "",
|
|
18
|
+
file=comment.path,
|
|
19
|
+
line=comment.line,
|
|
20
|
+
author=get_user_from_gitea_user(comment.user),
|
|
21
|
+
thread_id=comment.id
|
|
22
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from ai_review.clients.gitea.client import get_gitea_http_client
|
|
2
|
+
from ai_review.clients.gitea.pr.schema.comments import GiteaCreateCommentRequestSchema
|
|
3
|
+
from ai_review.config import settings
|
|
4
|
+
from ai_review.libs.logger import get_logger
|
|
5
|
+
from ai_review.services.vcs.gitea.adapter import get_review_comment_from_gitea_comment, get_user_from_gitea_user
|
|
6
|
+
from ai_review.services.vcs.types import (
|
|
7
|
+
VCSClientProtocol,
|
|
8
|
+
ThreadKind,
|
|
9
|
+
BranchRefSchema,
|
|
10
|
+
ReviewInfoSchema,
|
|
11
|
+
ReviewThreadSchema,
|
|
12
|
+
ReviewCommentSchema,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = get_logger("GITEA_VCS_CLIENT")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GiteaVCSClient(VCSClientProtocol):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.http_client = get_gitea_http_client()
|
|
21
|
+
self.owner = settings.vcs.pipeline.owner
|
|
22
|
+
self.repo = settings.vcs.pipeline.repo
|
|
23
|
+
self.pull_number = settings.vcs.pipeline.pull_number
|
|
24
|
+
self.pull_request_ref = f"{self.owner}/{self.repo}#{self.pull_number}"
|
|
25
|
+
|
|
26
|
+
# --- Review info ---
|
|
27
|
+
async def get_review_info(self) -> ReviewInfoSchema:
|
|
28
|
+
try:
|
|
29
|
+
pr = await self.http_client.pr.get_pull_request(
|
|
30
|
+
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
31
|
+
)
|
|
32
|
+
files = await self.http_client.pr.get_files(
|
|
33
|
+
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.info(f"Fetched PR info for {self.pull_request_ref}")
|
|
37
|
+
|
|
38
|
+
return ReviewInfoSchema(
|
|
39
|
+
id=pr.number,
|
|
40
|
+
title=pr.title,
|
|
41
|
+
description=pr.body or "",
|
|
42
|
+
author=get_user_from_gitea_user(pr.user),
|
|
43
|
+
labels=[],
|
|
44
|
+
base_sha=pr.base.sha,
|
|
45
|
+
head_sha=pr.head.sha,
|
|
46
|
+
assignees=[],
|
|
47
|
+
reviewers=[],
|
|
48
|
+
source_branch=BranchRefSchema(ref=pr.head.ref, sha=pr.head.sha),
|
|
49
|
+
target_branch=BranchRefSchema(ref=pr.base.ref, sha=pr.base.sha),
|
|
50
|
+
changed_files=[file.filename for file in files.root],
|
|
51
|
+
)
|
|
52
|
+
except Exception as error:
|
|
53
|
+
logger.exception(f"Failed to fetch PR info {self.pull_request_ref}: {error}")
|
|
54
|
+
return ReviewInfoSchema()
|
|
55
|
+
|
|
56
|
+
# --- Comments ---
|
|
57
|
+
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
58
|
+
try:
|
|
59
|
+
response = await self.http_client.pr.get_comments(
|
|
60
|
+
owner=self.owner, repo=self.repo, pull_number=self.pull_number
|
|
61
|
+
)
|
|
62
|
+
logger.info(f"Fetched comments for {self.pull_request_ref}")
|
|
63
|
+
|
|
64
|
+
return [get_review_comment_from_gitea_comment(comment) for comment in response.root]
|
|
65
|
+
except Exception as error:
|
|
66
|
+
logger.exception(f"Failed to fetch comments for {self.pull_request_ref}: {error}")
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
70
|
+
try:
|
|
71
|
+
comments = await self.get_general_comments()
|
|
72
|
+
return [comment for comment in comments if comment.file]
|
|
73
|
+
except Exception as error:
|
|
74
|
+
logger.exception(f"Failed to fetch inline comments for {self.pull_request_ref}: {error}")
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
async def create_general_comment(self, message: str) -> None:
|
|
78
|
+
try:
|
|
79
|
+
logger.info(f"Posting general comment to PR {self.pull_request_ref}: {message}")
|
|
80
|
+
request = GiteaCreateCommentRequestSchema(body=message)
|
|
81
|
+
await self.http_client.pr.create_comment(
|
|
82
|
+
owner=self.owner,
|
|
83
|
+
repo=self.repo,
|
|
84
|
+
pull_number=self.pull_number,
|
|
85
|
+
request=request,
|
|
86
|
+
)
|
|
87
|
+
logger.info(f"Created general comment in PR {self.pull_request_ref}")
|
|
88
|
+
except Exception as error:
|
|
89
|
+
logger.exception(f"Failed to create general comment in PR {self.pull_request_ref}: {error}")
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
93
|
+
try:
|
|
94
|
+
logger.info(f"Posting inline comment to {self.pull_request_ref} at {file}:{line}")
|
|
95
|
+
request = GiteaCreateCommentRequestSchema(
|
|
96
|
+
body=message,
|
|
97
|
+
path=file,
|
|
98
|
+
line=line,
|
|
99
|
+
)
|
|
100
|
+
await self.http_client.pr.create_comment(
|
|
101
|
+
owner=self.owner,
|
|
102
|
+
repo=self.repo,
|
|
103
|
+
pull_number=self.pull_number,
|
|
104
|
+
request=request,
|
|
105
|
+
)
|
|
106
|
+
logger.info(f"Created inline comment in {self.pull_request_ref} at {file}:{line}")
|
|
107
|
+
except Exception as error:
|
|
108
|
+
logger.exception(f"Failed to create inline comment in {self.pull_request_ref} at {file}:{line}: {error}")
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
|
|
112
|
+
await self.create_general_comment(message)
|
|
113
|
+
|
|
114
|
+
async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
|
|
115
|
+
await self.create_general_comment(message)
|
|
116
|
+
|
|
117
|
+
# --- Threads ---
|
|
118
|
+
async def get_inline_threads(self) -> list[ReviewThreadSchema]:
|
|
119
|
+
try:
|
|
120
|
+
comments = await self.get_inline_comments()
|
|
121
|
+
threads = {comment.thread_id: [comment] for comment in comments}
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
ReviewThreadSchema(
|
|
125
|
+
id=thread_id,
|
|
126
|
+
kind=ThreadKind.INLINE,
|
|
127
|
+
file=thread[0].file,
|
|
128
|
+
line=thread[0].line,
|
|
129
|
+
comments=thread,
|
|
130
|
+
)
|
|
131
|
+
for thread_id, thread in threads.items()
|
|
132
|
+
]
|
|
133
|
+
except Exception as error:
|
|
134
|
+
logger.exception(f"Failed to build inline threads for {self.pull_request_ref}: {error}")
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
async def get_general_threads(self) -> list[ReviewThreadSchema]:
|
|
138
|
+
try:
|
|
139
|
+
comments = await self.get_general_comments()
|
|
140
|
+
threads = [
|
|
141
|
+
ReviewThreadSchema(
|
|
142
|
+
id=comment.thread_id,
|
|
143
|
+
kind=ThreadKind.SUMMARY,
|
|
144
|
+
comments=[comment]
|
|
145
|
+
)
|
|
146
|
+
for comment in comments
|
|
147
|
+
]
|
|
148
|
+
return threads
|
|
149
|
+
except Exception as error:
|
|
150
|
+
logger.exception(f"Failed to build general threads for {self.pull_request_ref}: {error}")
|
|
151
|
+
return []
|