Skip to content

Commit 712ffd9

Browse files
jianyuanJoshFerge
andauthored
1 parent db0c0b9 commit 712ffd9

File tree

6 files changed

+102
-9
lines changed

6 files changed

+102
-9
lines changed

src/sentry/integrations/bitbucket/client.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from requests import PreparedRequest
99

10-
from sentry.integrations.base import IntegrationFeatureNotImplementedError
1110
from sentry.integrations.client import ApiClient
1211
from sentry.integrations.models.integration import Integration
1312
from sentry.integrations.services.integration.model import RpcIntegration
@@ -182,4 +181,13 @@ def check_file(self, repo: Repository, path: str, version: str | None) -> object
182181
def get_file(
183182
self, repo: Repository, path: str, ref: str | None, codeowners: bool = False
184183
) -> str:
185-
raise IntegrationFeatureNotImplementedError
184+
response = self.get_cached(
185+
path=BitbucketAPIPath.source.format(
186+
repo=repo.name,
187+
sha=ref,
188+
path=path,
189+
),
190+
allow_redirects=True,
191+
raw_response=True,
192+
)
193+
return response.text

src/sentry/integrations/bitbucket/integration.py

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@
7878
""",
7979
IntegrationFeatures.STACKTRACE_LINK,
8080
),
81+
FeatureDescription(
82+
"""
83+
Import your Bitbucket [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.
84+
""",
85+
IntegrationFeatures.CODEOWNERS,
86+
),
8187
]
8288

8389
metadata = IntegrationMetadata(
@@ -94,6 +100,8 @@
94100

95101

96102
class BitbucketIntegration(RepositoryIntegration, BitbucketIssuesSpec):
103+
codeowners_locations = [".bitbucket/CODEOWNERS"]
104+
97105
@property
98106
def integration_name(self) -> str:
99107
return "bitbucket"
@@ -185,6 +193,7 @@ class BitbucketIntegrationProvider(IntegrationProvider):
185193
IntegrationFeatures.ISSUE_BASIC,
186194
IntegrationFeatures.COMMITS,
187195
IntegrationFeatures.STACKTRACE_LINK,
196+
IntegrationFeatures.CODEOWNERS,
188197
]
189198
)
190199

src/sentry/models/commitfilechange.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ def is_valid_type(value: str) -> bool:
4848

4949

5050
def process_resource_change(instance, **kwargs):
51+
from sentry.integrations.bitbucket.integration import BitbucketIntegration
5152
from sentry.integrations.github.integration import GitHubIntegration
5253
from sentry.integrations.gitlab.integration import GitlabIntegration
5354
from sentry.tasks.codeowners import code_owners_auto_sync
5455

5556
def _spawn_task():
56-
filepaths = set(GitHubIntegration.codeowners_locations) | set(
57-
GitlabIntegration.codeowners_locations
57+
filepaths = (
58+
set(GitHubIntegration.codeowners_locations)
59+
| set(GitlabIntegration.codeowners_locations)
60+
| set(BitbucketIntegration.codeowners_locations)
5861
)
5962

6063
# CODEOWNERS file added or modified, trigger auto-sync

src/sentry/shared_integrations/client/base.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,15 @@ def request(self, *args: Any, **kwargs: Any) -> Any:
335335
def delete(self, *args: Any, **kwargs: Any) -> Any:
336336
return self.request("DELETE", *args, **kwargs)
337337

338-
def get_cache_key(self, path: str, query: str = "", data: str | None = "") -> str:
338+
def get_cache_key(self, path: str, method: str, query: str = "", data: str | None = "") -> str:
339339
if not data:
340-
return self.get_cache_prefix() + md5_text(self.build_url(path), query).hexdigest()
341-
return self.get_cache_prefix() + md5_text(self.build_url(path), query, data).hexdigest()
340+
return (
341+
self.get_cache_prefix() + md5_text(self.build_url(path), method, query).hexdigest()
342+
)
343+
return (
344+
self.get_cache_prefix()
345+
+ md5_text(self.build_url(path), method, query, data).hexdigest()
346+
)
342347

343348
def check_cache(self, cache_key: str) -> Any | None:
344349
return cache.get(cache_key)
@@ -352,7 +357,7 @@ def _get_cached(self, path: str, method: str, *args: Any, **kwargs: Any) -> Any:
352357
if kwargs.get("params", None):
353358
query = json.dumps(kwargs.get("params"))
354359

355-
key = self.get_cache_key(path, query, data)
360+
key = self.get_cache_key(path, method, query, data)
356361
result = self.check_cache(key)
357362
if result is None:
358363
cache_time = kwargs.pop("cache_time", None) or self.cache_time

tests/sentry/integrations/bitbucket/test_client.py

+33
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
control_address = "http://controlserver"
1818
secret = "hush-hush-im-invisible"
1919

20+
BITBUCKET_CODEOWNERS = {
21+
"filepath": ".bitbucket/CODEOWNERS",
22+
"html_url": "https://bitbucket.org/sentryuser/newsdiffs/src/master/.bitbucket/CODEOWNERS",
23+
"raw": "docs/* @jianyuan @getsentry/ecosystem\n* @jianyuan\n",
24+
}
25+
2026

2127
@control_silo_test
2228
class BitbucketApiClientTest(TestCase, BaseTestCase):
@@ -121,3 +127,30 @@ def test_get_stacktrace_link(self):
121127
source_url
122128
== "https://bitbucket.org/sentryuser/newsdiffs/src/master/src/sentry/integrations/bitbucket/client.py"
123129
)
130+
131+
@responses.activate
132+
def test_get_codeowner_file(self):
133+
self.config = self.create_code_mapping(
134+
repo=self.repo,
135+
project=self.project,
136+
)
137+
138+
path = ".bitbucket/CODEOWNERS"
139+
url = f"https://api.bitbucket.org/2.0/repositories/{self.config.repository.name}/src/{self.config.default_branch}/{path}"
140+
141+
responses.add(
142+
method=responses.HEAD,
143+
url=url,
144+
json={"text": 200},
145+
)
146+
responses.add(
147+
method=responses.GET,
148+
url=url,
149+
content_type="text/plain",
150+
body=BITBUCKET_CODEOWNERS["raw"],
151+
)
152+
153+
result = self.install.get_codeowner_file(
154+
self.config.repository, ref=self.config.default_branch
155+
)
156+
assert result == BITBUCKET_CODEOWNERS

tests/sentry/integrations/test_client.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_cache_mocked(self, cache):
7070
resp = ApiClient().get_cached("http://example.com")
7171
assert resp == {"key": "value1"}
7272

73-
key = "integration.undefined.client:a9b9f04336ce0181a08e774e01113b31"
73+
key = "integration.undefined.client:41c2952996340270af611f0d7fad7286"
7474
cache.get.assert_called_with(key)
7575
cache.set.assert_called_with(key, {"key": "value1"}, 900)
7676

@@ -121,6 +121,41 @@ def test_head_cached_query_param(self):
121121
ApiClient().head_cached("http://example.com", params={"param": "different"})
122122
assert len(responses.calls) == 2
123123

124+
@responses.activate
125+
def test_get_and_head_cached(self):
126+
# Same URL, different HTTP method
127+
url = "http://example.com"
128+
responses.add(
129+
responses.GET,
130+
url,
131+
json={"key": "response-for-get"},
132+
adding_headers={"x-method": "GET"},
133+
)
134+
responses.add(
135+
responses.HEAD,
136+
url,
137+
json={},
138+
adding_headers={"x-method": "HEAD"},
139+
)
140+
141+
resp = ApiClient().head_cached(url)
142+
assert resp.headers["x-method"] == "HEAD"
143+
assert len(responses.calls) == 1
144+
145+
resp = ApiClient().head_cached(url)
146+
assert resp.headers["x-method"] == "HEAD"
147+
assert len(responses.calls) == 1
148+
149+
resp = ApiClient().get_cached(url, raw_response=True)
150+
assert resp.headers["x-method"] == "GET"
151+
assert resp.json() == {"key": "response-for-get"}
152+
assert len(responses.calls) == 2
153+
154+
resp = ApiClient().get_cached(url, raw_response=True)
155+
assert resp.headers["x-method"] == "GET"
156+
assert resp.json() == {"key": "response-for-get"}
157+
assert len(responses.calls) == 2
158+
124159
@responses.activate
125160
def test_default_redirect_behaviour(self):
126161
destination_url = "http://example.com/destination"

0 commit comments

Comments
 (0)