Show More
@@ -0,0 +1,52 b'' | |||||
|
1 | # -*- coding: utf-8 -*- | |||
|
2 | ||||
|
3 | import logging | |||
|
4 | from sqlalchemy import * | |||
|
5 | ||||
|
6 | from alembic.migration import MigrationContext | |||
|
7 | from alembic.operations import Operations | |||
|
8 | from sqlalchemy import BigInteger | |||
|
9 | ||||
|
10 | from rhodecode.lib.dbmigrate.versions import _reset_base | |||
|
11 | from rhodecode.model import init_model_encryption | |||
|
12 | ||||
|
13 | ||||
|
14 | log = logging.getLogger(__name__) | |||
|
15 | ||||
|
16 | ||||
|
17 | def upgrade(migrate_engine): | |||
|
18 | """ | |||
|
19 | Upgrade operations go here. | |||
|
20 | Don't create your own engine; bind migrate_engine to your metadata | |||
|
21 | """ | |||
|
22 | _reset_base(migrate_engine) | |||
|
23 | from rhodecode.lib.dbmigrate.schema import db_4_18_0_1 as db | |||
|
24 | ||||
|
25 | init_model_encryption(db) | |||
|
26 | ||||
|
27 | context = MigrationContext.configure(migrate_engine.connect()) | |||
|
28 | op = Operations(context) | |||
|
29 | ||||
|
30 | pull_requests = db.PullRequest.__table__ | |||
|
31 | ||||
|
32 | with op.batch_alter_table(pull_requests.name) as batch_op: | |||
|
33 | new_column = Column( | |||
|
34 | 'last_merge_metadata', | |||
|
35 | db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) | |||
|
36 | batch_op.add_column(new_column) | |||
|
37 | ||||
|
38 | pull_request_version = db.PullRequestVersion.__table__ | |||
|
39 | with op.batch_alter_table(pull_request_version.name) as batch_op: | |||
|
40 | new_column = Column( | |||
|
41 | 'last_merge_metadata', | |||
|
42 | db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) | |||
|
43 | batch_op.add_column(new_column) | |||
|
44 | ||||
|
45 | ||||
|
46 | def downgrade(migrate_engine): | |||
|
47 | meta = MetaData() | |||
|
48 | meta.bind = migrate_engine | |||
|
49 | ||||
|
50 | ||||
|
51 | def fixups(models, _SESSION): | |||
|
52 | pass |
@@ -45,7 +45,7 b' PYRAMID_SETTINGS = {}' | |||||
45 | EXTENSIONS = {} |
|
45 | EXTENSIONS = {} | |
46 |
|
46 | |||
47 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
47 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
48 |
__dbversion__ = 10 |
|
48 | __dbversion__ = 104 # defines current db version for migrations | |
49 | __platform__ = platform.system() |
|
49 | __platform__ = platform.system() | |
50 | __license__ = 'AGPLv3, and Commercial License' |
|
50 | __license__ = 'AGPLv3, and Commercial License' | |
51 | __author__ = 'RhodeCode GmbH' |
|
51 | __author__ = 'RhodeCode GmbH' |
@@ -629,7 +629,7 b' class TestPullrequestsView(object):' | |||||
629 | model_patcher = mock.patch.multiple( |
|
629 | model_patcher = mock.patch.multiple( | |
630 | PullRequestModel, |
|
630 | PullRequestModel, | |
631 | merge_repo=mock.Mock(return_value=merge_resp), |
|
631 | merge_repo=mock.Mock(return_value=merge_resp), | |
632 | merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE'))) |
|
632 | merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE'))) | |
633 |
|
633 | |||
634 | with model_patcher: |
|
634 | with model_patcher: | |
635 | response = self.app.post( |
|
635 | response = self.app.post( | |
@@ -891,6 +891,8 b' class TestPullrequestsView(object):' | |||||
891 |
|
891 | |||
892 | vcs = repo.scm_instance() |
|
892 | vcs = repo.scm_instance() | |
893 | vcs.remove_ref('refs/heads/{}'.format(branch_name)) |
|
893 | vcs.remove_ref('refs/heads/{}'.format(branch_name)) | |
|
894 | # NOTE(marcink): run GC to ensure the commits are gone | |||
|
895 | vcs.run_gc() | |||
894 |
|
896 | |||
895 | response = self.app.get(route_path( |
|
897 | response = self.app.get(route_path( | |
896 | 'pullrequest_show', |
|
898 | 'pullrequest_show', |
@@ -396,6 +396,7 b' class RepoPullRequestsView(RepoAppView, ' | |||||
396 | pull_request_latest, auth_user=self._rhodecode_user, |
|
396 | pull_request_latest, auth_user=self._rhodecode_user, | |
397 | translator=self.request.translate, |
|
397 | translator=self.request.translate, | |
398 | force_shadow_repo_refresh=force_refresh) |
|
398 | force_shadow_repo_refresh=force_refresh) | |
|
399 | ||||
399 | c.pr_merge_errors = _merge_check.error_details |
|
400 | c.pr_merge_errors = _merge_check.error_details | |
400 | c.pr_merge_possible = not _merge_check.failed |
|
401 | c.pr_merge_possible = not _merge_check.failed | |
401 | c.pr_merge_message = _merge_check.merge_msg |
|
402 | c.pr_merge_message = _merge_check.merge_msg | |
@@ -537,6 +538,13 b' class RepoPullRequestsView(RepoAppView, ' | |||||
537 | (ancestor_commit, commit_cache, missing_requirements, |
|
538 | (ancestor_commit, commit_cache, missing_requirements, | |
538 | source_commit, target_commit) = cached_diff['commits'] |
|
539 | source_commit, target_commit) = cached_diff['commits'] | |
539 | else: |
|
540 | else: | |
|
541 | # NOTE(marcink): we reach potentially unreachable errors when a PR has | |||
|
542 | # merge errors resulting in potentially hidden commits in the shadow repo. | |||
|
543 | maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \ | |||
|
544 | and _merge_check.merge_response | |||
|
545 | maybe_unreachable = maybe_unreachable \ | |||
|
546 | and _merge_check.merge_response.metadata.get('unresolved_files') | |||
|
547 | log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation") | |||
540 | diff_commit_cache = \ |
|
548 | diff_commit_cache = \ | |
541 | (ancestor_commit, commit_cache, missing_requirements, |
|
549 | (ancestor_commit, commit_cache, missing_requirements, | |
542 | source_commit, target_commit) = self.get_commits( |
|
550 | source_commit, target_commit) = self.get_commits( | |
@@ -547,7 +555,7 b' class RepoPullRequestsView(RepoAppView, ' | |||||
547 | source_scm, |
|
555 | source_scm, | |
548 | target_commit, |
|
556 | target_commit, | |
549 | target_ref_id, |
|
557 | target_ref_id, | |
550 | target_scm) |
|
558 | target_scm, maybe_unreachable=maybe_unreachable) | |
551 |
|
559 | |||
552 | # register our commit range |
|
560 | # register our commit range | |
553 | for comm in commit_cache.values(): |
|
561 | for comm in commit_cache.values(): | |
@@ -698,15 +706,22 b' class RepoPullRequestsView(RepoAppView, ' | |||||
698 |
|
706 | |||
699 | def get_commits( |
|
707 | def get_commits( | |
700 | self, commits_source_repo, pull_request_at_ver, source_commit, |
|
708 | self, commits_source_repo, pull_request_at_ver, source_commit, | |
701 |
source_ref_id, source_scm, target_commit, target_ref_id, target_scm |
|
709 | source_ref_id, source_scm, target_commit, target_ref_id, target_scm, | |
|
710 | maybe_unreachable=False): | |||
|
711 | ||||
702 | commit_cache = collections.OrderedDict() |
|
712 | commit_cache = collections.OrderedDict() | |
703 | missing_requirements = False |
|
713 | missing_requirements = False | |
|
714 | ||||
704 | try: |
|
715 | try: | |
705 | pre_load = ["author", "date", "message", "branch", "parents"] |
|
716 | pre_load = ["author", "date", "message", "branch", "parents"] | |
706 | show_revs = pull_request_at_ver.revisions |
|
717 | ||
707 | for rev in show_revs: |
|
718 | pull_request_commits = pull_request_at_ver.revisions | |
708 | comm = commits_source_repo.get_commit( |
|
719 | log.debug('Loading %s commits from %s', | |
709 | commit_id=rev, pre_load=pre_load) |
|
720 | len(pull_request_commits), commits_source_repo) | |
|
721 | ||||
|
722 | for rev in pull_request_commits: | |||
|
723 | comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load, | |||
|
724 | maybe_unreachable=maybe_unreachable) | |||
710 | commit_cache[comm.raw_id] = comm |
|
725 | commit_cache[comm.raw_id] = comm | |
711 |
|
726 | |||
712 | # Order here matters, we first need to get target, and then |
|
727 | # Order here matters, we first need to get target, and then | |
@@ -715,14 +730,12 b' class RepoPullRequestsView(RepoAppView, ' | |||||
715 | commit_id=safe_str(target_ref_id)) |
|
730 | commit_id=safe_str(target_ref_id)) | |
716 |
|
731 | |||
717 | source_commit = commits_source_repo.get_commit( |
|
732 | source_commit = commits_source_repo.get_commit( | |
718 | commit_id=safe_str(source_ref_id)) |
|
733 | commit_id=safe_str(source_ref_id), maybe_unreachable=True) | |
719 | except CommitDoesNotExistError: |
|
734 | except CommitDoesNotExistError: | |
720 | log.warning( |
|
735 | log.warning('Failed to get commit from `{}` repo'.format( | |
721 | 'Failed to get commit from `{}` repo'.format( |
|
736 | commits_source_repo), exc_info=True) | |
722 | commits_source_repo), exc_info=True) |
|
|||
723 | except RepositoryRequirementError: |
|
737 | except RepositoryRequirementError: | |
724 | log.warning( |
|
738 | log.warning('Failed to get all required data from repo', exc_info=True) | |
725 | 'Failed to get all required data from repo', exc_info=True) |
|
|||
726 | missing_requirements = True |
|
739 | missing_requirements = True | |
727 | ancestor_commit = None |
|
740 | ancestor_commit = None | |
728 | try: |
|
741 | try: |
@@ -688,14 +688,17 b' def get_clone_url(request, uri_tmpl, rep' | |||||
688 | return safe_unicode(url) |
|
688 | return safe_unicode(url) | |
689 |
|
689 | |||
690 |
|
690 | |||
691 |
def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None |
|
691 | def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None, | |
|
692 | maybe_unreachable=False): | |||
692 | """ |
|
693 | """ | |
693 | Safe version of get_commit if this commit doesn't exists for a |
|
694 | Safe version of get_commit if this commit doesn't exists for a | |
694 | repository it returns a Dummy one instead |
|
695 | repository it returns a Dummy one instead | |
695 |
|
696 | |||
696 | :param repo: repository instance |
|
697 | :param repo: repository instance | |
697 | :param commit_id: commit id as str |
|
698 | :param commit_id: commit id as str | |
|
699 | :param commit_idx: numeric commit index | |||
698 | :param pre_load: optional list of commit attributes to load |
|
700 | :param pre_load: optional list of commit attributes to load | |
|
701 | :param maybe_unreachable: translate unreachable commits on git repos | |||
699 | """ |
|
702 | """ | |
700 | # TODO(skreft): remove these circular imports |
|
703 | # TODO(skreft): remove these circular imports | |
701 | from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit |
|
704 | from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit | |
@@ -706,7 +709,8 b' def get_commit_safe(repo, commit_id=None' | |||||
706 |
|
709 | |||
707 | try: |
|
710 | try: | |
708 | commit = repo.get_commit( |
|
711 | commit = repo.get_commit( | |
709 |
commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load |
|
712 | commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load, | |
|
713 | maybe_unreachable=maybe_unreachable) | |||
710 | except (RepositoryError, LookupError): |
|
714 | except (RepositoryError, LookupError): | |
711 | commit = EmptyCommit() |
|
715 | commit = EmptyCommit() | |
712 | return commit |
|
716 | return commit |
@@ -216,6 +216,7 b' class MergeResponse(object):' | |||||
216 | Return a human friendly error message for the given merge status code. |
|
216 | Return a human friendly error message for the given merge status code. | |
217 | """ |
|
217 | """ | |
218 | msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason]) |
|
218 | msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason]) | |
|
219 | ||||
219 | try: |
|
220 | try: | |
220 | return msg.format(**self.metadata) |
|
221 | return msg.format(**self.metadata) | |
221 | except Exception: |
|
222 | except Exception: | |
@@ -437,7 +438,8 b' class BaseRepository(object):' | |||||
437 | self._invalidate_prop_cache('commit_ids') |
|
438 | self._invalidate_prop_cache('commit_ids') | |
438 | self._is_empty = False |
|
439 | self._is_empty = False | |
439 |
|
440 | |||
440 |
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, |
|
441 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, | |
|
442 | translate_tag=None, maybe_unreachable=False): | |||
441 | """ |
|
443 | """ | |
442 | Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx` |
|
444 | Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx` | |
443 | are both None, most recent commit is returned. |
|
445 | are both None, most recent commit is returned. |
@@ -228,12 +228,13 b' class GitRepository(BaseRepository):' | |||||
228 | return [] |
|
228 | return [] | |
229 | return output.splitlines() |
|
229 | return output.splitlines() | |
230 |
|
230 | |||
231 | def _lookup_commit(self, commit_id_or_idx, translate_tag=True): |
|
231 | def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False): | |
232 | def is_null(value): |
|
232 | def is_null(value): | |
233 | return len(value) == commit_id_or_idx.count('0') |
|
233 | return len(value) == commit_id_or_idx.count('0') | |
234 |
|
234 | |||
235 | if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1): |
|
235 | if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1): | |
236 | return self.commit_ids[-1] |
|
236 | return self.commit_ids[-1] | |
|
237 | ||||
237 | commit_missing_err = "Commit {} does not exist for `{}`".format( |
|
238 | commit_missing_err = "Commit {} does not exist for `{}`".format( | |
238 | *map(safe_str, [commit_id_or_idx, self.name])) |
|
239 | *map(safe_str, [commit_id_or_idx, self.name])) | |
239 |
|
240 | |||
@@ -248,7 +249,8 b' class GitRepository(BaseRepository):' | |||||
248 | elif is_bstr: |
|
249 | elif is_bstr: | |
249 | # Need to call remote to translate id for tagging scenario |
|
250 | # Need to call remote to translate id for tagging scenario | |
250 | try: |
|
251 | try: | |
251 |
remote_data = self._remote.get_object(commit_id_or_idx |
|
252 | remote_data = self._remote.get_object(commit_id_or_idx, | |
|
253 | maybe_unreachable=maybe_unreachable) | |||
252 | commit_id_or_idx = remote_data["commit_id"] |
|
254 | commit_id_or_idx = remote_data["commit_id"] | |
253 | except (CommitDoesNotExistError,): |
|
255 | except (CommitDoesNotExistError,): | |
254 | raise CommitDoesNotExistError(commit_missing_err) |
|
256 | raise CommitDoesNotExistError(commit_missing_err) | |
@@ -410,7 +412,8 b' class GitRepository(BaseRepository):' | |||||
410 | except Exception: |
|
412 | except Exception: | |
411 | return |
|
413 | return | |
412 |
|
414 | |||
413 |
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, |
|
415 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, | |
|
416 | translate_tag=True, maybe_unreachable=False): | |||
414 | """ |
|
417 | """ | |
415 | Returns `GitCommit` object representing commit from git repository |
|
418 | Returns `GitCommit` object representing commit from git repository | |
416 | at the given `commit_id` or head (most recent commit) if None given. |
|
419 | at the given `commit_id` or head (most recent commit) if None given. | |
@@ -440,7 +443,7 b' class GitRepository(BaseRepository):' | |||||
440 | commit_id = "tip" |
|
443 | commit_id = "tip" | |
441 |
|
444 | |||
442 | if translate_tag: |
|
445 | if translate_tag: | |
443 | commit_id = self._lookup_commit(commit_id) |
|
446 | commit_id = self._lookup_commit(commit_id, maybe_unreachable=maybe_unreachable) | |
444 |
|
447 | |||
445 | try: |
|
448 | try: | |
446 | idx = self._commit_ids[commit_id] |
|
449 | idx = self._commit_ids[commit_id] | |
@@ -662,6 +665,13 b' class GitRepository(BaseRepository):' | |||||
662 | self._remote.remove_ref(ref_name) |
|
665 | self._remote.remove_ref(ref_name) | |
663 | self._invalidate_prop_cache('_refs') |
|
666 | self._invalidate_prop_cache('_refs') | |
664 |
|
667 | |||
|
668 | def run_gc(self, prune=True): | |||
|
669 | cmd = ['gc', '--aggressive'] | |||
|
670 | if prune: | |||
|
671 | cmd += ['--prune=now'] | |||
|
672 | _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False) | |||
|
673 | return stderr | |||
|
674 | ||||
665 | def _update_server_info(self): |
|
675 | def _update_server_info(self): | |
666 | """ |
|
676 | """ | |
667 | runs gits update-server-info command in this repo instance |
|
677 | runs gits update-server-info command in this repo instance | |
@@ -827,8 +837,7 b' class GitRepository(BaseRepository):' | |||||
827 |
|
837 | |||
828 | if self.is_empty(): |
|
838 | if self.is_empty(): | |
829 | # TODO(skreft): do something more robust in this case. |
|
839 | # TODO(skreft): do something more robust in this case. | |
830 | raise RepositoryError( |
|
840 | raise RepositoryError('Do not know how to merge into empty repositories yet') | |
831 | 'Do not know how to merge into empty repositories yet') |
|
|||
832 | unresolved = None |
|
841 | unresolved = None | |
833 |
|
842 | |||
834 | # N.B.(skreft): the --no-ff option is used to enforce the creation of a |
|
843 | # N.B.(skreft): the --no-ff option is used to enforce the creation of a | |
@@ -836,9 +845,11 b' class GitRepository(BaseRepository):' | |||||
836 | cmd = ['-c', 'user.name="%s"' % safe_str(user_name), |
|
845 | cmd = ['-c', 'user.name="%s"' % safe_str(user_name), | |
837 | '-c', 'user.email=%s' % safe_str(user_email), |
|
846 | '-c', 'user.email=%s' % safe_str(user_email), | |
838 | 'merge', '--no-ff', '-m', safe_str(merge_message)] |
|
847 | 'merge', '--no-ff', '-m', safe_str(merge_message)] | |
839 | cmd.extend(heads) |
|
848 | ||
|
849 | merge_cmd = cmd + heads | |||
|
850 | ||||
840 | try: |
|
851 | try: | |
841 |
|
|
852 | self.run_git_command(merge_cmd, fail_on_stderr=False) | |
842 | except RepositoryError: |
|
853 | except RepositoryError: | |
843 | files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'], |
|
854 | files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'], | |
844 | fail_on_stderr=False)[0].splitlines() |
|
855 | fail_on_stderr=False)[0].splitlines() | |
@@ -846,6 +857,7 b' class GitRepository(BaseRepository):' | |||||
846 | unresolved = ['U {}'.format(f) for f in files] |
|
857 | unresolved = ['U {}'.format(f) for f in files] | |
847 |
|
858 | |||
848 | # Cleanup any merge leftovers |
|
859 | # Cleanup any merge leftovers | |
|
860 | self._remote.invalidate_vcs_cache() | |||
849 | self.run_git_command(['merge', '--abort'], fail_on_stderr=False) |
|
861 | self.run_git_command(['merge', '--abort'], fail_on_stderr=False) | |
850 |
|
862 | |||
851 | if unresolved: |
|
863 | if unresolved: |
@@ -429,7 +429,8 b' class MercurialRepository(BaseRepository' | |||||
429 | """ |
|
429 | """ | |
430 | return os.path.join(self.path, '.hg', '.hgrc') |
|
430 | return os.path.join(self.path, '.hg', '.hgrc') | |
431 |
|
431 | |||
432 |
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, |
|
432 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, | |
|
433 | translate_tag=None, maybe_unreachable=False): | |||
433 | """ |
|
434 | """ | |
434 | Returns ``MercurialCommit`` object representing repository's |
|
435 | Returns ``MercurialCommit`` object representing repository's | |
435 | commit at the given `commit_id` or `commit_idx`. |
|
436 | commit at the given `commit_id` or `commit_idx`. |
@@ -276,7 +276,8 b' class SubversionRepository(base.BaseRepo' | |||||
276 | """ |
|
276 | """ | |
277 | return os.path.join(self.path, 'hooks') |
|
277 | return os.path.join(self.path, 'hooks') | |
278 |
|
278 | |||
279 |
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, |
|
279 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, | |
|
280 | translate_tag=None, maybe_unreachable=False): | |||
280 | if self.is_empty(): |
|
281 | if self.is_empty(): | |
281 | raise EmptyRepositoryError("There are no commits yet") |
|
282 | raise EmptyRepositoryError("There are no commits yet") | |
282 | if commit_id is not None: |
|
283 | if commit_id is not None: |
@@ -2339,9 +2339,10 b' class Repository(Base, BaseModel):' | |||||
2339 | # SCM PROPERTIES |
|
2339 | # SCM PROPERTIES | |
2340 | #========================================================================== |
|
2340 | #========================================================================== | |
2341 |
|
2341 | |||
2342 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): |
|
2342 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False): | |
2343 | return get_commit_safe( |
|
2343 | return get_commit_safe( | |
2344 |
self.scm_instance(), commit_id, commit_idx, pre_load=pre_load |
|
2344 | self.scm_instance(), commit_id, commit_idx, pre_load=pre_load, | |
|
2345 | maybe_unreachable=maybe_unreachable) | |||
2345 |
|
2346 | |||
2346 | def get_changeset(self, rev=None, pre_load=None): |
|
2347 | def get_changeset(self, rev=None, pre_load=None): | |
2347 | warnings.warn("Use get_commit", DeprecationWarning) |
|
2348 | warnings.warn("Use get_commit", DeprecationWarning) | |
@@ -4024,6 +4025,10 b' class _PullRequestBase(BaseModel):' | |||||
4024 | _last_merge_target_rev = Column( |
|
4025 | _last_merge_target_rev = Column( | |
4025 | 'last_merge_other_rev', String(40), nullable=True) |
|
4026 | 'last_merge_other_rev', String(40), nullable=True) | |
4026 | _last_merge_status = Column('merge_status', Integer(), nullable=True) |
|
4027 | _last_merge_status = Column('merge_status', Integer(), nullable=True) | |
|
4028 | last_merge_metadata = Column( | |||
|
4029 | 'last_merge_metadata', MutationObj.as_mutable( | |||
|
4030 | JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) | |||
|
4031 | ||||
4027 | merge_rev = Column('merge_rev', String(40), nullable=True) |
|
4032 | merge_rev = Column('merge_rev', String(40), nullable=True) | |
4028 |
|
4033 | |||
4029 | reviewer_data = Column( |
|
4034 | reviewer_data = Column( | |
@@ -4123,10 +4128,11 b' class _PullRequestBase(BaseModel):' | |||||
4123 |
|
4128 | |||
4124 | pull_request = self |
|
4129 | pull_request = self | |
4125 | if with_merge_state: |
|
4130 | if with_merge_state: | |
4126 | merge_status = PullRequestModel().merge_status(pull_request) |
|
4131 | merge_response, merge_status, msg = \ | |
|
4132 | PullRequestModel().merge_status(pull_request) | |||
4127 | merge_state = { |
|
4133 | merge_state = { | |
4128 |
'status': merge_status |
|
4134 | 'status': merge_status, | |
4129 |
'message': safe_unicode(m |
|
4135 | 'message': safe_unicode(msg), | |
4130 | } |
|
4136 | } | |
4131 | else: |
|
4137 | else: | |
4132 | merge_state = {'status': 'not_available', |
|
4138 | merge_state = {'status': 'not_available', |
@@ -885,6 +885,7 b' class PullRequestModel(BaseModel):' | |||||
885 | version._last_merge_source_rev = pull_request._last_merge_source_rev |
|
885 | version._last_merge_source_rev = pull_request._last_merge_source_rev | |
886 | version._last_merge_target_rev = pull_request._last_merge_target_rev |
|
886 | version._last_merge_target_rev = pull_request._last_merge_target_rev | |
887 | version.last_merge_status = pull_request.last_merge_status |
|
887 | version.last_merge_status = pull_request.last_merge_status | |
|
888 | version.last_merge_metadata = pull_request.last_merge_metadata | |||
888 | version.shadow_merge_ref = pull_request.shadow_merge_ref |
|
889 | version.shadow_merge_ref = pull_request.shadow_merge_ref | |
889 | version.merge_rev = pull_request.merge_rev |
|
890 | version.merge_rev = pull_request.merge_rev | |
890 | version.reviewer_data = pull_request.reviewer_data |
|
891 | version.reviewer_data = pull_request.reviewer_data | |
@@ -1349,30 +1350,28 b' class PullRequestModel(BaseModel):' | |||||
1349 |
|
1350 | |||
1350 | return comment, status |
|
1351 | return comment, status | |
1351 |
|
1352 | |||
1352 | def merge_status(self, pull_request, translator=None, |
|
1353 | def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False): | |
1353 | force_shadow_repo_refresh=False): |
|
|||
1354 | _ = translator or get_current_request().translate |
|
1354 | _ = translator or get_current_request().translate | |
1355 |
|
1355 | |||
1356 | if not self._is_merge_enabled(pull_request): |
|
1356 | if not self._is_merge_enabled(pull_request): | |
1357 | return False, _('Server-side pull request merging is disabled.') |
|
1357 | return None, False, _('Server-side pull request merging is disabled.') | |
|
1358 | ||||
1358 | if pull_request.is_closed(): |
|
1359 | if pull_request.is_closed(): | |
1359 | return False, _('This pull request is closed.') |
|
1360 | return None, False, _('This pull request is closed.') | |
|
1361 | ||||
1360 | merge_possible, msg = self._check_repo_requirements( |
|
1362 | merge_possible, msg = self._check_repo_requirements( | |
1361 | target=pull_request.target_repo, source=pull_request.source_repo, |
|
1363 | target=pull_request.target_repo, source=pull_request.source_repo, | |
1362 | translator=_) |
|
1364 | translator=_) | |
1363 | if not merge_possible: |
|
1365 | if not merge_possible: | |
1364 | return merge_possible, msg |
|
1366 | return None, merge_possible, msg | |
1365 |
|
1367 | |||
1366 | try: |
|
1368 | try: | |
1367 | resp = self._try_merge( |
|
1369 | merge_response = self._try_merge( | |
1368 | pull_request, |
|
1370 | pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh) | |
1369 | force_shadow_repo_refresh=force_shadow_repo_refresh) |
|
1371 | log.debug("Merge response: %s", merge_response) | |
1370 | log.debug("Merge response: %s", resp) |
|
1372 | return merge_response, merge_response.possible, merge_response.merge_status_message | |
1371 | status = resp.possible, resp.merge_status_message |
|
|||
1372 | except NotImplementedError: |
|
1373 | except NotImplementedError: | |
1373 |
|
|
1374 | return None, False, _('Pull request merging is not supported.') | |
1374 |
|
||||
1375 | return status |
|
|||
1376 |
|
1375 | |||
1377 | def _check_repo_requirements(self, target, source, translator): |
|
1376 | def _check_repo_requirements(self, target, source, translator): | |
1378 | """ |
|
1377 | """ | |
@@ -1439,6 +1438,9 b' class PullRequestModel(BaseModel):' | |||||
1439 | 'target_ref': pull_request.target_ref_parts, |
|
1438 | 'target_ref': pull_request.target_ref_parts, | |
1440 | 'source_ref': pull_request.source_ref_parts, |
|
1439 | 'source_ref': pull_request.source_ref_parts, | |
1441 | } |
|
1440 | } | |
|
1441 | if pull_request.last_merge_metadata: | |||
|
1442 | metadata.update(pull_request.last_merge_metadata) | |||
|
1443 | ||||
1442 | if not possible and target_ref.type == 'branch': |
|
1444 | if not possible and target_ref.type == 'branch': | |
1443 | # NOTE(marcink): case for mercurial multiple heads on branch |
|
1445 | # NOTE(marcink): case for mercurial multiple heads on branch | |
1444 | heads = target_vcs._heads(target_ref.name) |
|
1446 | heads = target_vcs._heads(target_ref.name) | |
@@ -1447,6 +1449,7 b' class PullRequestModel(BaseModel):' | |||||
1447 | metadata.update({ |
|
1449 | metadata.update({ | |
1448 | 'heads': heads |
|
1450 | 'heads': heads | |
1449 | }) |
|
1451 | }) | |
|
1452 | ||||
1450 | merge_state = MergeResponse( |
|
1453 | merge_state = MergeResponse( | |
1451 | possible, False, None, pull_request.last_merge_status, metadata=metadata) |
|
1454 | possible, False, None, pull_request.last_merge_status, metadata=metadata) | |
1452 |
|
1455 | |||
@@ -1487,6 +1490,8 b' class PullRequestModel(BaseModel):' | |||||
1487 | pull_request.source_ref_parts.commit_id |
|
1490 | pull_request.source_ref_parts.commit_id | |
1488 | pull_request._last_merge_target_rev = target_reference.commit_id |
|
1491 | pull_request._last_merge_target_rev = target_reference.commit_id | |
1489 | pull_request.last_merge_status = merge_state.failure_reason |
|
1492 | pull_request.last_merge_status = merge_state.failure_reason | |
|
1493 | pull_request.last_merge_metadata = merge_state.metadata | |||
|
1494 | ||||
1490 | pull_request.shadow_merge_ref = merge_state.merge_ref |
|
1495 | pull_request.shadow_merge_ref = merge_state.merge_ref | |
1491 | Session().add(pull_request) |
|
1496 | Session().add(pull_request) | |
1492 | Session().commit() |
|
1497 | Session().commit() | |
@@ -1627,7 +1632,7 b' class PullRequestModel(BaseModel):' | |||||
1627 | target_commit = source_repo.get_commit( |
|
1632 | target_commit = source_repo.get_commit( | |
1628 | commit_id=safe_str(target_ref_id)) |
|
1633 | commit_id=safe_str(target_ref_id)) | |
1629 | source_commit = source_repo.get_commit( |
|
1634 | source_commit = source_repo.get_commit( | |
1630 | commit_id=safe_str(source_ref_id)) |
|
1635 | commit_id=safe_str(source_ref_id), maybe_unreachable=True) | |
1631 | if isinstance(source_repo, Repository): |
|
1636 | if isinstance(source_repo, Repository): | |
1632 | vcs_repo = source_repo.scm_instance() |
|
1637 | vcs_repo = source_repo.scm_instance() | |
1633 | else: |
|
1638 | else: | |
@@ -1730,10 +1735,15 b' class MergeCheck(object):' | |||||
1730 | self.review_status = None |
|
1735 | self.review_status = None | |
1731 | self.merge_possible = None |
|
1736 | self.merge_possible = None | |
1732 | self.merge_msg = '' |
|
1737 | self.merge_msg = '' | |
|
1738 | self.merge_response = None | |||
1733 | self.failed = None |
|
1739 | self.failed = None | |
1734 | self.errors = [] |
|
1740 | self.errors = [] | |
1735 | self.error_details = OrderedDict() |
|
1741 | self.error_details = OrderedDict() | |
1736 |
|
1742 | |||
|
1743 | def __repr__(self): | |||
|
1744 | return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format( | |||
|
1745 | self.merge_possible, self.failed, self.errors) | |||
|
1746 | ||||
1737 | def push_error(self, error_type, message, error_key, details): |
|
1747 | def push_error(self, error_type, message, error_key, details): | |
1738 | self.failed = True |
|
1748 | self.failed = True | |
1739 | self.errors.append([error_type, message]) |
|
1749 | self.errors.append([error_type, message]) | |
@@ -1822,11 +1832,14 b' class MergeCheck(object):' | |||||
1822 | return merge_check |
|
1832 | return merge_check | |
1823 |
|
1833 | |||
1824 | # merge possible, here is the filesystem simulation + shadow repo |
|
1834 | # merge possible, here is the filesystem simulation + shadow repo | |
1825 | merge_status, msg = PullRequestModel().merge_status( |
|
1835 | merge_response, merge_status, msg = PullRequestModel().merge_status( | |
1826 | pull_request, translator=translator, |
|
1836 | pull_request, translator=translator, | |
1827 | force_shadow_repo_refresh=force_shadow_repo_refresh) |
|
1837 | force_shadow_repo_refresh=force_shadow_repo_refresh) | |
|
1838 | ||||
1828 | merge_check.merge_possible = merge_status |
|
1839 | merge_check.merge_possible = merge_status | |
1829 | merge_check.merge_msg = msg |
|
1840 | merge_check.merge_msg = msg | |
|
1841 | merge_check.merge_response = merge_response | |||
|
1842 | ||||
1830 | if not merge_status: |
|
1843 | if not merge_status: | |
1831 | log.debug("MergeCheck: cannot merge, pull request merge not possible.") |
|
1844 | log.debug("MergeCheck: cannot merge, pull request merge not possible.") | |
1832 | merge_check.push_error('warning', msg, cls.MERGE_CHECK, None) |
|
1845 | merge_check.push_error('warning', msg, cls.MERGE_CHECK, None) |
@@ -169,7 +169,7 b' class TestPullRequestModel(object):' | |||||
169 | assert pull_request._last_merge_target_rev is None |
|
169 | assert pull_request._last_merge_target_rev is None | |
170 | assert pull_request.last_merge_status is None |
|
170 | assert pull_request.last_merge_status is None | |
171 |
|
171 | |||
172 | status, msg = PullRequestModel().merge_status(pull_request) |
|
172 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
173 | assert status is True |
|
173 | assert status is True | |
174 | assert msg == 'This pull request can be automatically merged.' |
|
174 | assert msg == 'This pull request can be automatically merged.' | |
175 | self.merge_mock.assert_called_with( |
|
175 | self.merge_mock.assert_called_with( | |
@@ -184,7 +184,7 b' class TestPullRequestModel(object):' | |||||
184 | assert pull_request.last_merge_status is MergeFailureReason.NONE |
|
184 | assert pull_request.last_merge_status is MergeFailureReason.NONE | |
185 |
|
185 | |||
186 | self.merge_mock.reset_mock() |
|
186 | self.merge_mock.reset_mock() | |
187 | status, msg = PullRequestModel().merge_status(pull_request) |
|
187 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
188 | assert status is True |
|
188 | assert status is True | |
189 | assert msg == 'This pull request can be automatically merged.' |
|
189 | assert msg == 'This pull request can be automatically merged.' | |
190 | assert self.merge_mock.called is False |
|
190 | assert self.merge_mock.called is False | |
@@ -198,7 +198,7 b' class TestPullRequestModel(object):' | |||||
198 | assert pull_request._last_merge_target_rev is None |
|
198 | assert pull_request._last_merge_target_rev is None | |
199 | assert pull_request.last_merge_status is None |
|
199 | assert pull_request.last_merge_status is None | |
200 |
|
200 | |||
201 | status, msg = PullRequestModel().merge_status(pull_request) |
|
201 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
202 | assert status is False |
|
202 | assert status is False | |
203 | assert msg == 'This pull request cannot be merged because of merge conflicts. file1' |
|
203 | assert msg == 'This pull request cannot be merged because of merge conflicts. file1' | |
204 | self.merge_mock.assert_called_with( |
|
204 | self.merge_mock.assert_called_with( | |
@@ -213,9 +213,9 b' class TestPullRequestModel(object):' | |||||
213 | assert pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED |
|
213 | assert pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED | |
214 |
|
214 | |||
215 | self.merge_mock.reset_mock() |
|
215 | self.merge_mock.reset_mock() | |
216 | status, msg = PullRequestModel().merge_status(pull_request) |
|
216 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
217 | assert status is False |
|
217 | assert status is False | |
218 | assert msg == 'This pull request cannot be merged because of merge conflicts. ' |
|
218 | assert msg == 'This pull request cannot be merged because of merge conflicts. file1' | |
219 | assert self.merge_mock.called is False |
|
219 | assert self.merge_mock.called is False | |
220 |
|
220 | |||
221 | def test_merge_status_unknown_failure(self, pull_request): |
|
221 | def test_merge_status_unknown_failure(self, pull_request): | |
@@ -227,7 +227,7 b' class TestPullRequestModel(object):' | |||||
227 | assert pull_request._last_merge_target_rev is None |
|
227 | assert pull_request._last_merge_target_rev is None | |
228 | assert pull_request.last_merge_status is None |
|
228 | assert pull_request.last_merge_status is None | |
229 |
|
229 | |||
230 | status, msg = PullRequestModel().merge_status(pull_request) |
|
230 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
231 | assert status is False |
|
231 | assert status is False | |
232 | assert msg == ( |
|
232 | assert msg == ( | |
233 | 'This pull request cannot be merged because of an unhandled exception. ' |
|
233 | 'This pull request cannot be merged because of an unhandled exception. ' | |
@@ -244,7 +244,7 b' class TestPullRequestModel(object):' | |||||
244 | assert pull_request.last_merge_status is None |
|
244 | assert pull_request.last_merge_status is None | |
245 |
|
245 | |||
246 | self.merge_mock.reset_mock() |
|
246 | self.merge_mock.reset_mock() | |
247 | status, msg = PullRequestModel().merge_status(pull_request) |
|
247 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
248 | assert status is False |
|
248 | assert status is False | |
249 | assert msg == ( |
|
249 | assert msg == ( | |
250 | 'This pull request cannot be merged because of an unhandled exception. ' |
|
250 | 'This pull request cannot be merged because of an unhandled exception. ' | |
@@ -253,7 +253,7 b' class TestPullRequestModel(object):' | |||||
253 |
|
253 | |||
254 | def test_merge_status_when_target_is_locked(self, pull_request): |
|
254 | def test_merge_status_when_target_is_locked(self, pull_request): | |
255 | pull_request.target_repo.locked = [1, u'12345.50', 'lock_web'] |
|
255 | pull_request.target_repo.locked = [1, u'12345.50', 'lock_web'] | |
256 | status, msg = PullRequestModel().merge_status(pull_request) |
|
256 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
257 | assert status is False |
|
257 | assert status is False | |
258 | assert msg == ( |
|
258 | assert msg == ( | |
259 | 'This pull request cannot be merged because the target repository ' |
|
259 | 'This pull request cannot be merged because the target repository ' | |
@@ -266,7 +266,7 b' class TestPullRequestModel(object):' | |||||
266 |
|
266 | |||
267 | patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) |
|
267 | patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) | |
268 | with patcher: |
|
268 | with patcher: | |
269 | status, msg = PullRequestModel().merge_status(pull_request) |
|
269 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
270 |
|
270 | |||
271 | assert status is False |
|
271 | assert status is False | |
272 | assert msg == 'Target repository large files support is disabled.' |
|
272 | assert msg == 'Target repository large files support is disabled.' | |
@@ -278,7 +278,7 b' class TestPullRequestModel(object):' | |||||
278 |
|
278 | |||
279 | patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) |
|
279 | patcher = mock.patch.object(PullRequestModel, '_has_largefiles', has_largefiles) | |
280 | with patcher: |
|
280 | with patcher: | |
281 | status, msg = PullRequestModel().merge_status(pull_request) |
|
281 | merge_response, status, msg = PullRequestModel().merge_status(pull_request) | |
282 |
|
282 | |||
283 | assert status is False |
|
283 | assert status is False | |
284 | assert msg == 'Source repository large files support is disabled.' |
|
284 | assert msg == 'Source repository large files support is disabled.' |
General Comments 0
You need to be logged in to leave comments.
Login now