# HG changeset patch # User Marcin Kuzminski # Date 2019-11-15 00:00:13 # Node ID df62e32ad4a8b3e56503cde2b202d1beae80b642 # Parent da6b715380f1582fc2e0aec8f2fc85eeaf0a66f0 pull-requests: expose unresolved files in merge response. diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -153,7 +153,7 @@ class MergeResponse(object): u'This pull request cannot be merged because of an unhandled exception. ' u'{exception}'), MergeFailureReason.MERGE_FAILED: lazy_ugettext( - u'This pull request cannot be merged because of merge conflicts.'), + u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'), MergeFailureReason.PUSH_FAILED: lazy_ugettext( u'This pull request could not be merged because push to ' u'target:`{target}@{merge_commit}` failed.'), diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -42,7 +42,7 @@ from rhodecode.lib.vcs.backends.git.diff from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit from rhodecode.lib.vcs.exceptions import ( CommitDoesNotExistError, EmptyRepositoryError, - RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError) + RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo) SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$') @@ -826,9 +826,10 @@ class GitRepository(BaseRepository): return if self.is_empty(): - # TODO(skreft): do somehting more robust in this case. + # TODO(skreft): do something more robust in this case. raise RepositoryError( 'Do not know how to merge into empty repositories yet') + unresolved = None # N.B.(skreft): the --no-ff option is used to enforce the creation of a # commit message. We also specify the user who is doing the merge. @@ -839,9 +840,18 @@ class GitRepository(BaseRepository): try: output = self.run_git_command(cmd, fail_on_stderr=False) except RepositoryError: + files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'], + fail_on_stderr=False)[0].splitlines() + # NOTE(marcink): we add U notation for consistent with HG backend output + unresolved = ['U {}'.format(f) for f in files] + # Cleanup any merge leftovers self.run_git_command(['merge', '--abort'], fail_on_stderr=False) - raise + + if unresolved: + raise UnresolvedFilesInRepo(unresolved) + else: + raise def _local_push( self, source_branch, repository_path, target_branch, @@ -977,8 +987,11 @@ class GitRepository(BaseRepository): # the shadow repository. shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id) merge_ref = Reference('branch', 'pr-merge', merge_commit_id) - except RepositoryError: + except RepositoryError as e: log.exception('Failure when doing local merge on git shadow repo') + if isinstance(e, UnresolvedFilesInRepo): + metadata['unresolved_files'] = 'file: ' + (', file: '.join(e.args[0])) + merge_possible = False merge_failure_reason = MergeFailureReason.MERGE_FAILED diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -42,7 +42,7 @@ from rhodecode.lib.vcs.backends.hg.diff from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit from rhodecode.lib.vcs.exceptions import ( EmptyRepositoryError, RepositoryError, TagAlreadyExistError, - TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError) + TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError, UnresolvedFilesInRepo) from rhodecode.lib.vcs.compat import configparser hexlify = binascii.hexlify @@ -634,6 +634,7 @@ class MercurialRepository(BaseRepository # In this case we should force a commit message return source_ref.commit_id, True + unresolved = None if use_rebase: try: bookmark_name = 'rcbook%s%s' % (source_ref.commit_id, @@ -644,17 +645,23 @@ class MercurialRepository(BaseRepository self._remote.invalidate_vcs_cache() self._update(bookmark_name, clean=True) return self._identify(), True - except RepositoryError: + except RepositoryError as e: # The rebase-abort may raise another exception which 'hides' # the original one, therefore we log it here. log.exception('Error while rebasing shadow repo during merge.') + if 'unresolved conflicts' in e.message: + unresolved = self._remote.get_unresolved_files() + log.debug('unresolved files: %s', unresolved) # Cleanup any rebase leftovers self._remote.invalidate_vcs_cache() self._remote.rebase(abort=True) self._remote.invalidate_vcs_cache() self._remote.update(clean=True) - raise + if unresolved: + raise UnresolvedFilesInRepo(unresolved) + else: + raise else: try: self._remote.merge(source_ref.commit_id) @@ -664,10 +671,20 @@ class MercurialRepository(BaseRepository username=safe_str('%s <%s>' % (user_name, user_email))) self._remote.invalidate_vcs_cache() return self._identify(), True - except RepositoryError: + except RepositoryError as e: + # The merge-abort may raise another exception which 'hides' + # the original one, therefore we log it here. + log.exception('Error while merging shadow repo during merge.') + if 'unresolved merge conflicts' in e.message: + unresolved = self._remote.get_unresolved_files() + log.debug('unresolved files: %s', unresolved) + # Cleanup any merge leftovers self._remote.update(clean=True) - raise + if unresolved: + raise UnresolvedFilesInRepo(unresolved) + else: + raise def _local_close(self, target_ref, user_name, user_email, source_ref, close_message=''): @@ -810,8 +827,11 @@ class MercurialRepository(BaseRepository merge_possible = False merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED needs_push = False - except RepositoryError: + except RepositoryError as e: log.exception('Failure when doing local merge on hg shadow repo') + if isinstance(e, UnresolvedFilesInRepo): + metadata['unresolved_files'] = 'file: ' + (', file: '.join(e.args[0])) + merge_possible = False merge_failure_reason = MergeFailureReason.MERGE_FAILED needs_push = False diff --git a/rhodecode/lib/vcs/exceptions.py b/rhodecode/lib/vcs/exceptions.py --- a/rhodecode/lib/vcs/exceptions.py +++ b/rhodecode/lib/vcs/exceptions.py @@ -50,6 +50,10 @@ class RepositoryRequirementError(Reposit pass +class UnresolvedFilesInRepo(RepositoryError): + pass + + class VCSBackendNotSupportedError(VCSError): """ Exception raised when VCSServer does not support requested backend diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -1335,6 +1335,7 @@ class PullRequestModel(BaseModel): else: possible = pull_request.last_merge_status == MergeFailureReason.NONE metadata = { + 'unresolved_files': '', 'target_ref': pull_request.target_ref_parts, 'source_ref': pull_request.source_ref_parts, } diff --git a/rhodecode/tests/models/test_pullrequest.py b/rhodecode/tests/models/test_pullrequest.py --- a/rhodecode/tests/models/test_pullrequest.py +++ b/rhodecode/tests/models/test_pullrequest.py @@ -191,7 +191,8 @@ class TestPullRequestModel(object): def test_merge_status_known_failure(self, pull_request): self.merge_mock.return_value = MergeResponse( - False, False, None, MergeFailureReason.MERGE_FAILED) + False, False, None, MergeFailureReason.MERGE_FAILED, + metadata={'unresolved_files': 'file1'}) assert pull_request._last_merge_source_rev is None assert pull_request._last_merge_target_rev is None @@ -199,7 +200,7 @@ class TestPullRequestModel(object): status, msg = PullRequestModel().merge_status(pull_request) assert status is False - assert msg == 'This pull request cannot be merged because of merge conflicts.' + assert msg == 'This pull request cannot be merged because of merge conflicts. file1' self.merge_mock.assert_called_with( self.repo_id, self.workspace_id, pull_request.target_ref_parts, @@ -209,13 +210,12 @@ class TestPullRequestModel(object): 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) + 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 == 'This pull request cannot be merged because of merge conflicts.' + assert msg == 'This pull request cannot be merged because of merge conflicts. ' assert self.merge_mock.called is False def test_merge_status_unknown_failure(self, pull_request): @@ -518,7 +518,7 @@ def test_outdated_comments( (MergeFailureReason.UNKNOWN, 'This pull request cannot be merged because of an unhandled exception. CRASH'), (MergeFailureReason.MERGE_FAILED, - 'This pull request cannot be merged because of merge conflicts.'), + 'This pull request cannot be merged because of merge conflicts. CONFLICT_FILE'), (MergeFailureReason.PUSH_FAILED, 'This pull request could not be merged because push to target:`some-repo@merge_commit` failed.'), (MergeFailureReason.TARGET_IS_NOT_HEAD, @@ -540,13 +540,15 @@ def test_outdated_comments( def test_merge_response_message(mr_type, expected_msg): merge_ref = Reference('type', 'ref_name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6') metadata = { + 'unresolved_files': 'CONFLICT_FILE', 'exception': "CRASH", 'target': 'some-repo', 'merge_commit': 'merge_commit', 'target_ref': merge_ref, 'source_ref': merge_ref, 'heads': ','.join(['a', 'b', 'c']), - 'locked_by': 'user:123'} + 'locked_by': 'user:123' + } merge_response = MergeResponse(True, True, merge_ref, mr_type, metadata=metadata) assert merge_response.merge_status_message == expected_msg