xai-review 0.25.0__py3-none-any.whl → 0.26.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/bitbucket/__init__.py +0 -0
- ai_review/clients/bitbucket/client.py +31 -0
- ai_review/clients/bitbucket/pr/__init__.py +0 -0
- ai_review/clients/bitbucket/pr/client.py +104 -0
- ai_review/clients/bitbucket/pr/schema/__init__.py +0 -0
- ai_review/clients/bitbucket/pr/schema/comments.py +44 -0
- ai_review/clients/bitbucket/pr/schema/files.py +25 -0
- ai_review/clients/bitbucket/pr/schema/pull_request.py +38 -0
- ai_review/clients/bitbucket/pr/types.py +44 -0
- ai_review/libs/config/vcs/base.py +11 -1
- ai_review/libs/config/vcs/bitbucket.py +13 -0
- ai_review/libs/constants/vcs_provider.py +1 -0
- ai_review/services/vcs/bitbucket/__init__.py +0 -0
- ai_review/services/vcs/bitbucket/client.py +185 -0
- ai_review/services/vcs/factory.py +3 -0
- ai_review/tests/fixtures/clients/bitbucket.py +204 -0
- ai_review/tests/suites/services/vcs/bitbucket/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/bitbucket/test_service.py +117 -0
- ai_review/tests/suites/services/vcs/test_factory.py +8 -1
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/METADATA +4 -4
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/RECORD +25 -10
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/WHEEL +0 -0
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.25.0.dist-info → xai_review-0.26.0.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ai_review.clients.bitbucket.pr.client import BitbucketPullRequestsHTTPClient
|
|
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 BitbucketHTTPClient:
|
|
11
|
+
def __init__(self, client: AsyncClient):
|
|
12
|
+
self.pr = BitbucketPullRequestsHTTPClient(client)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_bitbucket_http_client() -> BitbucketHTTPClient:
|
|
16
|
+
logger = get_logger("BITBUCKET_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"Bearer {settings.vcs.http_client.api_token_value}"},
|
|
23
|
+
base_url=settings.vcs.http_client.api_url_value,
|
|
24
|
+
transport=retry_transport,
|
|
25
|
+
event_hooks={
|
|
26
|
+
"request": [logger_event_hook.request],
|
|
27
|
+
"response": [logger_event_hook.response],
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return BitbucketHTTPClient(client=client)
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from httpx import Response, QueryParams
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
4
|
+
BitbucketGetPRCommentsQuerySchema,
|
|
5
|
+
BitbucketGetPRCommentsResponseSchema,
|
|
6
|
+
BitbucketCreatePRCommentRequestSchema,
|
|
7
|
+
BitbucketCreatePRCommentResponseSchema,
|
|
8
|
+
)
|
|
9
|
+
from ai_review.clients.bitbucket.pr.schema.files import (
|
|
10
|
+
BitbucketGetPRFilesQuerySchema,
|
|
11
|
+
BitbucketGetPRFilesResponseSchema,
|
|
12
|
+
)
|
|
13
|
+
from ai_review.clients.bitbucket.pr.schema.pull_request import BitbucketGetPRResponseSchema
|
|
14
|
+
from ai_review.clients.bitbucket.pr.types import BitbucketPullRequestsHTTPClientProtocol
|
|
15
|
+
from ai_review.libs.http.client import HTTPClient
|
|
16
|
+
from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BitbucketPullRequestsHTTPClientError(HTTPClientError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClientProtocol):
|
|
24
|
+
@handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
|
|
25
|
+
async def get_pull_request_api(self, workspace: str, repo_slug: str, pull_request_id: str) -> Response:
|
|
26
|
+
return await self.get(f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}")
|
|
27
|
+
|
|
28
|
+
@handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
|
|
29
|
+
async def get_diffstat_api(
|
|
30
|
+
self,
|
|
31
|
+
workspace: str,
|
|
32
|
+
repo_slug: str,
|
|
33
|
+
pull_request_id: str,
|
|
34
|
+
query: BitbucketGetPRFilesQuerySchema,
|
|
35
|
+
) -> Response:
|
|
36
|
+
return await self.get(
|
|
37
|
+
f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat",
|
|
38
|
+
query=QueryParams(**query.model_dump()),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
|
|
42
|
+
async def get_comments_api(
|
|
43
|
+
self,
|
|
44
|
+
workspace: str,
|
|
45
|
+
repo_slug: str,
|
|
46
|
+
pull_request_id: str,
|
|
47
|
+
query: BitbucketGetPRCommentsQuerySchema,
|
|
48
|
+
) -> Response:
|
|
49
|
+
return await self.get(
|
|
50
|
+
f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments",
|
|
51
|
+
query=QueryParams(**query.model_dump()),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
|
|
55
|
+
async def create_comment_api(
|
|
56
|
+
self,
|
|
57
|
+
workspace: str,
|
|
58
|
+
repo_slug: str,
|
|
59
|
+
pull_request_id: str,
|
|
60
|
+
request: BitbucketCreatePRCommentRequestSchema,
|
|
61
|
+
) -> Response:
|
|
62
|
+
return await self.post(
|
|
63
|
+
f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments",
|
|
64
|
+
json=request.model_dump(by_alias=True),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def get_pull_request(
|
|
68
|
+
self,
|
|
69
|
+
workspace: str,
|
|
70
|
+
repo_slug: str,
|
|
71
|
+
pull_request_id: str
|
|
72
|
+
) -> BitbucketGetPRResponseSchema:
|
|
73
|
+
resp = await self.get_pull_request_api(workspace, repo_slug, pull_request_id)
|
|
74
|
+
return BitbucketGetPRResponseSchema.model_validate_json(resp.text)
|
|
75
|
+
|
|
76
|
+
async def get_files(
|
|
77
|
+
self,
|
|
78
|
+
workspace: str,
|
|
79
|
+
repo_slug: str,
|
|
80
|
+
pull_request_id: str
|
|
81
|
+
) -> BitbucketGetPRFilesResponseSchema:
|
|
82
|
+
query = BitbucketGetPRFilesQuerySchema(pagelen=100)
|
|
83
|
+
resp = await self.get_diffstat_api(workspace, repo_slug, pull_request_id, query)
|
|
84
|
+
return BitbucketGetPRFilesResponseSchema.model_validate_json(resp.text)
|
|
85
|
+
|
|
86
|
+
async def get_comments(
|
|
87
|
+
self,
|
|
88
|
+
workspace: str,
|
|
89
|
+
repo_slug: str,
|
|
90
|
+
pull_request_id: str
|
|
91
|
+
) -> BitbucketGetPRCommentsResponseSchema:
|
|
92
|
+
query = BitbucketGetPRCommentsQuerySchema(pagelen=100)
|
|
93
|
+
response = await self.get_comments_api(workspace, repo_slug, pull_request_id, query)
|
|
94
|
+
return BitbucketGetPRCommentsResponseSchema.model_validate_json(response.text)
|
|
95
|
+
|
|
96
|
+
async def create_comment(
|
|
97
|
+
self,
|
|
98
|
+
workspace: str,
|
|
99
|
+
repo_slug: str,
|
|
100
|
+
pull_request_id: str,
|
|
101
|
+
request: BitbucketCreatePRCommentRequestSchema
|
|
102
|
+
) -> BitbucketCreatePRCommentResponseSchema:
|
|
103
|
+
response = await self.create_comment_api(workspace, repo_slug, pull_request_id, request)
|
|
104
|
+
return BitbucketCreatePRCommentResponseSchema.model_validate_json(response.text)
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BitbucketCommentContentSchema(BaseModel):
|
|
5
|
+
raw: str
|
|
6
|
+
html: str | None = None
|
|
7
|
+
markup: str | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BitbucketCommentInlineSchema(BaseModel):
|
|
11
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
12
|
+
|
|
13
|
+
path: str
|
|
14
|
+
to_line: int | None = Field(alias="to", default=None)
|
|
15
|
+
from_line: int | None = Field(alias="from", default=None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BitbucketPRCommentSchema(BaseModel):
|
|
19
|
+
id: int
|
|
20
|
+
inline: BitbucketCommentInlineSchema | None = None
|
|
21
|
+
content: BitbucketCommentContentSchema
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BitbucketGetPRCommentsQuerySchema(BaseModel):
|
|
25
|
+
pagelen: int = 100
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BitbucketGetPRCommentsResponseSchema(BaseModel):
|
|
29
|
+
size: int
|
|
30
|
+
page: int | None = None
|
|
31
|
+
next: str | None = None
|
|
32
|
+
values: list[BitbucketPRCommentSchema]
|
|
33
|
+
pagelen: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BitbucketCreatePRCommentRequestSchema(BaseModel):
|
|
37
|
+
inline: BitbucketCommentInlineSchema | None = None
|
|
38
|
+
content: BitbucketCommentContentSchema
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BitbucketCreatePRCommentResponseSchema(BaseModel):
|
|
42
|
+
id: int
|
|
43
|
+
inline: BitbucketCommentInlineSchema | None = None
|
|
44
|
+
content: BitbucketCommentContentSchema
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BitbucketPRFilePathSchema(BaseModel):
|
|
5
|
+
path: str
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BitbucketPRFileSchema(BaseModel):
|
|
9
|
+
new: BitbucketPRFilePathSchema | None = None
|
|
10
|
+
old: BitbucketPRFilePathSchema | None = None
|
|
11
|
+
status: str
|
|
12
|
+
lines_added: int
|
|
13
|
+
lines_removed: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BitbucketGetPRFilesQuerySchema(BaseModel):
|
|
17
|
+
pagelen: int = 100
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BitbucketGetPRFilesResponseSchema(BaseModel):
|
|
21
|
+
size: int
|
|
22
|
+
page: int | None = None
|
|
23
|
+
next: str | None = None
|
|
24
|
+
values: list[BitbucketPRFileSchema]
|
|
25
|
+
pagelen: int
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BitbucketUserSchema(BaseModel):
|
|
5
|
+
uuid: str
|
|
6
|
+
nickname: str
|
|
7
|
+
display_name: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BitbucketBranchSchema(BaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BitbucketCommitSchema(BaseModel):
|
|
15
|
+
hash: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BitbucketRepositorySchema(BaseModel):
|
|
19
|
+
uuid: str
|
|
20
|
+
full_name: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BitbucketPRLocationSchema(BaseModel):
|
|
24
|
+
branch: BitbucketBranchSchema
|
|
25
|
+
commit: BitbucketCommitSchema
|
|
26
|
+
repository: BitbucketRepositorySchema
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BitbucketGetPRResponseSchema(BaseModel):
|
|
30
|
+
id: int
|
|
31
|
+
title: str
|
|
32
|
+
description: str | None = None
|
|
33
|
+
state: str
|
|
34
|
+
author: BitbucketUserSchema
|
|
35
|
+
source: BitbucketPRLocationSchema
|
|
36
|
+
destination: BitbucketPRLocationSchema
|
|
37
|
+
reviewers: list[BitbucketUserSchema] = Field(default_factory=list)
|
|
38
|
+
participants: list[BitbucketUserSchema] = Field(default_factory=list)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
4
|
+
BitbucketGetPRCommentsResponseSchema,
|
|
5
|
+
BitbucketCreatePRCommentRequestSchema,
|
|
6
|
+
BitbucketCreatePRCommentResponseSchema,
|
|
7
|
+
)
|
|
8
|
+
from ai_review.clients.bitbucket.pr.schema.files import BitbucketGetPRFilesResponseSchema
|
|
9
|
+
from ai_review.clients.bitbucket.pr.schema.pull_request import BitbucketGetPRResponseSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BitbucketPullRequestsHTTPClientProtocol(Protocol):
|
|
13
|
+
async def get_pull_request(
|
|
14
|
+
self,
|
|
15
|
+
workspace: str,
|
|
16
|
+
repo_slug: str,
|
|
17
|
+
pull_request_id: str
|
|
18
|
+
) -> BitbucketGetPRResponseSchema:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
async def get_files(
|
|
22
|
+
self,
|
|
23
|
+
workspace: str,
|
|
24
|
+
repo_slug: str,
|
|
25
|
+
pull_request_id: str
|
|
26
|
+
) -> BitbucketGetPRFilesResponseSchema:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
async def get_comments(
|
|
30
|
+
self,
|
|
31
|
+
workspace: str,
|
|
32
|
+
repo_slug: str,
|
|
33
|
+
pull_request_id: str
|
|
34
|
+
) -> BitbucketGetPRCommentsResponseSchema:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
async def create_comment(
|
|
38
|
+
self,
|
|
39
|
+
workspace: str,
|
|
40
|
+
repo_slug: str,
|
|
41
|
+
pull_request_id: str,
|
|
42
|
+
request: BitbucketCreatePRCommentRequestSchema,
|
|
43
|
+
) -> BitbucketCreatePRCommentResponseSchema:
|
|
44
|
+
...
|
|
@@ -2,6 +2,7 @@ from typing import Annotated, Literal
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
|
|
5
6
|
from ai_review.libs.config.vcs.github import GitHubPipelineConfig, GitHubHTTPClientConfig
|
|
6
7
|
from ai_review.libs.config.vcs.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
|
|
7
8
|
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
@@ -23,4 +24,13 @@ class GitHubVCSConfig(VCSConfigBase):
|
|
|
23
24
|
http_client: GitHubHTTPClientConfig
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
class BitbucketVCSConfig(VCSConfigBase):
|
|
28
|
+
provider: Literal[VCSProvider.BITBUCKET]
|
|
29
|
+
pipeline: BitbucketPipelineConfig
|
|
30
|
+
http_client: BitbucketHTTPClientConfig
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
VCSConfig = Annotated[
|
|
34
|
+
GitLabVCSConfig | GitHubVCSConfig | BitbucketVCSConfig,
|
|
35
|
+
Field(discriminator="provider")
|
|
36
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.config.http import HTTPClientWithTokenConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BitbucketPipelineConfig(BaseModel):
|
|
7
|
+
workspace: str
|
|
8
|
+
repo_slug: str
|
|
9
|
+
pull_request_id: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BitbucketHTTPClientConfig(HTTPClientWithTokenConfig):
|
|
13
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from ai_review.clients.bitbucket.client import get_bitbucket_http_client
|
|
2
|
+
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
3
|
+
BitbucketCommentInlineSchema,
|
|
4
|
+
BitbucketCommentContentSchema,
|
|
5
|
+
BitbucketCreatePRCommentRequestSchema,
|
|
6
|
+
)
|
|
7
|
+
from ai_review.config import settings
|
|
8
|
+
from ai_review.libs.logger import get_logger
|
|
9
|
+
from ai_review.services.vcs.types import (
|
|
10
|
+
VCSClientProtocol,
|
|
11
|
+
UserSchema,
|
|
12
|
+
BranchRefSchema,
|
|
13
|
+
ReviewInfoSchema,
|
|
14
|
+
ReviewCommentSchema,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = get_logger("BITBUCKET_VCS_CLIENT")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BitbucketVCSClient(VCSClientProtocol):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.http_client = get_bitbucket_http_client()
|
|
23
|
+
self.workspace = settings.vcs.pipeline.workspace
|
|
24
|
+
self.repo_slug = settings.vcs.pipeline.repo_slug
|
|
25
|
+
self.pull_request_id = settings.vcs.pipeline.pull_request_id
|
|
26
|
+
|
|
27
|
+
async def get_review_info(self) -> ReviewInfoSchema:
|
|
28
|
+
try:
|
|
29
|
+
pr = await self.http_client.pr.get_pull_request(
|
|
30
|
+
workspace=self.workspace,
|
|
31
|
+
repo_slug=self.repo_slug,
|
|
32
|
+
pull_request_id=self.pull_request_id,
|
|
33
|
+
)
|
|
34
|
+
files = await self.http_client.pr.get_files(
|
|
35
|
+
workspace=self.workspace,
|
|
36
|
+
repo_slug=self.repo_slug,
|
|
37
|
+
pull_request_id=self.pull_request_id,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger.info(f"Fetched PR info for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
|
|
41
|
+
|
|
42
|
+
return ReviewInfoSchema(
|
|
43
|
+
id=pr.id,
|
|
44
|
+
title=pr.title,
|
|
45
|
+
description=pr.description or "",
|
|
46
|
+
author=UserSchema(
|
|
47
|
+
id=pr.author.uuid,
|
|
48
|
+
name=pr.author.display_name,
|
|
49
|
+
username=pr.author.nickname,
|
|
50
|
+
),
|
|
51
|
+
labels=[],
|
|
52
|
+
base_sha=pr.destination.commit.hash,
|
|
53
|
+
head_sha=pr.source.commit.hash,
|
|
54
|
+
assignees=[
|
|
55
|
+
UserSchema(
|
|
56
|
+
id=user.uuid,
|
|
57
|
+
name=user.display_name,
|
|
58
|
+
username=user.nickname,
|
|
59
|
+
)
|
|
60
|
+
for user in pr.participants
|
|
61
|
+
],
|
|
62
|
+
reviewers=[
|
|
63
|
+
UserSchema(
|
|
64
|
+
id=user.uuid,
|
|
65
|
+
name=user.display_name,
|
|
66
|
+
username=user.nickname,
|
|
67
|
+
)
|
|
68
|
+
for user in pr.reviewers
|
|
69
|
+
],
|
|
70
|
+
source_branch=BranchRefSchema(
|
|
71
|
+
ref=pr.source.branch.name,
|
|
72
|
+
sha=pr.source.commit.hash,
|
|
73
|
+
),
|
|
74
|
+
target_branch=BranchRefSchema(
|
|
75
|
+
ref=pr.destination.branch.name,
|
|
76
|
+
sha=pr.destination.commit.hash,
|
|
77
|
+
),
|
|
78
|
+
changed_files=[
|
|
79
|
+
file.new.path if file.new else file.old.path
|
|
80
|
+
for file in files.values
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
except Exception as error:
|
|
84
|
+
logger.exception(
|
|
85
|
+
f"Failed to fetch PR info {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
86
|
+
)
|
|
87
|
+
return ReviewInfoSchema()
|
|
88
|
+
|
|
89
|
+
async def get_general_comments(self) -> list[ReviewCommentSchema]:
|
|
90
|
+
try:
|
|
91
|
+
response = await self.http_client.pr.get_comments(
|
|
92
|
+
workspace=self.workspace,
|
|
93
|
+
repo_slug=self.repo_slug,
|
|
94
|
+
pull_request_id=self.pull_request_id,
|
|
95
|
+
)
|
|
96
|
+
logger.info(f"Fetched general comments for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
ReviewCommentSchema(id=comment.id, body=comment.content.raw)
|
|
100
|
+
for comment in response.values
|
|
101
|
+
if comment.inline is None
|
|
102
|
+
]
|
|
103
|
+
except Exception as error:
|
|
104
|
+
logger.exception(
|
|
105
|
+
f"Failed to fetch general comments for "
|
|
106
|
+
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
107
|
+
)
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
async def get_inline_comments(self) -> list[ReviewCommentSchema]:
|
|
111
|
+
try:
|
|
112
|
+
response = await self.http_client.pr.get_comments(
|
|
113
|
+
workspace=self.workspace,
|
|
114
|
+
repo_slug=self.repo_slug,
|
|
115
|
+
pull_request_id=self.pull_request_id,
|
|
116
|
+
)
|
|
117
|
+
logger.info(f"Fetched inline comments for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
ReviewCommentSchema(
|
|
121
|
+
id=comment.id,
|
|
122
|
+
body=comment.content.raw,
|
|
123
|
+
file=comment.inline.path,
|
|
124
|
+
line=comment.inline.to_line,
|
|
125
|
+
)
|
|
126
|
+
for comment in response.values
|
|
127
|
+
if comment.inline is not None
|
|
128
|
+
]
|
|
129
|
+
except Exception as error:
|
|
130
|
+
logger.exception(
|
|
131
|
+
f"Failed to fetch inline comments for "
|
|
132
|
+
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
133
|
+
)
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
async def create_general_comment(self, message: str) -> None:
|
|
137
|
+
try:
|
|
138
|
+
logger.info(
|
|
139
|
+
f"Posting general comment to PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {message}"
|
|
140
|
+
)
|
|
141
|
+
request = BitbucketCreatePRCommentRequestSchema(
|
|
142
|
+
content=BitbucketCommentContentSchema(raw=message)
|
|
143
|
+
)
|
|
144
|
+
await self.http_client.pr.create_comment(
|
|
145
|
+
workspace=self.workspace,
|
|
146
|
+
repo_slug=self.repo_slug,
|
|
147
|
+
pull_request_id=self.pull_request_id,
|
|
148
|
+
request=request,
|
|
149
|
+
)
|
|
150
|
+
logger.info(
|
|
151
|
+
f"Created general comment in PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}"
|
|
152
|
+
)
|
|
153
|
+
except Exception as error:
|
|
154
|
+
logger.exception(
|
|
155
|
+
f"Failed to create general comment in PR "
|
|
156
|
+
f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
|
|
157
|
+
)
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
async def create_inline_comment(self, file: str, line: int, message: str) -> None:
|
|
161
|
+
try:
|
|
162
|
+
logger.info(
|
|
163
|
+
f"Posting inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
|
|
164
|
+
f"at {file}:{line}: {message}"
|
|
165
|
+
)
|
|
166
|
+
request = BitbucketCreatePRCommentRequestSchema(
|
|
167
|
+
content=BitbucketCommentContentSchema(raw=message),
|
|
168
|
+
inline=BitbucketCommentInlineSchema(path=file, to_line=line),
|
|
169
|
+
)
|
|
170
|
+
await self.http_client.pr.create_comment(
|
|
171
|
+
workspace=self.workspace,
|
|
172
|
+
repo_slug=self.repo_slug,
|
|
173
|
+
pull_request_id=self.pull_request_id,
|
|
174
|
+
request=request,
|
|
175
|
+
)
|
|
176
|
+
logger.info(
|
|
177
|
+
f"Created inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
|
|
178
|
+
f"at {file}:{line}"
|
|
179
|
+
)
|
|
180
|
+
except Exception as error:
|
|
181
|
+
logger.exception(
|
|
182
|
+
f"Failed to create inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
|
|
183
|
+
f"at {file}:{line}: {error}"
|
|
184
|
+
)
|
|
185
|
+
raise
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from ai_review.config import settings
|
|
2
2
|
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
3
|
+
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
3
4
|
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
4
5
|
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
5
6
|
from ai_review.services.vcs.types import VCSClientProtocol
|
|
@@ -11,5 +12,7 @@ def get_vcs_client() -> VCSClientProtocol:
|
|
|
11
12
|
return GitLabVCSClient()
|
|
12
13
|
case VCSProvider.GITHUB:
|
|
13
14
|
return GitHubVCSClient()
|
|
15
|
+
case VCSProvider.BITBUCKET:
|
|
16
|
+
return BitbucketVCSClient()
|
|
14
17
|
case _:
|
|
15
18
|
raise ValueError(f"Unsupported VCS provider: {settings.vcs.provider}")
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import HttpUrl, SecretStr
|
|
3
|
+
|
|
4
|
+
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
5
|
+
BitbucketPRCommentSchema,
|
|
6
|
+
BitbucketCommentContentSchema,
|
|
7
|
+
BitbucketCommentInlineSchema,
|
|
8
|
+
BitbucketGetPRCommentsResponseSchema,
|
|
9
|
+
BitbucketCreatePRCommentRequestSchema,
|
|
10
|
+
BitbucketCreatePRCommentResponseSchema,
|
|
11
|
+
)
|
|
12
|
+
from ai_review.clients.bitbucket.pr.schema.files import (
|
|
13
|
+
BitbucketGetPRFilesResponseSchema,
|
|
14
|
+
BitbucketPRFileSchema,
|
|
15
|
+
BitbucketPRFilePathSchema,
|
|
16
|
+
)
|
|
17
|
+
from ai_review.clients.bitbucket.pr.schema.pull_request import (
|
|
18
|
+
BitbucketUserSchema,
|
|
19
|
+
BitbucketBranchSchema,
|
|
20
|
+
BitbucketCommitSchema,
|
|
21
|
+
BitbucketRepositorySchema,
|
|
22
|
+
BitbucketPRLocationSchema,
|
|
23
|
+
BitbucketGetPRResponseSchema,
|
|
24
|
+
)
|
|
25
|
+
from ai_review.clients.bitbucket.pr.types import BitbucketPullRequestsHTTPClientProtocol
|
|
26
|
+
from ai_review.config import settings
|
|
27
|
+
from ai_review.libs.config.vcs.base import BitbucketVCSConfig
|
|
28
|
+
from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
|
|
29
|
+
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
30
|
+
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FakeBitbucketPullRequestsHTTPClient(BitbucketPullRequestsHTTPClientProtocol):
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.calls: list[tuple[str, dict]] = []
|
|
36
|
+
|
|
37
|
+
async def get_pull_request(
|
|
38
|
+
self,
|
|
39
|
+
workspace: str,
|
|
40
|
+
repo_slug: str,
|
|
41
|
+
pull_request_id: str
|
|
42
|
+
) -> BitbucketGetPRResponseSchema:
|
|
43
|
+
self.calls.append(
|
|
44
|
+
(
|
|
45
|
+
"get_pull_request",
|
|
46
|
+
{"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return BitbucketGetPRResponseSchema(
|
|
50
|
+
id=1,
|
|
51
|
+
title="Fake Bitbucket PR",
|
|
52
|
+
description="This is a fake PR for testing",
|
|
53
|
+
state="OPEN",
|
|
54
|
+
author=BitbucketUserSchema(uuid="u1", display_name="Tester", nickname="tester"),
|
|
55
|
+
source=BitbucketPRLocationSchema(
|
|
56
|
+
commit=BitbucketCommitSchema(hash="def456"),
|
|
57
|
+
branch=BitbucketBranchSchema(name="feature/test"),
|
|
58
|
+
repository=BitbucketRepositorySchema(uuid="r1", full_name="workspace/repo"),
|
|
59
|
+
),
|
|
60
|
+
destination=BitbucketPRLocationSchema(
|
|
61
|
+
commit=BitbucketCommitSchema(hash="abc123"),
|
|
62
|
+
branch=BitbucketBranchSchema(name="main"),
|
|
63
|
+
repository=BitbucketRepositorySchema(uuid="r1", full_name="workspace/repo"),
|
|
64
|
+
),
|
|
65
|
+
reviewers=[BitbucketUserSchema(uuid="u2", display_name="Reviewer", nickname="reviewer")],
|
|
66
|
+
participants=[BitbucketUserSchema(uuid="u3", display_name="Participant", nickname="participant")],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def get_files(
|
|
70
|
+
self,
|
|
71
|
+
workspace: str,
|
|
72
|
+
repo_slug: str,
|
|
73
|
+
pull_request_id: str
|
|
74
|
+
) -> BitbucketGetPRFilesResponseSchema:
|
|
75
|
+
self.calls.append(
|
|
76
|
+
(
|
|
77
|
+
"get_files",
|
|
78
|
+
{"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return BitbucketGetPRFilesResponseSchema(
|
|
82
|
+
size=2,
|
|
83
|
+
page=1,
|
|
84
|
+
pagelen=100,
|
|
85
|
+
next=None,
|
|
86
|
+
values=[
|
|
87
|
+
BitbucketPRFileSchema(
|
|
88
|
+
new=BitbucketPRFilePathSchema(path="app/main.py"),
|
|
89
|
+
old=None,
|
|
90
|
+
status="modified",
|
|
91
|
+
lines_added=10,
|
|
92
|
+
lines_removed=2,
|
|
93
|
+
),
|
|
94
|
+
BitbucketPRFileSchema(
|
|
95
|
+
new=BitbucketPRFilePathSchema(path="utils/helper.py"),
|
|
96
|
+
old=None,
|
|
97
|
+
status="added",
|
|
98
|
+
lines_added=5,
|
|
99
|
+
lines_removed=0,
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def get_comments(
|
|
105
|
+
self,
|
|
106
|
+
workspace: str,
|
|
107
|
+
repo_slug: str,
|
|
108
|
+
pull_request_id: str
|
|
109
|
+
) -> BitbucketGetPRCommentsResponseSchema:
|
|
110
|
+
self.calls.append(
|
|
111
|
+
(
|
|
112
|
+
"get_comments",
|
|
113
|
+
{"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return BitbucketGetPRCommentsResponseSchema(
|
|
117
|
+
size=2,
|
|
118
|
+
page=1,
|
|
119
|
+
next=None,
|
|
120
|
+
values=[
|
|
121
|
+
BitbucketPRCommentSchema(
|
|
122
|
+
id=1,
|
|
123
|
+
inline=None,
|
|
124
|
+
content=BitbucketCommentContentSchema(raw="General comment"),
|
|
125
|
+
),
|
|
126
|
+
BitbucketPRCommentSchema(
|
|
127
|
+
id=2,
|
|
128
|
+
inline=BitbucketCommentInlineSchema(path="file.py", to_line=5),
|
|
129
|
+
content=BitbucketCommentContentSchema(raw="Inline comment"),
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
pagelen=100,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def create_comment(
|
|
136
|
+
self,
|
|
137
|
+
workspace: str,
|
|
138
|
+
repo_slug: str,
|
|
139
|
+
pull_request_id: str,
|
|
140
|
+
request: BitbucketCreatePRCommentRequestSchema
|
|
141
|
+
) -> BitbucketCreatePRCommentResponseSchema:
|
|
142
|
+
self.calls.append(
|
|
143
|
+
(
|
|
144
|
+
"create_comment",
|
|
145
|
+
{
|
|
146
|
+
"workspace": workspace,
|
|
147
|
+
"repo_slug": repo_slug,
|
|
148
|
+
"pull_request_id": pull_request_id,
|
|
149
|
+
**request.model_dump(by_alias=True)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return BitbucketCreatePRCommentResponseSchema(
|
|
154
|
+
id=10,
|
|
155
|
+
content=request.content,
|
|
156
|
+
inline=request.inline,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class FakeBitbucketHTTPClient:
|
|
161
|
+
def __init__(self, pull_requests_client: BitbucketPullRequestsHTTPClientProtocol):
|
|
162
|
+
self.pr = pull_requests_client
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.fixture
|
|
166
|
+
def fake_bitbucket_pull_requests_http_client() -> FakeBitbucketPullRequestsHTTPClient:
|
|
167
|
+
return FakeBitbucketPullRequestsHTTPClient()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.fixture
|
|
171
|
+
def fake_bitbucket_http_client(
|
|
172
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient
|
|
173
|
+
) -> FakeBitbucketHTTPClient:
|
|
174
|
+
return FakeBitbucketHTTPClient(pull_requests_client=fake_bitbucket_pull_requests_http_client)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.fixture
|
|
178
|
+
def bitbucket_vcs_client(
|
|
179
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
180
|
+
fake_bitbucket_http_client: FakeBitbucketHTTPClient
|
|
181
|
+
) -> BitbucketVCSClient:
|
|
182
|
+
monkeypatch.setattr(
|
|
183
|
+
"ai_review.services.vcs.bitbucket.client.get_bitbucket_http_client",
|
|
184
|
+
lambda: fake_bitbucket_http_client,
|
|
185
|
+
)
|
|
186
|
+
return BitbucketVCSClient()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.fixture
|
|
190
|
+
def bitbucket_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
191
|
+
fake_config = BitbucketVCSConfig(
|
|
192
|
+
provider=VCSProvider.BITBUCKET,
|
|
193
|
+
pipeline=BitbucketPipelineConfig(
|
|
194
|
+
workspace="workspace",
|
|
195
|
+
repo_slug="repo",
|
|
196
|
+
pull_request_id="123",
|
|
197
|
+
),
|
|
198
|
+
http_client=BitbucketHTTPClientConfig(
|
|
199
|
+
timeout=10,
|
|
200
|
+
api_url=HttpUrl("https://api.bitbucket.org/2.0"),
|
|
201
|
+
api_token=SecretStr("fake-token"),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
monkeypatch.setattr(settings, "vcs", fake_config)
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema
|
|
5
|
+
from ai_review.tests.fixtures.clients.bitbucket import FakeBitbucketPullRequestsHTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
10
|
+
async def test_get_review_info_returns_valid_schema(
|
|
11
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
12
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
13
|
+
):
|
|
14
|
+
"""Should return detailed PR info with branches, author, reviewers, and files."""
|
|
15
|
+
info = await bitbucket_vcs_client.get_review_info()
|
|
16
|
+
|
|
17
|
+
assert isinstance(info, ReviewInfoSchema)
|
|
18
|
+
assert info.id == 1
|
|
19
|
+
assert info.title == "Fake Bitbucket PR"
|
|
20
|
+
assert info.description == "This is a fake PR for testing"
|
|
21
|
+
|
|
22
|
+
assert info.author.username == "tester"
|
|
23
|
+
assert {r.username for r in info.reviewers} == {"reviewer"}
|
|
24
|
+
|
|
25
|
+
assert info.source_branch.ref == "feature/test"
|
|
26
|
+
assert info.target_branch.ref == "main"
|
|
27
|
+
assert info.base_sha == "abc123"
|
|
28
|
+
assert info.head_sha == "def456"
|
|
29
|
+
|
|
30
|
+
assert "app/main.py" in info.changed_files
|
|
31
|
+
assert len(info.changed_files) == 2
|
|
32
|
+
|
|
33
|
+
called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
|
|
34
|
+
assert called_methods == ["get_pull_request", "get_files"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
39
|
+
async def test_get_general_comments_filters_inline(
|
|
40
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
41
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
42
|
+
):
|
|
43
|
+
"""Should return only general comments (without inline info)."""
|
|
44
|
+
comments = await bitbucket_vcs_client.get_general_comments()
|
|
45
|
+
|
|
46
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
47
|
+
assert len(comments) == 1
|
|
48
|
+
|
|
49
|
+
first = comments[0]
|
|
50
|
+
assert first.body == "General comment"
|
|
51
|
+
assert first.file is None
|
|
52
|
+
assert first.line is None
|
|
53
|
+
|
|
54
|
+
called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
|
|
55
|
+
assert called_methods == ["get_comments"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
60
|
+
async def test_get_inline_comments_filters_general(
|
|
61
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
62
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
63
|
+
):
|
|
64
|
+
"""Should return only inline comments with file and line references."""
|
|
65
|
+
comments = await bitbucket_vcs_client.get_inline_comments()
|
|
66
|
+
|
|
67
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
68
|
+
assert len(comments) == 1
|
|
69
|
+
|
|
70
|
+
first = comments[0]
|
|
71
|
+
assert first.body == "Inline comment"
|
|
72
|
+
assert first.file == "file.py"
|
|
73
|
+
assert first.line == 5
|
|
74
|
+
|
|
75
|
+
called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
|
|
76
|
+
assert called_methods == ["get_comments"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
81
|
+
async def test_create_general_comment_posts_comment(
|
|
82
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
83
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
84
|
+
):
|
|
85
|
+
"""Should post a general (non-inline) comment."""
|
|
86
|
+
message = "Hello from Bitbucket test!"
|
|
87
|
+
|
|
88
|
+
await bitbucket_vcs_client.create_general_comment(message)
|
|
89
|
+
|
|
90
|
+
calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
|
|
91
|
+
assert len(calls) == 1
|
|
92
|
+
call_args = calls[0]
|
|
93
|
+
assert call_args["content"]["raw"] == message
|
|
94
|
+
assert call_args["workspace"] == "workspace"
|
|
95
|
+
assert call_args["repo_slug"] == "repo"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
100
|
+
async def test_create_inline_comment_posts_comment(
|
|
101
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
102
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
103
|
+
):
|
|
104
|
+
"""Should post an inline comment with correct file and line."""
|
|
105
|
+
file = "file.py"
|
|
106
|
+
line = 10
|
|
107
|
+
message = "Looks good"
|
|
108
|
+
|
|
109
|
+
await bitbucket_vcs_client.create_inline_comment(file, line, message)
|
|
110
|
+
|
|
111
|
+
calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
|
|
112
|
+
assert len(calls) == 1
|
|
113
|
+
|
|
114
|
+
call_args = calls[0]
|
|
115
|
+
assert call_args["content"]["raw"] == message
|
|
116
|
+
assert call_args["inline"]["path"] == file
|
|
117
|
+
assert call_args["inline"]["to"] == line
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
+
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
3
4
|
from ai_review.services.vcs.factory import get_vcs_client
|
|
4
5
|
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
5
6
|
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
@@ -17,7 +18,13 @@ def test_get_vcs_client_returns_gitlab(monkeypatch: pytest.MonkeyPatch):
|
|
|
17
18
|
assert isinstance(client, GitLabVCSClient)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
22
|
+
def test_get_vcs_client_returns_bitbucket(monkeypatch: pytest.MonkeyPatch):
|
|
23
|
+
client = get_vcs_client()
|
|
24
|
+
assert isinstance(client, BitbucketVCSClient)
|
|
25
|
+
|
|
26
|
+
|
|
20
27
|
def test_get_vcs_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
|
|
21
|
-
monkeypatch.setattr("ai_review.services.vcs.factory.settings.vcs.provider", "
|
|
28
|
+
monkeypatch.setattr("ai_review.services.vcs.factory.settings.vcs.provider", "UNSUPPORTED")
|
|
22
29
|
with pytest.raises(ValueError):
|
|
23
30
|
get_vcs_client()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xai-review
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.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,7 +67,7 @@ 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
|
|
70
|
+
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**.
|
|
71
71
|
- **Customizable prompts** — adapt inline, context, and summary reviews to match your team’s coding guidelines.
|
|
72
72
|
- **Flexible configuration** — supports `YAML`, `JSON`, and `ENV`, with seamless overrides in CI/CD pipelines.
|
|
73
73
|
- **AI Review runs fully client-side** — it never proxies or inspects your requests.
|
|
@@ -170,7 +170,7 @@ Key things you can customize:
|
|
|
170
170
|
|
|
171
171
|
- **LLM provider** — OpenAI, Gemini, Claude, or Ollama
|
|
172
172
|
- **Model settings** — model name, temperature, max tokens
|
|
173
|
-
- **VCS integration** — works out of the box with **GitLab** and **
|
|
173
|
+
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**
|
|
174
174
|
- **Review policy** — which files to include/exclude, review modes
|
|
175
175
|
- **Prompts** — inline/context/summary prompt templates
|
|
176
176
|
|
|
@@ -209,7 +209,7 @@ jobs:
|
|
|
209
209
|
runs-on: ubuntu-latest
|
|
210
210
|
steps:
|
|
211
211
|
- uses: actions/checkout@v4
|
|
212
|
-
- uses: Nikita-Filonov/ai-review@v0.
|
|
212
|
+
- uses: Nikita-Filonov/ai-review@v0.26.0
|
|
213
213
|
with:
|
|
214
214
|
review-command: ${{ inputs.review-command }}
|
|
215
215
|
env:
|
|
@@ -8,6 +8,15 @@ ai_review/cli/commands/run_inline_review.py,sha256=u55K-Su0PR2-NcK7XI2rTCIi7HTEi
|
|
|
8
8
|
ai_review/cli/commands/run_review.py,sha256=i39IYNDE_lAiQQnKLmxG71Ao8WAIOSn82L9EpdbPcsI,261
|
|
9
9
|
ai_review/cli/commands/run_summary_review.py,sha256=NqjepGH5cbqczPzcuMEAxO4dI58FEUZl0b6uRVQ9SiA,224
|
|
10
10
|
ai_review/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
ai_review/clients/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
ai_review/clients/bitbucket/client.py,sha256=VaqaQ5USMPTOEeS5XPdr-RkMKsxUpJ2SBE6lcemkz-g,1174
|
|
13
|
+
ai_review/clients/bitbucket/pr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
ai_review/clients/bitbucket/pr/client.py,sha256=9C6vXBz8o0Df76N9WW4hORN-Q39Vd8I575AaidyW_HM,4359
|
|
15
|
+
ai_review/clients/bitbucket/pr/types.py,sha256=ZICV4ghYChj1Jl9Nlwyw1_kwmGybX51GhGdGzkRaLCk,1296
|
|
16
|
+
ai_review/clients/bitbucket/pr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
ai_review/clients/bitbucket/pr/schema/comments.py,sha256=DLi3LhThXfHB9MJ5Akv7Yf_n-VttjvJAausSMoksHTY,1152
|
|
18
|
+
ai_review/clients/bitbucket/pr/schema/files.py,sha256=A-h9Cgi0iJ6e9pGr5TcbpgSb3y9SMTqNi5FxJ7ySxpk,546
|
|
19
|
+
ai_review/clients/bitbucket/pr/schema/pull_request.py,sha256=buGULgaCkxCUFSdiw0XTwaSIYP_p1rAEuKXUyJ_Mzi8,863
|
|
11
20
|
ai_review/clients/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
21
|
ai_review/clients/claude/client.py,sha256=uEadbBNBJnzjHDczbxXiiw1V1H1PdUWKu-Gn-eIDEmw,1890
|
|
13
22
|
ai_review/clients/claude/schema.py,sha256=LE6KCjJKDXqBGU2Cno5XL5R8vUfScgskE9MqvE0Pt2A,887
|
|
@@ -65,12 +74,13 @@ ai_review/libs/config/llm/meta.py,sha256=cEcAHOwy-mQBKo9_KJrQe0I7qppq6h99lSmoWX4
|
|
|
65
74
|
ai_review/libs/config/llm/ollama.py,sha256=M6aiPb5GvYvkiGcgHTsh9bOw5JsBLqmfSKoIbHCejrU,372
|
|
66
75
|
ai_review/libs/config/llm/openai.py,sha256=jGVL4gJ2wIacoKeK9Zc9LCgY95TxdeYOThdglVPErFU,262
|
|
67
76
|
ai_review/libs/config/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
-
ai_review/libs/config/vcs/base.py,sha256=
|
|
77
|
+
ai_review/libs/config/vcs/base.py,sha256=ks9lrSalkPUuG8ijlaw-8d-F-dv59GdSywHS2TsIKjs,1085
|
|
78
|
+
ai_review/libs/config/vcs/bitbucket.py,sha256=on5sQaE57kM_zSmqdDUNrttVtTPGOzqLHM5s7eFN7DA,275
|
|
69
79
|
ai_review/libs/config/vcs/github.py,sha256=hk-kuDLd8wecqtEb8PSqF7Yy_pkihplJhi6nB6FZID4,256
|
|
70
80
|
ai_review/libs/config/vcs/gitlab.py,sha256=ecYfU158VgVlM6P5mgZn8FOqk3Xt60xx7gUqT5e22a4,252
|
|
71
81
|
ai_review/libs/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
82
|
ai_review/libs/constants/llm_provider.py,sha256=k7GzctIZ-TDsRlhTPbpGYgym_CO2YKVFp_oXG9dTBW0,143
|
|
73
|
-
ai_review/libs/constants/vcs_provider.py,sha256=
|
|
83
|
+
ai_review/libs/constants/vcs_provider.py,sha256=xJpRdJIdAf05iH2x2f362d1MuviOlPVP7In-JvDVotE,127
|
|
74
84
|
ai_review/libs/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
85
|
ai_review/libs/diff/models.py,sha256=RT4YJboOPA-AjNJGRj_HIZaJLEmROOhOgMh1wIGpIwY,2344
|
|
76
86
|
ai_review/libs/diff/parser.py,sha256=2BGxZnRN3SRjNnZK4qIOW28aM93Ij__1SltwclJrlno,3817
|
|
@@ -150,8 +160,10 @@ ai_review/services/review/summary/schema.py,sha256=GipVNWrEKtgZPkytNSrXwzvX9Zq8P
|
|
|
150
160
|
ai_review/services/review/summary/service.py,sha256=F4diIESc0y7YSiUKbInHWiSOW5tW_eQ0rpf78wKxLAo,562
|
|
151
161
|
ai_review/services/review/summary/types.py,sha256=iDsucvx9xJZ5Xb5FN70da3bub3YDtt4vpQeVEK532E8,235
|
|
152
162
|
ai_review/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
153
|
-
ai_review/services/vcs/factory.py,sha256=
|
|
163
|
+
ai_review/services/vcs/factory.py,sha256=AfhpZjQ257BkLjb_7zUyw_EUnfEiCUHgTph7GGm-MY4,753
|
|
154
164
|
ai_review/services/vcs/types.py,sha256=S49LhAGHVAd_0QwZUr4JMhfc6DR-HikHR6-T_ETlTus,1998
|
|
165
|
+
ai_review/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
166
|
+
ai_review/services/vcs/bitbucket/client.py,sha256=OceM48MBoiUVKGTh8ZrrpVt8a1fDczCvOMD9VlwoapY,7253
|
|
155
167
|
ai_review/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
156
168
|
ai_review/services/vcs/github/client.py,sha256=v6NV97xi_rtRQQi8atRdSrXKhSOQ7CeRHK7YjoyjU6Q,6353
|
|
157
169
|
ai_review/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -159,6 +171,7 @@ ai_review/services/vcs/gitlab/client.py,sha256=LK95m-uFSxhDEVU-cBGct61NTKjul-ieL
|
|
|
159
171
|
ai_review/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
160
172
|
ai_review/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
161
173
|
ai_review/tests/fixtures/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
174
|
+
ai_review/tests/fixtures/clients/bitbucket.py,sha256=XJK1nU7Wir5PnmwCUJ_2uTlByA5a_CTEuXc2a-WmWio,7122
|
|
162
175
|
ai_review/tests/fixtures/clients/claude.py,sha256=6ldJlSSea0zsZV0hRDMi9mqWm0hWT3mp_ROwG_sVU1c,2203
|
|
163
176
|
ai_review/tests/fixtures/clients/gemini.py,sha256=zhLJhm49keKEBCPOf_pLu8_zCatsKKAWM4-gXOhaXeM,2429
|
|
164
177
|
ai_review/tests/fixtures/clients/github.py,sha256=Mzr8LcvVlYLhimzDMG4tEOQwj_6E6kTvYvSrq04R3YI,6865
|
|
@@ -242,14 +255,16 @@ ai_review/tests/suites/services/review/summary/__init__.py,sha256=47DEQpj8HBSa-_
|
|
|
242
255
|
ai_review/tests/suites/services/review/summary/test_schema.py,sha256=HUbSDbQzBp-iTsGLs7hJfu-sz6sq9xLO0woGmZPWyx0,735
|
|
243
256
|
ai_review/tests/suites/services/review/summary/test_service.py,sha256=ibiYOWQMZuQKRutIT_EKGq7DEPQvp62YhscNHeSWFVQ,588
|
|
244
257
|
ai_review/tests/suites/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
245
|
-
ai_review/tests/suites/services/vcs/test_factory.py,sha256=
|
|
258
|
+
ai_review/tests/suites/services/vcs/test_factory.py,sha256=EergKSHW4b7RZg9vJJ5Cj0XfPsDTLEclV1kq2_9greA,1138
|
|
259
|
+
ai_review/tests/suites/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
260
|
+
ai_review/tests/suites/services/vcs/bitbucket/test_service.py,sha256=JnG5BYTgGMb-doNjis2BOeI8JrMmvqwv82UFD5f92kg,4448
|
|
246
261
|
ai_review/tests/suites/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
247
262
|
ai_review/tests/suites/services/vcs/github/test_service.py,sha256=c2sjecm4qzqYXuO9j6j35NQyJzqDpnXIJImRTcpkyHo,4378
|
|
248
263
|
ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
249
264
|
ai_review/tests/suites/services/vcs/gitlab/test_service.py,sha256=0dqgL5whzjcP-AQ4adP_12QfkYm_ZtdtMotmYm8Se7Y,4449
|
|
250
|
-
xai_review-0.
|
|
251
|
-
xai_review-0.
|
|
252
|
-
xai_review-0.
|
|
253
|
-
xai_review-0.
|
|
254
|
-
xai_review-0.
|
|
255
|
-
xai_review-0.
|
|
265
|
+
xai_review-0.26.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
|
|
266
|
+
xai_review-0.26.0.dist-info/METADATA,sha256=RXLUNKPnkpxjCcXWbKwFI8RN0UHvVjfWwtqGpVsukIs,11150
|
|
267
|
+
xai_review-0.26.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
268
|
+
xai_review-0.26.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
|
|
269
|
+
xai_review-0.26.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
|
|
270
|
+
xai_review-0.26.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|