xai-review 0.30.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.

Files changed (32) hide show
  1. ai_review/clients/gitea/__init__.py +0 -0
  2. ai_review/clients/gitea/client.py +31 -0
  3. ai_review/clients/gitea/pr/__init__.py +0 -0
  4. ai_review/clients/gitea/pr/client.py +118 -0
  5. ai_review/clients/gitea/pr/schema/__init__.py +0 -0
  6. ai_review/clients/gitea/pr/schema/comments.py +37 -0
  7. ai_review/clients/gitea/pr/schema/files.py +16 -0
  8. ai_review/clients/gitea/pr/schema/pull_request.py +18 -0
  9. ai_review/clients/gitea/pr/schema/user.py +6 -0
  10. ai_review/clients/gitea/pr/types.py +25 -0
  11. ai_review/clients/gitea/tools.py +6 -0
  12. ai_review/libs/config/vcs/base.py +8 -1
  13. ai_review/libs/config/vcs/gitea.py +13 -0
  14. ai_review/libs/constants/vcs_provider.py +1 -0
  15. ai_review/services/vcs/factory.py +3 -0
  16. ai_review/services/vcs/gitea/__init__.py +0 -0
  17. ai_review/services/vcs/gitea/adapter.py +22 -0
  18. ai_review/services/vcs/gitea/client.py +151 -0
  19. ai_review/tests/fixtures/clients/gitea.py +141 -0
  20. ai_review/tests/suites/clients/gitea/__init__.py +0 -0
  21. ai_review/tests/suites/clients/gitea/test_client.py +14 -0
  22. ai_review/tests/suites/clients/gitea/test_tools.py +26 -0
  23. ai_review/tests/suites/services/vcs/gitea/__init__.py +0 -0
  24. ai_review/tests/suites/services/vcs/gitea/test_adapter.py +52 -0
  25. ai_review/tests/suites/services/vcs/gitea/test_client.py +86 -0
  26. ai_review/tests/suites/services/vcs/test_factory.py +7 -0
  27. {xai_review-0.30.0.dist-info → xai_review-0.31.0.dist-info}/METADATA +21 -12
  28. {xai_review-0.30.0.dist-info → xai_review-0.31.0.dist-info}/RECORD +32 -10
  29. {xai_review-0.30.0.dist-info → xai_review-0.31.0.dist-info}/WHEEL +0 -0
  30. {xai_review-0.30.0.dist-info → xai_review-0.31.0.dist-info}/entry_points.txt +0 -0
  31. {xai_review-0.30.0.dist-info → xai_review-0.31.0.dist-info}/licenses/LICENSE +0 -0
  32. {xai_review-0.30.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,6 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class GiteaUserSchema(BaseModel):
5
+ id: int
6
+ login: str
@@ -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: ...
@@ -0,0 +1,6 @@
1
+ from httpx import Response
2
+
3
+
4
+ def gitea_has_next_page(response: Response) -> bool:
5
+ link_header = response.headers.get("Link")
6
+ return link_header is not None and 'rel="next"' in link_header
@@ -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
@@ -2,6 +2,7 @@ from enum import StrEnum
2
2
 
3
3
 
4
4
  class VCSProvider(StrEnum):
5
+ GITEA = "GITEA"
5
6
  GITHUB = "GITHUB"
6
7
  GITLAB = "GITLAB"
7
8
  BITBUCKET = "BITBUCKET"
@@ -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 []
@@ -0,0 +1,141 @@
1
+ import pytest
2
+ from pydantic import HttpUrl, SecretStr
3
+
4
+ from ai_review.clients.gitea.pr.schema.comments import (
5
+ GiteaPRCommentSchema,
6
+ GiteaGetPRCommentsResponseSchema,
7
+ GiteaCreateCommentRequestSchema,
8
+ GiteaCreateCommentResponseSchema,
9
+ )
10
+ from ai_review.clients.gitea.pr.schema.files import (
11
+ GiteaGetPRFilesResponseSchema,
12
+ GiteaPRFileSchema,
13
+ )
14
+ from ai_review.clients.gitea.pr.schema.pull_request import (
15
+ GiteaGetPRResponseSchema,
16
+ GiteaBranchSchema,
17
+ )
18
+ from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
19
+ from ai_review.clients.gitea.pr.types import GiteaPullRequestsHTTPClientProtocol
20
+ from ai_review.config import settings
21
+ from ai_review.libs.config.vcs.base import GiteaVCSConfig
22
+ from ai_review.libs.config.vcs.gitea import GiteaPipelineConfig, GiteaHTTPClientConfig
23
+ from ai_review.libs.constants.vcs_provider import VCSProvider
24
+ from ai_review.services.vcs.gitea.client import GiteaVCSClient
25
+
26
+
27
+ class FakeGiteaPullRequestsHTTPClient(GiteaPullRequestsHTTPClientProtocol):
28
+ def __init__(self):
29
+ self.calls: list[tuple[str, dict]] = []
30
+
31
+ async def get_pull_request(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRResponseSchema:
32
+ self.calls.append(("get_pull_request", {"owner": owner, "repo": repo, "pull_number": pull_number}))
33
+ return GiteaGetPRResponseSchema(
34
+ id=1,
35
+ number=1,
36
+ title="Fake Gitea PR",
37
+ body="This is a fake PR for testing",
38
+ user=GiteaUserSchema(id=101, login="tester"),
39
+ base=GiteaBranchSchema(ref="main", sha="abc123"),
40
+ head=GiteaBranchSchema(ref="feature", sha="def456"),
41
+ )
42
+
43
+ async def get_files(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRFilesResponseSchema:
44
+ self.calls.append(("get_files", {"owner": owner, "repo": repo, "pull_number": pull_number}))
45
+ return GiteaGetPRFilesResponseSchema(
46
+ root=[
47
+ GiteaPRFileSchema(
48
+ sha="abc",
49
+ status="modified",
50
+ filename="src/main.py",
51
+ patch="@@ -1,2 +1,2 @@\n- old\n+ new",
52
+ ),
53
+ GiteaPRFileSchema(
54
+ sha="def",
55
+ status="added",
56
+ filename="utils/helper.py",
57
+ patch="+ print('Hello')",
58
+ ),
59
+ ]
60
+ )
61
+
62
+ async def get_comments(self, owner: str, repo: str, pull_number: str) -> GiteaGetPRCommentsResponseSchema:
63
+ self.calls.append(("get_comments", {"owner": owner, "repo": repo, "pull_number": pull_number}))
64
+ return GiteaGetPRCommentsResponseSchema(
65
+ root=[
66
+ GiteaPRCommentSchema(
67
+ id=1,
68
+ body="General comment",
69
+ user=GiteaUserSchema(id=201, login="alice"),
70
+ ),
71
+ GiteaPRCommentSchema(
72
+ id=2,
73
+ body="Inline comment",
74
+ path="file.py",
75
+ line=5,
76
+ user=GiteaUserSchema(id=202, login="bob"),
77
+ ),
78
+ ]
79
+ )
80
+
81
+ async def create_comment(
82
+ self,
83
+ owner: str,
84
+ repo: str,
85
+ pull_number: str,
86
+ request: GiteaCreateCommentRequestSchema
87
+ ) -> GiteaCreateCommentResponseSchema:
88
+ self.calls.append(
89
+ (
90
+ "create_comment",
91
+ {"owner": owner, "repo": repo, "pull_number": pull_number, **request.model_dump()},
92
+ )
93
+ )
94
+ return GiteaCreateCommentResponseSchema(id=10, body=request.body)
95
+
96
+
97
+ class FakeGiteaHTTPClient:
98
+ def __init__(self, pull_requests_client: FakeGiteaPullRequestsHTTPClient):
99
+ self.pr = pull_requests_client
100
+
101
+
102
+ @pytest.fixture
103
+ def fake_gitea_pull_requests_http_client() -> FakeGiteaPullRequestsHTTPClient:
104
+ return FakeGiteaPullRequestsHTTPClient()
105
+
106
+
107
+ @pytest.fixture
108
+ def fake_gitea_http_client(
109
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient
110
+ ) -> FakeGiteaHTTPClient:
111
+ return FakeGiteaHTTPClient(pull_requests_client=fake_gitea_pull_requests_http_client)
112
+
113
+
114
+ @pytest.fixture
115
+ def gitea_vcs_client(
116
+ monkeypatch: pytest.MonkeyPatch,
117
+ fake_gitea_http_client: FakeGiteaHTTPClient
118
+ ) -> GiteaVCSClient:
119
+ monkeypatch.setattr(
120
+ "ai_review.services.vcs.gitea.client.get_gitea_http_client",
121
+ lambda: fake_gitea_http_client,
122
+ )
123
+ return GiteaVCSClient()
124
+
125
+
126
+ @pytest.fixture
127
+ def gitea_http_client_config(monkeypatch: pytest.MonkeyPatch):
128
+ fake_config = GiteaVCSConfig(
129
+ provider=VCSProvider.GITEA,
130
+ pipeline=GiteaPipelineConfig(
131
+ repo="repo",
132
+ owner="owner",
133
+ pull_number="1",
134
+ ),
135
+ http_client=GiteaHTTPClientConfig(
136
+ timeout=10,
137
+ api_url=HttpUrl("https://gitea.example.com"),
138
+ api_token=SecretStr("fake-token"),
139
+ ),
140
+ )
141
+ monkeypatch.setattr(settings, "vcs", fake_config)
File without changes
@@ -0,0 +1,14 @@
1
+ import pytest
2
+ from httpx import AsyncClient
3
+
4
+ from ai_review.clients.gitea.client import get_gitea_http_client, GiteaHTTPClient
5
+ from ai_review.clients.gitea.pr.client import GiteaPullRequestsHTTPClient
6
+
7
+
8
+ @pytest.mark.usefixtures("gitea_http_client_config")
9
+ def test_get_gitea_http_client_builds_ok():
10
+ gitea_http_client = get_gitea_http_client()
11
+
12
+ assert isinstance(gitea_http_client, GiteaHTTPClient)
13
+ assert isinstance(gitea_http_client.pr, GiteaPullRequestsHTTPClient)
14
+ assert isinstance(gitea_http_client.pr.client, AsyncClient)
@@ -0,0 +1,26 @@
1
+ from httpx import Response, Request
2
+
3
+ from ai_review.clients.gitea.tools import gitea_has_next_page
4
+
5
+
6
+ def make_response(headers: dict) -> Response:
7
+ return Response(
8
+ request=Request("GET", "http://gitea.test"),
9
+ headers=headers,
10
+ status_code=200,
11
+ )
12
+
13
+
14
+ def test_gitea_has_next_page_true():
15
+ resp = make_response({"Link": '<https://gitea.test?page=2>; rel="next"'})
16
+ assert gitea_has_next_page(resp) is True
17
+
18
+
19
+ def test_gitea_has_next_page_false_empty():
20
+ resp = make_response({"Link": ""})
21
+ assert gitea_has_next_page(resp) is False
22
+
23
+
24
+ def test_gitea_has_next_page_false_missing():
25
+ resp = make_response({})
26
+ assert gitea_has_next_page(resp) is False
File without changes
@@ -0,0 +1,52 @@
1
+ from ai_review.clients.gitea.pr.schema.comments import GiteaPRCommentSchema
2
+ from ai_review.clients.gitea.pr.schema.user import GiteaUserSchema
3
+ from ai_review.services.vcs.gitea.adapter import get_review_comment_from_gitea_comment, get_user_from_gitea_user
4
+ from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
5
+
6
+
7
+ def test_get_user_from_gitea_user_maps_fields_correctly():
8
+ user = GiteaUserSchema(id=42, login="tester")
9
+ result = get_user_from_gitea_user(user)
10
+
11
+ assert isinstance(result, UserSchema)
12
+ assert result.id == 42
13
+ assert result.username == "tester"
14
+ assert result.name == "tester"
15
+
16
+
17
+ def test_get_user_from_gitea_user_handles_none():
18
+ result = get_user_from_gitea_user(None)
19
+ assert result.id is None
20
+ assert result.username == ""
21
+ assert result.name == ""
22
+
23
+
24
+ def test_get_review_comment_from_gitea_comment_maps_all_fields():
25
+ comment = GiteaPRCommentSchema(
26
+ id=10,
27
+ body="Inline comment",
28
+ path="src/main.py",
29
+ line=15,
30
+ user=GiteaUserSchema(id=1, login="dev"),
31
+ )
32
+
33
+ result = get_review_comment_from_gitea_comment(comment)
34
+
35
+ assert isinstance(result, ReviewCommentSchema)
36
+ assert result.id == 10
37
+ assert result.body == "Inline comment"
38
+ assert result.file == "src/main.py"
39
+ assert result.line == 15
40
+ assert result.thread_id == 10
41
+ assert isinstance(result.author, UserSchema)
42
+ assert result.author.username == "dev"
43
+
44
+
45
+ def test_get_review_comment_handles_missing_user_and_body():
46
+ comment = GiteaPRCommentSchema(id=11, body="", path=None, line=None, user=None)
47
+
48
+ result = get_review_comment_from_gitea_comment(comment)
49
+ assert result.body == ""
50
+ assert result.author.username == ""
51
+ assert result.file is None
52
+ assert result.line is None
@@ -0,0 +1,86 @@
1
+ import pytest
2
+
3
+ from ai_review.services.vcs.gitea.client import GiteaVCSClient
4
+ from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema, ReviewThreadSchema, ThreadKind
5
+ from ai_review.tests.fixtures.clients.gitea import FakeGiteaPullRequestsHTTPClient
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ @pytest.mark.usefixtures("gitea_http_client_config")
10
+ async def test_get_review_info_returns_valid_schema(
11
+ gitea_vcs_client: GiteaVCSClient,
12
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient,
13
+ ):
14
+ info = await gitea_vcs_client.get_review_info()
15
+
16
+ assert isinstance(info, ReviewInfoSchema)
17
+ assert info.id == 1
18
+ assert info.title == "Fake Gitea PR"
19
+ assert info.author.username == "tester"
20
+ assert "src/main.py" in info.changed_files
21
+ assert info.source_branch.ref == "feature"
22
+ assert info.target_branch.ref == "main"
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ @pytest.mark.usefixtures("gitea_http_client_config")
27
+ async def test_get_general_comments_returns_list(
28
+ gitea_vcs_client: GiteaVCSClient,
29
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient,
30
+ ):
31
+ comments = await gitea_vcs_client.get_general_comments()
32
+ assert all(isinstance(c, ReviewCommentSchema) for c in comments)
33
+ assert len(comments) > 0
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ @pytest.mark.usefixtures("gitea_http_client_config")
38
+ async def test_get_inline_comments_filters_by_file(
39
+ gitea_vcs_client: GiteaVCSClient,
40
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient,
41
+ ):
42
+ comments = await gitea_vcs_client.get_inline_comments()
43
+ assert all(c.file for c in comments)
44
+ assert all(isinstance(c, ReviewCommentSchema) for c in comments)
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ @pytest.mark.usefixtures("gitea_http_client_config")
49
+ async def test_create_general_comment_posts_comment(
50
+ gitea_vcs_client: GiteaVCSClient,
51
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient,
52
+ ):
53
+ await gitea_vcs_client.create_general_comment("Test comment")
54
+ calls = [name for name, _ in fake_gitea_pull_requests_http_client.calls]
55
+ assert "create_comment" in calls
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ @pytest.mark.usefixtures("gitea_http_client_config")
60
+ async def test_create_inline_comment_posts_comment(
61
+ gitea_vcs_client: GiteaVCSClient,
62
+ fake_gitea_pull_requests_http_client: FakeGiteaPullRequestsHTTPClient,
63
+ ):
64
+ await gitea_vcs_client.create_inline_comment("src/main.py", 10, "Inline comment")
65
+ calls = [name for name, _ in fake_gitea_pull_requests_http_client.calls]
66
+ assert "create_comment" in calls
67
+
68
+
69
+ @pytest.mark.asyncio
70
+ @pytest.mark.usefixtures("gitea_http_client_config")
71
+ async def test_get_inline_threads_groups_by_comment(
72
+ gitea_vcs_client: GiteaVCSClient,
73
+ ):
74
+ threads = await gitea_vcs_client.get_inline_threads()
75
+ assert all(isinstance(t, ReviewThreadSchema) for t in threads)
76
+ assert all(t.kind == ThreadKind.INLINE for t in threads)
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ @pytest.mark.usefixtures("gitea_http_client_config")
81
+ async def test_get_general_threads_wraps_comments(
82
+ gitea_vcs_client: GiteaVCSClient,
83
+ ):
84
+ threads = await gitea_vcs_client.get_general_threads()
85
+ assert all(isinstance(t, ReviewThreadSchema) for t in threads)
86
+ assert all(t.kind == ThreadKind.SUMMARY for t in threads)
@@ -2,10 +2,17 @@ import pytest
2
2
 
3
3
  from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
4
4
  from ai_review.services.vcs.factory import get_vcs_client
5
+ from ai_review.services.vcs.gitea.client import GiteaVCSClient
5
6
  from ai_review.services.vcs.github.client import GitHubVCSClient
6
7
  from ai_review.services.vcs.gitlab.client import GitLabVCSClient
7
8
 
8
9
 
10
+ @pytest.mark.usefixtures("gitea_http_client_config")
11
+ def test_get_vcs_client_returns_gitea(monkeypatch: pytest.MonkeyPatch):
12
+ client = get_vcs_client()
13
+ assert isinstance(client, GiteaVCSClient)
14
+
15
+
9
16
  @pytest.mark.usefixtures("github_http_client_config")
10
17
  def test_get_vcs_client_returns_github(monkeypatch: pytest.MonkeyPatch):
11
18
  client = get_vcs_client()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: AI-powered code review tool
5
5
  Author-email: Nikita Filonov <nikita.filonov@example.com>
6
6
  Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
@@ -67,15 +67,15 @@ improve code quality, enforce consistency, and speed up the review process.
67
67
  ✨ Key features:
68
68
 
69
69
  - **Multiple LLM providers** — choose between **OpenAI**, **Claude**, **Gemini**, or **Ollama**, and switch anytime.
70
- - **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**.
70
+ - **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**, and **Gitea**.
71
71
  - **Customizable prompts** — adapt inline, context, and summary reviews to match your team’s coding guidelines.
72
72
  - **Reply modes** — AI can now **participate in existing review threads**, adding follow-up replies in both inline and
73
73
  summary discussions.
74
74
  - **Flexible configuration** — supports `YAML`, `JSON`, and `ENV`, with seamless overrides in CI/CD pipelines.
75
75
  - **AI Review runs fully client-side** — it never proxies or inspects your requests.
76
76
 
77
- AI Review runs automatically in your CI/CD pipeline and posts both **inline comments**, **summary reviews**, and now *
78
- *AI-generated replies** directly inside your merge requests. This makes reviews faster, more conversational, and still
77
+ AI Review runs automatically in your CI/CD pipeline and posts both **inline comments**, **summary reviews**, and now
78
+ **AI-generated replies** directly inside your merge requests. This makes reviews faster, more conversational, and still
79
79
  fully under human control.
80
80
 
81
81
  ---
@@ -85,11 +85,13 @@ fully under human control.
85
85
  Curious how **AI Review** works in practice? Here are three real Pull Requests reviewed entirely by the tool — one per
86
86
  mode:
87
87
 
88
- | Mode | Description | 🐙 GitHub | 🦊 GitLab |
89
- |------------|----------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------|
90
- | 🧩 Inline | Adds line-by-line comments directly in the diff | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/4) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/2) |
91
- | 🧠 Context | Performs broader analysis across multiple files | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/5) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/3) |
92
- | 📄 Summary | Posts a concise summary review with key highlights | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/6) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/4) |
88
+ | Mode | Description | 🐙 GitHub | 🦊 GitLab |
89
+ |------------------|----------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------|
90
+ | 🧩 Inline | Adds **line-by-line comments** directly in the diff. Focuses on specific code changes. | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/4) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/2) |
91
+ | 🧠 Context | Performs a **broader analysis across multiple files**, detecting cross-file issues and inconsistencies. | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/5) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/3) |
92
+ | 📄 Summary | Posts a **concise high-level summary** with key highlights, strengths, and major issues. | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/6) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/4) |
93
+ | 💬 Inline Reply | Generates a **context-aware reply** to an existing inline comment thread. Can clarify decisions, propose fixes, or provide code suggestions. | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/16) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/5) |
94
+ | 💬 Summary Reply | Continues the **summary-level review discussion**, responding to reviewer comments with clarifications, rationale, or actionable next steps. | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/17) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/6) |
93
95
 
94
96
  👉 Each review was generated automatically via GitHub Actions using the corresponding mode:
95
97
 
@@ -97,6 +99,8 @@ mode:
97
99
  ai-review run-inline
98
100
  ai-review run-summary
99
101
  ai-review run-context
102
+ ai-review run-inline-reply
103
+ ai-review run-summary-reply
100
104
  ```
101
105
 
102
106
  ---
@@ -162,6 +166,8 @@ vcs:
162
166
  > - ai-review run-inline
163
167
  > - ai-review run-context
164
168
  > - ai-review run-summary
169
+ > - ai-review run-inline-reply
170
+ > - ai-review run-summary-reply
165
171
 
166
172
  ---
167
173
 
@@ -172,7 +178,7 @@ Key things you can customize:
172
178
 
173
179
  - **LLM provider** — OpenAI, Gemini, Claude, or Ollama
174
180
  - **Model settings** — model name, temperature, max tokens
175
- - **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**
181
+ - **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**, and **Gitea**
176
182
  - **Review policy** — which files to include/exclude, review modes
177
183
  - **Prompts** — inline/context/summary prompt templates
178
184
 
@@ -202,7 +208,7 @@ on:
202
208
  review-command:
203
209
  type: choice
204
210
  default: run
205
- options: [ run, run-inline, run-context, run-summary ]
211
+ options: [ run, run-inline, run-context, run-summary, run-inline-reply, run-summary-reply ]
206
212
  pull-request-number:
207
213
  type: string
208
214
  required: true
@@ -211,7 +217,10 @@ jobs:
211
217
  runs-on: ubuntu-latest
212
218
  steps:
213
219
  - uses: actions/checkout@v4
214
- - uses: Nikita-Filonov/ai-review@v0.30.0
220
+ with:
221
+ fetch-depth: 0
222
+
223
+ - uses: Nikita-Filonov/ai-review@v0.31.0
215
224
  with:
216
225
  review-command: ${{ inputs.review-command }}
217
226
  env:
@@ -29,6 +29,17 @@ ai_review/clients/gemini/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
29
29
  ai_review/clients/gemini/client.py,sha256=4G1LBcpiFcrITOysQbMwhY1db4hHcSGgyI-0XazZMV0,1889
30
30
  ai_review/clients/gemini/schema.py,sha256=5oVvbI-h_sw8bFreS4JUmMj-aXa_frvxK3H8sg4iJIA,2264
31
31
  ai_review/clients/gemini/types.py,sha256=D-P0THorrQ8yq5P-NKAC65zzhEYRa9HkiXTORG9QoIk,267
32
+ ai_review/clients/gitea/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ ai_review/clients/gitea/client.py,sha256=cMxp1Ic44JFvSkuqLdWkAhsgMC6CwM3qmf-1CptqnV8,1152
34
+ ai_review/clients/gitea/tools.py,sha256=vg805Gk4hsEn8_zePtCZkvkxYPxT-A1o0mkU_RqAlC0,196
35
+ ai_review/clients/gitea/pr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ ai_review/clients/gitea/pr/client.py,sha256=eFAZai2zinr7ZGscVHa0cZIaYJJucNcLb08f73cKo-o,4945
37
+ ai_review/clients/gitea/pr/types.py,sha256=hy4win_nDvnEi_aHAbysmHtymHDq-zIATYKSEbpDcYY,984
38
+ ai_review/clients/gitea/pr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ ai_review/clients/gitea/pr/schema/comments.py,sha256=vQ-ATCmbEa2oeFndpkf-V-4jxGBH7gTMakCLVpkYunE,922
40
+ ai_review/clients/gitea/pr/schema/files.py,sha256=O4c4Z3R775OuGOJgf5baMH5ivzHCVsSBFaPXEgVmKs0,341
41
+ ai_review/clients/gitea/pr/schema/pull_request.py,sha256=lT4r-Am5MmFQqB5WnQ0OWw6cB3e-NlBAi08Jw1fmAE8,361
42
+ ai_review/clients/gitea/pr/schema/user.py,sha256=5YIys_MFFm612hKMMQdjwlsH-FG_bS-vSQsPbBueEPU,94
32
43
  ai_review/clients/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
44
  ai_review/clients/github/client.py,sha256=pprQcCYdrhRYtuqRsTFiCbj54Qb1Ll6_jmlm7AJg8pk,1149
34
45
  ai_review/clients/github/tools.py,sha256=sD2eS_iNhzT4ZLgTRmO2uBLuId-fa2aSvbu6VFeKSlc,201
@@ -83,14 +94,15 @@ ai_review/libs/config/llm/meta.py,sha256=cEcAHOwy-mQBKo9_KJrQe0I7qppq6h99lSmoWX4
83
94
  ai_review/libs/config/llm/ollama.py,sha256=M6aiPb5GvYvkiGcgHTsh9bOw5JsBLqmfSKoIbHCejrU,372
84
95
  ai_review/libs/config/llm/openai.py,sha256=jGVL4gJ2wIacoKeK9Zc9LCgY95TxdeYOThdglVPErFU,262
85
96
  ai_review/libs/config/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
- ai_review/libs/config/vcs/base.py,sha256=B0kKc6-A3mmV7dvHpo47u-1yTvdUCvLj_g4oCBo_NyY,1214
97
+ ai_review/libs/config/vcs/base.py,sha256=RJZhKysD-d8oYZQ2v1H74jyqdqtOCc8zZ0n9S4ovfHk,1471
87
98
  ai_review/libs/config/vcs/bitbucket.py,sha256=on5sQaE57kM_zSmqdDUNrttVtTPGOzqLHM5s7eFN7DA,275
99
+ ai_review/libs/config/vcs/gitea.py,sha256=elJjWnHxdC9kDWu0oHsxYsqv7FC-1zvmLfaWAlWYan4,254
88
100
  ai_review/libs/config/vcs/github.py,sha256=hk-kuDLd8wecqtEb8PSqF7Yy_pkihplJhi6nB6FZID4,256
89
101
  ai_review/libs/config/vcs/gitlab.py,sha256=ecYfU158VgVlM6P5mgZn8FOqk3Xt60xx7gUqT5e22a4,252
90
102
  ai_review/libs/config/vcs/pagination.py,sha256=S-XxWQYkIjhu3ffpHQ44d7UtRHH81fh9GaJ-xQXUUy4,175
91
103
  ai_review/libs/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
104
  ai_review/libs/constants/llm_provider.py,sha256=k7GzctIZ-TDsRlhTPbpGYgym_CO2YKVFp_oXG9dTBW0,143
93
- ai_review/libs/constants/vcs_provider.py,sha256=xJpRdJIdAf05iH2x2f362d1MuviOlPVP7In-JvDVotE,127
105
+ ai_review/libs/constants/vcs_provider.py,sha256=7A30fTSs9GM_A4J9B84MNY78c7do0RaoKytuiRwdhDY,147
94
106
  ai_review/libs/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
107
  ai_review/libs/diff/models.py,sha256=RT4YJboOPA-AjNJGRj_HIZaJLEmROOhOgMh1wIGpIwY,2344
96
108
  ai_review/libs/diff/parser.py,sha256=2BGxZnRN3SRjNnZK4qIOW28aM93Ij__1SltwclJrlno,3817
@@ -196,11 +208,14 @@ ai_review/services/review/runner/summary.py,sha256=KfQkfw_sqyvKnsi2-eD2YLfjfpOMa
196
208
  ai_review/services/review/runner/summary_reply.py,sha256=8YWMhw4dNjFgxTTgxyb6yLh-CkK_4AtfP36eD0DV4nY,3683
197
209
  ai_review/services/review/runner/types.py,sha256=lZJCiCAHnedXqYBIvnb8A0HzOQd3GEPWC_wu_mk46KY,113
198
210
  ai_review/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
- ai_review/services/vcs/factory.py,sha256=AfhpZjQ257BkLjb_7zUyw_EUnfEiCUHgTph7GGm-MY4,753
211
+ ai_review/services/vcs/factory.py,sha256=1lVkM0kwSMSzMMDZxURRDUlPC-3xw-IiBDYl6b42cTw,884
200
212
  ai_review/services/vcs/types.py,sha256=LemhQ4LAGlOdwMSF-HlYIo7taSRu4494YQ0Rp2PBgcg,3169
201
213
  ai_review/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
202
214
  ai_review/services/vcs/bitbucket/adapter.py,sha256=b-8KT46C8WT-Sos-gUGFJsxIWY7mXfzTuJjIqYuzrBA,928
203
215
  ai_review/services/vcs/bitbucket/client.py,sha256=MhqkFDewutX7DFBCFBDhDopFznwf92dAdZsxy_oS_mc,10726
216
+ ai_review/services/vcs/gitea/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
+ ai_review/services/vcs/gitea/adapter.py,sha256=DxzcXReKTGHkw4DCJa8X3Mnczcg9hqx6sIAQYY-8HAI,784
218
+ ai_review/services/vcs/gitea/client.py,sha256=HqejmtARi7-DqNcTb4P6E9tyYsC-7HtbYubmtK2OQnk,6242
204
219
  ai_review/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
220
  ai_review/services/vcs/github/adapter.py,sha256=pxcbSLEXkOg1c1NtzB0hkZJVvyC4An9pCzNK5sPJKbA,1212
206
221
  ai_review/services/vcs/github/client.py,sha256=rSVvpyovT1wDLq0fIQPe5UJqmgIgIFh2P-CPqIQ_sf0,9371
@@ -213,6 +228,7 @@ ai_review/tests/fixtures/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
213
228
  ai_review/tests/fixtures/clients/bitbucket.py,sha256=K4Ez_hTOVKz5-KlXbgTOVDec6iewxkm8hqDJvebhnpE,7124
214
229
  ai_review/tests/fixtures/clients/claude.py,sha256=6ldJlSSea0zsZV0hRDMi9mqWm0hWT3mp_ROwG_sVU1c,2203
215
230
  ai_review/tests/fixtures/clients/gemini.py,sha256=zhLJhm49keKEBCPOf_pLu8_zCatsKKAWM4-gXOhaXeM,2429
231
+ ai_review/tests/fixtures/clients/gitea.py,sha256=JTJLYRSr1hTOWXFknAq5eBSCSNxMKsdgMdjNpvVBA04,5027
216
232
  ai_review/tests/fixtures/clients/github.py,sha256=kC1L-nWZMn9O_uRfuT_B8R4sn8FRvISlBJMkRKaioS0,7814
217
233
  ai_review/tests/fixtures/clients/gitlab.py,sha256=AD6NJOJSw76hjAEiWewQ6Vu5g-cfQn0GTtdchuDBH9o,8042
218
234
  ai_review/tests/fixtures/clients/ollama.py,sha256=UUHDDPUraQAG8gBC-0UvftaK0BDYir5cJDlRKJymSQg,2109
@@ -260,6 +276,9 @@ ai_review/tests/suites/clients/claude/test_schema.py,sha256=MUZXvEROgLNpUVHfCsH5
260
276
  ai_review/tests/suites/clients/gemini/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
261
277
  ai_review/tests/suites/clients/gemini/test_client.py,sha256=f2R7KisiENrzf8gaK26NYQZpQ1dvGHykZZ-eN_xC1UQ,404
262
278
  ai_review/tests/suites/clients/gemini/test_schema.py,sha256=88dU28m7sEWvGx6tqYl7if7weWYuVc8erlkFkKKI3bk,3109
279
+ ai_review/tests/suites/clients/gitea/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
280
+ ai_review/tests/suites/clients/gitea/test_client.py,sha256=OvNLGywTsbyHKlaOOoiJu65UduJRbhEezkTIMK19gb8,544
281
+ ai_review/tests/suites/clients/gitea/test_tools.py,sha256=S9CkTccPFY1MgQ0Uilb6Kihs7iPJqNaE4Wvoa3jx8Xk,693
263
282
  ai_review/tests/suites/clients/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
264
283
  ai_review/tests/suites/clients/github/test_client.py,sha256=BiuLKCHIk83U1szYEZkB-n3vvyPgj6tAI5EqxKiT-CY,558
265
284
  ai_review/tests/suites/clients/github/test_tools.py,sha256=_RKMWNgfeynnpbiDebRLg-1Qz91Kyevf5drl4hCngzU,881
@@ -341,19 +360,22 @@ ai_review/tests/suites/services/review/runner/test_inline_reply.py,sha256=Q3gsOd
341
360
  ai_review/tests/suites/services/review/runner/test_summary.py,sha256=VLIcKffScWSaxUztYHNLAsNUMGiJQWn7j_Le8Zcrizo,3974
342
361
  ai_review/tests/suites/services/review/runner/test_summary_reply.py,sha256=UExBEkWh-EG0akVchgLdnnpcd7HFqEnDyMAVbFY_rtU,4576
343
362
  ai_review/tests/suites/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
344
- ai_review/tests/suites/services/vcs/test_factory.py,sha256=EergKSHW4b7RZg9vJJ5Cj0XfPsDTLEclV1kq2_9greA,1138
363
+ ai_review/tests/suites/services/vcs/test_factory.py,sha256=C_Ht4-zdZrAxFkw0PJYCU6IncvkQnF_0tNhvh_52P4Q,1404
345
364
  ai_review/tests/suites/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
346
365
  ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py,sha256=zOjAOxtSSN0le5_-3-vkucCnHZH0zIUer7ZAMUMo62A,3760
347
366
  ai_review/tests/suites/services/vcs/bitbucket/test_client.py,sha256=oIlUdUBPgO2bfNyAsodUsAU7kRnETQVqmRJimRtEfBU,7846
367
+ ai_review/tests/suites/services/vcs/gitea/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
368
+ ai_review/tests/suites/services/vcs/gitea/test_adapter.py,sha256=7UPsbDQhn99obPZZ2fKnLE3_h9uoY_ujMD0WbHGAegY,1782
369
+ ai_review/tests/suites/services/vcs/gitea/test_client.py,sha256=ntk-ijRqEAUZ4IGXXerhQ9nCfj6Je6hMNB666uaklws,3307
348
370
  ai_review/tests/suites/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
349
371
  ai_review/tests/suites/services/vcs/github/test_adapter.py,sha256=bK4k532v_8kDiud5RI9OlGetWMiLP8NaW1vEfHvcHfQ,4893
350
372
  ai_review/tests/suites/services/vcs/github/test_client.py,sha256=mNt1bA6aVU3REsJiU_tK1PokQxQTaCKun0tNBHuvIp8,8039
351
373
  ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
352
374
  ai_review/tests/suites/services/vcs/gitlab/test_adapter.py,sha256=BYBP2g1AKF_jCSJYJj16pW7M_6PprwD9reYEpdw3StU,4340
353
375
  ai_review/tests/suites/services/vcs/gitlab/test_client.py,sha256=dnI-YxYADmVF2GS9rp6-JPkcqsn4sN8Fjbe4MkeYMaE,8476
354
- xai_review-0.30.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
355
- xai_review-0.30.0.dist-info/METADATA,sha256=-Tmw1DyUEv-KEMfBUJCj59yzMM8STdCO-A-ZrCRet6Y,11358
356
- xai_review-0.30.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
357
- xai_review-0.30.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
358
- xai_review-0.30.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
359
- xai_review-0.30.0.dist-info/RECORD,,
376
+ xai_review-0.31.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
377
+ xai_review-0.31.0.dist-info/METADATA,sha256=yHKLyE_H6IOrbL2Sgje0LM-hcw0G4GLKm4pwwW2wtt4,12689
378
+ xai_review-0.31.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
379
+ xai_review-0.31.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
380
+ xai_review-0.31.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
381
+ xai_review-0.31.0.dist-info/RECORD,,