Skip to content

Commit 02bee7a

Browse files
committed
chore(integrations): Add canonical way to determine the external id for a repo
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.
1 parent 59f330b commit 02bee7a

File tree

18 files changed

+196
-78
lines changed

18 files changed

+196
-78
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
@@ -20,7 +21,10 @@
2021
from sentry.integrations.models.integration import Integration
2122
from sentry.integrations.pipeline import IntegrationPipeline
2223
from sentry.integrations.services.repository import RpcRepository, repository_service
23-
from sentry.integrations.source_code_management.repository import RepositoryIntegration
24+
from sentry.integrations.source_code_management.repository import (
25+
RepositoryInfo,
26+
RepositoryIntegration,
27+
)
2428
from sentry.integrations.tasks.migrate_repo import migrate_repo
2529
from sentry.integrations.types import IntegrationProviderSlug
2630
from sentry.integrations.utils.atlassian_connect import (
@@ -103,14 +107,14 @@
103107
scopes = ("issue:write", "pullrequest", "webhook", "repository")
104108

105109

106-
class BitbucketIntegration(RepositoryIntegration, BitbucketIssuesSpec):
110+
class BitbucketIntegration(RepositoryIntegration[BitbucketApiClient], BitbucketIssuesSpec):
107111
codeowners_locations = [".bitbucket/CODEOWNERS"]
108112

109113
@property
110114
def integration_name(self) -> str:
111115
return IntegrationProviderSlug.BITBUCKET.value
112116

113-
def get_client(self):
117+
def get_client(self) -> BitbucketApiClient:
114118
return BitbucketApiClient(integration=self.model)
115119

116120
# IntegrationInstallation methods
@@ -120,34 +124,50 @@ def error_message_from_json(self, data):
120124

121125
# RepositoryIntegration methods
122126

127+
def get_repo_external_id(self, repo: Mapping[str, Any]) -> str:
128+
return str(repo["uuid"])
129+
123130
def get_repositories(
124131
self,
125132
query: str | None = None,
126133
page_number_limit: int | None = None,
127134
accessible_only: bool = False,
128-
) -> list[dict[str, Any]]:
135+
) -> list[RepositoryInfo]:
129136
username = self.model.metadata.get("uuid", self.username)
130137
if not query:
131138
resp = self.get_client().get_repos(username)
132139
return [
133-
{"identifier": repo["full_name"], "name": repo["full_name"]}
140+
{
141+
"identifier": repo["full_name"],
142+
"name": repo["full_name"],
143+
"external_id": self.get_repo_external_id(repo),
144+
}
134145
for repo in resp.get("values", [])
135146
]
136147

148+
client = self.get_client()
137149
exact_query = f'name="{query}"'
138150
fuzzy_query = f'name~"{query}"'
139-
exact_search_resp = self.get_client().search_repositories(username, exact_query)
140-
fuzzy_search_resp = self.get_client().search_repositories(username, fuzzy_query)
141-
142-
result: OrderedSet[str] = OrderedSet()
143-
144-
for j in exact_search_resp.get("values", []):
145-
result.add(j["full_name"])
146-
147-
for i in fuzzy_search_resp.get("values", []):
148-
result.add(i["full_name"])
151+
exact_search_resp = client.search_repositories(username, exact_query)
152+
fuzzy_search_resp = client.search_repositories(username, fuzzy_query)
153+
154+
seen: OrderedSet[str] = OrderedSet()
155+
repos: list[RepositoryInfo] = []
156+
for repo in chain(
157+
exact_search_resp.get("values", []),
158+
fuzzy_search_resp.get("values", []),
159+
):
160+
if repo["full_name"] not in seen:
161+
seen.add(repo["full_name"])
162+
repos.append(
163+
{
164+
"identifier": repo["full_name"],
165+
"name": repo["full_name"],
166+
"external_id": self.get_repo_external_id(repo),
167+
}
168+
)
149169

150-
return [{"identifier": full_name, "name": full_name} for full_name in result]
170+
return repos
151171

152172
def has_repo_access(self, repo: RpcRepository) -> bool:
153173
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: 7 additions & 2 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
@@ -220,13 +223,14 @@ def get_repositories(
220223
query: str | None = None,
221224
page_number_limit: int | None = None,
222225
accessible_only: bool = False,
223-
) -> list[dict[str, Any]]:
226+
) -> list[RepositoryInfo]:
224227
if not query:
225228
all_repos = self.get_client().get_repos(page_number_limit=page_number_limit)
226229
return [
227230
{
228231
"name": i["name"],
229232
"identifier": i["full_name"],
233+
"external_id": self.get_repo_external_id(i),
230234
"default_branch": i.get("default_branch"),
231235
}
232236
for i in all_repos
@@ -239,6 +243,7 @@ def get_repositories(
239243
{
240244
"name": i["name"],
241245
"identifier": i["full_name"],
246+
"external_id": self.get_repo_external_id(i),
242247
"default_branch": i.get("default_branch"),
243248
}
244249
for i in response.get("items", [])

0 commit comments

Comments
 (0)