Show More
@@ -0,0 +1,47 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_19_0_0 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 | with op.batch_alter_table(pull_requests.name) as batch_op: | |||
|
32 | new_column = Column('common_ancestor_id', Unicode(255), nullable=True) | |||
|
33 | batch_op.add_column(new_column) | |||
|
34 | ||||
|
35 | pull_request_version = db.PullRequestVersion.__table__ | |||
|
36 | with op.batch_alter_table(pull_request_version.name) as batch_op: | |||
|
37 | new_column = Column('common_ancestor_id', Unicode(255), nullable=True) | |||
|
38 | batch_op.add_column(new_column) | |||
|
39 | ||||
|
40 | ||||
|
41 | def downgrade(migrate_engine): | |||
|
42 | meta = MetaData() | |||
|
43 | meta.bind = migrate_engine | |||
|
44 | ||||
|
45 | ||||
|
46 | def fixups(models, _SESSION): | |||
|
47 | pass |
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}' | |||||
48 | EXTENSIONS = {} |
|
48 | EXTENSIONS = {} | |
49 |
|
49 | |||
50 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
50 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
51 |
__dbversion__ = 10 |
|
51 | __dbversion__ = 107 # defines current db version for migrations | |
52 | __platform__ = platform.system() |
|
52 | __platform__ = platform.system() | |
53 | __license__ = 'AGPLv3, and Commercial License' |
|
53 | __license__ = 'AGPLv3, and Commercial License' | |
54 | __author__ = 'RhodeCode GmbH' |
|
54 | __author__ = 'RhodeCode GmbH' |
@@ -686,28 +686,9 b' def create_pull_request(' | |||||
686 | full_source_ref = resolve_ref_or_error(source_ref, source_db_repo) |
|
686 | full_source_ref = resolve_ref_or_error(source_ref, source_db_repo) | |
687 | full_target_ref = resolve_ref_or_error(target_ref, target_db_repo) |
|
687 | full_target_ref = resolve_ref_or_error(target_ref, target_db_repo) | |
688 |
|
688 | |||
689 | source_scm = source_db_repo.scm_instance() |
|
|||
690 | target_scm = target_db_repo.scm_instance() |
|
|||
691 |
|
||||
692 | source_commit = get_commit_or_error(full_source_ref, source_db_repo) |
|
689 | source_commit = get_commit_or_error(full_source_ref, source_db_repo) | |
693 | target_commit = get_commit_or_error(full_target_ref, target_db_repo) |
|
690 | target_commit = get_commit_or_error(full_target_ref, target_db_repo) | |
694 |
|
691 | |||
695 | ancestor = source_scm.get_common_ancestor( |
|
|||
696 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
|||
697 | if not ancestor: |
|
|||
698 | raise JSONRPCError('no common ancestor found') |
|
|||
699 |
|
||||
700 | # recalculate target ref based on ancestor |
|
|||
701 | target_ref_type, target_ref_name, __ = full_target_ref.split(':') |
|
|||
702 | full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor)) |
|
|||
703 |
|
||||
704 | commit_ranges = target_scm.compare( |
|
|||
705 | target_commit.raw_id, source_commit.raw_id, source_scm, |
|
|||
706 | merge=True, pre_load=[]) |
|
|||
707 |
|
||||
708 | if not commit_ranges: |
|
|||
709 | raise JSONRPCError('no commits found') |
|
|||
710 |
|
||||
711 | reviewer_objects = Optional.extract(reviewers) or [] |
|
692 | reviewer_objects = Optional.extract(reviewers) or [] | |
712 |
|
693 | |||
713 | # serialize and validate passed in given reviewers |
|
694 | # serialize and validate passed in given reviewers | |
@@ -727,16 +708,16 b' def create_pull_request(' | |||||
727 | PullRequestModel().get_reviewer_functions() |
|
708 | PullRequestModel().get_reviewer_functions() | |
728 |
|
709 | |||
729 | # recalculate reviewers logic, to make sure we can validate this |
|
710 | # recalculate reviewers logic, to make sure we can validate this | |
730 |
reviewer |
|
711 | default_reviewers_data = get_default_reviewers_data( | |
731 | owner, source_db_repo, |
|
712 | owner, source_db_repo, | |
732 | source_commit, target_db_repo, target_commit) |
|
713 | source_commit, target_db_repo, target_commit) | |
733 |
|
714 | |||
734 | # now MERGE our given with the calculated |
|
715 | # now MERGE our given with the calculated | |
735 |
reviewer_objects = reviewer |
|
716 | reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects | |
736 |
|
717 | |||
737 | try: |
|
718 | try: | |
738 | reviewers = validate_default_reviewers( |
|
719 | reviewers = validate_default_reviewers( | |
739 |
reviewer_objects, reviewer |
|
720 | reviewer_objects, default_reviewers_data) | |
740 | except ValueError as e: |
|
721 | except ValueError as e: | |
741 | raise JSONRPCError('Reviewers Validation: {}'.format(e)) |
|
722 | raise JSONRPCError('Reviewers Validation: {}'.format(e)) | |
742 |
|
723 | |||
@@ -748,6 +729,24 b' def create_pull_request(' | |||||
748 | source_ref=title_source_ref, |
|
729 | source_ref=title_source_ref, | |
749 | target=target_repo |
|
730 | target=target_repo | |
750 | ) |
|
731 | ) | |
|
732 | ||||
|
733 | diff_info = default_reviewers_data['diff_info'] | |||
|
734 | common_ancestor_id = diff_info['ancestor'] | |||
|
735 | commits = diff_info['commits'] | |||
|
736 | ||||
|
737 | if not common_ancestor_id: | |||
|
738 | raise JSONRPCError('no common ancestor found') | |||
|
739 | ||||
|
740 | if not commits: | |||
|
741 | raise JSONRPCError('no commits found') | |||
|
742 | ||||
|
743 | # NOTE(marcink): reversed is consistent with how we open it in the WEB interface | |||
|
744 | revisions = [commit.raw_id for commit in reversed(commits)] | |||
|
745 | ||||
|
746 | # recalculate target ref based on ancestor | |||
|
747 | target_ref_type, target_ref_name, __ = full_target_ref.split(':') | |||
|
748 | full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id)) | |||
|
749 | ||||
751 | # fetch renderer, if set fallback to plain in case of PR |
|
750 | # fetch renderer, if set fallback to plain in case of PR | |
752 | rc_config = SettingsModel().get_all_settings() |
|
751 | rc_config = SettingsModel().get_all_settings() | |
753 | default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain') |
|
752 | default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain') | |
@@ -760,12 +759,13 b' def create_pull_request(' | |||||
760 | source_ref=full_source_ref, |
|
759 | source_ref=full_source_ref, | |
761 | target_repo=target_repo, |
|
760 | target_repo=target_repo, | |
762 | target_ref=full_target_ref, |
|
761 | target_ref=full_target_ref, | |
763 | revisions=[commit.raw_id for commit in reversed(commit_ranges)], |
|
762 | common_ancestor_id=common_ancestor_id, | |
|
763 | revisions=revisions, | |||
764 | reviewers=reviewers, |
|
764 | reviewers=reviewers, | |
765 | title=title, |
|
765 | title=title, | |
766 | description=description, |
|
766 | description=description, | |
767 | description_renderer=description_renderer, |
|
767 | description_renderer=description_renderer, | |
768 |
reviewer_data=reviewer |
|
768 | reviewer_data=default_reviewers_data, | |
769 | auth_user=apiuser |
|
769 | auth_user=apiuser | |
770 | ) |
|
770 | ) | |
771 |
|
771 |
@@ -484,7 +484,7 b' class TestCompareView(object):' | |||||
484 |
|
484 | |||
485 | # outgoing commits between those commits |
|
485 | # outgoing commits between those commits | |
486 | compare_page = ComparePage(response) |
|
486 | compare_page = ComparePage(response) | |
487 |
compare_page.contains_commits(commits=[commit1] |
|
487 | compare_page.contains_commits(commits=[commit1]) | |
488 |
|
488 | |||
489 | def test_errors_when_comparing_unknown_source_repo(self, backend): |
|
489 | def test_errors_when_comparing_unknown_source_repo(self, backend): | |
490 | repo = backend.repo |
|
490 | repo = backend.repo | |
@@ -641,6 +641,7 b' class ComparePage(AssertResponse):' | |||||
641 | self.contains_one_link( |
|
641 | self.contains_one_link( | |
642 | 'r%s:%s' % (commit.idx, commit.short_id), |
|
642 | 'r%s:%s' % (commit.idx, commit.short_id), | |
643 | self._commit_url(commit)) |
|
643 | self._commit_url(commit)) | |
|
644 | ||||
644 | if ancestors: |
|
645 | if ancestors: | |
645 | response.mustcontain('Ancestor') |
|
646 | response.mustcontain('Ancestor') | |
646 | for ancestor in ancestors: |
|
647 | for ancestor in ancestors: |
@@ -20,6 +20,9 b'' | |||||
20 |
|
20 | |||
21 | from rhodecode.lib import helpers as h |
|
21 | from rhodecode.lib import helpers as h | |
22 | from rhodecode.lib.utils2 import safe_int |
|
22 | from rhodecode.lib.utils2 import safe_int | |
|
23 | from rhodecode.model.pull_request import get_diff_info | |||
|
24 | ||||
|
25 | REVIEWER_API_VERSION = 'V3' | |||
23 |
|
26 | |||
24 |
|
27 | |||
25 | def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None): |
|
28 | def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None): | |
@@ -47,15 +50,20 b' def reviewer_as_json(user, reasons=None,' | |||||
47 |
|
50 | |||
48 | def get_default_reviewers_data( |
|
51 | def get_default_reviewers_data( | |
49 | current_user, source_repo, source_commit, target_repo, target_commit): |
|
52 | current_user, source_repo, source_commit, target_repo, target_commit): | |
|
53 | """ | |||
|
54 | Return json for default reviewers of a repository | |||
|
55 | """ | |||
50 |
|
56 | |||
51 | """ Return json for default reviewers of a repository """ |
|
57 | diff_info = get_diff_info( | |
|
58 | source_repo, source_commit.raw_id, target_repo, target_commit.raw_id) | |||
52 |
|
59 | |||
53 | reasons = ['Default reviewer', 'Repository owner'] |
|
60 | reasons = ['Default reviewer', 'Repository owner'] | |
54 | json_reviewers = [reviewer_as_json( |
|
61 | json_reviewers = [reviewer_as_json( | |
55 | user=target_repo.user, reasons=reasons, mandatory=False, rules=None)] |
|
62 | user=target_repo.user, reasons=reasons, mandatory=False, rules=None)] | |
56 |
|
63 | |||
57 | return { |
|
64 | return { | |
58 |
'api_ver': |
|
65 | 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade | |
|
66 | 'diff_info': diff_info, | |||
59 | 'reviewers': json_reviewers, |
|
67 | 'reviewers': json_reviewers, | |
60 | 'rules': {}, |
|
68 | 'rules': {}, | |
61 | 'rules_data': {}, |
|
69 | 'rules_data': {}, |
@@ -40,12 +40,12 b' from rhodecode.lib.auth import (' | |||||
40 | NotAnonymous, CSRFRequired) |
|
40 | NotAnonymous, CSRFRequired) | |
41 | from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode |
|
41 | from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode | |
42 | from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason |
|
42 | from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason | |
43 |
from rhodecode.lib.vcs.exceptions import ( |
|
43 | from rhodecode.lib.vcs.exceptions import ( | |
44 | RepositoryRequirementError, EmptyRepositoryError) |
|
44 | CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError) | |
45 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
45 | from rhodecode.model.changeset_status import ChangesetStatusModel | |
46 | from rhodecode.model.comment import CommentsModel |
|
46 | from rhodecode.model.comment import CommentsModel | |
47 | from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion, |
|
47 | from rhodecode.model.db import ( | |
48 | ChangesetComment, ChangesetStatus, Repository) |
|
48 | func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository) | |
49 | from rhodecode.model.forms import PullRequestForm |
|
49 | from rhodecode.model.forms import PullRequestForm | |
50 | from rhodecode.model.meta import Session |
|
50 | from rhodecode.model.meta import Session | |
51 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
51 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck | |
@@ -210,10 +210,12 b' class RepoPullRequestsView(RepoAppView, ' | |||||
210 | return caching_enabled |
|
210 | return caching_enabled | |
211 |
|
211 | |||
212 | def _get_diffset(self, source_repo_name, source_repo, |
|
212 | def _get_diffset(self, source_repo_name, source_repo, | |
|
213 | ancestor_commit, | |||
213 | source_ref_id, target_ref_id, |
|
214 | source_ref_id, target_ref_id, | |
214 | target_commit, source_commit, diff_limit, file_limit, |
|
215 | target_commit, source_commit, diff_limit, file_limit, | |
215 | fulldiff, hide_whitespace_changes, diff_context): |
|
216 | fulldiff, hide_whitespace_changes, diff_context): | |
216 |
|
217 | |||
|
218 | target_ref_id = ancestor_commit.raw_id | |||
217 | vcs_diff = PullRequestModel().get_diff( |
|
219 | vcs_diff = PullRequestModel().get_diff( | |
218 | source_repo, source_ref_id, target_ref_id, |
|
220 | source_repo, source_ref_id, target_ref_id, | |
219 | hide_whitespace_changes, diff_context) |
|
221 | hide_whitespace_changes, diff_context) | |
@@ -278,6 +280,7 b' class RepoPullRequestsView(RepoAppView, ' | |||||
278 | _new_state = { |
|
280 | _new_state = { | |
279 | 'created': PullRequest.STATE_CREATED, |
|
281 | 'created': PullRequest.STATE_CREATED, | |
280 | }.get(self.request.GET.get('force_state')) |
|
282 | }.get(self.request.GET.get('force_state')) | |
|
283 | ||||
281 | if c.is_super_admin and _new_state: |
|
284 | if c.is_super_admin and _new_state: | |
282 | with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state): |
|
285 | with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state): | |
283 | h.flash( |
|
286 | h.flash( | |
@@ -557,7 +560,8 b' class RepoPullRequestsView(RepoAppView, ' | |||||
557 | source_scm, |
|
560 | source_scm, | |
558 | target_commit, |
|
561 | target_commit, | |
559 | target_ref_id, |
|
562 | target_ref_id, | |
560 |
target_scm, |
|
563 | target_scm, | |
|
564 | maybe_unreachable=maybe_unreachable) | |||
561 |
|
565 | |||
562 | # register our commit range |
|
566 | # register our commit range | |
563 | for comm in commit_cache.values(): |
|
567 | for comm in commit_cache.values(): | |
@@ -591,11 +595,10 b' class RepoPullRequestsView(RepoAppView, ' | |||||
591 | has_proper_diff_cache = cached_diff and cached_diff.get('commits') |
|
595 | has_proper_diff_cache = cached_diff and cached_diff.get('commits') | |
592 | if not force_recache and has_proper_diff_cache: |
|
596 | if not force_recache and has_proper_diff_cache: | |
593 | c.diffset = cached_diff['diff'] |
|
597 | c.diffset = cached_diff['diff'] | |
594 | (ancestor_commit, commit_cache, missing_requirements, |
|
|||
595 | source_commit, target_commit) = cached_diff['commits'] |
|
|||
596 | else: |
|
598 | else: | |
597 | c.diffset = self._get_diffset( |
|
599 | c.diffset = self._get_diffset( | |
598 | c.source_repo.repo_name, commits_source_repo, |
|
600 | c.source_repo.repo_name, commits_source_repo, | |
|
601 | c.ancestor_commit, | |||
599 | source_ref_id, target_ref_id, |
|
602 | source_ref_id, target_ref_id, | |
600 | target_commit, source_commit, |
|
603 | target_commit, source_commit, | |
601 | diff_limit, file_limit, c.fulldiff, |
|
604 | diff_limit, file_limit, c.fulldiff, | |
@@ -675,8 +678,10 b' class RepoPullRequestsView(RepoAppView, ' | |||||
675 |
|
678 | |||
676 | # calculate the diff for commits between versions |
|
679 | # calculate the diff for commits between versions | |
677 | c.commit_changes = [] |
|
680 | c.commit_changes = [] | |
678 | mark = lambda cs, fw: list( |
|
681 | ||
679 | h.itertools.izip_longest([], cs, fillvalue=fw)) |
|
682 | def mark(cs, fw): | |
|
683 | return list(h.itertools.izip_longest([], cs, fillvalue=fw)) | |||
|
684 | ||||
680 | for c_type, raw_id in mark(commit_changes.added, 'a') \ |
|
685 | for c_type, raw_id in mark(commit_changes.added, 'a') \ | |
681 | + mark(commit_changes.removed, 'r') \ |
|
686 | + mark(commit_changes.removed, 'r') \ | |
682 | + mark(commit_changes.common, 'c'): |
|
687 | + mark(commit_changes.common, 'c'): | |
@@ -739,13 +744,14 b' class RepoPullRequestsView(RepoAppView, ' | |||||
739 | except RepositoryRequirementError: |
|
744 | except RepositoryRequirementError: | |
740 | log.warning('Failed to get all required data from repo', exc_info=True) |
|
745 | log.warning('Failed to get all required data from repo', exc_info=True) | |
741 | missing_requirements = True |
|
746 | missing_requirements = True | |
742 | ancestor_commit = None |
|
747 | ||
|
748 | pr_ancestor_id = pull_request_at_ver.common_ancestor_id | |||
|
749 | ||||
743 | try: |
|
750 | try: | |
744 |
ancestor_ |
|
751 | ancestor_commit = source_scm.get_commit(pr_ancestor_id) | |
745 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
|||
746 | ancestor_commit = source_scm.get_commit(ancestor_id) |
|
|||
747 | except Exception: |
|
752 | except Exception: | |
748 | ancestor_commit = None |
|
753 | ancestor_commit = None | |
|
754 | ||||
749 | return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit |
|
755 | return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit | |
750 |
|
756 | |||
751 | def assure_not_empty_repo(self): |
|
757 | def assure_not_empty_repo(self): | |
@@ -949,6 +955,7 b' class RepoPullRequestsView(RepoAppView, ' | |||||
949 | target_repo = _form['target_repo'] |
|
955 | target_repo = _form['target_repo'] | |
950 | target_ref = _form['target_ref'] |
|
956 | target_ref = _form['target_ref'] | |
951 | commit_ids = _form['revisions'][::-1] |
|
957 | commit_ids = _form['revisions'][::-1] | |
|
958 | common_ancestor_id = _form['common_ancestor'] | |||
952 |
|
959 | |||
953 | # find the ancestor for this pr |
|
960 | # find the ancestor for this pr | |
954 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) |
|
961 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) | |
@@ -1035,6 +1042,7 b' class RepoPullRequestsView(RepoAppView, ' | |||||
1035 | target_repo=target_repo, |
|
1042 | target_repo=target_repo, | |
1036 | target_ref=target_ref, |
|
1043 | target_ref=target_ref, | |
1037 | revisions=commit_ids, |
|
1044 | revisions=commit_ids, | |
|
1045 | common_ancestor_id=common_ancestor_id, | |||
1038 | reviewers=reviewers, |
|
1046 | reviewers=reviewers, | |
1039 | title=pullrequest_title, |
|
1047 | title=pullrequest_title, | |
1040 | description=description, |
|
1048 | description=description, |
@@ -54,8 +54,20 b' class RepoReviewRulesView(RepoAppView):' | |||||
54 | renderer='json_ext') |
|
54 | renderer='json_ext') | |
55 | def repo_default_reviewers_data(self): |
|
55 | def repo_default_reviewers_data(self): | |
56 | self.load_default_context() |
|
56 | self.load_default_context() | |
57 | target_repo_name = self.request.GET.get('target_repo', self.db_repo.repo_name) |
|
57 | ||
|
58 | request = self.request | |||
|
59 | source_repo = self.db_repo | |||
|
60 | source_repo_name = source_repo.repo_name | |||
|
61 | target_repo_name = request.GET.get('target_repo', source_repo_name) | |||
58 | target_repo = Repository.get_by_repo_name(target_repo_name) |
|
62 | target_repo = Repository.get_by_repo_name(target_repo_name) | |
|
63 | ||||
|
64 | source_ref = request.GET['source_ref'] | |||
|
65 | target_ref = request.GET['target_ref'] | |||
|
66 | source_commit = source_repo.get_commit(source_ref) | |||
|
67 | target_commit = target_repo.get_commit(target_ref) | |||
|
68 | ||||
|
69 | current_user = request.user.get_instance() | |||
59 | review_data = get_default_reviewers_data( |
|
70 | review_data = get_default_reviewers_data( | |
60 |
|
|
71 | current_user, source_repo, source_commit, target_repo, target_commit) | |
|
72 | ||||
61 | return review_data |
|
73 | return review_data |
@@ -580,6 +580,9 b' class GitRepository(BaseRepository):' | |||||
580 | return len(self.commit_ids) |
|
580 | return len(self.commit_ids) | |
581 |
|
581 | |||
582 | def get_common_ancestor(self, commit_id1, commit_id2, repo2): |
|
582 | def get_common_ancestor(self, commit_id1, commit_id2, repo2): | |
|
583 | log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s', | |||
|
584 | self, commit_id1, repo2, commit_id2) | |||
|
585 | ||||
583 | if commit_id1 == commit_id2: |
|
586 | if commit_id1 == commit_id2: | |
584 | return commit_id1 |
|
587 | return commit_id1 | |
585 |
|
588 | |||
@@ -600,6 +603,8 b' class GitRepository(BaseRepository):' | |||||
600 | ['merge-base', commit_id1, commit_id2]) |
|
603 | ['merge-base', commit_id1, commit_id2]) | |
601 | ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0] |
|
604 | ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0] | |
602 |
|
605 | |||
|
606 | log.debug('Found common ancestor with sha: %s', ancestor_id) | |||
|
607 | ||||
603 | return ancestor_id |
|
608 | return ancestor_id | |
604 |
|
609 | |||
605 | def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): |
|
610 | def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): |
@@ -297,13 +297,20 b' class MercurialRepository(BaseRepository' | |||||
297 | return update_cache |
|
297 | return update_cache | |
298 |
|
298 | |||
299 | def get_common_ancestor(self, commit_id1, commit_id2, repo2): |
|
299 | def get_common_ancestor(self, commit_id1, commit_id2, repo2): | |
|
300 | log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s', | |||
|
301 | self, commit_id1, repo2, commit_id2) | |||
|
302 | ||||
300 | if commit_id1 == commit_id2: |
|
303 | if commit_id1 == commit_id2: | |
301 | return commit_id1 |
|
304 | return commit_id1 | |
302 |
|
305 | |||
303 | ancestors = self._remote.revs_from_revspec( |
|
306 | ancestors = self._remote.revs_from_revspec( | |
304 | "ancestor(id(%s), id(%s))", commit_id1, commit_id2, |
|
307 | "ancestor(id(%s), id(%s))", commit_id1, commit_id2, | |
305 | other_path=repo2.path) |
|
308 | other_path=repo2.path) | |
306 | return repo2[ancestors[0]].raw_id if ancestors else None |
|
309 | ||
|
310 | ancestor_id = repo2[ancestors[0]].raw_id if ancestors else None | |||
|
311 | ||||
|
312 | log.debug('Found common ancestor with sha: %s', ancestor_id) | |||
|
313 | return ancestor_id | |||
307 |
|
314 | |||
308 | def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): |
|
315 | def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): | |
309 | if commit_id1 == commit_id2: |
|
316 | if commit_id1 == commit_id2: |
@@ -3989,6 +3989,8 b' class _PullRequestBase(BaseModel):' | |||||
3989 | _revisions = Column( |
|
3989 | _revisions = Column( | |
3990 | 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) |
|
3990 | 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) | |
3991 |
|
3991 | |||
|
3992 | common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True) | |||
|
3993 | ||||
3992 | @declared_attr |
|
3994 | @declared_attr | |
3993 | def source_repo_id(cls): |
|
3995 | def source_repo_id(cls): | |
3994 | # TODO: dan: rename column to source_repo_id |
|
3996 | # TODO: dan: rename column to source_repo_id | |
@@ -4302,7 +4304,7 b' class PullRequest(Base, _PullRequestBase' | |||||
4302 | attrs.source_ref_parts = pull_request_obj.source_ref_parts |
|
4304 | attrs.source_ref_parts = pull_request_obj.source_ref_parts | |
4303 | attrs.target_ref_parts = pull_request_obj.target_ref_parts |
|
4305 | attrs.target_ref_parts = pull_request_obj.target_ref_parts | |
4304 | attrs.revisions = pull_request_obj.revisions |
|
4306 | attrs.revisions = pull_request_obj.revisions | |
4305 |
|
4307 | attrs.common_ancestor_id = pull_request_obj.common_ancestor_id | ||
4306 | attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref |
|
4308 | attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref | |
4307 | attrs.reviewer_data = org_pull_request_obj.reviewer_data |
|
4309 | attrs.reviewer_data = org_pull_request_obj.reviewer_data | |
4308 | attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json |
|
4310 | attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json |
@@ -35,6 +35,7 b' import collections' | |||||
35 | from pyramid import compat |
|
35 | from pyramid import compat | |
36 | from pyramid.threadlocal import get_current_request |
|
36 | from pyramid.threadlocal import get_current_request | |
37 |
|
37 | |||
|
38 | from rhodecode.lib.vcs.nodes import FileNode | |||
38 | from rhodecode.translation import lazy_ugettext |
|
39 | from rhodecode.translation import lazy_ugettext | |
39 | from rhodecode.lib import helpers as h, hooks_utils, diffs |
|
40 | from rhodecode.lib import helpers as h, hooks_utils, diffs | |
40 | from rhodecode.lib import audit_logger |
|
41 | from rhodecode.lib import audit_logger | |
@@ -82,6 +83,126 b' class UpdateResponse(object):' | |||||
82 | self.target_changed = target_changed |
|
83 | self.target_changed = target_changed | |
83 |
|
84 | |||
84 |
|
85 | |||
|
86 | def get_diff_info( | |||
|
87 | source_repo, source_ref, target_repo, target_ref, get_authors=False, | |||
|
88 | get_commit_authors=True): | |||
|
89 | """ | |||
|
90 | Calculates detailed diff information for usage in preview of creation of a pull-request. | |||
|
91 | This is also used for default reviewers logic | |||
|
92 | """ | |||
|
93 | ||||
|
94 | source_scm = source_repo.scm_instance() | |||
|
95 | target_scm = target_repo.scm_instance() | |||
|
96 | ||||
|
97 | ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm) | |||
|
98 | if not ancestor_id: | |||
|
99 | raise ValueError( | |||
|
100 | 'cannot calculate diff info without a common ancestor. ' | |||
|
101 | 'Make sure both repositories are related, and have a common forking commit.') | |||
|
102 | ||||
|
103 | # case here is that want a simple diff without incoming commits, | |||
|
104 | # previewing what will be merged based only on commits in the source. | |||
|
105 | log.debug('Using ancestor %s as source_ref instead of %s', | |||
|
106 | ancestor_id, source_ref) | |||
|
107 | ||||
|
108 | # source of changes now is the common ancestor | |||
|
109 | source_commit = source_scm.get_commit(commit_id=ancestor_id) | |||
|
110 | # target commit becomes the source ref as it is the last commit | |||
|
111 | # for diff generation this logic gives proper diff | |||
|
112 | target_commit = source_scm.get_commit(commit_id=source_ref) | |||
|
113 | ||||
|
114 | vcs_diff = \ | |||
|
115 | source_scm.get_diff(commit1=source_commit, commit2=target_commit, | |||
|
116 | ignore_whitespace=False, context=3) | |||
|
117 | ||||
|
118 | diff_processor = diffs.DiffProcessor( | |||
|
119 | vcs_diff, format='newdiff', diff_limit=None, | |||
|
120 | file_limit=None, show_full_diff=True) | |||
|
121 | ||||
|
122 | _parsed = diff_processor.prepare() | |||
|
123 | ||||
|
124 | all_files = [] | |||
|
125 | all_files_changes = [] | |||
|
126 | changed_lines = {} | |||
|
127 | stats = [0, 0] | |||
|
128 | for f in _parsed: | |||
|
129 | all_files.append(f['filename']) | |||
|
130 | all_files_changes.append({ | |||
|
131 | 'filename': f['filename'], | |||
|
132 | 'stats': f['stats'] | |||
|
133 | }) | |||
|
134 | stats[0] += f['stats']['added'] | |||
|
135 | stats[1] += f['stats']['deleted'] | |||
|
136 | ||||
|
137 | changed_lines[f['filename']] = [] | |||
|
138 | if len(f['chunks']) < 2: | |||
|
139 | continue | |||
|
140 | # first line is "context" information | |||
|
141 | for chunks in f['chunks'][1:]: | |||
|
142 | for chunk in chunks['lines']: | |||
|
143 | if chunk['action'] not in ('del', 'mod'): | |||
|
144 | continue | |||
|
145 | changed_lines[f['filename']].append(chunk['old_lineno']) | |||
|
146 | ||||
|
147 | commit_authors = [] | |||
|
148 | user_counts = {} | |||
|
149 | email_counts = {} | |||
|
150 | author_counts = {} | |||
|
151 | _commit_cache = {} | |||
|
152 | ||||
|
153 | commits = [] | |||
|
154 | if get_commit_authors: | |||
|
155 | commits = target_scm.compare( | |||
|
156 | target_ref, source_ref, source_scm, merge=True, | |||
|
157 | pre_load=["author"]) | |||
|
158 | ||||
|
159 | for commit in commits: | |||
|
160 | user = User.get_from_cs_author(commit.author) | |||
|
161 | if user and user not in commit_authors: | |||
|
162 | commit_authors.append(user) | |||
|
163 | ||||
|
164 | # lines | |||
|
165 | if get_authors: | |||
|
166 | target_commit = source_repo.get_commit(ancestor_id) | |||
|
167 | ||||
|
168 | for fname, lines in changed_lines.items(): | |||
|
169 | try: | |||
|
170 | node = target_commit.get_node(fname) | |||
|
171 | except Exception: | |||
|
172 | continue | |||
|
173 | ||||
|
174 | if not isinstance(node, FileNode): | |||
|
175 | continue | |||
|
176 | ||||
|
177 | for annotation in node.annotate: | |||
|
178 | line_no, commit_id, get_commit_func, line_text = annotation | |||
|
179 | if line_no in lines: | |||
|
180 | if commit_id not in _commit_cache: | |||
|
181 | _commit_cache[commit_id] = get_commit_func() | |||
|
182 | commit = _commit_cache[commit_id] | |||
|
183 | author = commit.author | |||
|
184 | email = commit.author_email | |||
|
185 | user = User.get_from_cs_author(author) | |||
|
186 | if user: | |||
|
187 | user_counts[user] = user_counts.get(user, 0) + 1 | |||
|
188 | author_counts[author] = author_counts.get(author, 0) + 1 | |||
|
189 | email_counts[email] = email_counts.get(email, 0) + 1 | |||
|
190 | ||||
|
191 | return { | |||
|
192 | 'commits': commits, | |||
|
193 | 'files': all_files_changes, | |||
|
194 | 'stats': stats, | |||
|
195 | 'ancestor': ancestor_id, | |||
|
196 | # original authors of modified files | |||
|
197 | 'original_authors': { | |||
|
198 | 'users': user_counts, | |||
|
199 | 'authors': author_counts, | |||
|
200 | 'emails': email_counts, | |||
|
201 | }, | |||
|
202 | 'commit_authors': commit_authors | |||
|
203 | } | |||
|
204 | ||||
|
205 | ||||
85 | class PullRequestModel(BaseModel): |
|
206 | class PullRequestModel(BaseModel): | |
86 |
|
207 | |||
87 | cls = PullRequest |
|
208 | cls = PullRequest | |
@@ -453,6 +574,7 b' class PullRequestModel(BaseModel):' | |||||
453 |
|
574 | |||
454 | def create(self, created_by, source_repo, source_ref, target_repo, |
|
575 | def create(self, created_by, source_repo, source_ref, target_repo, | |
455 | target_ref, revisions, reviewers, title, description=None, |
|
576 | target_ref, revisions, reviewers, title, description=None, | |
|
577 | common_ancestor_id=None, | |||
456 | description_renderer=None, |
|
578 | description_renderer=None, | |
457 | reviewer_data=None, translator=None, auth_user=None): |
|
579 | reviewer_data=None, translator=None, auth_user=None): | |
458 | translator = translator or get_current_request().translate |
|
580 | translator = translator or get_current_request().translate | |
@@ -474,6 +596,8 b' class PullRequestModel(BaseModel):' | |||||
474 | pull_request.author = created_by_user |
|
596 | pull_request.author = created_by_user | |
475 | pull_request.reviewer_data = reviewer_data |
|
597 | pull_request.reviewer_data = reviewer_data | |
476 | pull_request.pull_request_state = pull_request.STATE_CREATING |
|
598 | pull_request.pull_request_state = pull_request.STATE_CREATING | |
|
599 | pull_request.common_ancestor_id = common_ancestor_id | |||
|
600 | ||||
477 | Session().add(pull_request) |
|
601 | Session().add(pull_request) | |
478 | Session().flush() |
|
602 | Session().flush() | |
479 |
|
603 | |||
@@ -805,8 +929,17 b' class PullRequestModel(BaseModel):' | |||||
805 | target_commit.raw_id, source_commit.raw_id, source_repo, merge=True, |
|
929 | target_commit.raw_id, source_commit.raw_id, source_repo, merge=True, | |
806 | pre_load=pre_load) |
|
930 | pre_load=pre_load) | |
807 |
|
931 | |||
808 | ancestor_commit_id = source_repo.get_common_ancestor( |
|
932 | target_ref = target_commit.raw_id | |
809 | source_commit.raw_id, target_commit.raw_id, target_repo) |
|
933 | source_ref = source_commit.raw_id | |
|
934 | ancestor_commit_id = target_repo.get_common_ancestor( | |||
|
935 | target_ref, source_ref, source_repo) | |||
|
936 | ||||
|
937 | if not ancestor_commit_id: | |||
|
938 | raise ValueError( | |||
|
939 | 'cannot calculate diff info without a common ancestor. ' | |||
|
940 | 'Make sure both repositories are related, and have a common forking commit.') | |||
|
941 | ||||
|
942 | pull_request.common_ancestor_id = ancestor_commit_id | |||
810 |
|
943 | |||
811 | pull_request.source_ref = '%s:%s:%s' % ( |
|
944 | pull_request.source_ref = '%s:%s:%s' % ( | |
812 | source_ref_type, source_ref_name, source_commit.raw_id) |
|
945 | source_ref_type, source_ref_name, source_commit.raw_id) | |
@@ -913,6 +1046,7 b' class PullRequestModel(BaseModel):' | |||||
913 | version.reviewer_data = pull_request.reviewer_data |
|
1046 | version.reviewer_data = pull_request.reviewer_data | |
914 |
|
1047 | |||
915 | version.revisions = pull_request.revisions |
|
1048 | version.revisions = pull_request.revisions | |
|
1049 | version.common_ancestor_id = pull_request.common_ancestor_id | |||
916 | version.pull_request = pull_request |
|
1050 | version.pull_request = pull_request | |
917 | Session().add(version) |
|
1051 | Session().add(version) | |
918 | Session().flush() |
|
1052 | Session().flush() |
@@ -467,9 +467,9 b' ul.auth_plugins {' | |||||
467 | #pr_open_message { |
|
467 | #pr_open_message { | |
468 | border: @border-thickness solid #fff; |
|
468 | border: @border-thickness solid #fff; | |
469 | border-radius: @border-radius; |
|
469 | border-radius: @border-radius; | |
470 | padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0; |
|
|||
471 | text-align: left; |
|
470 | text-align: left; | |
472 | overflow: hidden; |
|
471 | overflow: hidden; | |
|
472 | white-space: pre-line; | |||
473 | } |
|
473 | } | |
474 |
|
474 | |||
475 | .pr-details-title { |
|
475 | .pr-details-title { | |
@@ -1796,7 +1796,7 b' table.integrations {' | |||||
1796 | } |
|
1796 | } | |
1797 |
|
1797 | |||
1798 | div.ancestor { |
|
1798 | div.ancestor { | |
1799 | line-height: 33px; |
|
1799 | ||
1800 | } |
|
1800 | } | |
1801 |
|
1801 | |||
1802 | .cs_icon_td input[type="checkbox"] { |
|
1802 | .cs_icon_td input[type="checkbox"] { |
@@ -205,7 +205,9 b' select.select2{height:28px;visibility:hi' | |||||
205 | font-family: @text-regular; |
|
205 | font-family: @text-regular; | |
206 | color: @grey2; |
|
206 | color: @grey2; | |
207 | cursor: pointer; |
|
207 | cursor: pointer; | |
|
208 | white-space: nowrap; | |||
208 | } |
|
209 | } | |
|
210 | ||||
209 | &.select2-result-with-children { |
|
211 | &.select2-result-with-children { | |
210 |
|
212 | |||
211 | .select2-result-label { |
|
213 | .select2-result-label { | |
@@ -220,6 +222,7 b' select.select2{height:28px;visibility:hi' | |||||
220 | font-family: @text-regular; |
|
222 | font-family: @text-regular; | |
221 | color: @grey2; |
|
223 | color: @grey2; | |
222 | cursor: pointer; |
|
224 | cursor: pointer; | |
|
225 | white-space: nowrap; | |||
223 | } |
|
226 | } | |
224 | } |
|
227 | } | |
225 | } |
|
228 | } |
@@ -22,7 +22,7 b' var _gettext = function (s) {' | |||||
22 | if (_TM.hasOwnProperty(s)) { |
|
22 | if (_TM.hasOwnProperty(s)) { | |
23 | return _TM[s]; |
|
23 | return _TM[s]; | |
24 | } |
|
24 | } | |
25 |
i18nLog. |
|
25 | i18nLog.warning( | |
26 | 'String `' + s + '` was requested but cannot be ' + |
|
26 | 'String `' + s + '` was requested but cannot be ' + | |
27 | 'found in translation table'); |
|
27 | 'found in translation table'); | |
28 | return s |
|
28 | return s | |
@@ -34,3 +34,5 b' var _ngettext = function (singular, plur' | |||||
34 | } |
|
34 | } | |
35 | return _gettext(plural) |
|
35 | return _gettext(plural) | |
36 | }; |
|
36 | }; | |
|
37 | ||||
|
38 |
@@ -75,12 +75,12 b' var getTitleAndDescription = function(so' | |||||
75 | var desc = ''; |
|
75 | var desc = ''; | |
76 |
|
76 | |||
77 | $.each($(elements).get().reverse().slice(0, limit), function(idx, value) { |
|
77 | $.each($(elements).get().reverse().slice(0, limit), function(idx, value) { | |
78 | var rawMessage = $(value).find('td.td-description .message').data('messageRaw').toString(); |
|
78 | var rawMessage = value['message']; | |
79 | desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n'; |
|
79 | desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n'; | |
80 | }); |
|
80 | }); | |
81 | // only 1 commit, use commit message as title |
|
81 | // only 1 commit, use commit message as title | |
82 | if (elements.length === 1) { |
|
82 | if (elements.length === 1) { | |
83 | var rawMessage = $(elements[0]).find('td.td-description .message').data('messageRaw').toString(); |
|
83 | var rawMessage = elements[0]['message']; | |
84 | title = rawMessage.split('\n')[0]; |
|
84 | title = rawMessage.split('\n')[0]; | |
85 | } |
|
85 | } | |
86 | else { |
|
86 | else { | |
@@ -92,7 +92,6 b' var getTitleAndDescription = function(so' | |||||
92 | }; |
|
92 | }; | |
93 |
|
93 | |||
94 |
|
94 | |||
95 |
|
||||
96 | ReviewersController = function () { |
|
95 | ReviewersController = function () { | |
97 | var self = this; |
|
96 | var self = this; | |
98 | this.$reviewRulesContainer = $('#review_rules'); |
|
97 | this.$reviewRulesContainer = $('#review_rules'); | |
@@ -100,28 +99,35 b' ReviewersController = function () {' | |||||
100 | this.forbidReviewUsers = undefined; |
|
99 | this.forbidReviewUsers = undefined; | |
101 | this.$reviewMembers = $('#review_members'); |
|
100 | this.$reviewMembers = $('#review_members'); | |
102 | this.currentRequest = null; |
|
101 | this.currentRequest = null; | |
|
102 | this.diffData = null; | |||
|
103 | //dummy handler, we might register our own later | |||
|
104 | this.diffDataHandler = function(data){}; | |||
103 |
|
105 | |||
104 | this.defaultForbidReviewUsers = function() { |
|
106 | this.defaultForbidReviewUsers = function () { | |
105 | return [ |
|
107 | return [ | |
106 | {'username': 'default', |
|
108 | { | |