Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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[ProjectRepositorySource, 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(ProjectRepositorySource(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
115 changes: 115 additions & 0 deletions tests/sentry/models/test_projectrepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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
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