Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/sentry/api/endpoints/project_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ def validate_repositoryId(self, value: int) -> int:
return value

def create(self, validated_data: dict[str, Any]) -> ProjectRepository:
project_repo, created = ProjectRepository.objects.get_or_create(
project=self.project,
project_repo, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=validated_data["repositoryId"],
defaults={"source": ProjectRepositorySource.SCM_ONBOARDING},
source=ProjectRepositorySource.SCM_ONBOARDING,
)
self._created = created
return project_repo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ def validate_default_branch(self, default_branch):

def create(self, validated_data):
with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)):
project_repo, _ = ProjectRepository.objects.get_or_create(
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=validated_data.pop("project_id"),
repository_id=validated_data.pop("repository_id"),
defaults={"source": ProjectRepositorySource.MANUAL},
source=ProjectRepositorySource.MANUAL,
)
return RepositoryProjectPathConfig.objects.create(
organization_integration_id=self.org_integration.id,
Expand All @@ -141,14 +141,14 @@ def update(self, instance, validated_data):
validated_data.pop("id")
if self.instance:
with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)):
project_repo, _ = ProjectRepository.objects.get_or_create(
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=validated_data.pop(
"project_id", self.instance.project_repository.project_id
),
repository_id=validated_data.pop(
"repository_id", self.instance.project_repository.repository_id
),
defaults={"source": ProjectRepositorySource.MANUAL},
source=ProjectRepositorySource.MANUAL,
)
self.instance.project_repository = project_repo
for key, value in validated_data.items():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ def post(self, request: Request, organization: Organization) -> Response:
has_errors = False
last_saved_config = None

project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
repository=repo,
defaults={"source": ProjectRepositorySource.MANUAL},
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=repo.id,
source=ProjectRepositorySource.MANUAL,
)

defaults = {
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/issues/auto_source_code_config/code_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,10 @@ def create_code_mapping(
},
)
with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)):
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
repository=repository,
defaults={"source": ProjectRepositorySource.MANUAL},
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=repository.id,
source=ProjectRepositorySource.MANUAL,
)
new_code_mapping, _ = RepositoryProjectPathConfig.objects.update_or_create(
project_repository=project_repo,
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/issues/auto_source_code_config/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,10 @@ def create_code_mapping(
created = False
if not tags["dry_run"] and repository is not None:
with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)):
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
repository=repository,
defaults={"source": ProjectRepositorySource.AUTO_EVENT},
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=repository.id,
source=ProjectRepositorySource.AUTO_EVENT,
)
_, created = RepositoryProjectPathConfig.objects.get_or_create(
project_repository=project_repo,
Expand Down
43 changes: 42 additions & 1 deletion src/sentry/models/projectrepository.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from django.db import models
from typing import ClassVar

from django.db import models, router, transaction

from sentry.backup.scopes import RelocationScope
from sentry.db.models import FlexibleForeignKey, cell_silo_model, sane_repr
from sentry.db.models.base import DefaultFieldsModel
from sentry.db.models.manager.base import BaseManager


class ProjectRepositorySource(models.IntegerChoices):
Expand All @@ -15,6 +18,42 @@ class ProjectRepositorySource(models.IntegerChoices):
SEER_PREFERENCE = 4, "seer_preference"


# Indicates how strong of a signal each source is. A higher number indicates
# more confidence that the project-repo linking is correct.
SOURCE_PRIORITY: dict[int, int] = {
ProjectRepositorySource.AUTO_NAME_MATCH: 100,
ProjectRepositorySource.AUTO_EVENT: 200,
ProjectRepositorySource.SCM_ONBOARDING: 500,
ProjectRepositorySource.MANUAL: 500,
ProjectRepositorySource.SEER_PREFERENCE: 500,
}


class ProjectRepositoryManager(BaseManager["ProjectRepository"]):
def get_or_create_with_source(
self,
project_id: int,
repository_id: int,
source: ProjectRepositorySource,
) -> tuple[ProjectRepository, bool]:
"""
Like get_or_create, but upgrades the source if the existing row
was created by a lower-priority mechanism.
"""
project_repo, created = self.get_or_create(
project_id=project_id,
repository_id=repository_id,
defaults={"source": source},
)
new_priority = SOURCE_PRIORITY.get(source, 0)
current_priority = SOURCE_PRIORITY.get(project_repo.source, 0)
if not created and new_priority > current_priority:
with transaction.atomic(router.db_for_write(type(project_repo))):
project_repo.source = source
project_repo.save(update_fields=["source", "date_updated"])
return project_repo, created
Comment thread
wedamija marked this conversation as resolved.


@cell_silo_model
class ProjectRepository(DefaultFieldsModel):
__relocation_scope__ = RelocationScope.Global
Expand All @@ -26,6 +65,8 @@ class ProjectRepository(DefaultFieldsModel):
default=ProjectRepositorySource.MANUAL,
)

objects: ClassVar[ProjectRepositoryManager] = ProjectRepositoryManager()

class Meta:
app_label = "sentry"
db_table = "sentry_projectrepository"
Expand Down
18 changes: 9 additions & 9 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,10 @@ def _write_preferences_to_sentry_db(

if project_repos_to_create:
for project, repository_id, spr in project_repos_to_create:
spr.project_repository, _ = ProjectRepository.objects.get_or_create(
project=project,
spr.project_repository, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=repository_id,
defaults={"source": ProjectRepositorySource.SEER_PREFERENCE},
source=ProjectRepositorySource.SEER_PREFERENCE,
)

created_project_repos = SeerProjectRepository.objects.bulk_create(
Expand Down Expand Up @@ -838,10 +838,10 @@ def add_seer_project_repos(project: Project, repos_data: list[ProjectRepoCreateD
result_ids = []
with transaction.atomic(router.db_for_write(SeerProjectRepository)):
for data in repos_data:
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=data["repository_id"],
defaults={"source": ProjectRepositorySource.SEER_PREFERENCE},
source=ProjectRepositorySource.SEER_PREFERENCE,
)
seer_project_repo, _ = SeerProjectRepository.objects.update_or_create(
project_repository=project_repo,
Expand All @@ -867,10 +867,10 @@ def replace_all_seer_project_repos(
).delete()

for data in repos_data:
project_repo, _ = ProjectRepository.objects.get_or_create(
project=project,
project_repo, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
repository_id=data["repository_id"],
defaults={"source": ProjectRepositorySource.SEER_PREFERENCE},
source=ProjectRepositorySource.SEER_PREFERENCE,
)
seer_project_repo, _ = SeerProjectRepository.objects.update_or_create(
project_repository=project_repo,
Expand Down
116 changes: 116 additions & 0 deletions tests/sentry/models/test_projectrepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from sentry.models.projectrepository import ProjectRepository, ProjectRepositorySource
from sentry.models.repository import Repository
from sentry.testutils.cases import TestCase


class ProjectRepositoryManagerTest(TestCase):
def setUp(self) -> None:
super().setUp()
self.org = self.create_organization()
self.project = self.create_project(organization=self.org)
self.repo = Repository.objects.create(
organization_id=self.org.id,
name="getsentry/sentry",
provider="integrations:github",
external_id="123",
)

def test_creates_with_source(self) -> None:
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.MANUAL,
)
assert created
assert pr.source == ProjectRepositorySource.MANUAL

def test_returns_existing_without_upgrade(self) -> None:
ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.MANUAL,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.SCM_ONBOARDING,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.MANUAL
Comment thread
wedamija marked this conversation as resolved.

def test_upgrades_auto_name_match_to_manual(self) -> None:
ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.AUTO_NAME_MATCH,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.MANUAL,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.MANUAL

def test_upgrades_auto_name_match_to_seer_preference(self) -> None:
ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.AUTO_NAME_MATCH,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.SEER_PREFERENCE,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.SEER_PREFERENCE

def test_upgrades_auto_name_match_to_auto_event(self) -> None:
ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.AUTO_NAME_MATCH,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.AUTO_EVENT,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.AUTO_EVENT

def test_upgrades_auto_event_to_manual(self) -> None:
ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.AUTO_EVENT,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.MANUAL,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.MANUAL

def test_does_not_upgrade_auto_name_match_to_auto_name_match(self) -> None:
pr_original = ProjectRepository.objects.create(
project=self.project,
repository=self.repo,
source=ProjectRepositorySource.AUTO_NAME_MATCH,
)
pr, created = ProjectRepository.objects.get_or_create_with_source(
project_id=self.project.id,
repository_id=self.repo.id,
source=ProjectRepositorySource.AUTO_NAME_MATCH,
)
assert not created
pr.refresh_from_db()
assert pr.source == ProjectRepositorySource.AUTO_NAME_MATCH
assert pr.date_updated == pr_original.date_updated
Loading