##// END OF EJS Templates
pull-requests: ensure merge response provide more details...
dan -
r3339:8c7a75f7 default
parent child Browse files
Show More
@@ -313,7 +313,7 b' def merge_pull_request('
313 # In previous versions the merge response directly contained the merge
313 # In previous versions the merge response directly contained the merge
314 # commit id. It is now contained in the merge reference object. To be
314 # commit id. It is now contained in the merge reference object. To be
315 # backwards compatible we have to extract it again.
315 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
316 merge_response = merge_response.asdict()
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318
318
319 return merge_response
319 return merge_response
@@ -32,7 +32,6 b' from rhodecode.model.pull_request import'
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
36
35
37
36
38 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
@@ -233,8 +232,7 b' class TestPullrequestsView(object):'
233 route_path('pullrequest_update',
232 route_path('pullrequest_update',
234 repo_name=pull_request.target_repo.repo_name,
233 repo_name=pull_request.target_repo.repo_name,
235 pull_request_id=pull_request_id),
234 pull_request_id=pull_request_id),
236 params={'update_commits': 'true',
235 params={'update_commits': 'true', 'csrf_token': csrf_token})
237 'csrf_token': csrf_token})
238
236
239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
237 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 UpdateFailureReason.MISSING_SOURCE_REF])
238 UpdateFailureReason.MISSING_SOURCE_REF])
@@ -244,7 +242,8 b' class TestPullrequestsView(object):'
244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
242 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 pull_request = pr_util.create_pull_request(
243 pull_request = pr_util.create_pull_request(
246 approved=True, mergeable=True)
244 approved=True, mergeable=True)
247 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
245 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
246 pull_request.target_ref = unicode_reference
248 Session().add(pull_request)
247 Session().add(pull_request)
249 Session().commit()
248 Session().commit()
250
249
@@ -255,12 +254,12 b' class TestPullrequestsView(object):'
255 pull_request_id=pull_request_id)
254 pull_request_id=pull_request_id)
256
255
257 response = self.app.get(pull_request_url)
256 response = self.app.get(pull_request_url)
258
257 target_ref_id = 'invalid-branch'
259 assertr = AssertResponse(response)
258 merge_resp = MergeResponse(
260 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
259 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
261 MergeFailureReason.MISSING_TARGET_REF]
260 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
262 assertr.element_contains(
261 response.assert_response().element_contains(
263 'span[data-role="merge-message"]', str(expected_msg))
262 'span[data-role="merge-message"]', merge_resp.merge_status_message)
264
263
265 def test_comment_and_close_pull_request_custom_message_approved(
264 def test_comment_and_close_pull_request_custom_message_approved(
266 self, pr_util, csrf_token, xhr_header):
265 self, pr_util, csrf_token, xhr_header):
@@ -272,8 +271,8 b' class TestPullrequestsView(object):'
272
271
273 self.app.post(
272 self.app.post(
274 route_path('pullrequest_comment_create',
273 route_path('pullrequest_comment_create',
275 repo_name=pull_request.target_repo.scm_instance().name,
274 repo_name=pull_request.target_repo.scm_instance().name,
276 pull_request_id=pull_request_id),
275 pull_request_id=pull_request_id),
277 params={
276 params={
278 'close_pull_request': '1',
277 'close_pull_request': '1',
279 'text': 'Closing a PR',
278 'text': 'Closing a PR',
@@ -608,8 +607,7 b' class TestPullrequestsView(object):'
608
607
609 response = self.app.post(
608 response = self.app.post(
610 route_path('pullrequest_merge',
609 route_path('pullrequest_merge',
611 repo_name=repo_name,
610 repo_name=repo_name, pull_request_id=pull_request_id),
612 pull_request_id=pull_request_id),
613 params={'csrf_token': csrf_token}).follow()
611 params={'csrf_token': csrf_token}).follow()
614
612
615 assert response.status_int == 200
613 assert response.status_int == 200
@@ -624,10 +622,13 b' class TestPullrequestsView(object):'
624 pull_request_id = pull_request.pull_request_id
622 pull_request_id = pull_request.pull_request_id
625 repo_name = pull_request.target_repo.scm_instance().name
623 repo_name = pull_request.target_repo.scm_instance().name
626
624
625 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
626 MergeFailureReason.PUSH_FAILED,
627 metadata={'target': 'shadow repo',
628 'merge_commit': 'xxx'})
627 model_patcher = mock.patch.multiple(
629 model_patcher = mock.patch.multiple(
628 PullRequestModel,
630 PullRequestModel,
629 merge_repo=mock.Mock(return_value=MergeResponse(
631 merge_repo=mock.Mock(return_value=merge_resp),
630 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
631 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632
633
633 with model_patcher:
634 with model_patcher:
@@ -637,8 +638,10 b' class TestPullrequestsView(object):'
637 pull_request_id=pull_request_id),
638 pull_request_id=pull_request_id),
638 params={'csrf_token': csrf_token}, status=302)
639 params={'csrf_token': csrf_token}, status=302)
639
640
640 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
641 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
641 MergeFailureReason.PUSH_FAILED])
642 metadata={'target': 'shadow repo',
643 'merge_commit': 'xxx'})
644 assert_session_flash(response, merge_resp.merge_status_message)
642
645
643 def test_update_source_revision(self, backend, csrf_token):
646 def test_update_source_revision(self, backend, csrf_token):
644 commits = [
647 commits = [
@@ -909,11 +912,11 b' class TestPullrequestsView(object):'
909 pull_request_id=pull_request.pull_request_id))
912 pull_request_id=pull_request.pull_request_id))
910
913
911 assert response.status_int == 200
914 assert response.status_int == 200
912 assert_response = AssertResponse(response)
915
913 assert_response.element_contains(
916 response.assert_response().element_contains(
914 '#changeset_compare_view_content .alert strong',
917 '#changeset_compare_view_content .alert strong',
915 'Missing commits')
918 'Missing commits')
916 assert_response.element_contains(
919 response.assert_response().element_contains(
917 '#changeset_compare_view_content .alert',
920 '#changeset_compare_view_content .alert',
918 'This pull request cannot be displayed, because one or more'
921 'This pull request cannot be displayed, because one or more'
919 ' commits no longer exist in the source repository.')
922 ' commits no longer exist in the source repository.')
@@ -941,15 +944,15 b' class TestPullrequestsView(object):'
941 pull_request_id=pull_request.pull_request_id))
944 pull_request_id=pull_request.pull_request_id))
942
945
943 assert response.status_int == 200
946 assert response.status_int == 200
944 assert_response = AssertResponse(response)
947
945 assert_response.element_contains(
948 response.assert_response().element_contains(
946 '#changeset_compare_view_content .alert strong',
949 '#changeset_compare_view_content .alert strong',
947 'Missing commits')
950 'Missing commits')
948 assert_response.element_contains(
951 response.assert_response().element_contains(
949 '#changeset_compare_view_content .alert',
952 '#changeset_compare_view_content .alert',
950 'This pull request cannot be displayed, because one or more'
953 'This pull request cannot be displayed, because one or more'
951 ' commits no longer exist in the source repository.')
954 ' commits no longer exist in the source repository.')
952 assert_response.element_contains(
955 response.assert_response().element_contains(
953 '#update_commits',
956 '#update_commits',
954 'Update commits')
957 'Update commits')
955
958
@@ -987,8 +990,7 b' class TestPullrequestsView(object):'
987 pull_request_id=pull_request.pull_request_id))
990 pull_request_id=pull_request.pull_request_id))
988
991
989 assert response.status_int == 200
992 assert response.status_int == 200
990 assert_response = AssertResponse(response)
993 response.assert_response().element_contains(
991 assert_response.element_contains(
992 '#changeset_compare_view_content .alert strong',
994 '#changeset_compare_view_content .alert strong',
993 'Missing commits')
995 'Missing commits')
994
996
@@ -1004,12 +1006,11 b' class TestPullrequestsView(object):'
1004 repo_name=pull_request.target_repo.scm_instance().name,
1006 repo_name=pull_request.target_repo.scm_instance().name,
1005 pull_request_id=pull_request.pull_request_id))
1007 pull_request_id=pull_request.pull_request_id))
1006 assert response.status_int == 200
1008 assert response.status_int == 200
1007 assert_response = AssertResponse(response)
1008
1009
1009 origin = assert_response.get_element('.pr-origininfo .tag')
1010 origin = response.assert_response().get_element('.pr-origininfo .tag')
1010 origin_children = origin.getchildren()
1011 origin_children = origin.getchildren()
1011 assert len(origin_children) == 1
1012 assert len(origin_children) == 1
1012 target = assert_response.get_element('.pr-targetinfo .tag')
1013 target = response.assert_response().get_element('.pr-targetinfo .tag')
1013 target_children = target.getchildren()
1014 target_children = target.getchildren()
1014 assert len(target_children) == 1
1015 assert len(target_children) == 1
1015
1016
@@ -1038,13 +1039,12 b' class TestPullrequestsView(object):'
1038 repo_name=pull_request.target_repo.scm_instance().name,
1039 repo_name=pull_request.target_repo.scm_instance().name,
1039 pull_request_id=pull_request.pull_request_id))
1040 pull_request_id=pull_request.pull_request_id))
1040 assert response.status_int == 200
1041 assert response.status_int == 200
1041 assert_response = AssertResponse(response)
1042
1042
1043 origin = assert_response.get_element('.pr-origininfo .tag')
1043 origin = response.assert_response().get_element('.pr-origininfo .tag')
1044 assert origin.text.strip() == 'bookmark: origin'
1044 assert origin.text.strip() == 'bookmark: origin'
1045 assert origin.getchildren() == []
1045 assert origin.getchildren() == []
1046
1046
1047 target = assert_response.get_element('.pr-targetinfo .tag')
1047 target = response.assert_response().get_element('.pr-targetinfo .tag')
1048 assert target.text.strip() == 'bookmark: target'
1048 assert target.text.strip() == 'bookmark: target'
1049 assert target.getchildren() == []
1049 assert target.getchildren() == []
1050
1050
@@ -1060,13 +1060,12 b' class TestPullrequestsView(object):'
1060 repo_name=pull_request.target_repo.scm_instance().name,
1060 repo_name=pull_request.target_repo.scm_instance().name,
1061 pull_request_id=pull_request.pull_request_id))
1061 pull_request_id=pull_request.pull_request_id))
1062 assert response.status_int == 200
1062 assert response.status_int == 200
1063 assert_response = AssertResponse(response)
1064
1063
1065 origin = assert_response.get_element('.pr-origininfo .tag')
1064 origin = response.assert_response().get_element('.pr-origininfo .tag')
1066 assert origin.text.strip() == 'tag: origin'
1065 assert origin.text.strip() == 'tag: origin'
1067 assert origin.getchildren() == []
1066 assert origin.getchildren() == []
1068
1067
1069 target = assert_response.get_element('.pr-targetinfo .tag')
1068 target = response.assert_response().get_element('.pr-targetinfo .tag')
1070 assert target.text.strip() == 'tag: target'
1069 assert target.text.strip() == 'tag: target'
1071 assert target.getchildren() == []
1070 assert target.getchildren() == []
1072
1071
@@ -1090,12 +1089,13 b' class TestPullrequestsView(object):'
1090 repo_name=target_repo.name,
1089 repo_name=target_repo.name,
1091 pull_request_id=pr_id))
1090 pull_request_id=pr_id))
1092
1091
1093 assertr = AssertResponse(response)
1094 if mergeable:
1092 if mergeable:
1095 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1093 response.assert_response().element_value_contains(
1096 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1094 'input.pr-mergeinfo', shadow_url)
1095 response.assert_response().element_value_contains(
1096 'input.pr-mergeinfo ', 'pr-merge')
1097 else:
1097 else:
1098 assertr.no_element_exists('.pr-mergeinfo')
1098 response.assert_response().no_element_exists('.pr-mergeinfo')
1099
1099
1100
1100
1101 @pytest.mark.usefixtures('app')
1101 @pytest.mark.usefixtures('app')
@@ -1181,10 +1181,8 b' class RepoPullRequestsView(RepoAppView, '
1181 h.flash(msg, category='success')
1181 h.flash(msg, category='success')
1182 else:
1182 else:
1183 log.debug(
1183 log.debug(
1184 "The merge was not successful. Merge response: %s",
1184 "The merge was not successful. Merge response: %s", merge_resp)
1185 merge_resp)
1185 msg = merge_resp.merge_status_message
1186 msg = PullRequestModel().merge_status_message(
1187 merge_resp.failure_reason)
1188 h.flash(msg, category='error')
1186 h.flash(msg, category='error')
1189
1187
1190 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1188 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
@@ -21,20 +21,20 b''
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24 import os
25 import collections
25 import re
26 import time
27 import shutil
26 import datetime
28 import datetime
27 import fnmatch
29 import fnmatch
28 import itertools
30 import itertools
29 import logging
31 import logging
30 import os
32 import collections
31 import re
32 import time
33 import warnings
33 import warnings
34 import shutil
35
34
36 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
37
36
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs import connection
40 from rhodecode.lib.vcs.utils import author_name, author_email
40 from rhodecode.lib.vcs.utils import author_name, author_email
@@ -54,9 +54,6 b' FILEMODE_DEFAULT = 0o100644'
54 FILEMODE_EXECUTABLE = 0o100755
54 FILEMODE_EXECUTABLE = 0o100755
55
55
56 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
56 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
57 MergeResponse = collections.namedtuple(
58 'MergeResponse',
59 ('possible', 'executed', 'merge_ref', 'failure_reason'))
60
57
61
58
62 class MergeFailureReason(object):
59 class MergeFailureReason(object):
@@ -142,6 +139,92 b' class UpdateFailureReason(object):'
142 MISSING_SOURCE_REF = 5
139 MISSING_SOURCE_REF = 5
143
140
144
141
142 class MergeResponse(object):
143
144 # uses .format(**metadata) for variables
145 MERGE_STATUS_MESSAGES = {
146 MergeFailureReason.NONE: lazy_ugettext(
147 u'This pull request can be automatically merged.'),
148 MergeFailureReason.UNKNOWN: lazy_ugettext(
149 u'This pull request cannot be merged because of an unhandled exception. '
150 u'{exception}'),
151 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
152 u'This pull request cannot be merged because of merge conflicts.'),
153 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
154 u'This pull request could not be merged because push to '
155 u'target:`{target}@{merge_commit}` failed.'),
156 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
157 u'This pull request cannot be merged because the target '
158 u'`{target_ref.name}` is not a head.'),
159 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
160 u'This pull request cannot be merged because the source contains '
161 u'more branches than the target.'),
162 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
163 u'This pull request cannot be merged because the target '
164 u'has multiple heads: `{heads}`.'),
165 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
166 u'This pull request cannot be merged because the target repository is '
167 u'locked by {locked_by}.'),
168
169 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
170 u'This pull request cannot be merged because the target '
171 u'reference `{target_ref.name}` is missing.'),
172 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
173 u'This pull request cannot be merged because the source '
174 u'reference `{source_ref.name}` is missing.'),
175 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
176 u'This pull request cannot be merged because of conflicts related '
177 u'to sub repositories.'),
178
179 # Deprecations
180 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
181 u'This pull request cannot be merged because the target or the '
182 u'source reference is missing.'),
183
184 }
185
186 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
187 self.possible = possible
188 self.executed = executed
189 self.merge_ref = merge_ref
190 self.failure_reason = failure_reason
191 self.metadata = metadata or {}
192
193 def __repr__(self):
194 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
195
196 def __eq__(self, other):
197 same_instance = isinstance(other, self.__class__)
198 return same_instance \
199 and self.possible == other.possible \
200 and self.executed == other.executed \
201 and self.failure_reason == other.failure_reason
202
203 @property
204 def label(self):
205 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
206 not k.startswith('_'))
207 return label_dict.get(self.failure_reason)
208
209 @property
210 def merge_status_message(self):
211 """
212 Return a human friendly error message for the given merge status code.
213 """
214 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
215 try:
216 return msg.format(**self.metadata)
217 except Exception:
218 log.exception('Failed to format %s message', self)
219 return msg
220
221 def asdict(self):
222 data = {}
223 for k in ['possible', 'executed', 'merge_ref', 'failure_reason']:
224 data[k] = getattr(self, k)
225 return data
226
227
145 class BaseRepository(object):
228 class BaseRepository(object):
146 """
229 """
147 Base Repository for final backends
230 Base Repository for final backends
@@ -501,12 +584,11 b' class BaseRepository(object):'
501 repo_id, workspace_id, target_ref, source_repo,
584 repo_id, workspace_id, target_ref, source_repo,
502 source_ref, message, user_name, user_email, dry_run=dry_run,
585 source_ref, message, user_name, user_email, dry_run=dry_run,
503 use_rebase=use_rebase, close_branch=close_branch)
586 use_rebase=use_rebase, close_branch=close_branch)
504 except RepositoryError:
587 except RepositoryError as exc:
505 log.exception(
588 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
506 'Unexpected failure when running merge, dry-run=%s',
507 dry_run)
508 return MergeResponse(
589 return MergeResponse(
509 False, False, None, MergeFailureReason.UNKNOWN)
590 False, False, None, MergeFailureReason.UNKNOWN,
591 metadata={'exception': str(exc)})
510
592
511 def _merge_repo(self, repo_id, workspace_id, target_ref,
593 def _merge_repo(self, repo_id, workspace_id, target_ref,
512 source_repo, source_ref, merge_message,
594 source_repo, source_ref, merge_message,
@@ -911,11 +911,15 b' class GitRepository(BaseRepository):'
911 source_repo, source_ref, merge_message,
911 source_repo, source_ref, merge_message,
912 merger_name, merger_email, dry_run=False,
912 merger_name, merger_email, dry_run=False,
913 use_rebase=False, close_branch=False):
913 use_rebase=False, close_branch=False):
914
915 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
916 'rebase' if use_rebase else 'merge', dry_run)
914 if target_ref.commit_id != self.branches[target_ref.name]:
917 if target_ref.commit_id != self.branches[target_ref.name]:
915 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
918 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
916 target_ref.commit_id, self.branches[target_ref.name])
919 target_ref.commit_id, self.branches[target_ref.name])
917 return MergeResponse(
920 return MergeResponse(
918 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
921 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
922 metadata={'target_ref': target_ref})
919
923
920 shadow_repository_path = self._maybe_prepare_merge_workspace(
924 shadow_repository_path = self._maybe_prepare_merge_workspace(
921 repo_id, workspace_id, target_ref, source_ref)
925 repo_id, workspace_id, target_ref, source_ref)
@@ -943,7 +947,8 b' class GitRepository(BaseRepository):'
943 target_ref, target_ref.commit_id,
947 target_ref, target_ref.commit_id,
944 shadow_repo.branches[target_ref.name])
948 shadow_repo.branches[target_ref.name])
945 return MergeResponse(
949 return MergeResponse(
946 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
950 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
951 metadata={'target_ref': target_ref})
947
952
948 # calculate new branch
953 # calculate new branch
949 pr_branch = shadow_repo._get_new_pr_branch(
954 pr_branch = shadow_repo._get_new_pr_branch(
@@ -954,12 +959,15 b' class GitRepository(BaseRepository):'
954 try:
959 try:
955 shadow_repo._local_fetch(source_repo.path, source_ref.name)
960 shadow_repo._local_fetch(source_repo.path, source_ref.name)
956 except RepositoryError:
961 except RepositoryError:
957 log.exception('Failure when doing local fetch on git shadow repo')
962 log.exception('Failure when doing local fetch on '
963 'shadow repo: %s', shadow_repo)
958 return MergeResponse(
964 return MergeResponse(
959 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
965 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
966 metadata={'source_ref': source_ref})
960
967
961 merge_ref = None
968 merge_ref = None
962 merge_failure_reason = MergeFailureReason.NONE
969 merge_failure_reason = MergeFailureReason.NONE
970 metadata = {}
963 try:
971 try:
964 shadow_repo._local_merge(merge_message, merger_name, merger_email,
972 shadow_repo._local_merge(merge_message, merger_name, merger_email,
965 [source_ref.commit_id])
973 [source_ref.commit_id])
@@ -988,12 +996,15 b' class GitRepository(BaseRepository):'
988 merge_succeeded = True
996 merge_succeeded = True
989 except RepositoryError:
997 except RepositoryError:
990 log.exception(
998 log.exception(
991 'Failure when doing local push on git shadow repo')
999 'Failure when doing local push from the shadow '
1000 'repository to the target repository at %s.', self.path)
992 merge_succeeded = False
1001 merge_succeeded = False
993 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1002 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1003 metadata['target'] = 'git shadow repo'
1004 metadata['merge_commit'] = pr_branch
994 else:
1005 else:
995 merge_succeeded = False
1006 merge_succeeded = False
996
1007
997 return MergeResponse(
1008 return MergeResponse(
998 merge_possible, merge_succeeded, merge_ref,
1009 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
999 merge_failure_reason)
1010 metadata=metadata)
@@ -610,7 +610,7 b' class MercurialRepository(BaseRepository'
610 Returns the commit id of the merge and a boolean indicating if the
610 Returns the commit id of the merge and a boolean indicating if the
611 commit needs to be pushed.
611 commit needs to be pushed.
612 """
612 """
613 self._update(target_ref.commit_id)
613 self._update(target_ref.commit_id, clean=True)
614
614
615 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
615 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
616 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
616 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
@@ -631,7 +631,7 b' class MercurialRepository(BaseRepository'
631 self._remote.rebase(
631 self._remote.rebase(
632 source=source_ref.commit_id, dest=target_ref.commit_id)
632 source=source_ref.commit_id, dest=target_ref.commit_id)
633 self._remote.invalidate_vcs_cache()
633 self._remote.invalidate_vcs_cache()
634 self._update(bookmark_name)
634 self._update(bookmark_name, clean=True)
635 return self._identify(), True
635 return self._identify(), True
636 except RepositoryError:
636 except RepositoryError:
637 # The rebase-abort may raise another exception which 'hides'
637 # The rebase-abort may raise another exception which 'hides'
@@ -710,18 +710,21 b' class MercurialRepository(BaseRepository'
710 'rebase' if use_rebase else 'merge', dry_run)
710 'rebase' if use_rebase else 'merge', dry_run)
711 if target_ref.commit_id not in self._heads():
711 if target_ref.commit_id not in self._heads():
712 return MergeResponse(
712 return MergeResponse(
713 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
713 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
714 metadata={'target_ref': target_ref})
714
715
715 try:
716 try:
716 if (target_ref.type == 'branch' and
717 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
717 len(self._heads(target_ref.name)) != 1):
718 heads = ','.join(self._heads(target_ref.name))
718 return MergeResponse(
719 return MergeResponse(
719 False, False, None,
720 False, False, None,
720 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
721 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
722 metadata={'heads': heads})
721 except CommitDoesNotExistError:
723 except CommitDoesNotExistError:
722 log.exception('Failure when looking up branch heads on hg target')
724 log.exception('Failure when looking up branch heads on hg target')
723 return MergeResponse(
725 return MergeResponse(
724 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
726 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
727 metadata={'target_ref': target_ref})
725
728
726 shadow_repository_path = self._maybe_prepare_merge_workspace(
729 shadow_repository_path = self._maybe_prepare_merge_workspace(
727 repo_id, workspace_id, target_ref, source_ref)
730 repo_id, workspace_id, target_ref, source_ref)
@@ -730,6 +733,7 b' class MercurialRepository(BaseRepository'
730 log.debug('Pulling in target reference %s', target_ref)
733 log.debug('Pulling in target reference %s', target_ref)
731 self._validate_pull_reference(target_ref)
734 self._validate_pull_reference(target_ref)
732 shadow_repo._local_pull(self.path, target_ref)
735 shadow_repo._local_pull(self.path, target_ref)
736
733 try:
737 try:
734 log.debug('Pulling in source reference %s', source_ref)
738 log.debug('Pulling in source reference %s', source_ref)
735 source_repo._validate_pull_reference(source_ref)
739 source_repo._validate_pull_reference(source_ref)
@@ -737,12 +741,14 b' class MercurialRepository(BaseRepository'
737 except CommitDoesNotExistError:
741 except CommitDoesNotExistError:
738 log.exception('Failure when doing local pull on hg shadow repo')
742 log.exception('Failure when doing local pull on hg shadow repo')
739 return MergeResponse(
743 return MergeResponse(
740 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
744 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
745 metadata={'source_ref': source_ref})
741
746
742 merge_ref = None
747 merge_ref = None
743 merge_commit_id = None
748 merge_commit_id = None
744 close_commit_id = None
749 close_commit_id = None
745 merge_failure_reason = MergeFailureReason.NONE
750 merge_failure_reason = MergeFailureReason.NONE
751 metadata = {}
746
752
747 # enforce that close branch should be used only in case we source from
753 # enforce that close branch should be used only in case we source from
748 # an actual Branch
754 # an actual Branch
@@ -758,8 +764,8 b' class MercurialRepository(BaseRepository'
758 target_ref, merger_name, merger_email, source_ref)
764 target_ref, merger_name, merger_email, source_ref)
759 merge_possible = True
765 merge_possible = True
760 except RepositoryError:
766 except RepositoryError:
761 log.exception(
767 log.exception('Failure when doing close branch on '
762 'Failure when doing close branch on hg shadow repo')
768 'shadow repo: %s', shadow_repo)
763 merge_possible = False
769 merge_possible = False
764 merge_failure_reason = MergeFailureReason.MERGE_FAILED
770 merge_failure_reason = MergeFailureReason.MERGE_FAILED
765 else:
771 else:
@@ -824,19 +830,21 b' class MercurialRepository(BaseRepository'
824 except RepositoryError:
830 except RepositoryError:
825 log.exception(
831 log.exception(
826 'Failure when doing local push from the shadow '
832 'Failure when doing local push from the shadow '
827 'repository to the target repository.')
833 'repository to the target repository at %s.', self.path)
828 merge_succeeded = False
834 merge_succeeded = False
829 merge_failure_reason = MergeFailureReason.PUSH_FAILED
835 merge_failure_reason = MergeFailureReason.PUSH_FAILED
836 metadata['target'] = 'hg shadow repo'
837 metadata['merge_commit'] = merge_commit_id
830 else:
838 else:
831 merge_succeeded = True
839 merge_succeeded = True
832 else:
840 else:
833 merge_succeeded = False
841 merge_succeeded = False
834
842
835 return MergeResponse(
843 return MergeResponse(
836 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
844 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
845 metadata=metadata)
837
846
838 def _get_shadow_instance(
847 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
839 self, shadow_repository_path, enable_hooks=False):
840 config = self.config.copy()
848 config = self.config.copy()
841 if not enable_hooks:
849 if not enable_hooks:
842 config.clear_section('hooks')
850 config.clear_section('hooks')
@@ -33,7 +33,7 b' import collections'
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
@@ -75,43 +75,6 b' class PullRequestModel(BaseModel):'
75
75
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77
77
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
113 }
114
115 UPDATE_STATUS_MESSAGES = {
78 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
79 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
80 'Pull request update successful.'),
@@ -593,8 +556,7 b' class PullRequestModel(BaseModel):'
593 extras['user_agent'] = 'internal-merge'
556 extras['user_agent'] = 'internal-merge'
594 merge_state = self._merge_pull_request(pull_request, user, extras)
557 merge_state = self._merge_pull_request(pull_request, user, extras)
595 if merge_state.executed:
558 if merge_state.executed:
596 log.debug(
559 log.debug("Merge was successful, updating the pull request comments.")
597 "Merge was successful, updating the pull request comments.")
598 self._comment_and_close_pr(pull_request, user, merge_state)
560 self._comment_and_close_pr(pull_request, user, merge_state)
599
561
600 self._log_audit_action(
562 self._log_audit_action(
@@ -1254,8 +1216,7 b' class PullRequestModel(BaseModel):'
1254 pull_request,
1216 pull_request,
1255 force_shadow_repo_refresh=force_shadow_repo_refresh)
1217 force_shadow_repo_refresh=force_shadow_repo_refresh)
1256 log.debug("Merge response: %s", resp)
1218 log.debug("Merge response: %s", resp)
1257 status = resp.possible, self.merge_status_message(
1219 status = resp.possible, resp.merge_status_message
1258 resp.failure_reason)
1259 except NotImplementedError:
1220 except NotImplementedError:
1260 status = False, _('Pull request merging is not supported.')
1221 status = False, _('Pull request merging is not supported.')
1261
1222
@@ -1297,21 +1258,23 b' class PullRequestModel(BaseModel):'
1297 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1258 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1298 pull_request.pull_request_id, force_shadow_repo_refresh)
1259 pull_request.pull_request_id, force_shadow_repo_refresh)
1299 target_vcs = pull_request.target_repo.scm_instance()
1260 target_vcs = pull_request.target_repo.scm_instance()
1300
1301 # Refresh the target reference.
1261 # Refresh the target reference.
1302 try:
1262 try:
1303 target_ref = self._refresh_reference(
1263 target_ref = self._refresh_reference(
1304 pull_request.target_ref_parts, target_vcs)
1264 pull_request.target_ref_parts, target_vcs)
1305 except CommitDoesNotExistError:
1265 except CommitDoesNotExistError:
1306 merge_state = MergeResponse(
1266 merge_state = MergeResponse(
1307 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1267 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1268 metadata={'target_ref': pull_request.target_ref_parts})
1308 return merge_state
1269 return merge_state
1309
1270
1310 target_locked = pull_request.target_repo.locked
1271 target_locked = pull_request.target_repo.locked
1311 if target_locked and target_locked[0]:
1272 if target_locked and target_locked[0]:
1312 log.debug("The target repository is locked.")
1273 locked_by = 'user:{}'.format(target_locked[0])
1274 log.debug("The target repository is locked by %s.", locked_by)
1313 merge_state = MergeResponse(
1275 merge_state = MergeResponse(
1314 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1276 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1277 metadata={'locked_by': locked_by})
1315 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1278 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1316 pull_request, target_ref):
1279 pull_request, target_ref):
1317 log.debug("Refreshing the merge status of the repository.")
1280 log.debug("Refreshing the merge status of the repository.")
@@ -1369,12 +1332,6 b' class PullRequestModel(BaseModel):'
1369 workspace_id = 'pr-%s' % pull_request.pull_request_id
1332 workspace_id = 'pr-%s' % pull_request.pull_request_id
1370 return workspace_id
1333 return workspace_id
1371
1334
1372 def merge_status_message(self, status_code):
1373 """
1374 Return a human friendly error message for the given merge status code.
1375 """
1376 return self.MERGE_STATUS_MESSAGES[status_code]
1377
1378 def generate_repo_data(self, repo, commit_id=None, branch=None,
1335 def generate_repo_data(self, repo, commit_id=None, branch=None,
1379 bookmark=None, translator=None):
1336 bookmark=None, translator=None):
1380 from rhodecode.model.repo import RepoModel
1337 from rhodecode.model.repo import RepoModel
@@ -50,9 +50,11 b' class TestPullRequestModel(object):'
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 merge_resp = MergeResponse(
54 False, False, None, MergeFailureReason.UNKNOWN,
55 metadata={'exception': 'MockError'})
53 self.merge_patcher = mock.patch.object(
56 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
57 BackendClass, 'merge', return_value=merge_resp)
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
58 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
59 BackendClass, 'cleanup_merge_workspace')
58
60
@@ -162,7 +164,7 b' class TestPullRequestModel(object):'
162
164
163 status, msg = PullRequestModel().merge_status(pull_request)
165 status, msg = PullRequestModel().merge_status(pull_request)
164 assert status is True
166 assert status is True
165 assert msg.eval() == 'This pull request can be automatically merged.'
167 assert msg == 'This pull request can be automatically merged.'
166 self.merge_mock.assert_called_with(
168 self.merge_mock.assert_called_with(
167 self.repo_id, self.workspace_id,
169 self.repo_id, self.workspace_id,
168 pull_request.target_ref_parts,
170 pull_request.target_ref_parts,
@@ -177,7 +179,7 b' class TestPullRequestModel(object):'
177 self.merge_mock.reset_mock()
179 self.merge_mock.reset_mock()