##// END OF EJS Templates
pull-requests: trigger merge simulation during PR creation. Fixes #5396
marcink -
r2168:41032fb6 default
parent child Browse files
Show More
@@ -274,7 +274,8 b' def merge_pull_request('
274
274
275 pull_request = get_pull_request_or_error(pullrequestid)
275 pull_request = get_pull_request_or_error(pullrequestid)
276
276
277 check = MergeCheck.validate(pull_request, user=apiuser)
277 check = MergeCheck.validate(
278 pull_request, user=apiuser, translator=request.translate)
278 merge_possible = not check.failed
279 merge_possible = not check.failed
279
280
280 if not merge_possible:
281 if not merge_possible:
@@ -232,8 +232,8 b' class TestPullrequestsView(object):'
232 params={'update_commits': 'true',
232 params={'update_commits': 'true',
233 'csrf_token': csrf_token})
233 'csrf_token': csrf_token})
234
234
235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
235 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
236 UpdateFailureReason.MISSING_SOURCE_REF]
236 UpdateFailureReason.MISSING_SOURCE_REF])
237 assert_session_flash(response, expected_msg, category='error')
237 assert_session_flash(response, expected_msg, category='error')
238
238
239 def test_missing_target_reference(self, pr_util, csrf_token):
239 def test_missing_target_reference(self, pr_util, csrf_token):
@@ -362,12 +362,14 b' class RepoPullRequestsView(RepoAppView, '
362
362
363 # check merge capabilities
363 # check merge capabilities
364 _merge_check = MergeCheck.validate(
364 _merge_check = MergeCheck.validate(
365 pull_request_latest, user=self._rhodecode_user)
365 pull_request_latest, user=self._rhodecode_user,
366 translator=self.request.translate)
366 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_message = _merge_check.merge_msg
369 c.pr_merge_message = _merge_check.merge_msg
369
370
370 c.pr_merge_info = MergeCheck.get_merge_conditions(pull_request_latest)
371 c.pr_merge_info = MergeCheck.get_merge_conditions(
372 pull_request_latest, translator=self.request.translate)
371
373
372 c.pull_request_review_status = _merge_check.review_status
374 c.pull_request_review_status = _merge_check.review_status
373 if merge_checks:
375 if merge_checks:
@@ -627,7 +629,7 b' class RepoPullRequestsView(RepoAppView, '
627 try:
629 try:
628 source_repo_data = PullRequestModel().generate_repo_data(
630 source_repo_data = PullRequestModel().generate_repo_data(
629 source_repo, commit_id=commit_id,
631 source_repo, commit_id=commit_id,
630 branch=branch_ref, bookmark=bookmark_ref)
632 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
631 except CommitDoesNotExistError as e:
633 except CommitDoesNotExistError as e:
632 log.exception(e)
634 log.exception(e)
633 h.flash(_('Commit does not exist'), 'error')
635 h.flash(_('Commit does not exist'), 'error')
@@ -643,7 +645,7 b' class RepoPullRequestsView(RepoAppView, '
643 default_target_repo = source_repo.parent
645 default_target_repo = source_repo.parent
644
646
645 target_repo_data = PullRequestModel().generate_repo_data(
647 target_repo_data = PullRequestModel().generate_repo_data(
646 default_target_repo)
648 default_target_repo, translator=self.request.translate)
647
649
648 selected_source_ref = source_repo_data['refs']['selected_ref']
650 selected_source_ref = source_repo_data['refs']['selected_ref']
649
651
@@ -676,7 +678,7 b' class RepoPullRequestsView(RepoAppView, '
676 repo = Repository.get_by_repo_name(target_repo_name)
678 repo = Repository.get_by_repo_name(target_repo_name)
677 if not repo:
679 if not repo:
678 raise HTTPNotFound()
680 raise HTTPNotFound()
679 return PullRequestModel().generate_repo_data(repo)
681 return PullRequestModel().generate_repo_data(repo, translator=self.request.translate)
680
682
681 @LoginRequired()
683 @LoginRequired()
682 @NotAnonymous()
684 @NotAnonymous()
@@ -806,11 +808,12 b' class RepoPullRequestsView(RepoAppView, '
806
808
807 try:
809 try:
808 pull_request = PullRequestModel().create(
810 pull_request = PullRequestModel().create(
809 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
811 self._rhodecode_user.user_id, source_repo, source_ref,
810 target_ref, commit_ids, reviewers, pullrequest_title,
812 target_repo, target_ref, commit_ids, reviewers,
811 description, reviewer_rules
813 pullrequest_title, description, reviewer_rules
812 )
814 )
813 Session().commit()
815 Session().commit()
816
814 h.flash(_('Successfully opened new pull request'),
817 h.flash(_('Successfully opened new pull request'),
815 category='success')
818 category='success')
816 except Exception:
819 except Exception:
@@ -938,7 +941,8 b' class RepoPullRequestsView(RepoAppView, '
938 pull_request = PullRequest.get_or_404(
941 pull_request = PullRequest.get_or_404(
939 self.request.matchdict['pull_request_id'])
942 self.request.matchdict['pull_request_id'])
940
943
941 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
944 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
945 translator=self.request.translate)
942 merge_possible = not check.failed
946 merge_possible = not check.failed
943
947
944 for err_type, error_msg in check.errors:
948 for err_type, error_msg in check.errors:
@@ -618,6 +618,9 b' def bootstrap_request(**kwargs):'
618 host = kwargs.pop('host', 'example.com:80')
618 host = kwargs.pop('host', 'example.com:80')
619 domain = kwargs.pop('domain', 'example.com')
619 domain = kwargs.pop('domain', 'example.com')
620
620
621 def translate(self, msg):
622 return msg
623
621 class TestDummySession(pyramid.testing.DummySession):
624 class TestDummySession(pyramid.testing.DummySession):
622 def save(*arg, **kw):
625 def save(*arg, **kw):
623 pass
626 pass
@@ -625,6 +628,7 b' def bootstrap_request(**kwargs):'
625 request = TestRequest(**kwargs)
628 request = TestRequest(**kwargs)
626 request.session = TestDummySession()
629 request.session = TestDummySession()
627
630
631
628 config = pyramid.testing.setUp(request=request)
632 config = pyramid.testing.setUp(request=request)
629 add_events_routes(config)
633 add_events_routes(config)
630 return request
634 return request
@@ -23,18 +23,17 b''
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31
32
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
36
34
37 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
@@ -51,7 +50,7 b' from rhodecode.model import BaseModel'
51 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
52 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
@@ -65,7 +64,7 b' log = logging.getLogger(__name__)'
65
64
66 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
67 # request update.
66 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
71
70
@@ -418,7 +417,8 b' class PullRequestModel(BaseModel):'
418
417
419 def create(self, created_by, source_repo, source_ref, target_repo,
418 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
419 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
420 reviewer_data=None, translator=None):
421 translator = translator or get_current_request().translate
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
@@ -466,6 +466,9 b' class PullRequestModel(BaseModel):'
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 MergeCheck.validate(
470 pull_request, user=created_by_user, translator=translator)
471
469 self.notify_reviewers(pull_request, reviewer_ids)
472 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
473 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
474 pull_request, created_by_user, 'create')
@@ -535,13 +538,13 b' class PullRequestModel(BaseModel):'
535 log.warn("Merge failed, not updating the pull request.")
538 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
539 return merge_state
537
540
538 def _merge_pull_request(self, pull_request, user, extras):
541 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
539 target_vcs = pull_request.target_repo.scm_instance()
542 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
543 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
544 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
545 pull_request.target_ref_parts, target_vcs)
543
546
544 message = _(
547 message = merge_msg or (
545 'Merge pull request #%(pr_id)s from '
548 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
549 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
550 'pr_id': pull_request.pull_request_id,
@@ -570,12 +573,13 b' class PullRequestModel(BaseModel):'
570 close_branch=close_branch)
573 close_branch=close_branch)
571 return merge_state
574 return merge_state
572
575
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
576 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
577 pull_request.merge_rev = merge_state.merge_ref.commit_id
575 pull_request.updated_on = datetime.datetime.now()
578 pull_request.updated_on = datetime.datetime.now()
579 close_msg = close_msg or 'Pull request merged and closed'
576
580
577 CommentsModel().create(
581 CommentsModel().create(
578 text=unicode(_('Pull request merged and closed')),
582 text=safe_unicode(close_msg),
579 repo=pull_request.target_repo.repo_id,
583 repo=pull_request.target_repo.repo_id,
580 user=user.user_id,
584 user=user.user_id,
581 pull_request=pull_request.pull_request_id,
585 pull_request=pull_request.pull_request_id,
@@ -1109,7 +1113,7 b' class PullRequestModel(BaseModel):'
1109 status_lbl = ChangesetStatus.get_status_lbl(status)
1113 status_lbl = ChangesetStatus.get_status_lbl(status)
1110
1114
1111 default_message = (
1115 default_message = (
1112 _('Closing with status change {transition_icon} {status}.')
1116 'Closing with status change {transition_icon} {status}.'
1113 ).format(transition_icon='>', status=status_lbl)
1117 ).format(transition_icon='>', status=status_lbl)
1114 text = message or default_message
1118 text = message or default_message
1115
1119
@@ -1151,13 +1155,16 b' class PullRequestModel(BaseModel):'
1151
1155
1152 return comment, status
1156 return comment, status
1153
1157
1154 def merge_status(self, pull_request):
1158 def merge_status(self, pull_request, translator=None):
1159 _ = translator or get_current_request().translate
1160
1155 if not self._is_merge_enabled(pull_request):
1161 if not self._is_merge_enabled(pull_request):
1156 return False, _('Server-side pull request merging is disabled.')
1162 return False, _('Server-side pull request merging is disabled.')
1157 if pull_request.is_closed():
1163 if pull_request.is_closed():
1158 return False, _('This pull request is closed.')
1164 return False, _('This pull request is closed.')
1159 merge_possible, msg = self._check_repo_requirements(
1165 merge_possible, msg = self._check_repo_requirements(
1160 target=pull_request.target_repo, source=pull_request.source_repo)
1166 target=pull_request.target_repo, source=pull_request.source_repo,
1167 translator=_)
1161 if not merge_possible:
1168 if not merge_possible:
1162 return merge_possible, msg
1169 return merge_possible, msg
1163
1170
@@ -1171,12 +1178,13 b' class PullRequestModel(BaseModel):'
1171
1178
1172 return status
1179 return status
1173
1180
1174 def _check_repo_requirements(self, target, source):
1181 def _check_repo_requirements(self, target, source, translator):
1175 """
1182 """
1176 Check if `target` and `source` have compatible requirements.
1183 Check if `target` and `source` have compatible requirements.
1177
1184
1178 Currently this is just checking for largefiles.
1185 Currently this is just checking for largefiles.
1179 """
1186 """
1187 _ = translator
1180 target_has_largefiles = self._has_largefiles(target)
1188 target_has_largefiles = self._has_largefiles(target)
1181 source_has_largefiles = self._has_largefiles(source)
1189 source_has_largefiles = self._has_largefiles(source)
1182 merge_possible = True
1190 merge_possible = True
@@ -1282,11 +1290,12 b' class PullRequestModel(BaseModel):'
1282 return self.MERGE_STATUS_MESSAGES[status_code]
1290 return self.MERGE_STATUS_MESSAGES[status_code]
1283
1291
1284 def generate_repo_data(self, repo, commit_id=None, branch=None,
1292 def generate_repo_data(self, repo, commit_id=None, branch=None,
1285 bookmark=None):
1293 bookmark=None, translator=None):
1294
1286 all_refs, selected_ref = \
1295 all_refs, selected_ref = \
1287 self._get_repo_pullrequest_sources(
1296 self._get_repo_pullrequest_sources(
1288 repo.scm_instance(), commit_id=commit_id,
1297 repo.scm_instance(), commit_id=commit_id,
1289 branch=branch, bookmark=bookmark)
1298 branch=branch, bookmark=bookmark, translator=translator)
1290
1299
1291 refs_select2 = []
1300 refs_select2 = []
1292 for element in all_refs:
1301 for element in all_refs:
@@ -1327,7 +1336,8 b' class PullRequestModel(BaseModel):'
1327 pass
1336 pass
1328
1337
1329 def _get_repo_pullrequest_sources(
1338 def _get_repo_pullrequest_sources(
1330 self, repo, commit_id=None, branch=None, bookmark=None):
1339 self, repo, commit_id=None, branch=None, bookmark=None,
1340 translator=None):
1331 """
1341 """
1332 Return a structure with repo's interesting commits, suitable for
1342 Return a structure with repo's interesting commits, suitable for
1333 the selectors in pullrequest controller
1343 the selectors in pullrequest controller
@@ -1338,6 +1348,7 b' class PullRequestModel(BaseModel):'
1338 by default - even if closed
1348 by default - even if closed
1339 :param bookmark: a bookmark that must be in the list and selected
1349 :param bookmark: a bookmark that must be in the list and selected
1340 """
1350 """
1351 _ = translator or get_current_request().translate
1341
1352
1342 commit_id = safe_str(commit_id) if commit_id else None
1353 commit_id = safe_str(commit_id) if commit_id else None
1343 branch = safe_str(branch) if branch else None
1354 branch = safe_str(branch) if branch else None
@@ -1505,10 +1516,8 b' class MergeCheck(object):'
1505 )
1516 )
1506
1517
1507 @classmethod
1518 @classmethod
1508 def validate(cls, pull_request, user, fail_early=False, translator=None):
1519 def validate(cls, pull_request, user, translator, fail_early=False):
1509 # if migrated to pyramid...
1520 _ = translator
1510 # _ = lambda: translator or _ # use passed in translator if any
1511
1512 merge_check = cls()
1521 merge_check = cls()
1513
1522
1514 # permissions to merge
1523 # permissions to merge
@@ -1557,7 +1566,8 b' class MergeCheck(object):'
1557 return merge_check
1566 return merge_check
1558
1567
1559 # merge possible
1568 # merge possible
1560 merge_status, msg = PullRequestModel().merge_status(pull_request)
1569 merge_status, msg = PullRequestModel().merge_status(
1570 pull_request, translator=translator)
1561 merge_check.merge_possible = merge_status
1571 merge_check.merge_possible = merge_status
1562 merge_check.merge_msg = msg
1572 merge_check.merge_msg = msg
1563 if not merge_status:
1573 if not merge_status:
@@ -1572,7 +1582,8 b' class MergeCheck(object):'
1572 return merge_check
1582 return merge_check
1573
1583
1574 @classmethod
1584 @classmethod
1575 def get_merge_conditions(cls, pull_request):
1585 def get_merge_conditions(cls, pull_request, translator):
1586 _ = translator
1576 merge_details = {}
1587 merge_details = {}
1577
1588
1578 model = PullRequestModel()
1589 model = PullRequestModel()
@@ -1604,8 +1615,8 b' class MergeCheck(object):'
1604
1615
1605 return merge_details
1616 return merge_details
1606
1617
1607 ChangeTuple = namedtuple('ChangeTuple',
1618 ChangeTuple = collections.namedtuple(
1608 ['added', 'common', 'removed', 'total'])
1619 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1609
1620
1610 FileChangeTuple = namedtuple('FileChangeTuple',
1621 FileChangeTuple = collections.namedtuple(
1611 ['added', 'modified', 'removed'])
1622 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -160,7 +160,7 b' class TestPullRequestModel(object):'
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_once_with(
163 self.merge_mock.assert_called_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
@@ -189,7 +189,7 b' class TestPullRequestModel(object):'
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_once_with(
192 self.merge_mock.assert_called_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
@@ -221,7 +221,7 b' class TestPullRequestModel(object):'
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_once_with(
224 self.merge_mock.assert_called_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
@@ -294,7 +294,7 b' class TestPullRequestModel(object):'
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_once_with(
297 self.merge_mock.assert_called_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
@@ -333,7 +333,7 b' class TestPullRequestModel(object):'
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_once_with(
336 self.merge_mock.assert_called_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
@@ -27,8 +27,20 b' class LazyString(object):'
27 self.args = args
27 self.args = args
28 self.kw = kw
28 self.kw = kw
29
29
30 def eval(self):
31 return _(*self.args, **self.kw)
32
33 def __unicode__(self):
34 return unicode(self.eval())
35
30 def __str__(self):
36 def __str__(self):
31 return _(*self.args, **self.kw)
37 return self.eval()
38
39 def __mod__(self, other):
40 return self.eval() % other
41
42 def format(self, *args):
43 return self.eval().format(*args)
32
44
33
45
34 def lazy_ugettext(*args, **kw):
46 def lazy_ugettext(*args, **kw):
General Comments 0
You need to be logged in to leave comments. Login now