Skip to content

Commit 4cef60f

Browse files
authored
chore(integrations): Add canonical way to determine the external id for a repo (#112327)
This is prep work for a pr to genericise the automatic repo syncing pr. To make it easy to genericise, I needed a way to easily get the external id for a repo from the client. As part of adding this, I ran into a bunch of type errors, so this pr also improves typing in this area. <!-- Describe your PR here. -->
1 parent 484c71b commit 4cef60f

File tree

29 files changed

+502
-180
lines changed

29 files changed

+502
-180
lines changed

src/sentry/integrations/bitbucket/integration.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping, Sequence
4+
from itertools import chain
45
from typing import Any
56

67
from django.http.request import HttpRequest
@@ -23,7 +24,10 @@
2324
from sentry.integrations.models.integration import Integration
2425
from sentry.integrations.pipeline import IntegrationPipeline
2526
from sentry.integrations.services.repository import RpcRepository, repository_service
26-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
27+
from sentry.integrations.source_code_management.repository import (
28+
RepositoryInfo,
29+
RepositoryIntegration,
30+
)
2731
from sentry.integrations.tasks.migrate_repo import migrate_repo
2832
from sentry.integrations.types import IntegrationProviderSlug
2933
from sentry.integrations.utils.atlassian_connect import (
@@ -108,14 +112,14 @@
108112
scopes = ("issue:write", "pullrequest", "webhook", "repository")
109113

110114

111-
class BitbucketIntegration(RepositoryIntegration, BitbucketIssuesSpec):
115+
class BitbucketIntegration(RepositoryIntegration[BitbucketApiClient], BitbucketIssuesSpec):
112116
codeowners_locations = [".bitbucket/CODEOWNERS"]
113117

114118
@property
115119
def integration_name(self) -> str:
116120
return IntegrationProviderSlug.BITBUCKET.value
117121

118-
def get_client(self):
122+
def get_client(self) -> BitbucketApiClient:
119123
return BitbucketApiClient(integration=self.model)
120124

121125
# IntegrationInstallation methods
@@ -125,34 +129,50 @@ def error_message_from_json(self, data):
125129

126130
# RepositoryIntegration methods
127131

132+
def get_repo_external_id(self, repo: Mapping[str, Any]) -> str:
133+
return str(repo["uuid"])
134+
128135
def get_repositories(
129136
self,
130137
query: str | None = None,
131138
page_number_limit: int | None = None,
132139
accessible_only: bool = False,
133-
) -> list[dict[str, Any]]:
140+
) -> list[RepositoryInfo]:
134141
username = self.model.metadata.get("uuid", self.username)
135142
if not query:
136143
resp = self.get_client().get_repos(username)
137144
return [
138-
{"identifier": repo["full_name"], "name": repo["full_name"]}
145+
{
146+
"identifier": repo["full_name"],
147+
"name": repo["full_name"],
148+
"external_id": self.get_repo_external_id(repo),
149+
}
139150
for repo in resp.get("values", [])
140151
]
141152

153+
client = self.get_client()
142154
exact_query = f'name="{query}"'
143155
fuzzy_query = f'name~"{query}"'
144-
exact_search_resp = self.get_client().search_repositories(username, exact_query)
145-
fuzzy_search_resp = self.get_client().search_repositories(username, fuzzy_query)
146-
147-
result: OrderedSet[str] = OrderedSet()
148-
149-
for j in exact_search_resp.get("values", []):
150-
result.add(j["full_name"])
151-
152-
for i in fuzzy_search_resp.get("values", []):
153-
result.add(i["full_name"])
156+
exact_search_resp = client.search_repositories(username, exact_query)
157+
fuzzy_search_resp = client.search_repositories(username, fuzzy_query)
158+
159+
seen: OrderedSet[str] = OrderedSet()
160+
repos: list[RepositoryInfo] = []
161+
for repo in chain(
162+
exact_search_resp.get("values", []),
163+
fuzzy_search_resp.get("values", []),
164+
):
165+
if repo["full_name"] not in seen:
166+
seen.add(repo["full_name"])
167+
repos.append(
168+
{
169+
"identifier": repo["full_name"],
170+
"name": repo["full_name"],
171+
"external_id": self.get_repo_external_id(repo),
172+
}
173+
)
154174

155-
return [{"identifier": full_name, "name": full_name} for full_name in result]
175+
return repos
156176

157177
def has_repo_access(self, repo: RpcRepository) -> bool:
158178
client = self.get_client()

src/sentry/integrations/bitbucket/repository.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from __future__ import annotations
2+
13
from collections.abc import Mapping
2-
from typing import Any
4+
from typing import TYPE_CHECKING, Any
35

46
from sentry.integrations.types import IntegrationProviderSlug
57
from sentry.locks import locks
@@ -12,8 +14,11 @@
1214
from sentry.utils.email import parse_email, parse_user_name
1315
from sentry.utils.http import absolute_uri
1416

17+
if TYPE_CHECKING:
18+
from sentry.integrations.bitbucket.integration import BitbucketIntegration # NOQA
19+
1520

16-
class BitbucketRepositoryProvider(IntegrationRepositoryProvider):
21+
class BitbucketRepositoryProvider(IntegrationRepositoryProvider["BitbucketIntegration"]):
1722
name = "Bitbucket"
1823
repo_provider = IntegrationProviderSlug.BITBUCKET.value
1924

@@ -25,7 +30,7 @@ def get_repository_data(self, organization, config):
2530
except Exception as e:
2631
installation.raise_error(e)
2732
else:
28-
config["external_id"] = str(repo["uuid"])
33+
config["external_id"] = installation.get_repo_external_id(repo)
2934
config["name"] = repo["full_name"]
3035
return config
3136

src/sentry/integrations/bitbucket_server/integration.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
from sentry.integrations.pipeline import IntegrationPipeline
2828
from sentry.integrations.services.repository import repository_service
2929
from sentry.integrations.services.repository.model import RpcRepository
30-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
30+
from sentry.integrations.source_code_management.repository import (
31+
RepositoryInfo,
32+
RepositoryIntegration,
33+
)
3134
from sentry.integrations.tasks.migrate_repo import migrate_repo
3235
from sentry.integrations.types import IntegrationProviderSlug
3336
from sentry.integrations.utils.metrics import (
@@ -253,7 +256,7 @@ def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpR
253256
)
254257

255258

256-
class BitbucketServerIntegration(RepositoryIntegration):
259+
class BitbucketServerIntegration(RepositoryIntegration[BitbucketServerClient]):
257260
"""
258261
IntegrationInstallation implementation for Bitbucket Server
259262
"""
@@ -285,13 +288,14 @@ def get_repositories(
285288
query: str | None = None,
286289
page_number_limit: int | None = None,
287290
accessible_only: bool = False,
288-
) -> list[dict[str, Any]]:
291+
) -> list[RepositoryInfo]:
289292
if not query:
290293
resp = self.get_client().get_repos()
291294

292295
return [
293296
{
294297
"identifier": repo["project"]["key"] + "/" + repo["slug"],
298+
"external_id": self.get_repo_external_id(repo),
295299
"project": repo["project"]["key"],
296300
"repo": repo["slug"],
297301
"name": repo["project"]["name"] + "/" + repo["name"],
@@ -304,6 +308,7 @@ def get_repositories(
304308
return [
305309
{
306310
"identifier": repo["project"]["key"] + "/" + repo["slug"],
311+
"external_id": self.get_repo_external_id(repo),
307312
"project": repo["project"]["key"],
308313
"repo": repo["slug"],
309314
"name": repo["project"]["name"] + "/" + repo["name"],

src/sentry/integrations/bitbucket_server/repository.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from __future__ import annotations
2+
13
from collections.abc import Mapping
24
from datetime import datetime, timezone
3-
from typing import Any
5+
from typing import TYPE_CHECKING, Any
46

57
from django.core.cache import cache
68
from django.urls import reverse
@@ -15,8 +17,13 @@
1517
from sentry.utils.hashlib import md5_text
1618
from sentry.utils.http import absolute_uri
1719

20+
if TYPE_CHECKING:
21+
from sentry.integrations.bitbucket_server.integration import BitbucketServerIntegration # NOQA
22+
1823

19-
class BitbucketServerRepositoryProvider(IntegrationRepositoryProvider):
24+
class BitbucketServerRepositoryProvider(
25+
IntegrationRepositoryProvider["BitbucketServerIntegration"]
26+
):
2027
name = "Bitbucket Server"
2128
repo_provider = IntegrationProviderSlug.BITBUCKET_SERVER.value
2229

@@ -29,7 +36,7 @@ def get_repository_data(self, organization, config):
2936
except Exception as e:
3037
installation.raise_error(e)
3138
else:
32-
config["external_id"] = str(repo["id"])
39+
config["external_id"] = installation.get_repo_external_id(repo)
3340
config["name"] = repo["project"]["key"] + "/" + repo["name"]
3441
config["project"] = repo["project"]["key"]
3542
config["repo"] = repo["name"]

src/sentry/integrations/example/integration.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
from sentry.integrations.services.integration.serial import serialize_integration
2323
from sentry.integrations.services.repository.model import RpcRepository
2424
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
25-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
25+
from sentry.integrations.source_code_management.repository import (
26+
RepositoryInfo,
27+
RepositoryIntegration,
28+
)
2629
from sentry.models.repository import Repository
2730
from sentry.organizations.services.organization.model import RpcOrganization
2831
from sentry.pipeline.views.base import PipelineView
@@ -151,8 +154,8 @@ def get_repositories(
151154
query: str | None = None,
152155
page_number_limit: int | None = None,
153156
accessible_only: bool = False,
154-
) -> list[dict[str, Any]]:
155-
return [{"name": "repo", "identifier": "user/repo"}]
157+
) -> list[RepositoryInfo]:
158+
return [{"name": "repo", "identifier": "user/repo", "external_id": "1"}]
156159

157160
def get_unmigratable_repositories(self):
158161
return []

src/sentry/integrations/github/integration.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@
4747
PRCommentWorkflow,
4848
)
4949
from sentry.integrations.source_code_management.repo_trees import RepoTreesIntegration
50-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
50+
from sentry.integrations.source_code_management.repository import (
51+
RepositoryInfo,
52+
RepositoryIntegration,
53+
)
5154
from sentry.integrations.tasks.migrate_repo import migrate_repo
5255
from sentry.integrations.types import IntegrationProviderSlug
5356
from sentry.integrations.utils.metrics import (
@@ -237,7 +240,7 @@ def get_document_origin(org) -> str:
237240

238241

239242
class GitHubIntegration(
240-
RepositoryIntegration,
243+
RepositoryIntegration[GitHubBaseClient],
241244
GitHubIssuesSpec,
242245
GitHubIssueSyncSpec,
243246
CommitContextIntegration,
@@ -322,7 +325,7 @@ def get_repositories(
322325
query: str | None = None,
323326
page_number_limit: int | None = None,
324327
accessible_only: bool = False,
325-
) -> list[dict[str, Any]]:
328+
) -> list[RepositoryInfo]:
326329
"""
327330
args:
328331
* query - a query to filter the repositories by
@@ -335,30 +338,33 @@ def get_repositories(
335338
"""
336339
if not query or accessible_only:
337340
all_repos = self.get_client().get_repos(page_number_limit=page_number_limit)
338-
repos = [
341+
repos: list[RepositoryInfo] = [
339342
{
340343
"name": i["name"],
341344
"identifier": i["full_name"],
345+
"external_id": self.get_repo_external_id(i),
342346
"default_branch": i.get("default_branch"),
343347
}
344348
for i in all_repos
345349
if not i.get("archived")
346350
]
347351
if query:
348352
query_lower = query.lower()
349-
repos = [r for r in repos if query_lower in r["identifier"].lower()]
353+
repos = [r for r in repos if query_lower in str(r["identifier"]).lower()]
350354
return repos
351355

352356
full_query = build_repository_query(self.model.metadata, self.model.name, query)
353357
response = self.get_client().search_repositories(full_query)
354-
return [
358+
search_repos: list[RepositoryInfo] = [
355359
{
356360
"name": i["name"],
357361
"identifier": i["full_name"],
362+
"external_id": self.get_repo_external_id(i),
358363
"default_branch": i.get("default_branch"),
359364
}
360365
for i in response.get("items", [])
361366
]
367+
return search_repos
362368

363369
def get_unmigratable_repositories(self) -> list[RpcRepository]:
364370
accessible_repos = self.get_repositories()

src/sentry/integrations/github/repository.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping, MutableMapping, Sequence
4-
from typing import Any
4+
from typing import TYPE_CHECKING, Any
55

66
from sentry.constants import ObjectStatus
77
from sentry.integrations.base import IntegrationInstallation
@@ -15,10 +15,13 @@
1515
from sentry.plugins.providers.integration_repository import RepositoryConfig
1616
from sentry.shared_integrations.exceptions import ApiError, IntegrationError
1717

18+
if TYPE_CHECKING:
19+
from sentry.integrations.github.integration import GitHubIntegration # NOQA
20+
1821
WEBHOOK_EVENTS = ["push", "pull_request"]
1922

2023

21-
class GitHubRepositoryProvider(IntegrationRepositoryProvider):
24+
class GitHubRepositoryProvider(IntegrationRepositoryProvider["GitHubIntegration"]):
2225
name = "GitHub"
2326
repo_provider = IntegrationProviderSlug.GITHUB.value
2427

@@ -46,7 +49,7 @@ def get_repository_data(
4649
client = installation.get_client()
4750

4851
repo = self._validate_repo(client, installation, config["identifier"])
49-
config["external_id"] = str(repo["id"])
52+
config["external_id"] = installation.get_repo_external_id(repo)
5053
config["integration_id"] = installation.model.id
5154

5255
return config

src/sentry/integrations/github_enterprise/integration.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
from sentry.integrations.services.integration import integration_service
3838
from sentry.integrations.services.repository import RpcRepository
3939
from sentry.integrations.source_code_management.commit_context import CommitContextIntegration
40-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
40+
from sentry.integrations.source_code_management.repository import (
41+
RepositoryInfo,
42+
RepositoryIntegration,
43+
)
4144
from sentry.integrations.types import IntegrationProviderSlug
4245
from sentry.models.repository import Repository
4346
from sentry.organizations.services.organization import organization_service
@@ -173,7 +176,10 @@ def get_user_info(url, access_token):
173176

174177

175178
class GitHubEnterpriseIntegration(
176-
RepositoryIntegration, GitHubIssuesSpec, GitHubIssueSyncSpec, CommitContextIntegration
179+
RepositoryIntegration[GitHubEnterpriseApiClient],
180+
GitHubIssuesSpec,
181+
GitHubIssueSyncSpec,
182+
CommitContextIntegration,
177183
):
178184
codeowners_locations = ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"]
179185

@@ -220,13 +226,14 @@ def get_repositories(
220226
query: str | None = None,
221227
page_number_limit: int | None = None,
222228
accessible_only: bool = False,
223-
) -> list[dict[str, Any]]:
229+
) -> list[RepositoryInfo]:
224230
if not query:
225231
all_repos = self.get_client().get_repos(page_number_limit=page_number_limit)
226232
return [
227233
{
228234
"name": i["name"],
229235
"identifier": i["full_name"],
236+
"external_id": self.get_repo_external_id(i),
230237
"default_branch": i.get("default_branch"),
231238
}
232239
for i in all_repos
@@ -239,6 +246,7 @@ def get_repositories(
239246
{
240247
"name": i["name"],
241248
"identifier": i["full_name"],
249+
"external_id": self.get_repo_external_id(i),
242250
"default_branch": i.get("default_branch"),
243251
}
244252
for i in response.get("items", [])

0 commit comments

Comments
 (0)