diff --git a/src/sentry/api/endpoints/organization_release_details.py b/src/sentry/api/endpoints/organization_release_details.py index c041bb0699b714..23d0cabc05185c 100644 --- a/src/sentry/api/endpoints/organization_release_details.py +++ b/src/sentry/api/endpoints/organization_release_details.py @@ -9,7 +9,8 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.api.serializers.rest_framework import ( - CommitSerializer, ListField, ReleaseHeadCommitSerializer + CommitSerializer, ListField, ReleaseHeadCommitSerializerDeprecated, + ReleaseHeadCommitSerializer, ) from sentry.models import Activity, Group, Release, ReleaseFile from sentry.utils.apidocs import scenario, attach_scenarios @@ -47,7 +48,8 @@ class ReleaseSerializer(serializers.Serializer): dateStarted = serializers.DateTimeField(required=False) dateReleased = serializers.DateTimeField(required=False) commits = ListField(child=CommitSerializer(), required=False) - headCommits = ListField(child=ReleaseHeadCommitSerializer(), required=False) + headCommits = ListField(child=ReleaseHeadCommitSerializerDeprecated(), required=False) + refs = ListField(child=ReleaseHeadCommitSerializer(), required=False) class OrganizationReleaseDetailsEndpoint(OrganizationReleasesBaseEndpoint): @@ -101,6 +103,17 @@ def put(self, request, organization, version): :param datetime dateReleased: an optional date that indicates when the release went live. If not provided the current time is assumed. + :param array commits: an optional list of commit data to be associated + with the release. Commits must include parameters + ``id`` (the sha of the commit), and can optionally + include ``repository``, ``message``, ``author_name``, + ``author_email``, and ``timestamp``. + :param array refs: an optional way to indicate the start and end commits + for each repository included in a release. Head commits + must include parameters ``repository`` and ``commit`` + (the HEAD sha). They can optionally include ``previousCommit`` + (the sha of the HEAD of the previous release), which should + be specified if this is the first time you've sent commit data. :auth: required """ try: @@ -141,10 +154,16 @@ def put(self, request, organization, version): # TODO(dcramer): handle errors with release payloads release.set_commits(commit_list) - head_commits = result.get('headCommits') - if head_commits: + refs = result.get('refs') + if not refs: + refs = [{ + 'repository': r['repository'], + 'previousCommit': r.get('previousId'), + 'commit': r['currentId'], + } for r in result.get('headCommits', [])] + if refs: fetch_commits = request.user.is_authenticated() and not commit_list - release.set_head_commits(head_commits, request.user, fetch_commits=fetch_commits) + release.set_refs(refs, request.user, fetch_commits=fetch_commits) if (not was_released and release.date_released): for project in release.projects.all(): diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index 27cf5b8de0543e..c9a2863c3bbc42 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -9,7 +9,9 @@ from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize -from sentry.api.serializers.rest_framework import ReleaseHeadCommitSerializer, ListField +from sentry.api.serializers.rest_framework import ( + ReleaseHeadCommitSerializer, ReleaseHeadCommitSerializerDeprecated, ListField +) from sentry.models import Activity, Release from sentry.utils.apidocs import scenario, attach_scenarios @@ -37,7 +39,8 @@ def list_org_releases_scenario(runner): class ReleaseSerializerWithProjects(ReleaseSerializer): projects = ListField() - headCommits = ListField(child=ReleaseHeadCommitSerializer(), required=False) + headCommits = ListField(child=ReleaseHeadCommitSerializerDeprecated(), required=False) + refs = ListField(child=ReleaseHeadCommitSerializer(), required=False) class OrganizationReleasesEndpoint(OrganizationReleasesBaseEndpoint): @@ -111,12 +114,12 @@ def post(self, request, organization): ``id`` (the sha of the commit), and can optionally include ``repository``, ``message``, ``author_name``, ``author_email``, and ``timestamp``. - :param array headCommits: an optional way to indicate the start and end commits - for each repository included in a release. Head commits - must include parameters ``repository`` and ``currentId`` - (the HEAD sha). They can optionally include ``previousId`` - (the sha of the HEAD of the previous release), which should - be specified if this is the first time you've sent commit data. + :param array refs: an optional way to indicate the start and end commits + for each repository included in a release. Head commits + must include parameters ``repository`` and ``commit`` + (the HEAD sha). They can optionally include ``previousCommit`` + (the sha of the HEAD of the previous release), which should + be specified if this is the first time you've sent commit data. :auth: required """ serializer = ReleaseSerializerWithProjects(data=request.DATA) @@ -173,10 +176,16 @@ def post(self, request, organization): if commit_list: release.set_commits(commit_list) - head_commits = result.get('headCommits') - if head_commits: + refs = result.get('refs') + if not refs: + refs = [{ + 'repository': r['repository'], + 'previousCommit': r.get('previousId'), + 'commit': r['currentId'], + } for r in result.get('headCommits', [])] + if refs: fetch_commits = request.user.is_authenticated() and not commit_list - release.set_head_commits(head_commits, request.user, fetch_commits=fetch_commits) + release.set_refs(refs, request.user, fetch_commits=fetch_commits) if not created and not new_projects: # This is the closest status code that makes sense, and we want diff --git a/src/sentry/api/serializers/rest_framework/release_head_commit.py b/src/sentry/api/serializers/rest_framework/release_head_commit.py index 32bc4e860a2a05..26c436b2a48f36 100644 --- a/src/sentry/api/serializers/rest_framework/release_head_commit.py +++ b/src/sentry/api/serializers/rest_framework/release_head_commit.py @@ -3,7 +3,13 @@ from rest_framework import serializers -class ReleaseHeadCommitSerializer(serializers.Serializer): +class ReleaseHeadCommitSerializerDeprecated(serializers.Serializer): currentId = serializers.CharField(max_length=64) repository = serializers.CharField(max_length=64) previousId = serializers.CharField(max_length=64, required=False) + + +class ReleaseHeadCommitSerializer(serializers.Serializer): + commit = serializers.CharField(max_length=64) + repository = serializers.CharField(max_length=64) + previousCommit = serializers.CharField(max_length=64, required=False) diff --git a/src/sentry/models/release.py b/src/sentry/models/release.py index de936c686ca24b..a6c84bd0e0cad1 100644 --- a/src/sentry/models/release.py +++ b/src/sentry/models/release.py @@ -219,7 +219,7 @@ def add_project(self, project): else: return True - def set_head_commits(self, head_commits, user, fetch_commits=False): + def set_refs(self, refs, user, fetch_commits=False): from sentry.models import Commit, ReleaseHeadCommit, Repository from sentry.plugins import bindings @@ -230,11 +230,11 @@ def set_head_commits(self, head_commits, user, fetch_commits=False): commit_list = [] - for head_commit in head_commits: + for ref in refs: try: repo = Repository.objects.get( organization_id=self.organization_id, - name=head_commit['repository'], + name=ref['repository'], ) except Repository.DoesNotExist: continue @@ -242,7 +242,7 @@ def set_head_commits(self, head_commits, user, fetch_commits=False): commit = Commit.objects.get_or_create( organization_id=self.organization_id, repository_id=repo.id, - key=head_commit['currentId'], + key=ref['commit'], )[0] # update head commit for repo/release if exists ReleaseHeadCommit.objects.create_or_update( @@ -261,8 +261,8 @@ def set_head_commits(self, head_commits, user, fetch_commits=False): # if previous commit isn't provided, try to get from # previous release otherwise, give up - if head_commit.get('previousId'): - start_sha = head_commit['previousId'] + if ref.get('previousCommit'): + start_sha = ref['previousCommit'] elif prev_release: try: start_sha = Commit.objects.filter( diff --git a/tests/sentry/api/endpoints/test_organization_release_details.py b/tests/sentry/api/endpoints/test_organization_release_details.py index 6f8f628a8f5166..bb4ad41b4ae79a 100644 --- a/tests/sentry/api/endpoints/test_organization_release_details.py +++ b/tests/sentry/api/endpoints/test_organization_release_details.py @@ -132,6 +132,90 @@ def test_simple(self): self.login_as(user=user) + url = reverse('sentry-api-0-organization-release-details', kwargs={ + 'organization_slug': org.slug, + 'version': release.version, + }) + response = self.client.put(url, { + 'ref': 'master', + 'refs': [ + {'commit': 'a' * 40, 'repository': repo.name}, + {'commit': 'b' * 40, 'repository': repo2.name}, + ], + }) + + assert response.status_code == 200, response.content + assert response.data['version'] == release.version + assert ReleaseCommit.objects.filter( + commit__repository_id=repo.id, + commit__key='62de626b7c7cfb8e77efb4273b1a3df4123e6216', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo.id, + commit__key='58de626b7c7cfb8e77efb4273b1a3df4123e6345', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo2.id, + commit__key='62de626b7c7cfb8e77efb4273b1a3df4123e6216', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo2.id, + commit__key='58de626b7c7cfb8e77efb4273b1a3df4123e6345', + release__version=response.data['version'], + ).exists() + + release = Release.objects.get(id=release.id) + assert release.ref == 'master' + + # no access + url = reverse('sentry-api-0-organization-release-details', kwargs={ + 'organization_slug': org.slug, + 'version': release2.version, + }) + response = self.client.put(url, {'ref': 'master'}) + assert response.status_code == 403 + + def test_deprecated_head_commits(self): + user = self.create_user(is_staff=False, is_superuser=False) + org = self.organization + org.flags.allow_joinleave = False + org.save() + + repo = Repository.objects.create( + organization_id=org.id, + name='example/example', + provider='dummy', + ) + repo2 = Repository.objects.create( + organization_id=org.id, + name='example/example2', + provider='dummy', + ) + + team1 = self.create_team(organization=org) + team2 = self.create_team(organization=org) + + project = self.create_project(team=team1, organization=org) + project2 = self.create_project(team=team2, organization=org) + + release = Release.objects.create( + organization_id=org.id, + version='abcabcabc', + ) + release2 = Release.objects.create( + organization_id=org.id, + version='12345678', + ) + release.add_project(project) + release2.add_project(project2) + + self.create_member(teams=[team1], user=user, organization=org) + + self.login_as(user=user) + url = reverse('sentry-api-0-organization-release-details', kwargs={ 'organization_slug': org.slug, 'version': release.version, diff --git a/tests/sentry/api/endpoints/test_organization_releases.py b/tests/sentry/api/endpoints/test_organization_releases.py index 4e5d8bb3a3a6df..73cf83228bd0da 100644 --- a/tests/sentry/api/endpoints/test_organization_releases.py +++ b/tests/sentry/api/endpoints/test_organization_releases.py @@ -464,6 +464,67 @@ def test_commits_from_provider(self): self.create_member(teams=[team], user=user, organization=org) self.login_as(user=user) + url = reverse('sentry-api-0-organization-releases', kwargs={ + 'organization_slug': org.slug + }) + response = self.client.post(url, data={ + 'version': '1.2.1', + 'refs': [ + {'commit': 'a' * 40, 'repository': repo.name}, + {'commit': 'b' * 40, 'repository': repo2.name}, + ], + 'projects': [project.slug] + }) + assert response.status_code == 201 + # check fake commits from dummy repo provider were created + assert ReleaseCommit.objects.filter( + commit__repository_id=repo.id, + commit__key='62de626b7c7cfb8e77efb4273b1a3df4123e6216', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo.id, + commit__key='58de626b7c7cfb8e77efb4273b1a3df4123e6345', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo2.id, + commit__key='62de626b7c7cfb8e77efb4273b1a3df4123e6216', + release__version=response.data['version'], + ).exists() + assert ReleaseCommit.objects.filter( + commit__repository_id=repo2.id, + commit__key='58de626b7c7cfb8e77efb4273b1a3df4123e6345', + release__version=response.data['version'], + ).exists() + + def test_commits_from_provider_deprecated_head_commits(self): + user = self.create_user(is_staff=False, is_superuser=False) + org = self.create_organization() + org.flags.allow_joinleave = False + org.save() + + repo = Repository.objects.create( + organization_id=org.id, + name='example/example', + provider='dummy', + ) + repo2 = Repository.objects.create( + organization_id=org.id, + name='example/example2', + provider='dummy', + ) + + team = self.create_team(organization=org) + project = self.create_project( + name='foo', + organization=org, + team=team + ) + + self.create_member(teams=[team], user=user, organization=org) + self.login_as(user=user) + url = reverse('sentry-api-0-organization-releases', kwargs={ 'organization_slug': org.slug }) @@ -674,7 +735,7 @@ def test_api_token(self): response = self.client.post(url, data={ 'version': '1.2.1', 'headCommits': [ - {'currentId': 'a' * 40, 'repository': repo.name}, + {'currentId': 'a' * 40, 'repository': repo.name, 'previousId': 'c' * 40}, {'currentId': 'b' * 40, 'repository': repo2.name}, ], 'projects': [project1.slug]