Skip to content

Commit a5c3365

Browse files
committed
feat(bitbucket-server): Commit context
1 parent 3f9dc48 commit a5c3365

File tree

5 files changed

+446
-3
lines changed

5 files changed

+446
-3
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import logging
2+
from collections.abc import Mapping, Sequence
3+
from dataclasses import asdict
4+
from datetime import datetime, timezone
5+
from typing import Any
6+
7+
from sentry.integrations.bitbucket_server.utils import BitbucketServerAPIPath
8+
from sentry.integrations.source_code_management.commit_context import (
9+
CommitInfo,
10+
FileBlameInfo,
11+
SourceLineInfo,
12+
)
13+
from sentry.shared_integrations.client.base import BaseApiClient
14+
from sentry.shared_integrations.exceptions import ApiError
15+
16+
logger = logging.getLogger("sentry.integrations.bitbucket_server")
17+
18+
19+
def _blame_file(
20+
client: BaseApiClient, file: SourceLineInfo, extra: Mapping[str, Any]
21+
) -> FileBlameInfo | None:
22+
if file.lineno is None:
23+
logger.warning("blame_file.no_lineno", extra=extra)
24+
return None
25+
26+
project = file.repo.config["project"]
27+
repo = file.repo.config["repo"]
28+
29+
browse_url = BitbucketServerAPIPath.get_browse(
30+
project=project,
31+
repo=repo,
32+
path=file.path,
33+
sha=file.ref,
34+
blame=True,
35+
no_content=True,
36+
)
37+
38+
try:
39+
data = client.get(browse_url)
40+
except ApiError as e:
41+
if e.code in (401, 403, 404):
42+
logger.warning(
43+
"blame_file.browse.api_error",
44+
extra={
45+
**extra,
46+
"code": e.code,
47+
"error_message": e.text,
48+
},
49+
)
50+
return None
51+
raise
52+
53+
for entry in data:
54+
start = entry["lineNumber"]
55+
span = entry["spannedLines"]
56+
end = start + span - 1 # inclusive range
57+
58+
if start <= file.lineno <= end:
59+
commit_id = entry["commitId"]
60+
commited_date = datetime.fromtimestamp(
61+
entry["committerTimestamp"] / 1000.0, tz=timezone.utc
62+
)
63+
64+
try:
65+
commit_data = client.get_cached(
66+
BitbucketServerAPIPath.repository_commit.format(
67+
project=project, repo=repo, commit=commit_id
68+
),
69+
)
70+
except ApiError as e:
71+
logger.warning(
72+
"blame_file.commit.api_error",
73+
extra={
74+
**extra,
75+
"code": e.code,
76+
"error_message": e.text,
77+
"commit_id": commit_id,
78+
},
79+
)
80+
commit_message = None
81+
else:
82+
commit_message = commit_data.get("message")
83+
84+
return FileBlameInfo(
85+
**asdict(file),
86+
commit=CommitInfo(
87+
commitId=commit_id,
88+
committedDate=commited_date,
89+
commitMessage=commit_message,
90+
commitAuthorName=entry["author"].get("name"),
91+
commitAuthorEmail=entry["author"].get("emailAddress"),
92+
),
93+
)
94+
95+
return None
96+
97+
98+
def fetch_file_blames(
99+
client: BaseApiClient, files: Sequence[SourceLineInfo], extra: Mapping[str, Any]
100+
) -> list[FileBlameInfo]:
101+
blames = []
102+
for file in files:
103+
extra_file = {
104+
**extra,
105+
"repo_name": file.repo.name,
106+
"file_path": file.path,
107+
"branch_name": file.ref,
108+
"file_lineno": file.lineno,
109+
}
110+
111+
blame = _blame_file(client, file, extra_file)
112+
if blame:
113+
blames.append(blame)
114+
else:
115+
logger.warning(
116+
"fetch_file_blames.no_blame",
117+
extra=extra_file,
118+
)
119+
return blames

src/sentry/integrations/bitbucket_server/client.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import logging
2+
from collections.abc import Mapping, Sequence
3+
from typing import Any
24
from urllib.parse import parse_qsl
35

46
from oauthlib.oauth1 import SIGNATURE_RSA
57
from requests import PreparedRequest
68
from requests_oauthlib import OAuth1
79

810
from sentry.identity.services.identity.model import RpcIdentity
11+
from sentry.integrations.base import IntegrationFeatureNotImplementedError
12+
from sentry.integrations.bitbucket_server.blame import fetch_file_blames
913
from sentry.integrations.bitbucket_server.utils import BitbucketServerAPIPath
1014
from sentry.integrations.client import ApiClient
1115
from sentry.integrations.models.integration import Integration
1216
from sentry.integrations.services.integration.model import RpcIntegration
17+
from sentry.integrations.source_code_management.commit_context import (
18+
CommitContextClient,
19+
FileBlameInfo,
20+
SourceLineInfo,
21+
)
1322
from sentry.integrations.source_code_management.repository import RepositoryClient
1423
from sentry.models.repository import Repository
1524
from sentry.shared_integrations.exceptions import ApiError
25+
from sentry.utils import metrics
1626

1727
logger = logging.getLogger("sentry.integrations.bitbucket_server")
1828

@@ -86,7 +96,7 @@ def request(self, *args, **kwargs):
8696
return self._request(*args, **kwargs)
8797

8898

89-
class BitbucketServerClient(ApiClient, RepositoryClient):
99+
class BitbucketServerClient(ApiClient, RepositoryClient, CommitContextClient):
90100
"""
91101
Contains the BitBucket Server specifics in order to communicate with bitbucket
92102
@@ -264,3 +274,28 @@ def get_file(
264274
raw_response=True,
265275
)
266276
return response.text
277+
278+
def get_blame_for_files(
279+
self, files: Sequence[SourceLineInfo], extra: Mapping[str, Any]
280+
) -> list[FileBlameInfo]:
281+
metrics.incr("integrations.bitbucket_server.get_blame_for_files")
282+
return fetch_file_blames(
283+
self,
284+
files,
285+
extra={
286+
**extra,
287+
"provider": "bitbucket_server",
288+
"org_integration_id": self.integration_id,
289+
},
290+
)
291+
292+
def create_comment(self, repo: str, issue_id: str, data: Mapping[str, Any]) -> Any:
293+
raise IntegrationFeatureNotImplementedError
294+
295+
def update_comment(
296+
self, repo: str, issue_id: str, comment_id: str, data: Mapping[str, Any]
297+
) -> Any:
298+
raise IntegrationFeatureNotImplementedError
299+
300+
def get_merge_commit_sha_from_commit(self, repo: str, sha: str) -> str | None:
301+
raise IntegrationFeatureNotImplementedError

src/sentry/integrations/bitbucket_server/integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from sentry.integrations.models.integration import Integration
2727
from sentry.integrations.services.repository import repository_service
2828
from sentry.integrations.services.repository.model import RpcRepository
29+
from sentry.integrations.source_code_management.commit_context import CommitContextIntegration
2930
from sentry.integrations.source_code_management.repository import RepositoryIntegration
3031
from sentry.integrations.tasks.migrate_repo import migrate_repo
3132
from sentry.integrations.utils.metrics import (
@@ -251,7 +252,7 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase
251252
)
252253

253254

254-
class BitbucketServerIntegration(RepositoryIntegration):
255+
class BitbucketServerIntegration(RepositoryIntegration, CommitContextIntegration):
255256
"""
256257
IntegrationInstallation implementation for Bitbucket Server
257258
"""

src/sentry/tasks/post_process.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,12 @@ def process_commits(job: PostProcessJob) -> None:
10811081

10821082
org_integrations = integration_service.get_organization_integrations(
10831083
organization_id=event.project.organization_id,
1084-
providers=["github", "gitlab", "github_enterprise"],
1084+
providers=[
1085+
"github",
1086+
"gitlab",
1087+
"github_enterprise",
1088+
"bitbucket_server",
1089+
],
10851090
)
10861091
has_integrations = len(org_integrations) > 0
10871092
# Cache the integrations check for 4 hours

0 commit comments

Comments
 (0)