xai-review 0.37.0__py3-none-any.whl → 0.38.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 (61) hide show
  1. ai_review/clients/{bitbucket → bitbucket_cloud}/client.py +6 -6
  2. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/client.py +51 -39
  3. ai_review/clients/bitbucket_cloud/pr/schema/comments.py +59 -0
  4. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/files.py +7 -7
  5. ai_review/clients/bitbucket_cloud/pr/schema/pull_request.py +34 -0
  6. ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/user.py +1 -1
  7. ai_review/clients/bitbucket_cloud/pr/types.py +44 -0
  8. ai_review/clients/{bitbucket → bitbucket_cloud}/tools.py +1 -1
  9. ai_review/clients/bitbucket_server/client.py +32 -0
  10. ai_review/clients/bitbucket_server/pr/client.py +163 -0
  11. ai_review/clients/bitbucket_server/pr/schema/changes.py +36 -0
  12. ai_review/clients/bitbucket_server/pr/schema/comments.py +55 -0
  13. ai_review/clients/bitbucket_server/pr/schema/pull_request.py +48 -0
  14. ai_review/clients/bitbucket_server/pr/schema/user.py +13 -0
  15. ai_review/clients/bitbucket_server/pr/types.py +44 -0
  16. ai_review/clients/bitbucket_server/tools.py +6 -0
  17. ai_review/libs/config/vcs/base.py +23 -6
  18. ai_review/libs/config/vcs/{bitbucket.py → bitbucket_cloud.py} +2 -2
  19. ai_review/libs/config/vcs/bitbucket_server.py +13 -0
  20. ai_review/libs/constants/vcs_provider.py +2 -1
  21. ai_review/libs/http/client.py +1 -1
  22. ai_review/services/vcs/bitbucket_cloud/__init__.py +0 -0
  23. ai_review/services/vcs/{bitbucket → bitbucket_cloud}/adapter.py +2 -2
  24. ai_review/services/vcs/{bitbucket → bitbucket_cloud}/client.py +24 -21
  25. ai_review/services/vcs/bitbucket_server/__init__.py +0 -0
  26. ai_review/services/vcs/bitbucket_server/adapter.py +27 -0
  27. ai_review/services/vcs/bitbucket_server/client.py +263 -0
  28. ai_review/services/vcs/factory.py +6 -3
  29. ai_review/tests/fixtures/clients/bitbucket_cloud.py +207 -0
  30. ai_review/tests/fixtures/clients/bitbucket_server.py +265 -0
  31. ai_review/tests/suites/clients/bitbucket_cloud/__init__.py +0 -0
  32. ai_review/tests/suites/clients/bitbucket_cloud/test_client.py +14 -0
  33. ai_review/tests/suites/clients/bitbucket_cloud/test_tools.py +31 -0
  34. ai_review/tests/suites/clients/bitbucket_server/__init__.py +0 -0
  35. ai_review/tests/suites/clients/bitbucket_server/test_client.py +14 -0
  36. ai_review/tests/suites/clients/bitbucket_server/test_tools.py +38 -0
  37. ai_review/tests/suites/services/vcs/bitbucket_cloud/__init__.py +0 -0
  38. ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_adapter.py +24 -24
  39. ai_review/tests/suites/services/vcs/{bitbucket → bitbucket_cloud}/test_client.py +51 -51
  40. ai_review/tests/suites/services/vcs/bitbucket_server/__init__.py +0 -0
  41. ai_review/tests/suites/services/vcs/bitbucket_server/test_adapter.py +115 -0
  42. ai_review/tests/suites/services/vcs/bitbucket_server/test_client.py +201 -0
  43. ai_review/tests/suites/services/vcs/test_factory.py +11 -4
  44. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/METADATA +9 -7
  45. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/RECORD +55 -33
  46. ai_review/clients/bitbucket/pr/schema/comments.py +0 -63
  47. ai_review/clients/bitbucket/pr/schema/pull_request.py +0 -34
  48. ai_review/clients/bitbucket/pr/types.py +0 -44
  49. ai_review/tests/fixtures/clients/bitbucket.py +0 -204
  50. ai_review/tests/suites/clients/bitbucket/test_client.py +0 -14
  51. ai_review/tests/suites/clients/bitbucket/test_tools.py +0 -31
  52. /ai_review/clients/{bitbucket → bitbucket_cloud}/__init__.py +0 -0
  53. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/__init__.py +0 -0
  54. /ai_review/clients/{bitbucket → bitbucket_cloud}/pr/schema/__init__.py +0 -0
  55. /ai_review/{services/vcs/bitbucket → clients/bitbucket_server}/__init__.py +0 -0
  56. /ai_review/{tests/suites/clients/bitbucket → clients/bitbucket_server/pr}/__init__.py +0 -0
  57. /ai_review/{tests/suites/services/vcs/bitbucket → clients/bitbucket_server/pr/schema}/__init__.py +0 -0
  58. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/WHEEL +0 -0
  59. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/entry_points.txt +0 -0
  60. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/licenses/LICENSE +0 -0
  61. {xai_review-0.37.0.dist-info → xai_review-0.38.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,55 @@
1
+ from pydantic import BaseModel, Field, ConfigDict
2
+
3
+ from ai_review.clients.bitbucket_server.pr.schema.user import BitbucketServerUserSchema
4
+
5
+
6
+ class BitbucketServerCommentParentSchema(BaseModel):
7
+ id: int
8
+
9
+
10
+ class BitbucketServerCommentAnchorSchema(BaseModel):
11
+ model_config = ConfigDict(populate_by_name=True)
12
+
13
+ path: str | None = None
14
+ line: int | None = None
15
+ line_type: str | None = Field(default=None, alias="lineType")
16
+
17
+
18
+ class BitbucketServerCommentSchema(BaseModel):
19
+ model_config = ConfigDict(populate_by_name=True)
20
+
21
+ id: int
22
+ text: str
23
+ author: BitbucketServerUserSchema
24
+ anchor: BitbucketServerCommentAnchorSchema | None = None
25
+ comments: list["BitbucketServerCommentSchema"] = Field(default_factory=list)
26
+ created_date: int = Field(alias="createdDate")
27
+ updated_date: int = Field(alias="updatedDate")
28
+
29
+
30
+ class BitbucketServerGetPRCommentsQuerySchema(BaseModel):
31
+ model_config = ConfigDict(populate_by_name=True)
32
+
33
+ start: int = 0
34
+ limit: int = 100
35
+
36
+
37
+ class BitbucketServerGetPRCommentsResponseSchema(BaseModel):
38
+ model_config = ConfigDict(populate_by_name=True)
39
+
40
+ size: int
41
+ limit: int
42
+ start: int
43
+ values: list[BitbucketServerCommentSchema]
44
+ is_last_page: bool = Field(alias="isLastPage")
45
+ next_page_start: int | None = Field(default=None, alias="nextPageStart")
46
+
47
+
48
+ class BitbucketServerCreatePRCommentRequestSchema(BaseModel):
49
+ text: str
50
+ parent: BitbucketServerCommentParentSchema | None = None
51
+ anchor: BitbucketServerCommentAnchorSchema | None = None
52
+
53
+
54
+ class BitbucketServerCreatePRCommentResponseSchema(BitbucketServerCommentSchema):
55
+ pass
@@ -0,0 +1,48 @@
1
+ from pydantic import BaseModel, Field, ConfigDict
2
+
3
+ from ai_review.clients.bitbucket_server.pr.schema.user import BitbucketServerUserSchema
4
+
5
+
6
+ class BitbucketServerProjectSchema(BaseModel):
7
+ key: str
8
+
9
+
10
+ class BitbucketServerRepositorySchema(BaseModel):
11
+ slug: str
12
+ name: str
13
+ project: BitbucketServerProjectSchema
14
+
15
+
16
+ class BitbucketServerRefSchema(BaseModel):
17
+ model_config = ConfigDict(populate_by_name=True)
18
+
19
+ id: str
20
+ display_id: str = Field(alias="displayId")
21
+ latest_commit: str = Field(alias="latestCommit")
22
+ repository: BitbucketServerRepositorySchema
23
+
24
+
25
+ class BitbucketServerParticipantSchema(BaseModel):
26
+ user: BitbucketServerUserSchema
27
+ role: str
28
+ approved: bool | None = None
29
+ status: str | None = None
30
+
31
+
32
+ class BitbucketServerGetPRResponseSchema(BaseModel):
33
+ model_config = ConfigDict(populate_by_name=True)
34
+
35
+ id: int
36
+ version: int | None = None
37
+ title: str
38
+ description: str | None = None
39
+ state: str
40
+ open: bool
41
+ locked: bool
42
+ author: BitbucketServerParticipantSchema
43
+ reviewers: list[BitbucketServerParticipantSchema] = Field(default_factory=list)
44
+ from_ref: BitbucketServerRefSchema = Field(alias="fromRef")
45
+ to_ref: BitbucketServerRefSchema = Field(alias="toRef")
46
+ created_date: int = Field(alias="createdDate")
47
+ updated_date: int = Field(alias="updatedDate")
48
+ links: dict | None = None
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel, Field, ConfigDict
2
+
3
+
4
+ class BitbucketServerUserSchema(BaseModel):
5
+ model_config = ConfigDict(populate_by_name=True)
6
+
7
+ id: int | None = None
8
+ name: str
9
+ slug: str | None = None
10
+ type: str | None = None
11
+ active: bool | None = None
12
+ display_name: str = Field(alias="displayName")
13
+ email_address: str | None = Field(default=None, alias="emailAddress")
@@ -0,0 +1,44 @@
1
+ from typing import Protocol
2
+
3
+ from ai_review.clients.bitbucket_server.pr.schema.changes import BitbucketServerGetPRChangesResponseSchema
4
+ from ai_review.clients.bitbucket_server.pr.schema.comments import (
5
+ BitbucketServerGetPRCommentsResponseSchema,
6
+ BitbucketServerCreatePRCommentRequestSchema,
7
+ BitbucketServerCreatePRCommentResponseSchema
8
+ )
9
+ from ai_review.clients.bitbucket_server.pr.schema.pull_request import BitbucketServerGetPRResponseSchema
10
+
11
+
12
+ class BitbucketServerPullRequestsHTTPClientProtocol(Protocol):
13
+ async def get_pull_request(
14
+ self,
15
+ project_key: str,
16
+ repo_slug: str,
17
+ pull_request_id: int,
18
+ ) -> BitbucketServerGetPRResponseSchema:
19
+ ...
20
+
21
+ async def get_changes(
22
+ self,
23
+ project_key: str,
24
+ repo_slug: str,
25
+ pull_request_id: int,
26
+ ) -> BitbucketServerGetPRChangesResponseSchema:
27
+ ...
28
+
29
+ async def get_comments(
30
+ self,
31
+ project_key: str,
32
+ repo_slug: str,
33
+ pull_request_id: int,
34
+ ) -> BitbucketServerGetPRCommentsResponseSchema:
35
+ ...
36
+
37
+ async def create_comment(
38
+ self,
39
+ project_key: str,
40
+ repo_slug: str,
41
+ pull_request_id: int,
42
+ request: BitbucketServerCreatePRCommentRequestSchema,
43
+ ) -> BitbucketServerCreatePRCommentResponseSchema:
44
+ ...
@@ -0,0 +1,6 @@
1
+ from httpx import Response
2
+
3
+
4
+ def bitbucket_server_has_next_page(response: Response) -> bool:
5
+ data = response.json()
6
+ return not data.get("isLastPage", True)
@@ -2,7 +2,14 @@ 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
+ from ai_review.libs.config.vcs.bitbucket_cloud import (
6
+ BitbucketCloudPipelineConfig,
7
+ BitbucketCloudHTTPClientConfig
8
+ )
9
+ from ai_review.libs.config.vcs.bitbucket_server import (
10
+ BitbucketServerPipelineConfig,
11
+ BitbucketServerHTTPClientConfig
12
+ )
6
13
  from ai_review.libs.config.vcs.gitea import GiteaPipelineConfig, GiteaHTTPClientConfig
7
14
  from ai_review.libs.config.vcs.github import GitHubPipelineConfig, GitHubHTTPClientConfig
8
15
  from ai_review.libs.config.vcs.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
@@ -33,13 +40,23 @@ class GitHubVCSConfig(VCSConfigBase):
33
40
  http_client: GitHubHTTPClientConfig
34
41
 
35
42
 
36
- class BitbucketVCSConfig(VCSConfigBase):
37
- provider: Literal[VCSProvider.BITBUCKET]
38
- pipeline: BitbucketPipelineConfig
39
- http_client: BitbucketHTTPClientConfig
43
+ class BitbucketCloudVCSConfig(VCSConfigBase):
44
+ provider: Literal[VCSProvider.BITBUCKET_CLOUD]
45
+ pipeline: BitbucketCloudPipelineConfig
46
+ http_client: BitbucketCloudHTTPClientConfig
47
+
48
+
49
+ class BitbucketServerVCSConfig(VCSConfigBase):
50
+ provider: Literal[VCSProvider.BITBUCKET_SERVER]
51
+ pipeline: BitbucketServerPipelineConfig
52
+ http_client: BitbucketServerHTTPClientConfig
40
53
 
41
54
 
42
55
  VCSConfig = Annotated[
43
- GiteaVCSConfig | GitLabVCSConfig | GitHubVCSConfig | BitbucketVCSConfig,
56
+ GiteaVCSConfig
57
+ | GitLabVCSConfig
58
+ | GitHubVCSConfig
59
+ | BitbucketCloudVCSConfig
60
+ | BitbucketServerVCSConfig,
44
61
  Field(discriminator="provider")
45
62
  ]
@@ -3,11 +3,11 @@ from pydantic import BaseModel
3
3
  from ai_review.libs.config.http import HTTPClientWithTokenConfig
4
4
 
5
5
 
6
- class BitbucketPipelineConfig(BaseModel):
6
+ class BitbucketCloudPipelineConfig(BaseModel):
7
7
  workspace: str
8
8
  repo_slug: str
9
9
  pull_request_id: str
10
10
 
11
11
 
12
- class BitbucketHTTPClientConfig(HTTPClientWithTokenConfig):
12
+ class BitbucketCloudHTTPClientConfig(HTTPClientWithTokenConfig):
13
13
  pass
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+ from ai_review.libs.config.http import HTTPClientWithTokenConfig
4
+
5
+
6
+ class BitbucketServerPipelineConfig(BaseModel):
7
+ project_key: str
8
+ repo_slug: str
9
+ pull_request_id: int
10
+
11
+
12
+ class BitbucketServerHTTPClientConfig(HTTPClientWithTokenConfig):
13
+ pass
@@ -5,4 +5,5 @@ class VCSProvider(StrEnum):
5
5
  GITEA = "GITEA"
6
6
  GITHUB = "GITHUB"
7
7
  GITLAB = "GITLAB"
8
- BITBUCKET = "BITBUCKET"
8
+ BITBUCKET_CLOUD = "BITBUCKET_CLOUD"
9
+ BITBUCKET_SERVER = "BITBUCKET_SERVER"
@@ -8,7 +8,7 @@ class HTTPClient:
8
8
  self.client = client
9
9
 
10
10
  async def get(self, url: str, query: QueryParams | None = None) -> Response:
11
- return await self.client.get(url=url, params=query)
11
+ return await self.client.get(url=url, params=query, follow_redirects=True)
12
12
 
13
13
  async def post(self, url: str, json: Any | None = None) -> Response:
14
14
  return await self.client.post(url=url, json=json)
File without changes
@@ -1,8 +1,8 @@
1
- from ai_review.clients.bitbucket.pr.schema.comments import BitbucketPRCommentSchema
1
+ from ai_review.clients.bitbucket_cloud.pr.schema.comments import BitbucketCloudPRCommentSchema
2
2
  from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
3
3
 
4
4
 
5
- def get_review_comment_from_bitbucket_pr_comment(comment: BitbucketPRCommentSchema) -> ReviewCommentSchema:
5
+ def get_review_comment_from_bitbucket_pr_comment(comment: BitbucketCloudPRCommentSchema) -> ReviewCommentSchema:
6
6
  parent_id = comment.parent.id if comment.parent else None
7
7
  thread_id = parent_id or comment.id
8
8
 
@@ -1,28 +1,31 @@
1
1
  from collections import defaultdict
2
2
 
3
- from ai_review.clients.bitbucket.client import get_bitbucket_http_client
4
- from ai_review.clients.bitbucket.pr.schema.comments import (
5
- BitbucketCommentInlineSchema,
6
- BitbucketCommentContentSchema,
7
- BitbucketCreatePRCommentRequestSchema, BitbucketParentSchema,
3
+ from ai_review.clients.bitbucket_cloud.client import get_bitbucket_cloud_http_client
4
+ from ai_review.clients.bitbucket_cloud.pr.schema.comments import (
5
+ BitbucketCloudCommentParentSchema,
6
+ BitbucketCloudCommentInlineSchema,
7
+ BitbucketCloudCommentContentSchema,
8
+ BitbucketCloudCreatePRCommentRequestSchema,
8
9
  )
9
10
  from ai_review.config import settings
10
11
  from ai_review.libs.logger import get_logger
11
- from ai_review.services.vcs.bitbucket.adapter import get_review_comment_from_bitbucket_pr_comment
12
+ from ai_review.services.vcs.bitbucket_cloud.adapter import get_review_comment_from_bitbucket_pr_comment
12
13
  from ai_review.services.vcs.types import (
13
14
  VCSClientProtocol,
15
+ ThreadKind,
14
16
  UserSchema,
15
17
  BranchRefSchema,
16
18
  ReviewInfoSchema,
17
- ReviewCommentSchema, ReviewThreadSchema, ThreadKind,
19
+ ReviewThreadSchema,
20
+ ReviewCommentSchema,
18
21
  )
19
22
 
20
- logger = get_logger("BITBUCKET_VCS_CLIENT")
23
+ logger = get_logger("BITBUCKET_CLOUD_VCS_CLIENT")
21
24
 
22
25
 
23
- class BitbucketVCSClient(VCSClientProtocol):
26
+ class BitbucketCloudVCSClient(VCSClientProtocol):
24
27
  def __init__(self):
25
- self.http_client = get_bitbucket_http_client()
28
+ self.http_client = get_bitbucket_cloud_http_client()
26
29
  self.workspace = settings.vcs.pipeline.workspace
27
30
  self.repo_slug = settings.vcs.pipeline.repo_slug
28
31
  self.pull_request_id = settings.vcs.pipeline.pull_request_id
@@ -129,8 +132,8 @@ class BitbucketVCSClient(VCSClientProtocol):
129
132
  async def create_general_comment(self, message: str) -> None:
130
133
  try:
131
134
  logger.info(f"Posting general comment to PR {self.pull_request_ref}: {message}")
132
- request = BitbucketCreatePRCommentRequestSchema(
133
- content=BitbucketCommentContentSchema(raw=message)
135
+ request = BitbucketCloudCreatePRCommentRequestSchema(
136
+ content=BitbucketCloudCommentContentSchema(raw=message)
134
137
  )
135
138
  await self.http_client.pr.create_comment(
136
139
  workspace=self.workspace,
@@ -146,9 +149,9 @@ class BitbucketVCSClient(VCSClientProtocol):
146
149
  async def create_inline_comment(self, file: str, line: int, message: str) -> None:
147
150
  try:
148
151
  logger.info(f"Posting inline comment in {self.pull_request_ref} at {file}:{line}: {message}")
149
- request = BitbucketCreatePRCommentRequestSchema(
150
- content=BitbucketCommentContentSchema(raw=message),
151
- inline=BitbucketCommentInlineSchema(path=file, to_line=line),
152
+ request = BitbucketCloudCreatePRCommentRequestSchema(
153
+ content=BitbucketCloudCommentContentSchema(raw=message),
154
+ inline=BitbucketCloudCommentInlineSchema(path=file, to_line=line),
152
155
  )
153
156
  await self.http_client.pr.create_comment(
154
157
  workspace=self.workspace,
@@ -165,9 +168,9 @@ class BitbucketVCSClient(VCSClientProtocol):
165
168
  async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
166
169
  try:
167
170
  logger.info(f"Replying to inline thread {thread_id=} in PR {self.pull_request_ref}")
168
- request = BitbucketCreatePRCommentRequestSchema(
169
- parent=BitbucketParentSchema(id=int(thread_id)),
170
- content=BitbucketCommentContentSchema(raw=message),
171
+ request = BitbucketCloudCreatePRCommentRequestSchema(
172
+ parent=BitbucketCloudCommentParentSchema(id=int(thread_id)),
173
+ content=BitbucketCloudCommentContentSchema(raw=message),
171
174
  )
172
175
  await self.http_client.pr.create_comment(
173
176
  workspace=self.workspace,
@@ -185,9 +188,9 @@ class BitbucketVCSClient(VCSClientProtocol):
185
188
  async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
186
189
  try:
187
190
  logger.info(f"Replying to summary thread {thread_id=} in PR {self.pull_request_ref}")
188
- request = BitbucketCreatePRCommentRequestSchema(
189
- content=BitbucketCommentContentSchema(raw=message),
190
- parent=BitbucketParentSchema(id=int(thread_id)),
191
+ request = BitbucketCloudCreatePRCommentRequestSchema(
192
+ content=BitbucketCloudCommentContentSchema(raw=message),
193
+ parent=BitbucketCloudCommentParentSchema(id=int(thread_id)),
191
194
  )
192
195
  await self.http_client.pr.create_comment(
193
196
  workspace=self.workspace,
File without changes
@@ -0,0 +1,27 @@
1
+ from ai_review.clients.bitbucket_server.pr.schema.comments import BitbucketServerCommentSchema
2
+ from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
3
+
4
+
5
+ def get_review_comment_from_bitbucket_server_comment(comment: BitbucketServerCommentSchema) -> ReviewCommentSchema:
6
+ parent_id = None
7
+ thread_id = comment.id
8
+
9
+ user = comment.author
10
+ author = UserSchema(
11
+ id=user.id if user else None,
12
+ name=user.display_name or user.name or "",
13
+ username=user.slug or user.name or "",
14
+ )
15
+
16
+ file = comment.anchor.path if comment.anchor and comment.anchor.path else None
17
+ line = comment.anchor.line if comment.anchor else None
18
+
19
+ return ReviewCommentSchema(
20
+ id=comment.id,
21
+ body=comment.text or "",
22
+ file=file,
23
+ line=line,
24
+ author=author,
25
+ parent_id=parent_id,
26
+ thread_id=thread_id,
27
+ )
@@ -0,0 +1,263 @@
1
+ from collections import defaultdict
2
+
3
+ from ai_review.clients.bitbucket_server.client import get_bitbucket_server_http_client
4
+ from ai_review.clients.bitbucket_server.pr.schema.comments import (
5
+ BitbucketServerCommentAnchorSchema,
6
+ BitbucketServerCommentParentSchema,
7
+ BitbucketServerCreatePRCommentRequestSchema
8
+ )
9
+ from ai_review.config import settings
10
+ from ai_review.libs.logger import get_logger
11
+ from ai_review.services.vcs.bitbucket_server.adapter import get_review_comment_from_bitbucket_server_comment
12
+ from ai_review.services.vcs.types import (
13
+ VCSClientProtocol,
14
+ ThreadKind,
15
+ UserSchema,
16
+ BranchRefSchema,
17
+ ReviewInfoSchema,
18
+ ReviewThreadSchema,
19
+ ReviewCommentSchema,
20
+ )
21
+
22
+ logger = get_logger("BITBUCKET_SERVER_VCS_CLIENT")
23
+
24
+
25
+ class BitbucketServerVCSClient(VCSClientProtocol):
26
+ def __init__(self):
27
+ self.http_client = get_bitbucket_server_http_client()
28
+ self.project_key = settings.vcs.pipeline.project_key
29
+ self.repo_slug = settings.vcs.pipeline.repo_slug
30
+ self.pull_request_id = settings.vcs.pipeline.pull_request_id
31
+ self.pull_request_ref = f"{self.project_key}/{self.repo_slug}#{self.pull_request_id}"
32
+
33
+ # --- Review info ---
34
+ async def get_review_info(self) -> ReviewInfoSchema:
35
+ try:
36
+ pr = await self.http_client.pr.get_pull_request(
37
+ project_key=self.project_key,
38
+ repo_slug=self.repo_slug,
39
+ pull_request_id=self.pull_request_id,
40
+ )
41
+ changes = await self.http_client.pr.get_changes(
42
+ project_key=self.project_key,
43
+ repo_slug=self.repo_slug,
44
+ pull_request_id=self.pull_request_id,
45
+ )
46
+
47
+ logger.info(f"Fetched PR info for {self.pull_request_ref}")
48
+
49
+ return ReviewInfoSchema(
50
+ id=pr.id,
51
+ title=pr.title,
52
+ description=pr.description or "",
53
+ author=UserSchema(
54
+ id=pr.author.user.id,
55
+ name=pr.author.user.display_name,
56
+ username=pr.author.user.slug or pr.author.user.name,
57
+ ),
58
+ labels=[], # Bitbucket Server не поддерживает labels напрямую
59
+ base_sha=pr.to_ref.latest_commit,
60
+ head_sha=pr.from_ref.latest_commit,
61
+ assignees=[], # нет отдельного поля в Bitbucket Server API
62
+ reviewers=[
63
+ UserSchema(
64
+ id=user.user.id,
65
+ name=user.user.display_name,
66
+ username=user.user.slug or user.user.name,
67
+ )
68
+ for user in pr.reviewers
69
+ ],
70
+ source_branch=BranchRefSchema(
71
+ ref=pr.from_ref.display_id,
72
+ sha=pr.from_ref.latest_commit,
73
+ ),
74
+ target_branch=BranchRefSchema(
75
+ ref=pr.to_ref.display_id,
76
+ sha=pr.to_ref.latest_commit,
77
+ ),
78
+ changed_files=[
79
+ change.path.to_string
80
+ for change in changes.values
81
+ if change.path and change.path.to_string
82
+ ],
83
+ )
84
+ except Exception as error:
85
+ logger.exception(f"Failed to fetch PR info {self.pull_request_ref}: {error}")
86
+ return ReviewInfoSchema()
87
+
88
+ # --- Comments ---
89
+ async def get_general_comments(self) -> list[ReviewCommentSchema]:
90
+ try:
91
+ response = await self.http_client.pr.get_comments(
92
+ project_key=self.project_key,
93
+ repo_slug=self.repo_slug,
94
+ pull_request_id=self.pull_request_id,
95
+ )
96
+ logger.info(f"Fetched general comments for {self.pull_request_ref}")
97
+
98
+ return [
99
+ get_review_comment_from_bitbucket_server_comment(comment)
100
+ for comment in response.values
101
+ if comment.anchor is None # нет привязки к файлу/строке — значит общий комментарий
102
+ ]
103
+ except Exception as error:
104
+ logger.exception(f"Failed to fetch general comments for {self.pull_request_ref}: {error}")
105
+ return []
106
+
107
+ async def get_inline_comments(self) -> list[ReviewCommentSchema]:
108
+ try:
109
+ response = await self.http_client.pr.get_comments(
110
+ project_key=self.project_key,
111
+ repo_slug=self.repo_slug,
112
+ pull_request_id=self.pull_request_id,
113
+ )
114
+ logger.info(f"Fetched inline comments for {self.pull_request_ref}")
115
+
116
+ return [
117
+ get_review_comment_from_bitbucket_server_comment(comment)
118
+ for comment in response.values
119
+ if comment.anchor is not None and comment.anchor.path is not None
120
+ ]
121
+ except Exception as error:
122
+ logger.exception(f"Failed to fetch inline comments for {self.pull_request_ref}: {error}")
123
+ return []
124
+
125
+ async def create_general_comment(self, message: str) -> None:
126
+ try:
127
+ logger.info(f"Posting general comment to PR {self.pull_request_ref}: {message}")
128
+
129
+ request = BitbucketServerCreatePRCommentRequestSchema(text=message)
130
+
131
+ await self.http_client.pr.create_comment(
132
+ project_key=self.project_key,
133
+ repo_slug=self.repo_slug,
134
+ pull_request_id=self.pull_request_id,
135
+ request=request,
136
+ )
137
+
138
+ logger.info(f"Created general comment in PR {self.pull_request_ref}")
139
+
140
+ except Exception as error:
141
+ logger.exception(f"Failed to create general comment in PR {self.pull_request_ref}: {error}")
142
+ raise
143
+
144
+ async def create_inline_comment(self, file: str, line: int, message: str) -> None:
145
+ try:
146
+ logger.info(f"Posting inline comment in {self.pull_request_ref} at {file}:{line}: {message}")
147
+
148
+ anchor = BitbucketServerCommentAnchorSchema(path=file, line=line, line_type="ADDED")
149
+ request = BitbucketServerCreatePRCommentRequestSchema(text=message, anchor=anchor)
150
+
151
+ await self.http_client.pr.create_comment(
152
+ project_key=self.project_key,
153
+ repo_slug=self.repo_slug,
154
+ pull_request_id=self.pull_request_id,
155
+ request=request,
156
+ )
157
+
158
+ logger.info(f"Created inline comment in {self.pull_request_ref} at {file}:{line}")
159
+
160
+ except Exception as error:
161
+ logger.exception(
162
+ f"Failed to create inline comment in {self.pull_request_ref} at {file}:{line}: {error}")
163
+ raise
164
+
165
+ # --- Replies ---
166
+ async def create_inline_reply(self, thread_id: int | str, message: str) -> None:
167
+ try:
168
+ logger.info(f"Replying to inline thread {thread_id=} in PR {self.pull_request_ref}")
169
+ request = BitbucketServerCreatePRCommentRequestSchema(
170
+ text=message,
171
+ parent=BitbucketServerCommentParentSchema(id=int(thread_id)),
172
+ )
173
+ await self.http_client.pr.create_comment(
174
+ project_key=self.project_key,
175
+ repo_slug=self.repo_slug,
176
+ pull_request_id=self.pull_request_id,
177
+ request=request,
178
+ )
179
+ logger.info(f"Created inline reply to thread {thread_id=} in PR {self.pull_request_ref}")
180
+ except Exception as error:
181
+ logger.exception(
182
+ f"Failed to create inline reply to thread {thread_id=} in PR {self.pull_request_ref}: {error}"
183
+ )
184
+ raise
185
+
186
+ async def create_summary_reply(self, thread_id: int | str, message: str) -> None:
187
+ try:
188
+ logger.info(f"Replying to summary thread {thread_id=} in PR {self.pull_request_ref}")
189
+ request = BitbucketServerCreatePRCommentRequestSchema(
190
+ text=message,
191
+ parent=BitbucketServerCommentParentSchema(id=int(thread_id)),
192
+ )
193
+ await self.http_client.pr.create_comment(
194
+ project_key=self.project_key,
195
+ repo_slug=self.repo_slug,
196
+ pull_request_id=self.pull_request_id,
197
+ request=request,
198
+ )
199
+ logger.info(f"Created summary reply to thread {thread_id=} in PR {self.pull_request_ref}")
200
+ except Exception as error:
201
+ logger.exception(
202
+ f"Failed to create summary reply to thread {thread_id=} in PR {self.pull_request_ref}: {error}"
203
+ )
204
+ raise
205
+
206
+ # --- Threads ---
207
+ async def get_inline_threads(self) -> list[ReviewThreadSchema]:
208
+ try:
209
+ comments = await self.get_inline_comments()
210
+
211
+ threads_by_id: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
212
+ for comment in comments:
213
+ if not comment.file:
214
+ continue
215
+ threads_by_id[comment.thread_id].append(comment)
216
+
217
+ logger.info(f"Built {len(threads_by_id)} inline threads for {self.pull_request_ref}")
218
+
219
+ threads: list[ReviewThreadSchema] = []
220
+ for thread_id, thread in threads_by_id.items():
221
+ file = thread[0].file
222
+ line = thread[0].line
223
+ if not file:
224
+ continue
225
+
226
+ threads.append(
227
+ ReviewThreadSchema(
228
+ id=thread_id,
229
+ kind=ThreadKind.INLINE,
230
+ file=file,
231
+ line=line,
232
+ comments=sorted(thread, key=lambda c: int(c.id)),
233
+ )
234
+ )
235
+
236
+ return threads
237
+
238
+ except Exception as error:
239
+ logger.exception(f"Failed to fetch inline threads for {self.pull_request_ref}: {error}")
240
+ return []
241
+
242
+ async def get_general_threads(self) -> list[ReviewThreadSchema]:
243
+ try:
244
+ comments = await self.get_general_comments()
245
+
246
+ threads_by_id: dict[str | int, list[ReviewCommentSchema]] = defaultdict(list)
247
+ for comment in comments:
248
+ threads_by_id[comment.thread_id].append(comment)
249
+
250
+ logger.info(f"Built {len(threads_by_id)} general threads for {self.pull_request_ref}")
251
+
252
+ return [
253
+ ReviewThreadSchema(
254
+ id=thread_id,
255
+ kind=ThreadKind.SUMMARY,
256
+ comments=sorted(thread, key=lambda c: int(c.id)),
257
+ )
258
+ for thread_id, thread in threads_by_id.items()
259
+ ]
260
+
261
+ except Exception as error:
262
+ logger.exception(f"Failed to fetch general threads for {self.pull_request_ref}: {error}")
263
+ return []