pull-requests: ensure merge response provide more details...
dan -
r3339:8c7a75f7 default
Not Reviewed
Show More
Add another comment
TODOs: 0 unresolved 0 Resolved
COMMENTS: 0 General 0 Inline
@@ -313,7 +313,7
313 313 # In previous versions the merge response directly contained the merge
314 314 # commit id. It is now contained in the merge reference object. To be
315 315 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
316 merge_response = merge_response.asdict()
317 317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318 318
319 319 return merge_response
@@ -32,7 +32,6
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
36 35
37 36
38 37 def route_path(name, params=None, **kwargs):
@@ -233,8 +232,7
233 232 route_path('pullrequest_update',
234 233 repo_name=pull_request.target_repo.repo_name,
235 234 pull_request_id=pull_request_id),
236 params={'update_commits': 'true',
237 'csrf_token': csrf_token})
235 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 236
239 237 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 238 UpdateFailureReason.MISSING_SOURCE_REF])
@@ -244,7 +242,8
244 242 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 243 pull_request = pr_util.create_pull_request(
246 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 247 Session().add(pull_request)
249 248 Session().commit()
250 249
@@ -255,12 +254,12
255 254 pull_request_id=pull_request_id)
256 255
257 256 response = self.app.get(pull_request_url)
258
259 assertr = AssertResponse(response)
260 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
261 MergeFailureReason.MISSING_TARGET_REF]
262 assertr.element_contains(
263 'span[data-role="merge-message"]', str(expected_msg))
257 target_ref_id = 'invalid-branch'
258 merge_resp = MergeResponse(
259 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
260 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
261 response.assert_response().element_contains(
262 'span[data-role="merge-message"]', merge_resp.merge_status_message)
264 263
265 264 def test_comment_and_close_pull_request_custom_message_approved(
266 265 self, pr_util, csrf_token, xhr_header):
@@ -272,8 +271,8
272 271
273 272 self.app.post(
274 273 route_path('pullrequest_comment_create',
275 repo_name=pull_request.target_repo.scm_instance().name,
276 pull_request_id=pull_request_id),
274 repo_name=pull_request.target_repo.scm_instance().name,
275 pull_request_id=pull_request_id),
277 276 params={
278 277 'close_pull_request': '1',
279 278 'text': 'Closing a PR',
@@ -608,8 +607,7
608 607
609 608 response = self.app.post(
610 609 route_path('pullrequest_merge',
611 repo_name=repo_name,
612 pull_request_id=pull_request_id),
610 repo_name=repo_name, pull_request_id=pull_request_id),
613 611 params={'csrf_token': csrf_token}).follow()
614 612
615 613 assert response.status_int == 200
@@ -624,10 +622,13
624 622 pull_request_id = pull_request.pull_request_id
625 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 629 model_patcher = mock.patch.multiple(
628 630 PullRequestModel,
629 merge_repo=mock.Mock(return_value=MergeResponse(
630 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
631 merge_repo=mock.Mock(return_value=merge_resp),
631 632 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632 633
633 634 with model_patcher:
@@ -637,8 +638,10
637 638 pull_request_id=pull_request_id),
638 639 params={'csrf_token': csrf_token}, status=302)
639 640
640 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
641 MergeFailureReason.PUSH_FAILED])
641 merge_resp = MergeResponse(True, True, '', 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 646 def test_update_source_revision(self, backend, csrf_token):
644 647 commits = [
@@ -909,11 +912,11
909 912 pull_request_id=pull_request.pull_request_id))
910 913
911 914 assert response.status_int == 200
912 assert_response = AssertResponse(response)
913 assert_response.element_contains(
915
916 response.assert_response().element_contains(
914 917 '#changeset_compare_view_content .alert strong',
915 918 'Missing commits')
916 assert_response.element_contains(
919 response.assert_response().element_contains(
917 920 '#changeset_compare_view_content .alert',
918 921 'This pull request cannot be displayed, because one or more'
919 922 ' commits no longer exist in the source repository.')
@@ -941,15 +944,15
941 944 pull_request_id=pull_request.pull_request_id))
942 945
943 946 assert response.status_int == 200
944 assert_response = AssertResponse(response)
945 assert_response.element_contains(
947
948 response.assert_response().element_contains(
946 949 '#changeset_compare_view_content .alert strong',
947 950 'Missing commits')
948 assert_response.element_contains(
951 response.assert_response().element_contains(
949 952 '#changeset_compare_view_content .alert',
950 953 'This pull request cannot be displayed, because one or more'
951 954 ' commits no longer exist in the source repository.')
952 assert_response.element_contains(
955 response.assert_response().element_contains(
953 956 '#update_commits',
954 957 'Update commits')
955 958
@@ -987,8 +990,7
987 990 pull_request_id=pull_request.pull_request_id))
988 991
989 992 assert response.status_int == 200
990 assert_response = AssertResponse(response)
991 assert_response.element_contains(
993 response.assert_response().element_contains(
992 994 '#changeset_compare_view_content .alert strong',
993 995 'Missing commits')
994 996
@@ -1004,12 +1006,11
1004 1006 repo_name=pull_request.target_repo.scm_instance().name,
1005 1007 pull_request_id=pull_request.pull_request_id))
1006 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 1011 origin_children = origin.getchildren()
1011 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 1014 target_children = target.getchildren()
1014 1015 assert len(target_children) == 1
1015 1016
@@ -1038,13 +1039,12
1038 1039 repo_name=pull_request.target_repo.scm_instance().name,
1039 1040 pull_request_id=pull_request.pull_request_id))
1040 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 1044 assert origin.text.strip() == 'bookmark: origin'
1045 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 1048 assert target.text.strip() == 'bookmark: target'
1049 1049 assert target.getchildren() == []
1050 1050
@@ -1060,13 +1060,12
1060 1060 repo_name=pull_request.target_repo.scm_instance().name,
1061 1061 pull_request_id=pull_request.pull_request_id))
1062 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 1065 assert origin.text.strip() == 'tag: origin'
1067 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 1069 assert target.text.strip() == 'tag: target'
1071 1070 assert target.getchildren() == []
1072 1071
@@ -1090,12 +1089,13
1090 1089 repo_name=target_repo.name,
1091 1090 pull_request_id=pr_id))
1092 1091
1093 assertr = AssertResponse(response)
1094 1092 if mergeable:
1095 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1096 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1093 response.assert_response().element_value_contains(
1094 'input.pr-mergeinfo', shadow_url)
1095 response.assert_response().element_value_contains(
1096 'input.pr-mergeinfo ', 'pr-merge')
1097 1097 else:
1098 assertr.no_element_exists('.pr-mergeinfo')
1098 response.assert_response().no_element_exists('.pr-mergeinfo')
1099 1099
1100 1100
1101 1101 @pytest.mark.usefixtures('app')
@@ -1181,10 +1181,8
1181 1181 h.flash(msg, category='success')
1182 1182 else:
1183 1183 log.debug(
1184 "The merge was not successful. Merge response: %s",
1185 merge_resp)
1186 msg = PullRequestModel().merge_status_message(
1187 merge_resp.failure_reason)
1184 "The merge was not successful. Merge response: %s", merge_resp)
1185 msg = merge_resp.merge_status_message
1188 1186 h.flash(msg, category='error')
1189 1187
1190 1188 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
@@ -21,20 +21,20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24
25 import collections
24 import os
25 import re
26 import time
27 import shutil
26 28 import datetime
27 29 import fnmatch
28 30 import itertools
29 31 import logging
30 import os
31 import re
32 import time
32 import collections
33 33 import warnings
34 import shutil
35 34
36 35 from zope.cachedescriptors.property import Lazy as LazyProperty
37 36
37 from rhodecode.translation import lazy_ugettext
38 38 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 39 from rhodecode.lib.vcs import connection
40 40 from rhodecode.lib.vcs.utils import author_name, author_email
@@ -54,9 +54,6
54 54 FILEMODE_EXECUTABLE = 0o100755
55 55
56 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 59 class MergeFailureReason(object):
@@ -142,6 +139,92
142 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 228 class BaseRepository(object):
146 229 """
147 230 Base Repository for final backends
@@ -501,12 +584,11
501 584 repo_id, workspace_id, target_ref, source_repo,
502 585 source_ref, message, user_name, user_email, dry_run=dry_run,
503 586 use_rebase=use_rebase, close_branch=close_branch)
504 except RepositoryError:
505 log.exception(
506 'Unexpected failure when running merge, dry-run=%s',
507 dry_run)
587 except RepositoryError as exc:
588 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
508 589 return MergeResponse(
509 False, False, None, MergeFailureReason.UNKNOWN)
590 False, False, None, MergeFailureReason.UNKNOWN,
591 metadata={'exception': str(exc)})
510 592
511 593 def _merge_repo(self, repo_id, workspace_id, target_ref,
512 594 source_repo, source_ref, merge_message,
@@ -911,11 +911,15
911 911 source_repo, source_ref, merge_message,
912 912 merger_name, merger_email, dry_run=False,
913 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 917 if target_ref.commit_id != self.branches[target_ref.name]:
915 918 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
916 919 target_ref.commit_id, self.branches[target_ref.name])
917 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 924 shadow_repository_path = self._maybe_prepare_merge_workspace(
921 925 repo_id, workspace_id, target_ref, source_ref)
@@ -943,7 +947,8
943 947 target_ref, target_ref.commit_id,
944 948 shadow_repo.branches[target_ref.name])
945 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 953 # calculate new branch
949 954 pr_branch = shadow_repo._get_new_pr_branch(
@@ -954,12 +959,15
954 959 try:
955 960 shadow_repo._local_fetch(source_repo.path, source_ref.name)
956 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 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 968 merge_ref = None
962 969 merge_failure_reason = MergeFailureReason.NONE
970 metadata = {}
963 971 try:
964 972 shadow_repo._local_merge(merge_message, merger_name, merger_email,
965 973 [source_ref.commit_id])
@@ -988,12 +996,15
988 996 merge_succeeded = True
989 997 except RepositoryError:
990 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 1001 merge_succeeded = False
993 1002 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1003 metadata['target'] = 'git shadow repo'
1004 metadata['merge_commit'] = pr_branch
994 1005 else:
995 1006 merge_succeeded = False
996 1007
997 1008 return MergeResponse(
998 merge_possible, merge_succeeded, merge_ref,
999 merge_failure_reason)
1009 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1010 metadata=metadata)
@@ -610,7 +610,7
610 610 Returns the commit id of the merge and a boolean indicating if the
611 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 615 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
616 616 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
@@ -631,7 +631,7
631 631 self._remote.rebase(
632 632 source=source_ref.commit_id, dest=target_ref.commit_id)
633 633 self._remote.invalidate_vcs_cache()
634 self._update(bookmark_name)
634 self._update(bookmark_name, clean=True)
635 635 return self._identify(), True
636 636 except RepositoryError:
637 637 # The rebase-abort may raise another exception which 'hides'
@@ -710,18 +710,21
710 710 'rebase' if use_rebase else 'merge', dry_run)
711 711 if target_ref.commit_id not in self._heads():
712 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 716 try:
716 if (target_ref.type == 'branch' and
717 len(self._heads(target_ref.name)) != 1):
717 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
718 heads = ','.join(self._heads(target_ref.name))
718 719 return MergeResponse(
719 720 False, False, None,
720 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
721 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
722 metadata={'heads': heads})
721 723 except CommitDoesNotExistError:
722 724 log.exception('Failure when looking up branch heads on hg target')
723 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 729 shadow_repository_path = self._maybe_prepare_merge_workspace(
727 730 repo_id, workspace_id, target_ref, source_ref)
@@ -730,6 +733,7
730 733 log.debug('Pulling in target reference %s', target_ref)
731 734 self._validate_pull_reference(target_ref)
732 735 shadow_repo._local_pull(self.path, target_ref)
736
733 737 try:
734 738 log.debug('Pulling in source reference %s', source_ref)
735 739 source_repo._validate_pull_reference(source_ref)
@@ -737,12 +741,14
737 741 except CommitDoesNotExistError:
738 742 log.exception('Failure when doing local pull on hg shadow repo')
739 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 747 merge_ref = None
743 748 merge_commit_id = None
744 749 close_commit_id = None
745 750 merge_failure_reason = MergeFailureReason.NONE
751 metadata = {}
746 752
747 753 # enforce that close branch should be used only in case we source from
748 754 # an actual Branch
@@ -758,8 +764,8
758 764 target_ref, merger_name, merger_email, source_ref)
759 765 merge_possible = True
760 766 except RepositoryError:
761 log.exception(
762 'Failure when doing close branch on hg shadow repo')
767 log.exception('Failure when doing close branch on '
768 'shadow repo: %s', shadow_repo)
763 769 merge_possible = False
764 770 merge_failure_reason = MergeFailureReason.MERGE_FAILED
765 771 else:
@@ -824,19 +830,21
824 830 except RepositoryError:
825 831 log.exception(
826 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 834 merge_succeeded = False
829 835 merge_failure_reason = MergeFailureReason.PUSH_FAILED
836 metadata['target'] = 'hg shadow repo'
837 metadata['merge_commit'] = merge_commit_id
830 838 else:
831 839 merge_succeeded = True
832 840 else:
833 841 merge_succeeded = False
834 842
835 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(
839 self, shadow_repository_path, enable_hooks=False):
847 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
840 848 config = self.config.copy()
841 849 if not enable_hooks:
842 850 config.clear_section('hooks')
@@ -33,7 +33,7
33 33 from pyramid.threadlocal import get_current_request
34 34
35 35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext
37 37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.compat import OrderedDict
@@ -75,43 +75,6
75 75
76 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 78 UPDATE_STATUS_MESSAGES = {
116 79 UpdateFailureReason.NONE: lazy_ugettext(
117 80 'Pull request update successful.'),
@@ -593,8 +556,7
593 556 extras['user_agent'] = 'internal-merge'
594 557 merge_state = self._merge_pull_request(pull_request, user, extras)
595 558 if merge_state.executed:
596 log.debug(
597 "Merge was successful, updating the pull request comments.")
559 log.debug("Merge was successful, updating the pull request comments.")
598 560 self._comment_and_close_pr(pull_request, user, merge_state)
599 561
600 562 self._log_audit_action(
@@ -1254,8 +1216,7
1254 1216 pull_request,
1255 1217 force_shadow_repo_refresh=force_shadow_repo_refresh)
1256 1218 log.debug("Merge response: %s", resp)
1257 status = resp.possible, self.merge_status_message(
1258 resp.failure_reason)
1219 status = resp.possible, resp.merge_status_message
1259 1220 except NotImplementedError:
1260 1221 status = False, _('Pull request merging is not supported.')
1261 1222
@@ -1297,21 +1258,23
1297 1258 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1298 1259 pull_request.pull_request_id, force_shadow_repo_refresh)
1299 1260 target_vcs = pull_request.target_repo.scm_instance()
1300
1301 1261 # Refresh the target reference.
1302 1262 try:
1303 1263 target_ref = self._refresh_reference(
1304 1264 pull_request.target_ref_parts, target_vcs)
1305 1265 except CommitDoesNotExistError:
1306 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 1269 return merge_state
1309 1270
1310 1271 target_locked = pull_request.target_repo.locked
1311 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 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 1278 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1316 1279 pull_request, target_ref):
1317 1280 log.debug("Refreshing the merge status of the repository.")
@@ -1369,12 +1332,6
1369 1332 workspace_id = 'pr-%s' % pull_request.pull_request_id
1370 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 1335 def generate_repo_data(self, repo, commit_id=None, branch=None,
1379 1336 bookmark=None, translator=None):
1380 1337 from rhodecode.model.repo import RepoModel
@@ -50,9 +50,11
50 50 A pull request combined with multiples patches.
51 51 """
52 52 BackendClass = get_backend(backend.alias)
53 merge_resp = MergeResponse(
54 False, False, None, MergeFailureReason.UNKNOWN,
55 metadata={'exception': 'MockError'})
53 56 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
57 BackendClass, 'merge', return_value=merge_resp)
56 58 self.workspace_remove_patcher = mock.patch.object(
57 59 BackendClass, 'cleanup_merge_workspace')
58 60
@@ -162,7 +164,7
162 164
163 165 status, msg = PullRequestModel().merge_status(pull_request)
164 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 168 self.merge_mock.assert_called_with(
167 169 self.repo_id, self.workspace_id,
168 170 pull_request.target_ref_parts,
@@ -177,7 +179,7
177 179 self.merge_mock.reset_mock()
178 180 status, msg = PullRequestModel().merge_status(pull_request)
179 181 assert status is True
180 assert msg.eval() == 'This pull request can be automatically merged.'
182 assert msg == 'This pull request can be automatically merged.'
181 183 assert self.merge_mock.called is False
182 184
183 185 def test_merge_status_known_failure(self, pull_request):
@@ -190,9 +192,7
190 192
191 193 status, msg = PullRequestModel().merge_status(pull_request)
192 194 assert status is False
193 assert (
194 msg.eval() ==
195 'This pull request cannot be merged because of merge conflicts.')
195 assert msg == 'This pull request cannot be merged because of merge conflicts.'
196 196 self.merge_mock.assert_called_with(
197 197 self.repo_id, self.workspace_id,
198 198 pull_request.target_ref_parts,
@@ -208,14 +208,13
208 208 self.merge_mock.reset_mock()
209 209 status, msg = PullRequestModel().merge_status(pull_request)
210 210 assert status is False
211 assert (
212 msg.eval() ==
213 'This pull request cannot be merged because of merge conflicts.')
211 assert msg == 'This pull request cannot be merged because of merge conflicts.'
214 212 assert self.merge_mock.called is False
215 213
216 214 def test_merge_status_unknown_failure(self, pull_request):
217 215 self.merge_mock.return_value = MergeResponse(
218 False, False, None, MergeFailureReason.UNKNOWN)
216 False, False, None, MergeFailureReason.UNKNOWN,
217 metadata={'exception': 'MockError'})
219 218
220 219 assert pull_request._last_merge_source_rev is None
221 220 assert pull_request._last_merge_target_rev is None
@@ -223,9 +222,9
223 222
224 223 status, msg = PullRequestModel().merge_status(pull_request)
225 224 assert status is False
226 assert msg.eval() == (
227 'This pull request cannot be merged because of an unhandled'
228 ' exception.')
225 assert msg == (
226 'This pull request cannot be merged because of an unhandled exception. '
227 'MockError')
229 228 self.merge_mock.assert_called_with(
230 229 self.repo_id, self.workspace_id,
231 230 pull_request.target_ref_parts,
@@ -240,18 +239,18
240 239 self.merge_mock.reset_mock()
241 240 status, msg = PullRequestModel().merge_status(pull_request)
242 241 assert status is False
243 assert msg.eval() == (
244 'This pull request cannot be merged because of an unhandled'
245 ' exception.')
242 assert msg == (
243 'This pull request cannot be merged because of an unhandled exception. '
244 'MockError')
246 245 assert self.merge_mock.called is True
247 246
248 247 def test_merge_status_when_target_is_locked(self, pull_request):
249 248 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
250 249 status, msg = PullRequestModel().merge_status(pull_request)
251 250 assert status is False
252 assert msg.eval() == (
253 'This pull request cannot be merged because the target repository'
254 ' is locked.')
251 assert msg == (
252 'This pull request cannot be merged because the target repository '
253 'is locked by user:1.')
255 254
256 255 def test_merge_status_requirements_check_target(self, pull_request):
257 256
@@ -460,6 +459,46
460 459 outdated_comment_mock.assert_called_with(pull_request)
461 460
462 461
462 @pytest.mark.parametrize('mr_type, expected_msg', [
463 (MergeFailureReason.NONE,
464 'This pull request can be automatically merged.'),
465 (MergeFailureReason.UNKNOWN,
466 'This pull request cannot be merged because of an unhandled exception. CRASH'),
467 (MergeFailureReason.MERGE_FAILED,
468 'This pull request cannot be merged because of merge conflicts.'),
469 (MergeFailureReason.PUSH_FAILED,
470 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'),
471 (MergeFailureReason.TARGET_IS_NOT_HEAD,
472 'This pull request cannot be merged because the target `ref_name` is not a head.'),
473 (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
474 'This pull request cannot be merged because the source contains more branches than the target.'),
475 (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
476 'This pull request cannot be merged because the target has multiple heads: `a,b,c`.'),
477 (MergeFailureReason.TARGET_IS_LOCKED,
478 'This pull request cannot be merged because the target repository is locked by user:123.'),
479 (MergeFailureReason.MISSING_TARGET_REF,
480 'This pull request cannot be merged because the target reference `ref_name` is missing.'),
481 (MergeFailureReason.MISSING_SOURCE_REF,
482 'This pull request cannot be merged because the source reference `ref_name` is missing.'),
483 (MergeFailureReason.SUBREPO_MERGE_FAILED,
484 'This pull request cannot be merged because of conflicts related to sub repositories.'),
485
486 ])
487 def test_merge_response_message(mr_type, expected_msg):
488 merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
489 metadata = {
490 'exception': "CRASH",
491 'target': 'some-repo',
492 'merge_commit': 'merge_commit',
493 'target_ref': merge_ref,
494 'source_ref': merge_ref,
495 'heads': ','.join(['a', 'b', 'c']),
496 'locked_by': 'user:123'}
497
498 merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata)
499 assert merge_response.merge_status_message == expected_msg
500
501
463 502 @pytest.fixture
464 503 def merge_extras(user_regular):
465 504 """
@@ -661,8 +661,7
661 661
662 662 def _next_repo_name(self):
663 663 return u"%s_%s" % (
664 self.invalid_repo_name.sub(u'_', self._test_name),
665 len(self._cleanup_repos))
664 self.invalid_repo_name.sub(u'_', self._test_name), len(self._cleanup_repos))
666 665
667 666 def ensure_file(self, filename, content='Test content\n'):
668 667 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
@@ -681,9 +681,11
681 681 workspace_id = 'test-merge'
682 682
683 683 assert len(target_repo._heads(branch='default')) == 2
684 heads = target_repo._heads(branch='default')
684 685 expected_merge_response = MergeResponse(
685 686 False, False, None,
686 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
687 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
688 metadata={'heads': heads})
687 689 repo_id = repo_id_generator(target_repo.path)
688 690 merge_response = target_repo.merge(
689 691 repo_id, workspace_id, target_ref, source_repo, source_ref,
@@ -284,11 +284,9
284 284 self.source_commit = self.source_repo.get_commit()
285 285 # This only works for Git and Mercurial
286 286 default_branch = self.target_repo.DEFAULT_BRANCH_NAME
287 self.target_ref = Reference(
288 'branch', default_branch, self.target_commit.raw_id)
289 self.source_ref = Reference(
290 'branch', default_branch, self.source_commit.raw_id)
291 self.workspace_id = 'test-merge'
287 self.target_ref = Reference('branch', default_branch, self.target_commit.raw_id)
288 self.source_ref = Reference('branch', default_branch, self.source_commit.raw_id)
289 self.workspace_id = 'test-merge-{}'.format(vcsbackend.alias)
292 290 self.repo_id = repo_id_generator(self.target_repo.path)
293 291
294 292 def prepare_for_conflict(self, vcsbackend):
@@ -300,11 +298,9
300 298 self.source_commit = self.source_repo.get_commit()
301 299 # This only works for Git and Mercurial
302 300 default_branch = self.target_repo.DEFAULT_BRANCH_NAME
303 self.target_ref = Reference(
304 'branch', default_branch, self.target_commit.raw_id)
305 self.source_ref = Reference(
306 'branch', default_branch, self.source_commit.raw_id)
307 self.workspace_id = 'test-merge'
301 self.target_ref = Reference('branch', default_branch, self.target_commit.raw_id)
302 self.source_ref = Reference('branch', default_branch, self.source_commit.raw_id)
303 self.workspace_id = 'test-merge-{}'.format(vcsbackend.alias)
308 304 self.repo_id = repo_id_generator(self.target_repo.path)