##// END OF EJS Templates
pull-requests: fix way how pull-request calculates common ancestors....
marcink -
r4346:4dcd6440 default
parent child Browse files
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__ = 106 # defines current db version for migrations
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_rules = get_default_reviewers_data(
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_rules['reviewers'] + reviewer_objects
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_rules)
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_rules,
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], ancestors=[commit0])
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': 'v1', # define version for later possible schema upgrade
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 (CommitDoesNotExistError,
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, maybe_unreachable=maybe_unreachable)
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_id = source_scm.get_common_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 self.db_repo.user, None, None, target_repo, None)
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.error(
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 {