def _try_merge(self, pull_request):
        """
        Try to merge the pull request and return the merge status.
        """
        log.debug("Trying out if the pull request %s can be merged.",
                  pull_request.pull_request_id)
        target_vcs = pull_request.target_repo.scm_instance()
        target_ref = self._refresh_reference(pull_request.target_ref_parts,
                                             target_vcs)

        target_locked = pull_request.target_repo.locked
        if target_locked and target_locked[0]:
            log.debug("The target repository is locked.")
            merge_state = MergeResponse(False, False, None,
                                        MergeFailureReason.TARGET_IS_LOCKED)
        elif self._needs_merge_state_refresh(pull_request, target_ref):
            log.debug("Refreshing the merge status of the repository.")
            merge_state = self._refresh_merge_state(pull_request, target_vcs,
                                                    target_ref)
        else:
            possible = pull_request.\
                _last_merge_status == MergeFailureReason.NONE
            merge_state = MergeResponse(possible, False, None,
                                        pull_request._last_merge_status)
        log.debug("Merge response: %s", merge_state)
        return merge_state
    def _merge_repo(self, shadow_repository_path, target_ref,
                    source_repo, source_ref, merge_message,
                    merger_name, merger_email, dry_run=False):
        if target_ref.commit_id != self.branches[target_ref.name]:
            return MergeResponse(
                False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)

        shadow_repo = GitRepository(shadow_repository_path)
        shadow_repo._checkout(target_ref.name)
        shadow_repo._local_pull(self.path, target_ref.name)
        # Need to reload repo to invalidate the cache, or otherwise we cannot
        # retrieve the last target commit.
        shadow_repo = GitRepository(shadow_repository_path)
        if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
            return MergeResponse(
                False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)

        pr_branch = shadow_repo._get_new_pr_branch(
            source_ref.name, target_ref.name)
        shadow_repo._checkout(pr_branch, create=True)
        try:
            shadow_repo._local_fetch(source_repo.path, source_ref.name)
        except RepositoryError as e:
            log.exception('Failure when doing local fetch on git shadow repo')
            return MergeResponse(
                False, False, None, MergeFailureReason.MISSING_COMMIT)

        merge_commit_id = None
        merge_failure_reason = MergeFailureReason.NONE
        try:
            shadow_repo._local_merge(merge_message, merger_name, merger_email,
                                     [source_ref.commit_id])
            merge_possible = True
        except RepositoryError as e:
            log.exception('Failure when doing local merge on git shadow repo')
            merge_possible = False
            merge_failure_reason = MergeFailureReason.MERGE_FAILED

        if merge_possible and not dry_run:
            try:
                shadow_repo._local_push(
                    pr_branch, self.path, target_ref.name, enable_hooks=True,
                    rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
                merge_succeeded = True
                # Need to reload repo to invalidate the cache, or otherwise we
                # cannot retrieve the merge commit.
                shadow_repo = GitRepository(shadow_repository_path)
                merge_commit_id = shadow_repo.branches[pr_branch]
            except RepositoryError as e:
                log.exception(
                    'Failure when doing local push on git shadow repo')
                merge_succeeded = False
                merge_failure_reason = MergeFailureReason.PUSH_FAILED
        else:
            merge_succeeded = False

        return MergeResponse(
            merge_possible, merge_succeeded, merge_commit_id,
            merge_failure_reason)
    def test_merge_success(self, vcsbackend):
        self.prepare_for_success(vcsbackend)

        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                'test user',
                                                '*****@*****.**',
                                                'merge message 1',
                                                dry_run=False)
        expected_merge_response = MergeResponse(True, True,
                                                merge_response.merge_commit_id,
                                                MergeFailureReason.NONE)
        assert merge_response == expected_merge_response

        target_repo = backends.get_backend(vcsbackend.alias)(
            self.target_repo.path)
        target_commits = list(target_repo.get_commits())
        commit_ids = [c.raw_id for c in target_commits[:-1]]
        assert self.source_ref.commit_id in commit_ids
        assert self.target_ref.commit_id in commit_ids

        merge_commit = target_commits[-1]
        assert merge_commit.raw_id == merge_response.merge_commit_id
        assert merge_commit.message.strip() == 'merge message 1'
        assert merge_commit.author == 'test user <*****@*****.**>'

        # We call it twice so to make sure we can handle updates
        target_ref = Reference(self.target_ref.type, self.target_ref.name,
                               merge_response.merge_commit_id)

        merge_response = target_repo.merge(target_ref,
                                           self.source_repo,
                                           self.source_ref,
                                           self.workspace,
                                           'test user',
                                           '*****@*****.**',
                                           'merge message 2',
                                           dry_run=False)
        expected_merge_response = MergeResponse(True, True,
                                                merge_response.merge_commit_id,
                                                MergeFailureReason.NONE)
        assert merge_response == expected_merge_response

        target_repo = backends.get_backend(vcsbackend.alias)(
            self.target_repo.path)
        merge_commit = target_repo.get_commit(merge_response.merge_commit_id)
        assert merge_commit.message.strip() == 'merge message 1'
        assert merge_commit.author == 'test user <*****@*****.**>'
    def test_merge_failed(self, pull_request, merge_extras):
        user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
        self.merge_mock.return_value = MergeResponse(
            False, False, '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
            MergeFailureReason.MERGE_FAILED)

        PullRequestModel().merge(pull_request,
                                 pull_request.author,
                                 extras=merge_extras)

        message = (
            u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
            u'\n\n {pr_title}'.format(
                pr_id=pull_request.pull_request_id,
                source_repo=safe_unicode(
                    pull_request.source_repo.scm_instance().name),
                source_ref_name=pull_request.source_ref_parts.name,
                pr_title=safe_unicode(pull_request.title)))
        self.merge_mock.assert_called_once_with(
            pull_request.target_ref_parts,
            pull_request.source_repo.scm_instance(),
            pull_request.source_ref_parts,
            self.workspace_id,
            user_name=user.username,
            user_email=user.email,
            message=message)

        pull_request = PullRequest.get(pull_request.pull_request_id)
        assert self.invalidation_mock.called is False
        assert pull_request.merge_rev is None
    def test_merge_status_known_failure(self, pull_request):
        self.merge_mock.return_value = MergeResponse(
            False, False, None, MergeFailureReason.MERGE_FAILED)

        assert pull_request._last_merge_source_rev is None
        assert pull_request._last_merge_target_rev is None
        assert pull_request._last_merge_status is None

        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is False
        assert (msg.eval() ==
                'This pull request cannot be merged because of conflicts.')
        self.merge_mock.assert_called_once_with(
            pull_request.target_ref_parts,
            pull_request.source_repo.scm_instance(),
            pull_request.source_ref_parts,
            self.workspace_id,
            dry_run=True)

        assert pull_request._last_merge_source_rev == self.source_commit
        assert pull_request._last_merge_target_rev == self.target_commit
        assert (pull_request._last_merge_status is
                MergeFailureReason.MERGE_FAILED)

        self.merge_mock.reset_mock()
        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is False
        assert (msg.eval() ==
                'This pull request cannot be merged because of conflicts.')
        assert self.merge_mock.called is False
    def test_merge_status_unknown_failure(self, pull_request):
        self.merge_mock.return_value = MergeResponse(
            False, False, None, MergeFailureReason.UNKNOWN)

        assert pull_request._last_merge_source_rev is None
        assert pull_request._last_merge_target_rev is None
        assert pull_request._last_merge_status is None

        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is False
        assert msg.eval() == (
            'This pull request cannot be merged because of an unhandled'
            ' exception.')
        self.merge_mock.assert_called_once_with(
            pull_request.target_ref_parts,
            pull_request.source_repo.scm_instance(),
            pull_request.source_ref_parts,
            self.workspace_id,
            dry_run=True)

        assert pull_request._last_merge_source_rev is None
        assert pull_request._last_merge_target_rev is None
        assert pull_request._last_merge_status is None

        self.merge_mock.reset_mock()
        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is False
        assert msg.eval() == (
            'This pull request cannot be merged because of an unhandled'
            ' exception.')
        assert self.merge_mock.called is True
    def test_merge_conflict(self, vcsbackend, dry_run):
        self.prepare_for_conflict(vcsbackend)
        expected_merge_response = MergeResponse(
            False, False, None, MergeFailureReason.MERGE_FAILED)

        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                'test_user',
                                                '*****@*****.**',
                                                'test message',
                                                dry_run=dry_run)
        assert merge_response == expected_merge_response

        # We call it twice so to make sure we can handle updates
        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                'test_user',
                                                '*****@*****.**',
                                                'test message',
                                                dry_run=dry_run)
        assert merge_response == expected_merge_response
    def test_merge_status(self, pull_request):
        self.merge_mock.return_value = MergeResponse(True, False, None,
                                                     MergeFailureReason.NONE)

        assert pull_request._last_merge_source_rev is None
        assert pull_request._last_merge_target_rev is None
        assert pull_request._last_merge_status is None

        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is True
        assert msg.eval() == 'This pull request can be automatically merged.'
        self.merge_mock.assert_called_once_with(
            pull_request.target_ref_parts,
            pull_request.source_repo.scm_instance(),
            pull_request.source_ref_parts,
            self.workspace_id,
            dry_run=True)

        assert pull_request._last_merge_source_rev == self.source_commit
        assert pull_request._last_merge_target_rev == self.target_commit
        assert pull_request._last_merge_status is MergeFailureReason.NONE

        self.merge_mock.reset_mock()
        status, msg = PullRequestModel().merge_status(pull_request)
        assert status is True
        assert msg.eval() == 'This pull request can be automatically merged.'
        assert self.merge_mock.called is False
def test_merge_pull_request_renders_failure_reason(user_regular):
    pull_request = mock.Mock()
    controller = pullrequests.PullrequestsController()
    model_patcher = mock.patch.multiple(
        PullRequestModel,
        merge=mock.Mock(return_value=MergeResponse(
            True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
        merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
    with model_patcher:
        controller._merge_pull_request(pull_request, user_regular, extras={})

    assert_session_flash(msg=PullRequestModel.MERGE_STATUS_MESSAGES[
        MergeFailureReason.PUSH_FAILED])
Example #10
0
    def test_merge_rebase_source_is_updated_bookmark(self, vcsbackend_hg):
        target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
        source_repo = vcsbackend_hg.clone_repo(target_repo)
        vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
        vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
        imc = source_repo.in_memory_commit
        imc.add(FileNode('file_x', content=source_repo.name))
        imc.commit(message=u'Automatic commit from repo merge test',
                   author=u'Automatic')
        target_commit = target_repo.get_commit()
        source_commit = source_repo.get_commit()

        vcsbackend_hg.add_file(source_repo, 'LICENSE', 'LICENSE Info')

        default_branch = target_repo.DEFAULT_BRANCH_NAME
        bookmark_name = 'bookmark'
        source_repo._update(default_branch)
        source_repo.bookmark(bookmark_name)

        target_ref = Reference('branch', default_branch, target_commit.raw_id)
        source_ref = Reference('book', bookmark_name, source_commit.raw_id)
        workspace = 'test-merge'

        with mock.patch.object(rhodecode.lib.vcs.conf.settings,
                               'HG_USE_REBASE_FOR_MERGING',
                               return_value=True):
            merge_response = target_repo.merge(target_ref,
                                               source_repo,
                                               source_ref,
                                               workspace,
                                               'test user',
                                               '*****@*****.**',
                                               'merge message 1',
                                               dry_run=False)

        expected_merge_response = MergeResponse(True, True,
                                                merge_response.merge_commit_id,
                                                MergeFailureReason.NONE)
        assert merge_response == expected_merge_response

        target_repo = backends.get_backend(vcsbackend_hg.alias)(
            target_repo.path)
        last_commit = target_repo.get_commit()
        assert last_commit.message == source_commit.message
        assert last_commit.author == source_commit.author
        # This checks that we effectively did a rebase
        assert last_commit.raw_id != source_commit.raw_id

        # Check the target has only 4 commits: 2 were already in target and
        # only two should have been added
        assert len(target_repo.commit_ids) == 2 + 2
    def test_merge_missing_commit(self, vcsbackend):
        self.prepare_for_success(vcsbackend)
        expected_merge_response = MergeResponse(
            False, False, None, MergeFailureReason.MISSING_COMMIT)

        source_ref = Reference(self.source_ref.type, 'not_existing',
                               self.source_ref.commit_id)

        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                source_ref,
                                                self.workspace,
                                                dry_run=True)

        assert merge_response == expected_merge_response
    def test_merge_raises_exception(self, vcsbackend):
        self.prepare_for_success(vcsbackend)
        expected_merge_response = MergeResponse(False, False, None,
                                                MergeFailureReason.UNKNOWN)

        with mock.patch.object(self.target_repo,
                               '_merge_repo',
                               side_effect=RepositoryError()):
            merge_response = self.target_repo.merge(self.target_ref,
                                                    self.source_repo,
                                                    self.source_ref,
                                                    self.workspace,
                                                    dry_run=True)

        assert merge_response == expected_merge_response
    def test_merge_target_is_not_head(self, vcsbackend):
        self.prepare_for_success(vcsbackend)
        expected_merge_response = MergeResponse(
            False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)

        target_ref = Reference(self.target_ref.type, self.target_ref.name,
                               '0' * 40)

        merge_response = self.target_repo.merge(target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                dry_run=True)

        assert merge_response == expected_merge_response
Example #14
0
    def test_merge_target_is_bookmark(self, vcsbackend_hg):
        target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
        source_repo = vcsbackend_hg.clone_repo(target_repo)
        vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
        vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')
        imc = source_repo.in_memory_commit
        imc.add(FileNode('file_x', content=source_repo.name))
        imc.commit(message=u'Automatic commit from repo merge test',
                   author=u'Automatic')
        target_commit = target_repo.get_commit()
        source_commit = source_repo.get_commit()
        default_branch = target_repo.DEFAULT_BRANCH_NAME
        bookmark_name = 'bookmark'
        target_repo._update(default_branch)
        target_repo.bookmark(bookmark_name)
        target_ref = Reference('book', bookmark_name, target_commit.raw_id)
        source_ref = Reference('branch', default_branch, source_commit.raw_id)
        workspace = 'test-merge'

        merge_response = target_repo.merge(target_ref,
                                           source_repo,
                                           source_ref,
                                           workspace,
                                           'test user',
                                           '*****@*****.**',
                                           'merge message 1',
                                           dry_run=False)
        expected_merge_response = MergeResponse(True, True,
                                                merge_response.merge_commit_id,
                                                MergeFailureReason.NONE)
        assert merge_response == expected_merge_response

        target_repo = backends.get_backend(vcsbackend_hg.alias)(
            target_repo.path)
        target_commits = list(target_repo.get_commits())
        commit_ids = [c.raw_id for c in target_commits[:-1]]
        assert source_ref.commit_id in commit_ids
        assert target_ref.commit_id in commit_ids

        merge_commit = target_commits[-1]
        assert merge_commit.raw_id == merge_response.merge_commit_id
        assert merge_commit.message.strip() == 'merge message 1'
        assert merge_commit.author == 'test user <*****@*****.**>'

        # Check the bookmark was updated in the target repo
        assert (target_repo.bookmarks[bookmark_name] ==
                merge_response.merge_commit_id)
    def test_merge_success_dry_run(self, vcsbackend):
        self.prepare_for_success(vcsbackend)
        expected_merge_response = MergeResponse(True, False, None,
                                                MergeFailureReason.NONE)

        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                dry_run=True)
        assert merge_response == expected_merge_response

        # We call it twice so to make sure we can handle updates
        merge_response = self.target_repo.merge(self.target_ref,
                                                self.source_repo,
                                                self.source_ref,
                                                self.workspace,
                                                dry_run=True)
        assert merge_response == expected_merge_response
Example #16
0
    def test_merge_source_is_bookmark(self, vcsbackend_hg):
        target_repo = vcsbackend_hg.create_repo(number_of_commits=1)
        source_repo = vcsbackend_hg.clone_repo(target_repo)
        imc = source_repo.in_memory_commit
        imc.add(FileNode('file_x', content=source_repo.name))
        imc.commit(message=u'Automatic commit from repo merge test',
                   author=u'Automatic')
        target_commit = target_repo.get_commit()
        source_commit = source_repo.get_commit()
        default_branch = target_repo.DEFAULT_BRANCH_NAME
        bookmark_name = 'bookmark'
        target_ref = Reference('branch', default_branch, target_commit.raw_id)
        source_repo._update(default_branch)
        source_repo.bookmark(bookmark_name)
        source_ref = Reference('book', bookmark_name, source_commit.raw_id)
        workspace = 'test-merge'

        merge_response = target_repo.merge(target_ref,
                                           source_repo,
                                           source_ref,
                                           workspace,
                                           'test user',
                                           '*****@*****.**',
                                           'merge message 1',
                                           dry_run=False)
        expected_merge_response = MergeResponse(True, True,
                                                merge_response.merge_commit_id,
                                                MergeFailureReason.NONE)
        assert merge_response == expected_merge_response

        target_repo = backends.get_backend(vcsbackend_hg.alias)(
            target_repo.path)
        target_commits = list(target_repo.get_commits())
        commit_ids = [c.raw_id for c in target_commits]
        assert source_ref.commit_id == commit_ids[-1]
        assert target_ref.commit_id == commit_ids[-2]
Example #17
0
    def test_merge_target_has_multiple_heads(self, vcsbackend_hg):
        target_repo = vcsbackend_hg.create_repo(number_of_commits=2)
        source_repo = vcsbackend_hg.clone_repo(target_repo)
        vcsbackend_hg.add_file(target_repo, 'README_MERGE1', 'Version 1')
        vcsbackend_hg.add_file(source_repo, 'README_MERGE2', 'Version 2')

        # add an extra head to the target repo
        imc = target_repo.in_memory_commit
        imc.add(FileNode('file_x', content='foo'))
        commits = list(target_repo.get_commits())
        imc.commit(message=u'Automatic commit from repo merge test',
                   author=u'Automatic',
                   parents=commits[0:1])

        target_commit = target_repo.get_commit()
        source_commit = source_repo.get_commit()
        default_branch = target_repo.DEFAULT_BRANCH_NAME
        target_repo._update(default_branch)

        target_ref = Reference('branch', default_branch, target_commit.raw_id)
        source_ref = Reference('branch', default_branch, source_commit.raw_id)
        workspace = 'test-merge'

        assert len(target_repo._heads(branch='default')) == 2
        expected_merge_response = MergeResponse(
            False, False, None,
            MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
        merge_response = target_repo.merge(target_ref,
                                           source_repo,
                                           source_ref,
                                           workspace,
                                           'test user',
                                           '*****@*****.**',
                                           'merge message 1',
                                           dry_run=False)
        assert merge_response == expected_merge_response
Example #18
0
    def _merge_repo(self,
                    shadow_repository_path,
                    target_ref,
                    source_repo,
                    source_ref,
                    merge_message,
                    merger_name,
                    merger_email,
                    dry_run=False):
        if target_ref.commit_id not in self._heads():
            return MergeResponse(False, False, None,
                                 MergeFailureReason.TARGET_IS_NOT_HEAD)

        if (target_ref.type == 'branch'
                and len(self._heads(target_ref.name)) != 1):
            return MergeResponse(
                False, False, None,
                MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)

        shadow_repo = self._get_shadow_instance(shadow_repository_path)

        log.debug('Pulling in target reference %s', target_ref)
        self._validate_pull_reference(target_ref)
        shadow_repo._local_pull(self.path, target_ref)
        try:
            log.debug('Pulling in source reference %s', source_ref)
            source_repo._validate_pull_reference(source_ref)
            shadow_repo._local_pull(source_repo.path, source_ref)
        except CommitDoesNotExistError as e:
            log.exception('Failure when doing local pull on hg shadow repo')
            return MergeResponse(False, False, None,
                                 MergeFailureReason.MISSING_COMMIT)

        merge_commit_id = None
        merge_failure_reason = MergeFailureReason.NONE

        try:
            merge_commit_id, needs_push = shadow_repo._local_merge(
                target_ref, merge_message, merger_name, merger_email,
                source_ref)
            merge_possible = True
        except RepositoryError as e:
            log.exception('Failure when doing local merge on hg shadow repo')
            merge_possible = False
            merge_failure_reason = MergeFailureReason.MERGE_FAILED

        if merge_possible and not dry_run:
            if needs_push:
                # In case the target is a bookmark, update it, so after pushing
                # the bookmarks is also updated in the target.
                if target_ref.type == 'book':
                    shadow_repo.bookmark(target_ref.name,
                                         revision=merge_commit_id)

                try:
                    shadow_repo_with_hooks = self._get_shadow_instance(
                        shadow_repository_path, enable_hooks=True)
                    # Note: the push_branches option will push any new branch
                    # defined in the source repository to the target. This may
                    # be dangerous as branches are permanent in Mercurial.
                    # This feature was requested in issue #441.
                    shadow_repo_with_hooks._local_push(merge_commit_id,
                                                       self.path,
                                                       push_branches=True,
                                                       enable_hooks=True)
                    merge_succeeded = True
                except RepositoryError:
                    log.exception(
                        'Failure when doing local push from the shadow '
                        'repository to the target repository.')
                    merge_succeeded = False
                    merge_failure_reason = MergeFailureReason.PUSH_FAILED
            else:
                merge_succeeded = True
        else:
            merge_succeeded = False

        if dry_run:
            merge_commit_id = None

        return MergeResponse(merge_possible, merge_succeeded, merge_commit_id,
                             merge_failure_reason)