diff --git a/src/sentry/api/endpoints/project_stacktrace_coverage.py b/src/sentry/api/endpoints/project_stacktrace_coverage.py index 764583bc20e336..0f3d785623a105 100644 --- a/src/sentry/api/endpoints/project_stacktrace_coverage.py +++ b/src/sentry/api/endpoints/project_stacktrace_coverage.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -53,8 +52,7 @@ def get(self, request: Request, project: Project) -> Response: if not configs: return Response({"detail": "No code mappings found for this project"}, status=400) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = get_stacktrace_config(configs, ctx, use_project_repository_fk=use_fk) + result = get_stacktrace_config(configs, ctx) error = result["error"] serialized_config = None diff --git a/src/sentry/api/serializers/models/projectcodeowners.py b/src/sentry/api/serializers/models/projectcodeowners.py index c2a6bc0f5dfc33..5631e3bc3790bb 100644 --- a/src/sentry/api/serializers/models/projectcodeowners.py +++ b/src/sentry/api/serializers/models/projectcodeowners.py @@ -1,7 +1,6 @@ import logging from typing import Any -from sentry import features from sentry.api.serializers import Serializer, register, serialize from sentry.integrations.api.serializers.models.repository_project_path_config import ( RepositoryProjectPathConfigSerializer, @@ -24,12 +23,6 @@ def __init__( def get_attrs(self, item_list, user, **kwargs): attrs = {} - use_fk = False - if item_list: - use_fk = features.has( - "organizations:project-repository-fk-reads", - item_list[0].project.organization, - ) integrations = { i.id: i for i in integration_service.get_integrations( @@ -38,9 +31,7 @@ def get_attrs(self, item_list, user, **kwargs): } for item in item_list: code_mapping = item.repository_project_path_config - repository = ( - code_mapping.project_repository.repository if use_fk else code_mapping.repository - ) + repository = code_mapping.project_repository.repository integration = integrations[item.repository_project_path_config.integration_id] install = integration.get_installation( diff --git a/src/sentry/autopilot/tasks/missing_sdk_integration.py b/src/sentry/autopilot/tasks/missing_sdk_integration.py index f5f99c70f3cd08..c164c3d55af665 100644 --- a/src/sentry/autopilot/tasks/missing_sdk_integration.py +++ b/src/sentry/autopilot/tasks/missing_sdk_integration.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from sentry import features, options +from sentry import options from sentry.autopilot.tasks.common import AutopilotDetectorName, create_instrumentation_issue from sentry.constants import INTEGRATION_ID_TO_PLATFORM_DATA, ObjectStatus from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig @@ -88,28 +88,16 @@ def run_missing_sdk_integration_detector() -> None: continue # Get repo configs for this project - if features.has("organizations:project-repository-fk-reads", project.organization): - repo_names = ( - RepositoryProjectPathConfig.objects.filter( - project_repository__project=project, - project_repository__repository__status=ObjectStatus.ACTIVE, - project_repository__repository__provider="integrations:github", - ) - .order_by("project_repository__repository__name") - .distinct("project_repository__repository__name") - .values_list("project_repository__repository__name", flat=True) - ) - else: - repo_names = ( - RepositoryProjectPathConfig.objects.filter( - project=project, - repository__status=ObjectStatus.ACTIVE, - repository__provider="integrations:github", - ) - .order_by("repository__name") - .distinct("repository__name") - .values_list("repository__name", flat=True) + repo_names = ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=project, + project_repository__repository__status=ObjectStatus.ACTIVE, + project_repository__repository__provider="integrations:github", ) + .order_by("project_repository__repository__name") + .distinct("project_repository__repository__name") + .values_list("project_repository__repository__name", flat=True) + ) for repo_name in repo_names: run_missing_sdk_integration_detector_for_project_task.apply_async( args=( diff --git a/src/sentry/incidents/serializers/alert_rule.py b/src/sentry/incidents/serializers/alert_rule.py index 790c35a502d40e..20b65449ce9cbc 100644 --- a/src/sentry/incidents/serializers/alert_rule.py +++ b/src/sentry/incidents/serializers/alert_rule.py @@ -24,6 +24,7 @@ from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer from sentry.api.serializers.rest_framework.environment import EnvironmentField from sentry.api.serializers.rest_framework.project import ProjectField +from sentry.api.utils import to_valid_int_id_list from sentry.incidents.logic import ( CRITICAL_TRIGGER_LABEL, WARNING_TRIGGER_LABEL, @@ -43,7 +44,6 @@ from sentry.snuba.dataset import Dataset from sentry.snuba.models import QuerySubscription, SnubaQueryEventType from sentry.snuba.snuba_query_validator import SnubaQueryValidator -from sentry.workflow_engine.endpoints.utils.ids import to_valid_int_id_list from sentry.workflow_engine.migration_helpers.alert_rule import ( dual_delete_migrated_alert_rule_trigger, dual_update_alert_rule, diff --git a/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py b/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py index b6f14db63f1405..b239590e885c29 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -16,7 +15,7 @@ from sentry.shared_integrations.exceptions import ApiError -def get_codeowner_contents(config, use_project_repository_fk: bool = False): +def get_codeowner_contents(config): if not config.organization_integration_id: raise NotFound(detail="No associated integration") @@ -25,12 +24,8 @@ def get_codeowner_contents(config, use_project_repository_fk: bool = False): ) if not integration: return None - if use_project_repository_fk: - org_id = config.project_repository.project.organization_id - repository = config.project_repository.repository - else: - org_id = config.project.organization_id - repository = config.repository + org_id = config.project_repository.project.organization_id + repository = config.project_repository.repository install = integration.get_installation(organization_id=org_id) if isinstance(install, RepositoryIntegration): return install.get_codeowner_file(repository, ref=config.default_branch) @@ -52,8 +47,6 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar try: kwargs["config"] = RepositoryProjectPathConfig.objects.select_related( - "project", - "repository", "project_repository__project", "project_repository__repository", ).get( @@ -67,8 +60,7 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar def get(self, request: Request, config_id, organization, config) -> Response: try: - use_fk = features.has("organizations:project-repository-fk-reads", organization) - codeowner_contents = get_codeowner_contents(config, use_project_repository_fk=use_fk) + codeowner_contents = get_codeowner_contents(config) except ApiError as e: return self.respond({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py b/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py index 27b91b277f32ca..67770f09cfdc96 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -40,7 +39,7 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar ) try: kwargs["config"] = RepositoryProjectPathConfig.objects.select_related( - "project", "project_repository__project" + "project_repository__project" ).get( id=config_id, organization_integration_id__in=[oi.id for oi in ois], @@ -69,10 +68,7 @@ def put(self, request: Request, config_id, organization, config, new_project) -> :param string default_branch: :auth: required """ - if features.has("organizations:project-repository-fk-reads", organization): - project = config.project_repository.project - else: - project = config.project + project = config.project_repository.project if not request.access.has_projects_access([project, new_project]): return self.respond(status=status.HTTP_403_FORBIDDEN) @@ -109,10 +105,7 @@ def delete(self, request: Request, config_id, organization, config) -> Response: :auth: required """ - if features.has("organizations:project-repository-fk-reads", organization): - project = config.project_repository.project - else: - project = config.project + project = config.project_repository.project if not request.access.has_project_access(project): return self.respond(status=status.HTTP_403_FORBIDDEN) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings.py b/src/sentry/integrations/api/endpoints/organization_code_mappings.py index c521b39ef13b2d..445b5768b178c8 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings.py @@ -7,7 +7,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -72,18 +71,11 @@ def organization(self): return self.context["organization"] def validate(self, attrs): - if features.has("organizations:project-repository-fk-reads", self.organization): - query = RepositoryProjectPathConfig.objects.filter( - project_repository__project_id=attrs.get("project_id"), - stack_root=attrs.get("stack_root"), - source_root=attrs.get("source_root"), - ) - else: - query = RepositoryProjectPathConfig.objects.filter( - project_id=attrs.get("project_id"), - stack_root=attrs.get("stack_root"), - source_root=attrs.get("source_root"), - ) + query = RepositoryProjectPathConfig.objects.filter( + project_repository__project_id=attrs.get("project_id"), + stack_root=attrs.get("stack_root"), + source_root=attrs.get("source_root"), + ) if self.instance: query = query.exclude(id=self.instance.id) if query.exists(): @@ -149,8 +141,12 @@ def update(self, instance, validated_data): validated_data.pop("id") if self.instance: with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)): - project_id = validated_data.get("project_id", self.instance.project_id) - repository_id = validated_data.get("repository_id", self.instance.repository_id) + project_id = validated_data.get( + "project_id", self.instance.project_repository.project_id + ) + repository_id = validated_data.get( + "repository_id", self.instance.project_repository.repository_id + ) project_repo, _ = ProjectRepository.objects.get_or_create( project_id=project_id, repository_id=repository_id, @@ -211,19 +207,12 @@ def get(self, request: Request, organization: Organization) -> Response: projects = self.get_projects( request, organization, include_all_accessible=not has_explicit_projects ) - if features.has("organizations:project-repository-fk-reads", organization): - queryset = RepositoryProjectPathConfig.objects.filter( - project_repository__project__in=projects - ).select_related( - "project", - "repository", - "project_repository__project", - "project_repository__repository", - ) - else: - queryset = RepositoryProjectPathConfig.objects.filter( - project__in=projects - ).select_related("project", "repository") + queryset = RepositoryProjectPathConfig.objects.filter( + project_repository__project__in=projects + ).select_related( + "project_repository__project", + "project_repository__repository", + ) if integration_id: # get_organization_integration will raise a 404 if no org_integration is found diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py index 9fc4b361bed1d7..edaf9854a4be9f 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py @@ -267,6 +267,7 @@ def post(self, request: Request, organization: Organization) -> Response: ) defaults = { + "project": project, "repository": repo, "organization_integration_id": org_integration.id, "organization_id": organization.id, @@ -281,7 +282,7 @@ def post(self, request: Request, organization: Organization) -> Response: with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)): try: config = RepositoryProjectPathConfig.objects.select_for_update().get( - project=project, + project_repository__project=project, stack_root=mapping["stack_root"], source_root=mapping["source_root"], ) @@ -290,7 +291,6 @@ def post(self, request: Request, organization: Organization) -> Response: created = False except RepositoryProjectPathConfig.DoesNotExist: config = RepositoryProjectPathConfig( - project=project, stack_root=mapping["stack_root"], source_root=mapping["source_root"], **defaults, diff --git a/src/sentry/integrations/api/serializers/models/repository_project_path_config.py b/src/sentry/integrations/api/serializers/models/repository_project_path_config.py index 39bc9cbcc6adf7..2a7a0d9cfb667f 100644 --- a/src/sentry/integrations/api/serializers/models/repository_project_path_config.py +++ b/src/sentry/integrations/api/serializers/models/repository_project_path_config.py @@ -1,6 +1,5 @@ from django.db.models import prefetch_related_objects -from sentry import features from sentry.api.serializers import Serializer, register from sentry.integrations.api.serializers.models.integration import serialize_provider from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig @@ -13,16 +12,11 @@ def get_attrs(self, item_list, user, **kwargs): if not item_list: return {} - organization = item_list[0].project.organization - use_fk = features.has("organizations:project-repository-fk-reads", organization) - if use_fk: - prefetch_related_objects( - item_list, "project_repository__project", "project_repository__repository" - ) - else: - prefetch_related_objects(item_list, "project", "repository") + prefetch_related_objects( + item_list, "project_repository__project", "project_repository__repository" + ) - return {item: {"use_project_repository_fk": use_fk} for item in item_list} + return {item: {} for item in item_list} def serialize(self, obj, attrs, user, **kwargs): integration = None @@ -35,12 +29,8 @@ def serialize(self, obj, attrs, user, **kwargs): serialized_provider = serialize_provider(provider) if provider else None integration_id = str(integration.id) if integration else None - if attrs.get("use_project_repository_fk"): - project = obj.project_repository.project - repository = obj.project_repository.repository - else: - project = obj.project - repository = obj.repository + project = obj.project_repository.project + repository = obj.project_repository.repository return { "id": str(obj.id), diff --git a/src/sentry/integrations/utils/commit_context.py b/src/sentry/integrations/utils/commit_context.py index 7003c23924d6d3..5fac2157995abf 100644 --- a/src/sentry/integrations/utils/commit_context.py +++ b/src/sentry/integrations/utils/commit_context.py @@ -8,7 +8,7 @@ from django.utils.datastructures import OrderedSet -from sentry import analytics, features, options +from sentry import analytics, options from sentry.analytics.events.integration_commit_context_all_frames import ( IntegrationsFailedToFetchCommitContextAllFrames, IntegrationsSuccessfullyFetchedCommitContextAllFrames, @@ -199,7 +199,6 @@ def _generate_integration_to_files_mapping( this function is used to separate files into each integration so that we can later call get_commit_context_all_frames on each integration. """ - use_fk = features.has("organizations:project-repository-fk-reads", organization) integration_to_files_mapping: dict[int, list[SourceLineInfo]] = {} num_successfully_mapped_frames = 0 @@ -258,11 +257,7 @@ def _generate_integration_to_files_mapping( lineno=frame.lineno, path=src_path, ref=code_mapping.default_branch or "master", - repo=( - code_mapping.project_repository.repository - if use_fk - else code_mapping.repository - ), + repo=code_mapping.project_repository.repository, code_mapping=code_mapping, ) ) diff --git a/src/sentry/integrations/utils/source_context.py b/src/sentry/integrations/utils/source_context.py index 1761b20e891b71..d27306cdad4225 100644 --- a/src/sentry/integrations/utils/source_context.py +++ b/src/sentry/integrations/utils/source_context.py @@ -61,7 +61,6 @@ def _format_context( def _resolve_integration( config: RepositoryProjectPathConfig, - use_project_repository_fk: bool = False, ) -> tuple[RpcIntegration, RepositoryIntegration] | None: """Resolve the integration and installation for a code mapping config.""" integration = integration_service.get_integration( @@ -71,11 +70,7 @@ def _resolve_integration( if not integration: return None - org_id = ( - config.project_repository.project.organization_id - if use_project_repository_fk - else config.project.organization_id - ) + org_id = config.project_repository.project.organization_id install = integration.get_installation(organization_id=org_id) if not isinstance(install, RepositoryIntegration): return None @@ -159,7 +154,6 @@ def fetch_source_context_from_scm( configs: Sequence[RepositoryProjectPathConfig], ctx: StacktraceLinkContext, context_lines: int = LINES_OF_CONTEXT, - use_project_repository_fk: bool = False, ) -> SourceContextResult: """ Fetch source context lines from an SCM integration for a stack trace frame. @@ -204,9 +198,7 @@ def fetch_source_context_from_scm( org_integration_id = config.organization_integration_id if org_integration_id not in resolved_integrations: - resolved_integrations[org_integration_id] = _resolve_integration( - config, use_project_repository_fk=use_project_repository_fk - ) + resolved_integrations[org_integration_id] = _resolve_integration(config) resolved = resolved_integrations[org_integration_id] if resolved is None: @@ -216,9 +208,7 @@ def fetch_source_context_from_scm( ref = ctx.get("commit_id") or str(config.default_branch or "") - repository = ( - config.project_repository.repository if use_project_repository_fk else config.repository - ) + repository = config.project_repository.repository file_content, fetch_error = _fetch_file_from_scm( install, integration.id, repository, src_path, ref ) diff --git a/src/sentry/integrations/utils/stacktrace_link.py b/src/sentry/integrations/utils/stacktrace_link.py index 0357d711e7cbe3..17d320a834c1eb 100644 --- a/src/sentry/integrations/utils/stacktrace_link.py +++ b/src/sentry/integrations/utils/stacktrace_link.py @@ -32,16 +32,11 @@ def get_link( config: RepositoryProjectPathConfig, src_path: str, version: str | None = None, - use_project_repository_fk: bool = False, ) -> RepositoryLinkOutcome: result: RepositoryLinkOutcome = {} - if use_project_repository_fk: - project = config.project_repository.project - repository = config.project_repository.repository - else: - project = config.project - repository = config.repository + project = config.project_repository.project + repository = config.project_repository.repository integration = integration_service.get_integration( organization_integration_id=config.organization_integration_id, status=ObjectStatus.ACTIVE @@ -94,7 +89,6 @@ class StacktraceLinkOutcome(TypedDict): def get_stacktrace_config( configs: Sequence[RepositoryProjectPathConfig], ctx: StacktraceLinkContext, - use_project_repository_fk: bool = False, ) -> StacktraceLinkOutcome: result: StacktraceLinkOutcome = { "source_url": None, @@ -119,13 +113,10 @@ def get_stacktrace_config( config, src_path, ctx["commit_id"], - use_project_repository_fk=use_project_repository_fk, ) result["iteration_count"] += 1 - repository = ( - config.project_repository.repository if use_project_repository_fk else config.repository - ) + repository = config.project_repository.repository result["current_config"] = { "config": config, "outcome": outcome, diff --git a/src/sentry/issues/auto_source_code_config/code_mapping.py b/src/sentry/issues/auto_source_code_config/code_mapping.py index f636db3f60531d..6a92220548bd8e 100644 --- a/src/sentry/issues/auto_source_code_config/code_mapping.py +++ b/src/sentry/issues/auto_source_code_config/code_mapping.py @@ -7,7 +7,6 @@ from django.db import router, transaction -from sentry import features from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.integrations.source_code_management.repo_trees import ( RepoAndBranch, @@ -410,27 +409,18 @@ def get_sorted_code_mapping_configs(project: Project) -> list[RepositoryProjectP # sure that we are still ordering by `id` because we want to make sure # the ordering is deterministic # codepath mappings must have an associated integration for stacktrace linking. - if features.has("organizations:project-repository-fk-reads", project.organization): - configs = ( - RepositoryProjectPathConfig.objects.filter( - project_repository__project=project, - organization_integration_id__isnull=False, - ) - .select_related( - "project_repository", - "project_repository__project", - "project_repository__repository", - ) - .order_by("id") + configs = ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=project, + organization_integration_id__isnull=False, ) - else: - configs = ( - RepositoryProjectPathConfig.objects.filter( - project=project, organization_integration_id__isnull=False - ) - .select_related("repository") - .order_by("id") + .select_related( + "project_repository", + "project_repository__project", + "project_repository__repository", ) + .order_by("id") + ) sorted_configs: list[RepositoryProjectPathConfig] = [] diff --git a/src/sentry/issues/endpoints/project_stacktrace_link.py b/src/sentry/issues/endpoints/project_stacktrace_link.py index 978d3802930b2e..b9b91680f4ca1b 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_link.py +++ b/src/sentry/issues/endpoints/project_stacktrace_link.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from sentry_sdk import Scope -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -150,8 +150,7 @@ def get(self, request: Request, project: Project) -> Response: scope = Scope.get_isolation_scope() set_top_tags(scope, project, ctx, len(configs) > 0) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = get_stacktrace_config(configs, ctx, use_project_repository_fk=use_fk) + result = get_stacktrace_config(configs, ctx) error = result["error"] src_path = result["src_path"] # Post-processing before exiting scope context diff --git a/src/sentry/issues/endpoints/project_stacktrace_source_context.py b/src/sentry/issues/endpoints/project_stacktrace_source_context.py index aadd4460e381c0..395cb9d4d43c0a 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_source_context.py +++ b/src/sentry/issues/endpoints/project_stacktrace_source_context.py @@ -5,7 +5,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -61,8 +60,7 @@ def get(self, request: Request, project: Project) -> Response: } ) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = fetch_source_context_from_scm(configs, ctx, use_project_repository_fk=use_fk) + result = fetch_source_context_from_scm(configs, ctx) return Response( { diff --git a/src/sentry/issues/endpoints/serializers.py b/src/sentry/issues/endpoints/serializers.py index c2d34cf91c0846..0a367e6c331b40 100644 --- a/src/sentry/issues/endpoints/serializers.py +++ b/src/sentry/issues/endpoints/serializers.py @@ -8,7 +8,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from sentry import analytics, features +from sentry import analytics from sentry.analytics.events.codeowners_max_length_exceeded import CodeOwnersMaxLengthExceeded from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer from sentry.api.validators.project_codeowners import build_codeowners_associations @@ -97,11 +97,9 @@ def validate_code_mapping_id(self, code_mapping_id: int) -> RepositoryProjectPat project = self.context["project"] try: - if features.has("organizations:project-repository-fk-reads", project.organization): - return RepositoryProjectPathConfig.objects.get( - id=code_mapping_id, project_repository__project=project - ) - return RepositoryProjectPathConfig.objects.get(id=code_mapping_id, project=project) + return RepositoryProjectPathConfig.objects.get( + id=code_mapping_id, project_repository__project=project + ) except RepositoryProjectPathConfig.DoesNotExist: raise serializers.ValidationError("This code mapping does not exist.") diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index 44d122a5f1c024..20082b4ff89ca5 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -16,7 +16,6 @@ from django.utils.translation import gettext_lazy as _ from bitfield import TypedClassBitField -from sentry import features from sentry.backup.dependencies import ImportKind, PrimaryKeyMap from sentry.backup.helpers import ImportFlags from sentry.backup.scopes import ImportScope, RelocationScope @@ -738,12 +737,7 @@ def transfer_to(self, organization: Organization) -> None: # Delete issue ownership objects to prevent them from being stuck on the old org ProjectCodeOwners.objects.filter(project_id=self.id).delete() - if features.has("organizations:project-repository-fk-reads", organization): - RepositoryProjectPathConfig.objects.filter( - project_repository__project_id=self.id - ).delete() - else: - RepositoryProjectPathConfig.objects.filter(project_id=self.id).delete() + RepositoryProjectPathConfig.objects.filter(project_repository__project_id=self.id).delete() ProjectRepository.objects.filter(project_id=self.id).delete() for external_issues in chunked( diff --git a/src/sentry/tasks/codeowners/code_owners_auto_sync.py b/src/sentry/tasks/codeowners/code_owners_auto_sync.py index 0c431e6ac9abaa..6c239b06aa8c56 100644 --- a/src/sentry/tasks/codeowners/code_owners_auto_sync.py +++ b/src/sentry/tasks/codeowners/code_owners_auto_sync.py @@ -5,7 +5,6 @@ from rest_framework.exceptions import NotFound from taskbroker_client.retry import Retry -from sentry import features from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.commit import Commit from sentry.models.organization import Organization @@ -36,24 +35,16 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: commit = Commit.objects.get(id=commit_id) - org = Organization.objects.get(id=commit.organization_id) - use_fk = features.has("organizations:project-repository-fk-reads", org) - if use_fk: - base_qs = RepositoryProjectPathConfig.objects.filter( + code_mappings = list( + RepositoryProjectPathConfig.objects.filter( project_repository__repository_id=commit.repository_id, project_repository__project__organization_id=commit.organization_id, - ).select_related("project", "project_repository__project") - else: - base_qs = RepositoryProjectPathConfig.objects.filter( - repository_id=commit.repository_id, - project__organization_id=commit.organization_id, - ).select_related("project", "project_repository__project") - code_mappings = list( - base_qs.annotate( + ) + .annotate( # By default, we don't create a ProjectOwnership record (bc we treat as a negative cache) when we create ProjectCodeOwners records. ownership_exists=Exists( ProjectOwnership.objects.filter( - project=OuterRef("project"), + project=OuterRef("project_repository__project"), ) ) ) @@ -64,9 +55,9 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: When( ownership_exists=True, then=Subquery( - ProjectOwnership.objects.filter(project=OuterRef("project")).values_list( - "codeowners_auto_sync", flat=True - )[:1], + ProjectOwnership.objects.filter( + project=OuterRef("project_repository__project") + ).values_list("codeowners_auto_sync", flat=True)[:1], ), ), default=True, @@ -80,13 +71,12 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: ) ) .filter(codeowners_auto_sync=True, has_codeowners=True) + .select_related("project_repository__project") ) for code_mapping in code_mappings: try: - codeowner_contents = get_codeowner_contents( - code_mapping, use_project_repository_fk=use_fk - ) + codeowner_contents = get_codeowner_contents(code_mapping) except (NotImplementedError, NotFound): logger.warning( "code_owners_auto_sync.fetch_error", @@ -100,10 +90,7 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: "code_owners_auto_sync.fetch_failed", extra={"commit_id": commit_id, "code_mapping_id": code_mapping.id}, ) - if use_fk: - project = code_mapping.project_repository.project - else: - project = code_mapping.project + project = code_mapping.project_repository.project AutoSyncNotification(project).send() return diff --git a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py index 487a889e9c5747..6745cff679a860 100644 --- a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py +++ b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py @@ -374,10 +374,10 @@ def test_post_existing_code_mapping(self, mock_get_repos: MagicMock) -> None: ) RepositoryProjectPathConfig.objects.create( project=self.project, + repository=self.repo, stack_root="/stack/root", source_root="/source/root/wrong", default_branch="master", - repository=self.repo, organization_integration_id=self.organization_integration.id, organization_id=self.organization_integration.organization_id, integration_id=self.organization_integration.integration_id, @@ -396,7 +396,7 @@ def test_post_existing_code_mapping(self, mock_get_repos: MagicMock) -> None: # Both mappings should coexist: the original and the newly derived one mappings = RepositoryProjectPathConfig.objects.filter( - project=self.project, stack_root="/stack/root" + project_repository__project=self.project, stack_root="/stack/root" ) assert mappings.count() == 2 assert set(mappings.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py index 70aa75d92b264d..e97c4328e5675c 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py @@ -44,8 +44,8 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.config = RepositoryProjectPathConfig.objects.create( - repository_id=self.repo.id, - project_id=self.project.id, + project=self.project, + repository=self.repo, organization_integration_id=self.org_integration.id, integration_id=self.org_integration.integration_id, organization_id=self.org_integration.organization_id, diff --git a/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py b/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py index f7b3f5c5bebc95..e32659db4dd665 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py @@ -60,11 +60,11 @@ def test_create_single_mapping(self) -> None: assert response.data["mappings"][0]["status"] == "created" config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert config.source_root == "modules/maps/src/main/java/com/example/maps" assert config.default_branch == "main" - assert config.repository == self.repo1 + assert config.project_repository.repository == self.repo1 assert config.organization_id == self.organization.id assert config.automatically_generated is False assert config.project_repository is not None @@ -94,7 +94,12 @@ def test_create_multiple_mappings(self) -> None: assert response.status_code == 200, response.content assert response.data["created"] == 3 assert response.data["updated"] == 0 - assert RepositoryProjectPathConfig.objects.filter(project=self.project1).count() == 3 + assert ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=self.project1 + ).count() + == 3 + ) def test_update_existing_mapping(self) -> None: self.create_code_mapping( @@ -120,7 +125,7 @@ def test_update_existing_mapping(self) -> None: assert response.data["updated"] == 1 config = RepositoryProjectPathConfig.objects.get( - project=self.project1, + project_repository__project=self.project1, stack_root="com/example/maps", source_root="modules/maps/src/main/java/com/example/maps", ) @@ -291,7 +296,7 @@ def test_missing_default_branch_inferred_from_integration(self) -> None: response = self.client.post(self.url, data=payload, format="json") assert response.status_code == 200, response.content config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/a" + project_repository__project=self.project1, stack_root="com/example/a" ) assert config.default_branch == "develop" @@ -389,7 +394,7 @@ def test_skip_post_save_does_not_leak_to_fetched_instances(self) -> None: carry the suppressed flag, so normal post_save signals fire for them.""" self.make_post() config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert config._skip_post_save is False @@ -483,7 +488,7 @@ def test_same_stack_root_different_source_roots_creates_both(self) -> None: assert response.data["updated"] == 0 configs = RepositoryProjectPathConfig.objects.filter( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert configs.count() == 2 assert set(configs.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/integrations/perforce/test_code_mapping.py b/tests/sentry/integrations/perforce/test_code_mapping.py index 84ffc5b4e48246..1a612cad082e2e 100644 --- a/tests/sentry/integrations/perforce/test_code_mapping.py +++ b/tests/sentry/integrations/perforce/test_code_mapping.py @@ -56,8 +56,8 @@ def test_code_mapping_depot_root_to_slash(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -98,8 +98,8 @@ def test_code_mapping_with_symbolic_revision_syntax(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -143,8 +143,8 @@ def test_code_mapping_multiple_depots(self) -> None: ) depot_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=depot_repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -160,8 +160,8 @@ def test_code_mapping_multiple_depots(self) -> None: ) myproject_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=myproject_repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="myproject/", @@ -211,8 +211,8 @@ def test_code_mapping_no_match_different_depot(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -250,8 +250,8 @@ def test_code_mapping_abs_path_fallback(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -286,8 +286,8 @@ def test_code_mapping_nested_depot_paths(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/game/project/", @@ -329,8 +329,8 @@ def test_code_mapping_preserves_windows_backslash_conversion(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -387,8 +387,8 @@ def setUp(self) -> None: ) self.code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=self.repo, + organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -486,8 +486,8 @@ def test_full_flow_with_web_viewer(self) -> None: ) code_mapping_web = RepositoryProjectPathConfig.objects.create( project=project_web, - organization_id=self.organization.id, repository=repo_web, + organization_id=self.organization.id, organization_integration_id=org_integration_web.id, integration_id=integration_with_web.id, stack_root="depot/", diff --git a/tests/sentry/integrations/perforce/test_stacktrace_link.py b/tests/sentry/integrations/perforce/test_stacktrace_link.py index 7ce134b2c13929..5d92d92c41241e 100644 --- a/tests/sentry/integrations/perforce/test_stacktrace_link.py +++ b/tests/sentry/integrations/perforce/test_stacktrace_link.py @@ -40,8 +40,8 @@ def setUp(self) -> None: ) self.code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=self.repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", @@ -156,8 +156,8 @@ def test_get_stacktrace_config_multiple_code_mappings(self, mock_check_file): ) myproject_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=myproject_repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="myproject/", @@ -221,8 +221,8 @@ def test_get_stacktrace_config_with_web_viewer(self, mock_check_file): ) code_mapping_web = RepositoryProjectPathConfig.objects.create( project=project_web, - organization_id=self.organization.id, repository=repo_web, + organization_id=self.organization.id, organization_integration_id=org_integration.id, integration_id=integration_with_web.id, stack_root="depot/", @@ -297,8 +297,8 @@ def test_get_stacktrace_config_iteration_count(self, mock_check_file): ) other_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=other_repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="other/", @@ -348,8 +348,8 @@ def test_get_stacktrace_config_stops_on_first_match(self, mock_check_file): ) myproject_mapping = RepositoryProjectPathConfig.objects.create( project=project2, - organization_id=self.organization.id, repository=myproject_repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", # Same stack_root as depot mapping (but different project) @@ -429,8 +429,8 @@ def test_stacktrace_link_empty_stack_root(self) -> None: ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="", @@ -475,8 +475,8 @@ def test_stacktrace_link_with_special_characters_in_path(self, mock_check_file): ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", @@ -522,8 +522,8 @@ def test_stacktrace_link_deeply_nested_path(self, mock_check_file): ) code_mapping = RepositoryProjectPathConfig.objects.create( project=self.project, - organization_id=self.organization.id, repository=repo, + organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", diff --git a/tests/sentry/issues/auto_source_code_config/test_process_event.py b/tests/sentry/issues/auto_source_code_config/test_process_event.py index 02454eb175dd22..647284f5c99855 100644 --- a/tests/sentry/issues/auto_source_code_config/test_process_event.py +++ b/tests/sentry/issues/auto_source_code_config/test_process_event.py @@ -106,11 +106,11 @@ def create_repo_and_code_mapping( defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( - project_id=self.project.id, + project=self.project, + repository=repository, stack_root=stack_root, source_root=source_root, default_branch=default_branch, - repository=repository, organization_integration_id=organization_integration.id, integration_id=organization_integration.integration_id, organization_id=organization_integration.organization_id, @@ -184,17 +184,20 @@ def _process_and_assert_configuration_changes( ) for expected_cm in expected_new_code_mappings: code_mapping = current_code_mappings.get( - project_id=self.project.id, + project_repository__project_id=self.project.id, stack_root=expected_cm["stack_root"], source_root=expected_cm["source_root"], ) assert code_mapping is not None - assert code_mapping.repository.name == expected_cm["repo_name"] + assert ( + code_mapping.project_repository.repository.name + == expected_cm["repo_name"] + ) assert code_mapping.project_repository is not None assert code_mapping.project_repository.project_id == self.project.id assert ( code_mapping.project_repository.repository_id - == code_mapping.repository_id + == code_mapping.project_repository.repository_id ) else: assert current_code_mappings.count() == starting_code_mappings_count @@ -970,7 +973,7 @@ def test_prevent_creating_duplicate_rules(self) -> None: ) # Both mappings should coexist: the manual one and the auto-created one mappings = RepositoryProjectPathConfig.objects.filter( - project=self.project, stack_root="foo/bar/" + project_repository__project=self.project, stack_root="foo/bar/" ) assert mappings.count() == 2 assert set(mappings.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/migrations/test_1092_backfill_projectrepository.py b/tests/sentry/migrations/test_1092_backfill_projectrepository.py index e132d040d337a1..e5293b0b396d8d 100644 --- a/tests/sentry/migrations/test_1092_backfill_projectrepository.py +++ b/tests/sentry/migrations/test_1092_backfill_projectrepository.py @@ -53,6 +53,11 @@ def setup_before_migration(self, apps): ) # Case 1: Auto-generated code mapping only → AUTO_EVENT + pr_a = ProjectRepository.objects.create( + project=self.proj, + repository=self.repo_a, + source=ProjectRepositorySource.AUTO_EVENT, + ) RepositoryProjectPathConfig.objects.create( project=self.proj, repository=self.repo_a, @@ -62,9 +67,15 @@ def setup_before_migration(self, apps): stack_root="src/", source_root="src/", automatically_generated=True, + project_repository=pr_a, ) # Case 2: Manual code mapping only → MANUAL + pr_b = ProjectRepository.objects.create( + project=self.proj, + repository=self.repo_b, + source=ProjectRepositorySource.MANUAL, + ) RepositoryProjectPathConfig.objects.create( project=self.proj, repository=self.repo_b, @@ -74,15 +85,29 @@ def setup_before_migration(self, apps): stack_root="lib/", source_root="lib/", automatically_generated=False, + project_repository=pr_b, ) # Case 3: Seer preference only → SEER_PREFERENCE + pr_c = ProjectRepository.objects.create( + project=self.proj, + repository=self.repo_c, + source=ProjectRepositorySource.SEER_PREFERENCE, + ) SeerProjectRepository.objects.create( - project=self.proj, repository=self.repo_c, branch_name="main" + project=self.proj, + repository=self.repo_c, + project_repository=pr_c, + branch_name="main", ) # Case 4: Both manual code mapping AND Seer preference for same # (project, repo) → SEER_PREFERENCE wins (higher priority). + pr_d = ProjectRepository.objects.create( + project=self.proj, + repository=self.repo_d, + source=ProjectRepositorySource.SEER_PREFERENCE, + ) RepositoryProjectPathConfig.objects.create( project=self.proj, repository=self.repo_d, @@ -92,9 +117,13 @@ def setup_before_migration(self, apps): stack_root="app/", source_root="app/", automatically_generated=False, + project_repository=pr_d, ) SeerProjectRepository.objects.create( - project=self.proj, repository=self.repo_d, branch_name="develop" + project=self.proj, + repository=self.repo_d, + project_repository=pr_d, + branch_name="develop", ) # Case 5: Dual-write already created a ProjectRepository row. @@ -114,6 +143,7 @@ def setup_before_migration(self, apps): stack_root="pkg/", source_root="pkg/", automatically_generated=True, + project_repository=self.existing_pr, ) def test(self) -> None: @@ -143,7 +173,7 @@ def get_pr(repo): # All RepositoryProjectPathConfig rows have project_repository_id set assert ( RepositoryProjectPathConfig.objects.filter( - project=self.proj, project_repository_id__isnull=True + project_repository__project=self.proj, project_repository_id__isnull=True ).count() == 0 ) @@ -151,18 +181,20 @@ def get_pr(repo): # All SeerProjectRepository rows have project_repository_id set assert ( SeerProjectRepository.objects.filter( - project=self.proj, project_repository_id__isnull=True + project_repository__project=self.proj, project_repository_id__isnull=True ).count() == 0 ) # FK consistency: each row's project_repository points to the right pair - for config in RepositoryProjectPathConfig.objects.filter(project=self.proj): + for config in RepositoryProjectPathConfig.objects.filter( + project_repository__project=self.proj + ): pr = ProjectRepository.objects.get(id=config.project_repository_id) - assert pr.project_id == config.project_id - assert pr.repository_id == config.repository_id + assert pr.project_id == config.project_repository.project_id + assert pr.repository_id == config.project_repository.repository_id - for spr in SeerProjectRepository.objects.filter(project=self.proj): + for spr in SeerProjectRepository.objects.filter(project_repository__project=self.proj): pr = ProjectRepository.objects.get(id=spr.project_repository_id) - assert pr.project_id == spr.project_id - assert pr.repository_id == spr.repository_id + assert pr.project_id == spr.project_repository.project_id + assert pr.repository_id == spr.project_repository.repository_id diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index ff1fb70f4e2cfd..a07697a78ba709 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -253,8 +253,8 @@ def test_delete_on_transfer_repository_project_path_configs(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) repository_project_path_config = RepositoryProjectPathConfig.objects.create( - repository=repository, project=project, + repository=repository, organization_integration_id=org_integration.id, organization_id=from_org.id, integration_id=integration.id, @@ -275,7 +275,12 @@ def test_delete_on_transfer_repository_project_path_configs(self) -> None: assert RepositoryProjectPathConfig.objects.filter(organization_id=from_org.id).count() == 0 assert RepositoryProjectPathConfig.objects.filter(organization_id=to_org.id).count() == 0 - assert RepositoryProjectPathConfig.objects.filter(project_id=project.id).count() == 0 + assert ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project_id=project.id + ).count() + == 0 + ) assert ProjectCodeOwners.objects.filter(project_id=project.id).count() == 0 diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 3586340d3456df..2c0c97468cd337 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1097,8 +1097,8 @@ def test_has_repo_code_mappings_with_mappings(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( - repository=repo, project=project, + repository=repo, organization_integration_id=org_integration.id, integration_id=org_integration.integration_id, organization_id=self.organization.id,