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])
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
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
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]
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
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)