Skip to content

Commit 5362d14

Browse files
authored
feat(bitbucket-server): CODEOWNERS support and stacktrace linking (#86639)
1 parent f2589ed commit 5362d14

File tree

6 files changed

+393
-32
lines changed

6 files changed

+393
-32
lines changed

src/sentry/integrations/bitbucket_server/client.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from requests_oauthlib import OAuth1
77

88
from sentry.identity.services.identity.model import RpcIdentity
9-
from sentry.integrations.base import IntegrationFeatureNotImplementedError
9+
from sentry.integrations.bitbucket_server.utils import BitbucketServerAPIPath
1010
from sentry.integrations.client import ApiClient
1111
from sentry.integrations.models.integration import Integration
1212
from sentry.integrations.services.integration.model import RpcIntegration
@@ -17,20 +17,6 @@
1717
logger = logging.getLogger("sentry.integrations.bitbucket_server")
1818

1919

20-
class BitbucketServerAPIPath:
21-
"""
22-
project is the short key of the project
23-
repo is the fully qualified slug
24-
"""
25-
26-
repository = "/rest/api/1.0/projects/{project}/repos/{repo}"
27-
repositories = "/rest/api/1.0/repos"
28-
repository_hook = "/rest/api/1.0/projects/{project}/repos/{repo}/webhooks/{id}"
29-
repository_hooks = "/rest/api/1.0/projects/{project}/repos/{repo}/webhooks"
30-
repository_commits = "/rest/api/1.0/projects/{project}/repos/{repo}/commits"
31-
commit_changes = "/rest/api/1.0/projects/{project}/repos/{repo}/commits/{commit}/changes"
32-
33-
3420
class BitbucketServerSetupClient(ApiClient):
3521
"""
3622
Client for making requests to Bitbucket Server to follow OAuth1 flow.
@@ -256,9 +242,25 @@ def _get_values(self, uri, params, max_pages=1000000):
256242
return values
257243

258244
def check_file(self, repo: Repository, path: str, version: str | None) -> object | None:
259-
raise IntegrationFeatureNotImplementedError
245+
return self.head_cached(
246+
path=BitbucketServerAPIPath.build_source(
247+
project=repo.config["project"],
248+
repo=repo.config["repo"],
249+
path=path,
250+
sha=version,
251+
),
252+
)
260253

261254
def get_file(
262255
self, repo: Repository, path: str, ref: str | None, codeowners: bool = False
263256
) -> str:
264-
raise IntegrationFeatureNotImplementedError
257+
response = self.get_cached(
258+
path=BitbucketServerAPIPath.build_raw(
259+
project=repo.config["project"],
260+
repo=repo.config["repo"],
261+
path=path,
262+
sha=ref,
263+
),
264+
raw_response=True,
265+
)
266+
return response.text

src/sentry/integrations/bitbucket_server/integration.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections.abc import Mapping
44
from typing import Any
5-
from urllib.parse import urlparse
5+
from urllib.parse import parse_qs, quote, urlencode, urlparse
66

77
from cryptography.hazmat.backends import default_backend
88
from cryptography.hazmat.primitives.serialization import load_pem_private_key
@@ -19,7 +19,6 @@
1919
FeatureDescription,
2020
IntegrationData,
2121
IntegrationDomain,
22-
IntegrationFeatureNotImplementedError,
2322
IntegrationFeatures,
2423
IntegrationMetadata,
2524
IntegrationProvider,
@@ -62,6 +61,19 @@
6261
""",
6362
IntegrationFeatures.COMMITS,
6463
),
64+
FeatureDescription(
65+
"""
66+
Link your Sentry stack traces back to your Bitbucket Server source code with stack
67+
trace linking.
68+
""",
69+
IntegrationFeatures.STACKTRACE_LINK,
70+
),
71+
FeatureDescription(
72+
"""
73+
Import your Bitbucket Server [CODEOWNERS file](https://support.atlassian.com/bitbucket-cloud/docs/set-up-and-use-code-owners/) and use it alongside your ownership rules to assign Sentry issues.
74+
""",
75+
IntegrationFeatures.CODEOWNERS,
76+
),
6577
]
6678

6779
setup_alert = {
@@ -244,6 +256,8 @@ class BitbucketServerIntegration(RepositoryIntegration):
244256
IntegrationInstallation implementation for Bitbucket Server
245257
"""
246258

259+
codeowners_locations = [".bitbucket/CODEOWNERS"]
260+
247261
@property
248262
def integration_name(self) -> str:
249263
return "bitbucket_server"
@@ -307,16 +321,40 @@ def get_unmigratable_repositories(self):
307321
return list(filter(lambda repo: repo.name not in accessible_repos, repos))
308322

309323
def source_url_matches(self, url: str) -> bool:
310-
raise IntegrationFeatureNotImplementedError
324+
return url.startswith(self.model.metadata["base_url"])
311325

312326
def format_source_url(self, repo: Repository, filepath: str, branch: str | None) -> str:
313-
raise IntegrationFeatureNotImplementedError
327+
project = quote(repo.config["project"])
328+
repo_name = quote(repo.config["repo"])
329+
source_url = f"{self.model.metadata["base_url"]}/projects/{project}/repos/{repo_name}/browse/{filepath}"
330+
331+
if branch:
332+
source_url += "?" + urlencode({"at": branch})
333+
334+
return source_url
314335

315336
def extract_branch_from_source_url(self, repo: Repository, url: str) -> str:
316-
raise IntegrationFeatureNotImplementedError
337+
parsed_url = urlparse(url)
338+
qs = parse_qs(parsed_url.query)
339+
340+
if "at" in qs and len(qs["at"]) == 1:
341+
branch = qs["at"][0]
342+
343+
# branch name may be prefixed with refs/heads/, so we strip that
344+
refs_prefix = "refs/heads/"
345+
if branch.startswith(refs_prefix):
346+
branch = branch[len(refs_prefix) :]
347+
348+
return branch
349+
350+
return ""
317351

318352
def extract_source_path_from_source_url(self, repo: Repository, url: str) -> str:
319-
raise IntegrationFeatureNotImplementedError
353+
if repo.url is None:
354+
return ""
355+
parsed_repo_url = urlparse(repo.url)
356+
parsed_url = urlparse(url)
357+
return parsed_url.path.replace(parsed_repo_url.path + "/", "")
320358

321359
# Bitbucket Server only methods
322360

@@ -331,7 +369,13 @@ class BitbucketServerIntegrationProvider(IntegrationProvider):
331369
metadata = metadata
332370
integration_cls = BitbucketServerIntegration
333371
needs_default_identity = True
334-
features = frozenset([IntegrationFeatures.COMMITS])
372+
features = frozenset(
373+
[
374+
IntegrationFeatures.COMMITS,
375+
IntegrationFeatures.STACKTRACE_LINK,
376+
IntegrationFeatures.CODEOWNERS,
377+
]
378+
)
335379
setup_dialog_config = {"width": 1030, "height": 1000}
336380

337381
def get_pipeline_views(self) -> list[PipelineView]:
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from urllib.parse import quote, urlencode
2+
3+
4+
class BitbucketServerAPIPath:
5+
"""
6+
project is the short key of the project
7+
repo is the fully qualified slug
8+
"""
9+
10+
repository = "/rest/api/1.0/projects/{project}/repos/{repo}"
11+
repositories = "/rest/api/1.0/repos"
12+
repository_hook = "/rest/api/1.0/projects/{project}/repos/{repo}/webhooks/{id}"
13+
repository_hooks = "/rest/api/1.0/projects/{project}/repos/{repo}/webhooks"
14+
repository_commits = "/rest/api/1.0/projects/{project}/repos/{repo}/commits"
15+
commit_changes = "/rest/api/1.0/projects/{project}/repos/{repo}/commits/{commit}/changes"
16+
17+
@staticmethod
18+
def build_raw(project: str, repo: str, path: str, sha: str | None) -> str:
19+
project = quote(project)
20+
repo = quote(repo)
21+
22+
params = {}
23+
if sha:
24+
params["at"] = sha
25+
26+
return f"/projects/{project}/repos/{repo}/raw/{path}?{urlencode(params)}"
27+
28+
@staticmethod
29+
def build_source(project: str, repo: str, path: str, sha: str | None) -> str:
30+
project = quote(project)
31+
repo = quote(repo)
32+
33+
params = {}
34+
if sha:
35+
params["at"] = sha
36+
37+
return f"/rest/api/1.0/projects/{project}/repos/{repo}/browse/{path}?{urlencode(params)}"

src/sentry/models/commitfilechange.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def is_valid_type(value: str) -> bool:
5252

5353
def process_resource_change(instance, **kwargs):
5454
from sentry.integrations.bitbucket.integration import BitbucketIntegration
55+
from sentry.integrations.bitbucket_server.integration import BitbucketServerIntegration
5556
from sentry.integrations.github.integration import GitHubIntegration
5657
from sentry.integrations.gitlab.integration import GitlabIntegration
5758
from sentry.integrations.vsts.integration import VstsIntegration
@@ -62,6 +63,7 @@ def _spawn_task():
6263
set(GitHubIntegration.codeowners_locations)
6364
| set(GitlabIntegration.codeowners_locations)
6465
| set(BitbucketIntegration.codeowners_locations)
66+
| set(BitbucketServerIntegration.codeowners_locations)
6567
| set(VstsIntegration.codeowners_locations)
6668
)
6769

0 commit comments

Comments
 (0)