##// END OF EJS Templates
pull-requests: introduce operation state for pull requests to prevent from...
marcink -
r3371:e7214a9f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_13_0_0 as db
18
19 pull_request = db.PullRequest.__table__
20 pull_request_version = db.PullRequestVersion.__table__
21
22 repo_state_1 = Column("pull_request_state", String(255), nullable=True)
23 repo_state_1.create(table=pull_request)
24
25 repo_state_2 = Column("pull_request_state", String(255), nullable=True)
26 repo_state_2.create(table=pull_request_version)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,41 b''
1 import logging
2
3 from sqlalchemy import *
4
5 from rhodecode.model import meta
6 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
7
8 log = logging.getLogger(__name__)
9
10
11 def upgrade(migrate_engine):
12 """
13 Upgrade operations go here.
14 Don't create your own engine; bind migrate_engine to your metadata
15 """
16 _reset_base(migrate_engine)
17 from rhodecode.lib.dbmigrate.schema import db_4_16_0_0 as db
18
19 fixups(db, meta.Session)
20
21
22 def downgrade(migrate_engine):
23 meta = MetaData()
24 meta.bind = migrate_engine
25
26
27 def fixups(models, _SESSION):
28 # move the builtin token to external tokens
29
30 log.info('Updating pull request pull_request_state to %s',
31 models.PullRequest.STATE_CREATED)
32 qry = _SESSION().query(models.PullRequest)
33 qry.update({"pull_request_state": models.PullRequest.STATE_CREATED})
34 _SESSION().commit()
35
36 log.info('Updating pull_request_version pull_request_state to %s',
37 models.PullRequest.STATE_CREATED)
38 qry = _SESSION().query(models.PullRequestVersion)
39 qry.update({"pull_request_state": models.PullRequest.STATE_CREATED})
40 _SESSION().commit()
41
@@ -18,12 +18,6 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 """
22
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
26
27 21 import os
28 22 import sys
29 23 import platform
@@ -51,7 +45,7 b' PYRAMID_SETTINGS = {}'
51 45 EXTENSIONS = {}
52 46
53 47 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 91 # defines current db version for migrations
48 __dbversion__ = 93 # defines current db version for migrations
55 49 __platform__ = platform.system()
56 50 __license__ = 'AGPLv3, and Commercial License'
57 51 __author__ = 'RhodeCode GmbH'
@@ -65,6 +65,7 b' class TestGetPullRequest(object):'
65 65 'title': pull_request.title,
66 66 'description': pull_request.description,
67 67 'status': pull_request.status,
68 'state': pull_request.pull_request_state,
68 69 'created_on': pull_request.created_on,
69 70 'updated_on': pull_request.updated_on,
70 71 'commit_ids': pull_request.revisions,
@@ -29,11 +29,10 b' from rhodecode.api.tests.utils import ('
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestMergePullRequest(object):
32
32 33 @pytest.mark.backends("git", "hg")
33 34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
34 35 pull_request = pr_util.create_pull_request(mergeable=True)
35 author = pull_request.user_id
36 repo = pull_request.target_repo.repo_id
37 36 pull_request_id = pull_request.pull_request_id
38 37 pull_request_repo = pull_request.target_repo.repo_name
39 38
@@ -46,8 +45,7 b' class TestMergePullRequest(object):'
46 45
47 46 # The above api call detaches the pull request DB object from the
48 47 # session because of an unconditional transaction rollback in our
49 # middleware. Therefore we need to add it back here if we want to use
50 # it.
48 # middleware. Therefore we need to add it back here if we want to use it.
51 49 Session().add(pull_request)
52 50
53 51 expected = 'merge not possible for following reasons: ' \
@@ -55,6 +53,29 b' class TestMergePullRequest(object):'
55 53 assert_error(id_, expected, given=response.body)
56 54
57 55 @pytest.mark.backends("git", "hg")
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 self, pr_util, no_notifications):
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 pull_request_id = pull_request.pull_request_id
60 pull_request_repo = pull_request.target_repo.repo_name
61
62 pr = PullRequest.get(pull_request_id)
63 pr.pull_request_state = pull_request.STATE_UPDATING
64 Session().add(pr)
65 Session().commit()
66
67 id_, params = build_data(
68 self.apikey, 'merge_pull_request',
69 repoid=pull_request_repo,
70 pullrequestid=pull_request_id)
71
72 response = api_call(self.app, params)
73 expected = 'Operation forbidden because pull request is in state {}, '\
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 PullRequest.STATE_CREATED)
76 assert_error(id_, expected, given=response.body)
77
78 @pytest.mark.backends("git", "hg")
58 79 def test_api_merge_pull_request(self, pr_util, no_notifications):
59 80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
60 81 author = pull_request.user_id
@@ -123,8 +144,7 b' class TestMergePullRequest(object):'
123 144 assert_error(id_, expected, given=response.body)
124 145
125 146 @pytest.mark.backends("git", "hg")
126 def test_api_merge_pull_request_non_admin_with_userid_error(self,
127 pr_util):
147 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
128 148 pull_request = pr_util.create_pull_request(mergeable=True)
129 149 id_, params = build_data(
130 150 self.apikey_regular, 'merge_pull_request',
@@ -32,7 +32,7 b' from rhodecode.lib.base import vcs_opera'
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 37 from rhodecode.model.settings import SettingsModel
38 38 from rhodecode.model.validation_schema import Invalid
@@ -128,11 +128,15 b' def get_pull_request(request, apiuser, p'
128 128 else:
129 129 repo = pull_request.target_repo
130 130
131 if not PullRequestModel().check_user_read(
132 pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
133 132 raise JSONRPCError('repository `%s` or pull request `%s` '
134 133 'does not exist' % (repoid, pullrequestid))
135 data = pull_request.get_api_data()
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
136 140 return data
137 141
138 142
@@ -283,8 +287,16 b' def merge_pull_request('
283 287 else:
284 288 raise JSONRPCError('userid is not the same as your user')
285 289
286 check = MergeCheck.validate(
287 pull_request, auth_user=apiuser, translator=request.translate)
290 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
291 raise JSONRPCError(
292 'Operation forbidden because pull request is in state {}, '
293 'only state {} is allowed.'.format(
294 pull_request.pull_request_state, PullRequest.STATE_CREATED))
295
296 with pull_request.set_state(PullRequest.STATE_UPDATING):
297 check = MergeCheck.validate(
298 pull_request, auth_user=apiuser,
299 translator=request.translate)
288 300 merge_possible = not check.failed
289 301
290 302 if not merge_possible:
@@ -302,8 +314,9 b' def merge_pull_request('
302 314 request.environ, repo_name=target_repo.repo_name,
303 315 username=apiuser.username, action='push',
304 316 scm=target_repo.repo_type)
305 merge_response = PullRequestModel().merge_repo(
306 pull_request, apiuser, extras=extras)
317 with pull_request.set_state(PullRequest.STATE_UPDATING):
318 merge_response = PullRequestModel().merge_repo(
319 pull_request, apiuser, extras=extras)
307 320 if merge_response.executed:
308 321 PullRequestModel().close_pull_request(
309 322 pull_request.pull_request_id, apiuser)
@@ -829,11 +842,18 b' def update_pull_request('
829 842
830 843 commit_changes = {"added": [], "common": [], "removed": []}
831 844 if str2bool(Optional.extract(update_commits)):
832 if PullRequestModel().has_valid_update_type(pull_request):
833 update_response = PullRequestModel().update_commits(
834 pull_request)
835 commit_changes = update_response.changes or commit_changes
836 Session().commit()
845
846 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
847 raise JSONRPCError(
848 'Operation forbidden because pull request is in state {}, '
849 'only state {} is allowed.'.format(
850 pull_request.pull_request_state, PullRequest.STATE_CREATED))
851
852 with pull_request.set_state(PullRequest.STATE_UPDATING):
853 if PullRequestModel().has_valid_update_type(pull_request):
854 update_response = PullRequestModel().update_commits(pull_request)
855 commit_changes = update_response.changes or commit_changes
856 Session().commit()
837 857
838 858 reviewers_changes = {"added": [], "removed": []}
839 859 if reviewers:
@@ -655,20 +655,20 b' class TestPullrequestsView(object):'
655 655
656 656 # create pr from a in source to A in target
657 657 pull_request = PullRequest()
658
658 659 pull_request.source_repo = source
659 # TODO: johbo: Make sure that we write the source ref this way!
660 660 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
661 661 branch=backend.default_branch_name, commit_id=commit_ids['change'])
662
662 663 pull_request.target_repo = target
663
664 664 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
665 branch=backend.default_branch_name,
666 commit_id=commit_ids['ancestor'])
665 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
666
667 667 pull_request.revisions = [commit_ids['change']]
668 668 pull_request.title = u"Test"
669 669 pull_request.description = u"Description"
670 pull_request.author = UserModel().get_by_username(
671 TEST_USER_ADMIN_LOGIN)
670 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
671 pull_request.pull_request_state = PullRequest.STATE_CREATED
672 672 Session().add(pull_request)
673 673 Session().commit()
674 674 pull_request_id = pull_request.pull_request_id
@@ -679,23 +679,21 b' class TestPullrequestsView(object):'
679 679 # update PR
680 680 self.app.post(
681 681 route_path('pullrequest_update',
682 repo_name=target.repo_name,
683 pull_request_id=pull_request_id),
684 params={'update_commits': 'true',
685 'csrf_token': csrf_token})
682 repo_name=target.repo_name, pull_request_id=pull_request_id),
683 params={'update_commits': 'true', 'csrf_token': csrf_token})
684
685 response = self.app.get(
686 route_path('pullrequest_show',
687 repo_name=target.repo_name,
688 pull_request_id=pull_request.pull_request_id))
689
690 assert response.status_int == 200
691 assert 'Pull request updated to' in response.body
692 assert 'with 1 added, 0 removed commits.' in response.body
686 693
687 694 # check that we have now both revisions
688 695 pull_request = PullRequest.get(pull_request_id)
689 assert pull_request.revisions == [
690 commit_ids['change-2'], commit_ids['change']]
691
692 # TODO: johbo: this should be a test on its own
693 response = self.app.get(route_path(
694 'pullrequest_new',
695 repo_name=target.repo_name))
696 assert response.status_int == 200
697 assert 'Pull request updated to' in response.body
698 assert 'with 1 added, 0 removed commits.' in response.body
696 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
699 697
700 698 def test_update_target_revision(self, backend, csrf_token):
701 699 commits = [
@@ -710,21 +708,21 b' class TestPullrequestsView(object):'
710 708
711 709 # create pr from a in source to A in target
712 710 pull_request = PullRequest()
711
713 712 pull_request.source_repo = source
714 # TODO: johbo: Make sure that we write the source ref this way!
715 713 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
716 714 branch=backend.default_branch_name, commit_id=commit_ids['change'])
715
717 716 pull_request.target_repo = target
718 # TODO: johbo: Target ref should be branch based, since tip can jump
719 # from branch to branch
720 717 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
721 branch=backend.default_branch_name,
722 commit_id=commit_ids['ancestor'])
718 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
719
723 720 pull_request.revisions = [commit_ids['change']]
724 721 pull_request.title = u"Test"
725 722 pull_request.description = u"Description"
726 pull_request.author = UserModel().get_by_username(
727 TEST_USER_ADMIN_LOGIN)
723 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
724 pull_request.pull_request_state = PullRequest.STATE_CREATED
725
728 726 Session().add(pull_request)
729 727 Session().commit()
730 728 pull_request_id = pull_request.pull_request_id
@@ -737,23 +735,21 b' class TestPullrequestsView(object):'
737 735 # update PR
738 736 self.app.post(
739 737 route_path('pullrequest_update',
740 repo_name=target.repo_name,
741 pull_request_id=pull_request_id),
742 params={'update_commits': 'true',
743 'csrf_token': csrf_token},
738 repo_name=target.repo_name,
739 pull_request_id=pull_request_id),
740 params={'update_commits': 'true', 'csrf_token': csrf_token},
744 741 status=200)
745 742
746 743 # check that we have now both revisions
747 744 pull_request = PullRequest.get(pull_request_id)
748 745 assert pull_request.revisions == [commit_ids['change-rebased']]
749 746 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
750 branch=backend.default_branch_name,
751 commit_id=commit_ids['ancestor-new'])
747 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
752 748
753 # TODO: johbo: This should be a test on its own
754 response = self.app.get(route_path(
755 'pullrequest_new',
756 repo_name=target.repo_name))
749 response = self.app.get(
750 route_path('pullrequest_show',
751 repo_name=target.repo_name,
752 pull_request_id=pull_request.pull_request_id))
757 753 assert response.status_int == 200
758 754 assert 'Pull request updated to' in response.body
759 755 assert 'with 1 added, 1 removed commits.' in response.body
@@ -775,17 +771,14 b' class TestPullrequestsView(object):'
775 771 # create pr from a in source to A in target
776 772 pull_request = PullRequest()
777 773 pull_request.source_repo = source
778 # TODO: johbo: Make sure that we write the source ref this way!
774
779 775 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
780 776 branch=backend.default_branch_name,
781 777 commit_id=commit_ids['master-commit-3-change-2'])
782 778
783 779 pull_request.target_repo = target
784 # TODO: johbo: Target ref should be branch based, since tip can jump
785 # from branch to branch
786 780 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
787 branch=backend.default_branch_name,
788 commit_id=commit_ids['feat-commit-2'])
781 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
789 782
790 783 pull_request.revisions = [
791 784 commit_ids['feat-commit-1'],
@@ -793,8 +786,8 b' class TestPullrequestsView(object):'
793 786 ]
794 787 pull_request.title = u"Test"
795 788 pull_request.description = u"Description"
796 pull_request.author = UserModel().get_by_username(
797 TEST_USER_ADMIN_LOGIN)
789 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
790 pull_request.pull_request_state = PullRequest.STATE_CREATED
798 791 Session().add(pull_request)
799 792 Session().commit()
800 793 pull_request_id = pull_request.pull_request_id
@@ -810,13 +803,10 b' class TestPullrequestsView(object):'
810 803 route_path('pullrequest_update',
811 804 repo_name=target.repo_name,
812 805 pull_request_id=pull_request_id),
813 params={'update_commits': 'true',
814 'csrf_token': csrf_token},
806 params={'update_commits': 'true', 'csrf_token': csrf_token},
815 807 status=200)
816 808
817 response = self.app.get(route_path(
818 'pullrequest_new',
819 repo_name=target.repo_name))
809 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
820 810 assert response.status_int == 200
821 811 response.mustcontain('Pull request updated to')
822 812 response.mustcontain('with 0 added, 0 removed commits.')
@@ -836,21 +826,17 b' class TestPullrequestsView(object):'
836 826 # create pr from a in source to A in target
837 827 pull_request = PullRequest()
838 828 pull_request.source_repo = source
839 # TODO: johbo: Make sure that we write the source ref this way!
829
840 830 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
841 branch=backend.default_branch_name,
842 commit_id=commit_ids['change'])
831 branch=backend.default_branch_name, commit_id=commit_ids['change'])
843 832 pull_request.target_repo = target
844 # TODO: johbo: Target ref should be branch based, since tip can jump
845 # from branch to branch
846 833 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
847 branch=backend.default_branch_name,
848 commit_id=commit_ids['ancestor'])
834 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
849 835 pull_request.revisions = [commit_ids['change']]
850 836 pull_request.title = u"Test"
851 837 pull_request.description = u"Description"
852 pull_request.author = UserModel().get_by_username(
853 TEST_USER_ADMIN_LOGIN)
838 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
839 pull_request.pull_request_state = PullRequest.STATE_CREATED
854 840 Session().add(pull_request)
855 841 Session().commit()
856 842 pull_request_id = pull_request.pull_request_id
@@ -863,10 +849,8 b' class TestPullrequestsView(object):'
863 849 # update PR
864 850 self.app.post(
865 851 route_path('pullrequest_update',
866 repo_name=target.repo_name,
867 pull_request_id=pull_request_id),
868 params={'update_commits': 'true',
869 'csrf_token': csrf_token},
852 repo_name=target.repo_name, pull_request_id=pull_request_id),
853 params={'update_commits': 'true', 'csrf_token': csrf_token},
870 854 status=200)
871 855
872 856 # Expect the target reference to be updated correctly
@@ -893,13 +877,12 b' class TestPullrequestsView(object):'
893 877 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
894 878 branch=branch_name, commit_id=commit_ids['new-feature'])
895 879 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
896 branch=backend_git.default_branch_name,
897 commit_id=commit_ids['old-feature'])
880 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
898 881 pull_request.revisions = [commit_ids['new-feature']]
899 882 pull_request.title = u"Test"
900 883 pull_request.description = u"Description"
901 pull_request.author = UserModel().get_by_username(
902 TEST_USER_ADMIN_LOGIN)
884 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
885 pull_request.pull_request_state = PullRequest.STATE_CREATED
903 886 Session().add(pull_request)
904 887 Session().commit()
905 888
@@ -273,9 +273,22 b' class RepoPullRequestsView(RepoAppView, '
273 273 route_name='pullrequest_show', request_method='GET',
274 274 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
275 275 def pull_request_show(self):
276 pull_request_id = self.request.matchdict['pull_request_id']
276 _ = self.request.translate
277 c = self.load_default_context()
278
279 pull_request = PullRequest.get_or_404(
280 self.request.matchdict['pull_request_id'])
281 pull_request_id = pull_request.pull_request_id
277 282
278 c = self.load_default_context()
283 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
284 log.debug('show: forbidden because pull request is in state %s',
285 pull_request.pull_request_state)
286 msg = _(u'Cannot show pull requests in state other than `{}`. '
287 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
288 pull_request.pull_request_state)
289 h.flash(msg, category='error')
290 raise HTTPFound(h.route_path('pullrequest_show_all',
291 repo_name=self.db_repo_name))
279 292
280 293 version = self.request.GET.get('version')
281 294 from_version = self.request.GET.get('from_version') or version
@@ -919,12 +932,17 b' class RepoPullRequestsView(RepoAppView, '
919 932 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
920 933 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
921 934
935 if not (source_db_repo or target_db_repo):
936 h.flash(_('source_repo or target repo not found'), category='error')
937 raise HTTPFound(
938 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
939
922 940 # re-check permissions again here
923 941 # source_repo we must have read permissions
924 942
925 943 source_perm = HasRepoPermissionAny(
926 'repository.read',
927 'repository.write', 'repository.admin')(source_db_repo.repo_name)
944 'repository.read', 'repository.write', 'repository.admin')(
945 source_db_repo.repo_name)
928 946 if not source_perm:
929 947 msg = _('Not Enough permissions to source repo `{}`.'.format(
930 948 source_db_repo.repo_name))
@@ -938,8 +956,8 b' class RepoPullRequestsView(RepoAppView, '
938 956 # target repo we must have read permissions, and also later on
939 957 # we want to check branch permissions here
940 958 target_perm = HasRepoPermissionAny(
941 'repository.read',
942 'repository.write', 'repository.admin')(target_db_repo.repo_name)
959 'repository.read', 'repository.write', 'repository.admin')(
960 target_db_repo.repo_name)
943 961 if not target_perm:
944 962 msg = _('Not Enough permissions to target repo `{}`.'.format(
945 963 target_db_repo.repo_name))
@@ -1042,6 +1060,15 b' class RepoPullRequestsView(RepoAppView, '
1042 1060 h.flash(msg, category='error')
1043 1061 return True
1044 1062
1063 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1064 log.debug('update: forbidden because pull request is in state %s',
1065 pull_request.pull_request_state)
1066 msg = _(u'Cannot update pull requests in state other than `{}`. '
1067 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1068 pull_request.pull_request_state)
1069 h.flash(msg, category='error')
1070 return True
1071
1045 1072 # only owner or admin can update it
1046 1073 allowed_to_update = PullRequestModel().check_user_update(
1047 1074 pull_request, self._rhodecode_user)
@@ -1084,7 +1111,9 b' class RepoPullRequestsView(RepoAppView, '
1084 1111
1085 1112 def _update_commits(self, pull_request):
1086 1113 _ = self.request.translate
1087 resp = PullRequestModel().update_commits(pull_request)
1114
1115 with pull_request.set_state(PullRequest.STATE_UPDATING):
1116 resp = PullRequestModel().update_commits(pull_request)
1088 1117
1089 1118 if resp.executed:
1090 1119
@@ -1097,10 +1126,9 b' class RepoPullRequestsView(RepoAppView, '
1097 1126 else:
1098 1127 changed = 'nothing'
1099 1128
1100 msg = _(
1101 u'Pull request updated to "{source_commit_id}" with '
1102 u'{count_added} added, {count_removed} removed commits. '
1103 u'Source of changes: {change_source}')
1129 msg = _(u'Pull request updated to "{source_commit_id}" with '
1130 u'{count_added} added, {count_removed} removed commits. '
1131 u'Source of changes: {change_source}')
1104 1132 msg = msg.format(
1105 1133 source_commit_id=pull_request.source_ref_parts.commit_id,
1106 1134 count_added=len(resp.changes.added),
@@ -1109,8 +1137,7 b' class RepoPullRequestsView(RepoAppView, '
1109 1137 h.flash(msg, category='success')
1110 1138
1111 1139 channel = '/repo${}$/pr/{}'.format(
1112 pull_request.target_repo.repo_name,
1113 pull_request.pull_request_id)
1140 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1114 1141 message = msg + (
1115 1142 ' - <a onclick="window.location.reload()">'
1116 1143 '<strong>{}</strong></a>'.format(_('Reload page')))
@@ -1143,11 +1170,26 b' class RepoPullRequestsView(RepoAppView, '
1143 1170 """
1144 1171 pull_request = PullRequest.get_or_404(
1145 1172 self.request.matchdict['pull_request_id'])
1173 _ = self.request.translate
1174
1175 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1176 log.debug('show: forbidden because pull request is in state %s',
1177 pull_request.pull_request_state)
1178 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1179 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1180 pull_request.pull_request_state)
1181 h.flash(msg, category='error')
1182 raise HTTPFound(
1183 h.route_path('pullrequest_show',
1184 repo_name=pull_request.target_repo.repo_name,
1185 pull_request_id=pull_request.pull_request_id))
1146 1186
1147 1187 self.load_default_context()
1148 check = MergeCheck.validate(
1149 pull_request, auth_user=self._rhodecode_user,
1150 translator=self.request.translate)
1188
1189 with pull_request.set_state(PullRequest.STATE_UPDATING):
1190 check = MergeCheck.validate(
1191 pull_request, auth_user=self._rhodecode_user,
1192 translator=self.request.translate)
1151 1193 merge_possible = not check.failed
1152 1194
1153 1195 for err_type, error_msg in check.errors:
@@ -1159,8 +1201,9 b' class RepoPullRequestsView(RepoAppView, '
1159 1201 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1160 1202 username=self._rhodecode_db_user.username, action='push',
1161 1203 scm=pull_request.target_repo.repo_type)
1162 self._merge_pull_request(
1163 pull_request, self._rhodecode_db_user, extras)
1204 with pull_request.set_state(PullRequest.STATE_UPDATING):
1205 self._merge_pull_request(
1206 pull_request, self._rhodecode_db_user, extras)
1164 1207 else:
1165 1208 log.debug("Pre-conditions failed, NOT merging.")
1166 1209
@@ -3538,6 +3538,32 b' class ChangesetStatus(Base, BaseModel):'
3538 3538 return data
3539 3539
3540 3540
3541 class _SetState(object):
3542 """
3543 Context processor allowing changing state for sensitive operation such as
3544 pull request update or merge
3545 """
3546
3547 def __init__(self, pull_request, pr_state, back_state=None):
3548 self._pr = pull_request
3549 self._org_state = back_state or pull_request.pull_request_state
3550 self._pr_state = pr_state
3551
3552 def __enter__(self):
3553 log.debug('StateLock: entering set state context, setting state to: `%s`',
3554 self._pr_state)
3555 self._pr.pull_request_state = self._pr_state
3556 Session().add(self._pr)
3557 Session().commit()
3558
3559 def __exit__(self, exc_type, exc_val, exc_tb):
3560 log.debug('StateLock: exiting set state context, setting state to: `%s`',
3561 self._org_state)
3562 self._pr.pull_request_state = self._org_state
3563 Session().add(self._pr)
3564 Session().commit()
3565
3566
3541 3567 class _PullRequestBase(BaseModel):
3542 3568 """
3543 3569 Common attributes of pull request and version entries.
@@ -3548,6 +3574,12 b' class _PullRequestBase(BaseModel):'
3548 3574 STATUS_OPEN = u'open'
3549 3575 STATUS_CLOSED = u'closed'
3550 3576
3577 # available states
3578 STATE_CREATING = u'creating'
3579 STATE_UPDATING = u'updating'
3580 STATE_MERGING = u'merging'
3581 STATE_CREATED = u'created'
3582
3551 3583 title = Column('title', Unicode(255), nullable=True)
3552 3584 description = Column(
3553 3585 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
@@ -3563,6 +3595,8 b' class _PullRequestBase(BaseModel):'
3563 3595 'updated_on', DateTime(timezone=False), nullable=False,
3564 3596 default=datetime.datetime.now)
3565 3597
3598 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3599
3566 3600 @declared_attr
3567 3601 def user_id(cls):
3568 3602 return Column(
@@ -3737,6 +3771,7 b' class _PullRequestBase(BaseModel):'
3737 3771 'title': pull_request.title,
3738 3772 'description': pull_request.description,
3739 3773 'status': pull_request.status,
3774 'state': pull_request.pull_request_state,
3740 3775 'created_on': pull_request.created_on,
3741 3776 'updated_on': pull_request.updated_on,
3742 3777 'commit_ids': pull_request.revisions,
@@ -3777,6 +3812,20 b' class _PullRequestBase(BaseModel):'
3777 3812
3778 3813 return data
3779 3814
3815 def set_state(self, pull_request_state, final_state=None):
3816 """
3817 # goes from initial state to updating to initial state.
3818 # initial state can be changed by specifying back_state=
3819 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
3820 pull_request.merge()
3821
3822 :param pull_request_state:
3823 :param final_state:
3824
3825 """
3826
3827 return _SetState(self, pull_request_state, back_state=final_state)
3828
3780 3829
3781 3830 class PullRequest(Base, _PullRequestBase):
3782 3831 __tablename__ = 'pull_requests'
@@ -138,7 +138,7 b' class PullRequestModel(BaseModel):'
138 138
139 139 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 140 opened_by=None, order_by=None,
141 order_dir='desc'):
141 order_dir='desc', only_created=True):
142 142 repo = None
143 143 if repo_name:
144 144 repo = self._get_repo(repo_name)
@@ -159,6 +159,10 b' class PullRequestModel(BaseModel):'
159 159 if opened_by:
160 160 q = q.filter(PullRequest.user_id.in_(opened_by))
161 161
162 # only get those that are in "created" state
163 if only_created:
164 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165
162 166 if order_by:
163 167 order_map = {
164 168 'name_raw': PullRequest.pull_request_id,
@@ -429,7 +433,7 b' class PullRequestModel(BaseModel):'
429 433 pull_request.description_renderer = description_renderer
430 434 pull_request.author = created_by_user
431 435 pull_request.reviewer_data = reviewer_data
432
436 pull_request.pull_request_state = pull_request.STATE_CREATING
433 437 Session().add(pull_request)
434 438 Session().flush()
435 439
@@ -497,9 +501,16 b' class PullRequestModel(BaseModel):'
497 501 # that for large repos could be long resulting in long row locks
498 502 Session().commit()
499 503
500 # prepare workspace, and run initial merge simulation
501 MergeCheck.validate(
502 pull_request, auth_user=auth_user, translator=translator)
504 # prepare workspace, and run initial merge simulation. Set state during that
505 # operation
506 pull_request = PullRequest.get(pull_request.pull_request_id)
507
508 # set as merging, for simulation, and if finished to created so we mark
509 # simulation is working fine
510 with pull_request.set_state(PullRequest.STATE_MERGING,
511 final_state=PullRequest.STATE_CREATED):
512 MergeCheck.validate(
513 pull_request, auth_user=auth_user, translator=translator)
503 514
504 515 self.notify_reviewers(pull_request, reviewer_ids)
505 516 self._trigger_pull_request_hook(
@@ -653,9 +664,8 b' class PullRequestModel(BaseModel):'
653 664 target_ref_id = pull_request.target_ref_parts.commit_id
654 665
655 666 if not self.has_valid_update_type(pull_request):
656 log.debug(
657 "Skipping update of pull request %s due to ref type: %s",
658 pull_request, source_ref_type)
667 log.debug("Skipping update of pull request %s due to ref type: %s",
668 pull_request, source_ref_type)
659 669 return UpdateResponse(
660 670 executed=False,
661 671 reason=UpdateFailureReason.WRONG_REF_TYPE,
@@ -801,8 +811,7 b' class PullRequestModel(BaseModel):'
801 811 pull_request.source_ref_parts.commit_id,
802 812 pull_request_version.pull_request_version_id)
803 813 Session().commit()
804 self._trigger_pull_request_hook(
805 pull_request, pull_request.author, 'update')
814 self._trigger_pull_request_hook(pull_request, pull_request.author, 'update')
806 815
807 816 return UpdateResponse(
808 817 executed=True, reason=UpdateFailureReason.NONE,
@@ -814,6 +823,7 b' class PullRequestModel(BaseModel):'
814 823 version.title = pull_request.title
815 824 version.description = pull_request.description
816 825 version.status = pull_request.status
826 version.pull_request_state = pull_request.pull_request_state
817 827 version.created_on = datetime.datetime.now()
818 828 version.updated_on = pull_request.updated_on
819 829 version.user_id = pull_request.user_id
@@ -1614,8 +1624,7 b' class MergeCheck(object):'
1614 1624
1615 1625 msg = _('Pull request reviewer approval is pending.')
1616 1626
1617 merge_check.push_error(
1618 'warning', msg, cls.REVIEW_CHECK, review_status)
1627 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1619 1628
1620 1629 if fail_early:
1621 1630 return merge_check
@@ -1624,7 +1633,7 b' class MergeCheck(object):'
1624 1633 todos = CommentsModel().get_unresolved_todos(pull_request)
1625 1634 if todos:
1626 1635 log.debug("MergeCheck: cannot merge, {} "
1627 "unresolved todos left.".format(len(todos)))
1636 "unresolved TODOs left.".format(len(todos)))
1628 1637
1629 1638 if len(todos) == 1:
1630 1639 msg = _('Cannot merge, {} TODO still not resolved.').format(
@@ -1645,8 +1654,7 b' class MergeCheck(object):'
1645 1654 merge_check.merge_possible = merge_status
1646 1655 merge_check.merge_msg = msg
1647 1656 if not merge_status:
1648 log.debug(
1649 "MergeCheck: cannot merge, pull request merge not possible.")
1657 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1650 1658 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1651 1659
1652 1660 if fail_early:
@@ -1677,6 +1685,7 b' class MergeCheck(object):'
1677 1685 close_branch = model._close_branch_before_merging(pull_request)
1678 1686 if close_branch:
1679 1687 repo_type = pull_request.target_repo.repo_type
1688 close_msg = ''
1680 1689 if repo_type == 'hg':
1681 1690 close_msg = _('Source branch will be closed after merge.')
1682 1691 elif repo_type == 'git':
@@ -1689,6 +1698,7 b' class MergeCheck(object):'
1689 1698
1690 1699 return merge_details
1691 1700
1701
1692 1702 ChangeTuple = collections.namedtuple(
1693 1703 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1694 1704
@@ -257,8 +257,7 b' class TestPullRequestModel(object):'
257 257 def has_largefiles(self, repo):
258 258 return repo == pull_request.source_repo
259 259
260 patcher = mock.patch.object(
261 PullRequestModel, '_has_largefiles', has_largefiles)
260 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
262 261 with patcher:
263 262 status, msg = PullRequestModel().merge_status(pull_request)
264 263
@@ -270,8 +269,7 b' class TestPullRequestModel(object):'
270 269 def has_largefiles(self, repo):
271 270 return repo == pull_request.target_repo
272 271
273 patcher = mock.patch.object(
274 PullRequestModel, '_has_largefiles', has_largefiles)
272 patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles)
275 273 with patcher:
276 274 status, msg = PullRequestModel().merge_status(pull_request)
277 275
@@ -314,9 +312,50 b' class TestPullRequestModel(object):'
314 312 self.pull_request, self.pull_request.author, 'merge')
315 313
316 314 pull_request = PullRequest.get(pull_request.pull_request_id)
317 assert (
318 pull_request.merge_rev ==
319 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
315 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
316
317 def test_merge_with_status_lock(self, pull_request, merge_extras):
318 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
319 merge_ref = Reference(
320 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
321 self.merge_mock.return_value = MergeResponse(
322 True, True, merge_ref, MergeFailureReason.NONE)
323
324 merge_extras['repository'] = pull_request.target_repo.repo_name
325
326 with pull_request.set_state(PullRequest.STATE_UPDATING):
327 assert pull_request.pull_request_state == PullRequest.STATE_UPDATING
328 PullRequestModel().merge_repo(
329 pull_request, pull_request.author, extras=merge_extras)
330
331 assert pull_request.pull_request_state == PullRequest.STATE_CREATED
332
333 message = (
334 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
335 u'\n\n {pr_title}'.format(
336 pr_id=pull_request.pull_request_id,
337 source_repo=safe_unicode(
338 pull_request.source_repo.scm_instance().name),
339 source_ref_name=pull_request.source_ref_parts.name,
340 pr_title=safe_unicode(pull_request.title)
341 )
342 )
343 self.merge_mock.assert_called_with(
344 self.repo_id, self.workspace_id,
345 pull_request.target_ref_parts,
346 pull_request.source_repo.scm_instance(),
347 pull_request.source_ref_parts,
348 user_name=user.short_contact, user_email=user.email, message=message,
349 use_rebase=False, close_branch=False
350 )
351 self.invalidation_mock.assert_called_once_with(
352 pull_request.target_repo.repo_name)
353
354 self.hook_mock.assert_called_with(
355 self.pull_request, self.pull_request.author, 'merge')
356
357 pull_request = PullRequest.get(pull_request.pull_request_id)
358 assert pull_request.merge_rev == '6126b7bfcc82ad2d3deaee22af926b082ce54cc6'
320 359
321 360 def test_merge_failed(self, pull_request, merge_extras):
322 361 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
General Comments 0
You need to be logged in to leave comments. Login now