##// 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 48 EXTENSIONS = {}
49 49
50 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 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -686,28 +686,9 b' def create_pull_request('
686 686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 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 689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
693 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 692 reviewer_objects = Optional.extract(reviewers) or []
712 693
713 694 # serialize and validate passed in given reviewers
@@ -727,16 +708,16 b' def create_pull_request('
727 708 PullRequestModel().get_reviewer_functions()
728 709
729 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 712 owner, source_db_repo,
732 713 source_commit, target_db_repo, target_commit)
733 714
734 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 718 try:
738 719 reviewers = validate_default_reviewers(
739 reviewer_objects, reviewer_rules)
720 reviewer_objects, default_reviewers_data)
740 721 except ValueError as e:
741 722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
742 723
@@ -748,6 +729,24 b' def create_pull_request('
748 729 source_ref=title_source_ref,
749 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 750 # fetch renderer, if set fallback to plain in case of PR
752 751 rc_config = SettingsModel().get_all_settings()
753 752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
@@ -760,12 +759,13 b' def create_pull_request('
760 759 source_ref=full_source_ref,
761 760 target_repo=target_repo,
762 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 764 reviewers=reviewers,
765 765 title=title,
766 766 description=description,
767 767 description_renderer=description_renderer,
768 reviewer_data=reviewer_rules,
768 reviewer_data=default_reviewers_data,
769 769 auth_user=apiuser
770 770 )
771 771
@@ -484,7 +484,7 b' class TestCompareView(object):'
484 484
485 485 # outgoing commits between those commits
486 486 compare_page = ComparePage(response)
487 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
487 compare_page.contains_commits(commits=[commit1])
488 488
489 489 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 490 repo = backend.repo
@@ -641,6 +641,7 b' class ComparePage(AssertResponse):'
641 641 self.contains_one_link(
642 642 'r%s:%s' % (commit.idx, commit.short_id),
643 643 self._commit_url(commit))
644
644 645 if ancestors:
645 646 response.mustcontain('Ancestor')
646 647 for ancestor in ancestors:
@@ -20,6 +20,9 b''
20 20
21 21 from rhodecode.lib import helpers as h
22 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 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 51 def get_default_reviewers_data(
49 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 60 reasons = ['Default reviewer', 'Repository owner']
54 61 json_reviewers = [reviewer_as_json(
55 62 user=target_repo.user, reasons=reasons, mandatory=False, rules=None)]
56 63
57 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 67 'reviewers': json_reviewers,
60 68 'rules': {},
61 69 'rules_data': {},
@@ -40,12 +40,12 b' from rhodecode.lib.auth import ('
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
47 from rhodecode.model.db import (
48 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
@@ -210,10 +210,12 b' class RepoPullRequestsView(RepoAppView, '
210 210 return caching_enabled
211 211
212 212 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
213 214 source_ref_id, target_ref_id,
214 215 target_commit, source_commit, diff_limit, file_limit,
215 216 fulldiff, hide_whitespace_changes, diff_context):
216 217
218 target_ref_id = ancestor_commit.raw_id
217 219 vcs_diff = PullRequestModel().get_diff(
218 220 source_repo, source_ref_id, target_ref_id,
219 221 hide_whitespace_changes, diff_context)
@@ -278,6 +280,7 b' class RepoPullRequestsView(RepoAppView, '
278 280 _new_state = {
279 281 'created': PullRequest.STATE_CREATED,
280 282 }.get(self.request.GET.get('force_state'))
283
281 284 if c.is_super_admin and _new_state:
282 285 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
283 286 h.flash(
@@ -557,7 +560,8 b' class RepoPullRequestsView(RepoAppView, '
557 560 source_scm,
558 561 target_commit,
559 562 target_ref_id,
560 target_scm, maybe_unreachable=maybe_unreachable)
563 target_scm,
564 maybe_unreachable=maybe_unreachable)
561 565
562 566 # register our commit range
563 567 for comm in commit_cache.values():
@@ -591,11 +595,10 b' class RepoPullRequestsView(RepoAppView, '
591 595 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
592 596 if not force_recache and has_proper_diff_cache:
593 597 c.diffset = cached_diff['diff']
594 (ancestor_commit, commit_cache, missing_requirements,
595 source_commit, target_commit) = cached_diff['commits']
596 598 else:
597 599 c.diffset = self._get_diffset(
598 600 c.source_repo.repo_name, commits_source_repo,
601 c.ancestor_commit,
599 602 source_ref_id, target_ref_id,
600 603 target_commit, source_commit,
601 604 diff_limit, file_limit, c.fulldiff,
@@ -675,8 +678,10 b' class RepoPullRequestsView(RepoAppView, '
675 678
676 679 # calculate the diff for commits between versions
677 680 c.commit_changes = []
678 mark = lambda cs, fw: list(
679 h.itertools.izip_longest([], cs, fillvalue=fw))
681
682 def mark(cs, fw):
683 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
684
680 685 for c_type, raw_id in mark(commit_changes.added, 'a') \
681 686 + mark(commit_changes.removed, 'r') \
682 687 + mark(commit_changes.common, 'c'):
@@ -739,13 +744,14 b' class RepoPullRequestsView(RepoAppView, '
739 744 except RepositoryRequirementError:
740 745 log.warning('Failed to get all required data from repo', exc_info=True)
741 746 missing_requirements = True
742 ancestor_commit = None
747
748 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
749
743 750 try:
744 ancestor_id = source_scm.get_common_ancestor(
745 source_commit.raw_id, target_commit.raw_id, target_scm)
746 ancestor_commit = source_scm.get_commit(ancestor_id)
751 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
747 752 except Exception:
748 753 ancestor_commit = None
754
749 755 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
750 756
751 757 def assure_not_empty_repo(self):
@@ -949,6 +955,7 b' class RepoPullRequestsView(RepoAppView, '
949 955 target_repo = _form['target_repo']
950 956 target_ref = _form['target_ref']
951 957 commit_ids = _form['revisions'][::-1]
958 common_ancestor_id = _form['common_ancestor']
952 959
953 960 # find the ancestor for this pr
954 961 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
@@ -1035,6 +1042,7 b' class RepoPullRequestsView(RepoAppView, '
1035 1042 target_repo=target_repo,
1036 1043 target_ref=target_ref,
1037 1044 revisions=commit_ids,
1045 common_ancestor_id=common_ancestor_id,
1038 1046 reviewers=reviewers,
1039 1047 title=pullrequest_title,
1040 1048 description=description,
@@ -54,8 +54,20 b' class RepoReviewRulesView(RepoAppView):'
54 54 renderer='json_ext')
55 55 def repo_default_reviewers_data(self):
56 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 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 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 73 return review_data
@@ -580,6 +580,9 b' class GitRepository(BaseRepository):'
580 580 return len(self.commit_ids)
581 581
582 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 586 if commit_id1 == commit_id2:
584 587 return commit_id1
585 588
@@ -600,6 +603,8 b' class GitRepository(BaseRepository):'
600 603 ['merge-base', commit_id1, commit_id2])
601 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 608 return ancestor_id
604 609
605 610 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
@@ -297,13 +297,20 b' class MercurialRepository(BaseRepository'
297 297 return update_cache
298 298
299 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 303 if commit_id1 == commit_id2:
301 304 return commit_id1
302 305
303 306 ancestors = self._remote.revs_from_revspec(
304 307 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
305 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 315 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
309 316 if commit_id1 == commit_id2:
@@ -3989,6 +3989,8 b' class _PullRequestBase(BaseModel):'
3989 3989 _revisions = Column(
3990 3990 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3991 3991
3992 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
3993
3992 3994 @declared_attr
3993 3995 def source_repo_id(cls):
3994 3996 # TODO: dan: rename column to source_repo_id
@@ -4302,7 +4304,7 b' class PullRequest(Base, _PullRequestBase'
4302 4304 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4303 4305 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4304 4306 attrs.revisions = pull_request_obj.revisions
4305
4307 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4306 4308 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4307 4309 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4308 4310 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
@@ -35,6 +35,7 b' import collections'
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 from rhodecode.lib.vcs.nodes import FileNode
38 39 from rhodecode.translation import lazy_ugettext
39 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 41 from rhodecode.lib import audit_logger
@@ -82,6 +83,126 b' class UpdateResponse(object):'
82 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 206 class PullRequestModel(BaseModel):
86 207
87 208 cls = PullRequest
@@ -453,6 +574,7 b' class PullRequestModel(BaseModel):'
453 574
454 575 def create(self, created_by, source_repo, source_ref, target_repo,
455 576 target_ref, revisions, reviewers, title, description=None,
577 common_ancestor_id=None,
456 578 description_renderer=None,
457 579 reviewer_data=None, translator=None, auth_user=None):
458 580 translator = translator or get_current_request().translate
@@ -474,6 +596,8 b' class PullRequestModel(BaseModel):'
474 596 pull_request.author = created_by_user
475 597 pull_request.reviewer_data = reviewer_data
476 598 pull_request.pull_request_state = pull_request.STATE_CREATING
599 pull_request.common_ancestor_id = common_ancestor_id
600
477 601 Session().add(pull_request)
478 602 Session().flush()
479 603
@@ -805,8 +929,17 b' class PullRequestModel(BaseModel):'
805 929 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
806 930 pre_load=pre_load)
807 931
808 ancestor_commit_id = source_repo.get_common_ancestor(
809 source_commit.raw_id, target_commit.raw_id, target_repo)
932 target_ref = target_commit.raw_id
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 944 pull_request.source_ref = '%s:%s:%s' % (
812 945 source_ref_type, source_ref_name, source_commit.raw_id)
@@ -913,6 +1046,7 b' class PullRequestModel(BaseModel):'
913 1046 version.reviewer_data = pull_request.reviewer_data
914 1047
915 1048 version.revisions = pull_request.revisions
1049 version.common_ancestor_id = pull_request.common_ancestor_id
916 1050 version.pull_request = pull_request
917 1051 Session().add(version)
918 1052 Session().flush()
@@ -467,9 +467,9 b' ul.auth_plugins {'
467 467 #pr_open_message {
468 468 border: @border-thickness solid #fff;
469 469 border-radius: @border-radius;
470 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
471 470 text-align: left;
472 471 overflow: hidden;
472 white-space: pre-line;
473 473 }
474 474
475 475 .pr-details-title {
@@ -1796,7 +1796,7 b' table.integrations {'
1796 1796 }
1797 1797
1798 1798 div.ancestor {
1799 line-height: 33px;
1799
1800 1800 }
1801 1801
1802 1802 .cs_icon_td input[type="checkbox"] {
@@ -205,7 +205,9 b' select.select2{height:28px;visibility:hi'
205 205 font-family: @text-regular;
206 206 color: @grey2;
207 207 cursor: pointer;
208 white-space: nowrap;
208 209 }
210
209 211 &.select2-result-with-children {
210 212
211 213 .select2-result-label {
@@ -220,6 +222,7 b' select.select2{height:28px;visibility:hi'
220 222 font-family: @text-regular;
221 223 color: @grey2;
222 224 cursor: pointer;
225 white-space: nowrap;
223 226 }
224 227 }
225 228 }
@@ -22,7 +22,7 b' var _gettext = function (s) {'
22 22 if (_TM.hasOwnProperty(s)) {
23 23 return _TM[s];
24 24 }
25 i18nLog.error(
25 i18nLog.warning(
26 26 'String `' + s + '` was requested but cannot be ' +
27 27 'found in translation table');
28 28 return s
@@ -34,3 +34,5 b' var _ngettext = function (singular, plur'
34 34 }
35 35 return _gettext(plural)
36 36 };
37
38
@@ -75,12 +75,12 b' var getTitleAndDescription = function(so'
75 75 var desc = '';
76 76
77 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 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 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 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
@@ -92,7 +92,6 b' var getTitleAndDescription = function(so'
92 92 };
93 93
94 94
95
96 95 ReviewersController = function () {
97 96 var self = this;
98 97 this.$reviewRulesContainer = $('#review_rules');
@@ -100,28 +99,35 b' ReviewersController = function () {'
100 99 this.forbidReviewUsers = undefined;
101 100 this.$reviewMembers = $('#review_members');
102 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 107 return [
106 {'username': 'default',
107 'user_id': templateContext.default_user.user_id}
108 {
109 'username': 'default',
110 'user_id': templateContext.default_user.user_id
111 }
108 112 ];
109 113 };
110 114
111 this.hideReviewRules = function() {
115 this.hideReviewRules = function () {
112 116 self.$reviewRulesContainer.hide();
113 117 };
114 118
115 this.showReviewRules = function() {
119 this.showReviewRules = function () {
116 120 self.$reviewRulesContainer.show();
117 121 };
118 122
119 this.addRule = function(ruleText) {
123 this.addRule = function (ruleText) {
120 124 self.showReviewRules();
121 125 return '<div>- {0}</div>'.format(ruleText)
122 126 };
123 127
124 this.loadReviewRules = function(data) {
128 this.loadReviewRules = function (data) {
129 self.diffData = data;
130
125 131 // reset forbidden Users
126 132 this.forbidReviewUsers = self.defaultForbidReviewUsers();
127 133
@@ -141,7 +147,7 b' ReviewersController = function () {'
141 147 if (data.rules.voting < 0) {
142 148 self.$rulesList.append(
143 149 self.addRule(
144 _gettext('All individual reviewers must vote.'))
150 _gettext('All individual reviewers must vote.'))
145 151 )
146 152 } else if (data.rules.voting === 1) {
147 153 self.$rulesList.append(
@@ -158,7 +164,7 b' ReviewersController = function () {'
158 164 }
159 165
160 166 if (data.rules.voting_groups !== undefined) {
161 $.each(data.rules.voting_groups, function(index, rule_data) {
167 $.each(data.rules.voting_groups, function (index, rule_data) {
162 168 self.$rulesList.append(
163 169 self.addRule(rule_data.text)
164 170 )
@@ -188,7 +194,7 b' ReviewersController = function () {'
188 194 if (data.rules.forbid_commit_author_to_review) {
189 195
190 196 if (data.rules_data.forbidden_users) {
191 $.each(data.rules_data.forbidden_users, function(index, member_data) {
197 $.each(data.rules_data.forbidden_users, function (index, member_data) {
192 198 self.forbidReviewUsers.push(member_data)
193 199 });
194 200
@@ -203,11 +209,10 b' ReviewersController = function () {'
203 209 return self.forbidReviewUsers
204 210 };
205 211
206 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
212 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
207 213
208 214 if (self.currentRequest) {
209 // make sure we cleanup old running requests before triggering this
210 // again
215 // make sure we cleanup old running requests before triggering this again
211 216 self.currentRequest.abort();
212 217 }
213 218
@@ -218,6 +223,9 b' ReviewersController = function () {'
218 223 prButtonLock(true, null, 'reviewers');
219 224 $('#user').hide(); // hide user autocomplete before load
220 225
226 // lock PR button, so we cannot send PR before it's calculated
227 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
228
221 229 if (sourceRef.length !== 3 || targetRef.length !== 3) {
222 230 // don't load defaults in case we're missing some refs...
223 231 $('.calculate-reviewers').hide();
@@ -225,58 +233,80 b' ReviewersController = function () {'
225 233 }
226 234
227 235 var url = pyroutes.url('repo_default_reviewers_data',
228 {
229 'repo_name': templateContext.repo_name,
230 'source_repo': sourceRepo,
231 'source_ref': sourceRef[2],
232 'target_repo': targetRepo,
233 'target_ref': targetRef[2]
234 });
236 {
237 'repo_name': templateContext.repo_name,
238 'source_repo': sourceRepo,
239 'source_ref': sourceRef[2],
240 'target_repo': targetRepo,
241 'target_ref': targetRef[2]
242 });
235 243
236 self.currentRequest = $.get(url)
237 .done(function(data) {
244 self.currentRequest = $.ajax({
245 url: url,
246 headers: {'X-PARTIAL-XHR': true},
247 type: 'GET',
248 success: function (data) {
249
238 250 self.currentRequest = null;
239 251
240 252 // review rules
241 253 self.loadReviewRules(data);
254 self.handleDiffData(data["diff_info"]);
242 255
243 256 for (var i = 0; i < data.reviewers.length; i++) {
244 var reviewer = data.reviewers[i];
245 self.addReviewMember(
246 reviewer, reviewer.reasons, reviewer.mandatory);
257 var reviewer = data.reviewers[i];
258 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
247 259 }
248 260 $('.calculate-reviewers').hide();
249 261 prButtonLock(false, null, 'reviewers');
250 262 $('#user').show(); // show user autocomplete after load
251 });
263
264 var commitElements = data["diff_info"]['commits'];
265 if (commitElements.length === 0) {
266 prButtonLock(true, _gettext('no commits'), 'all');
267
268 } else {
269 // un-lock PR button, so we cannot send PR before it's calculated
270 prButtonLock(false, null, 'compare');
271 }
272
273 },
274 error: function (jqXHR, textStatus, errorThrown) {
275 var prefix = "Loading diff and reviewers failed\n"
276 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
277 ajaxErrorSwal(message);
278 }
279 });
280
252 281 };
253 282
254 283 // check those, refactor
255 this.removeReviewMember = function(reviewer_id, mark_delete) {
284 this.removeReviewMember = function (reviewer_id, mark_delete) {
256 285 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
257 286
258 if(typeof(mark_delete) === undefined){
287 if (typeof (mark_delete) === undefined) {
259 288 mark_delete = false;
260 289 }
261 290
262 if(mark_delete === true){
263 if (reviewer){
291 if (mark_delete === true) {
292 if (reviewer) {
264 293 // now delete the input
265 294 $('#reviewer_{0} input'.format(reviewer_id)).remove();
266 295 // mark as to-delete
267 296 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
268 297 obj.addClass('to-delete');
269 obj.css({"text-decoration":"line-through", "opacity": 0.5});
298 obj.css({"text-decoration": "line-through", "opacity": 0.5});
270 299 }
271 }
272 else{
300 } else {
273 301 $('#reviewer_{0}'.format(reviewer_id)).remove();
274 302 }
275 303 };
276 this.reviewMemberEntry = function() {
304
305 this.reviewMemberEntry = function () {
277 306
278 307 };
279 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
308
309 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
280 310 var members = self.$reviewMembers.get(0);
281 311 var id = reviewer_obj.user_id;
282 312 var username = reviewer_obj.username;
@@ -287,13 +317,13 b' ReviewersController = function () {'
287 317 // register IDS to check if we don't have this ID already in
288 318 var currentIds = [];
289 319 var _els = self.$reviewMembers.find('li').toArray();
290 for (el in _els){
320 for (el in _els) {
291 321 currentIds.push(_els[el].id)
292 322 }
293 323
294 var userAllowedReview = function(userId) {
324 var userAllowedReview = function (userId) {
295 325 var allowed = true;
296 $.each(self.forbidReviewUsers, function(index, member_data) {
326 $.each(self.forbidReviewUsers, function (index, member_data) {
297 327 if (parseInt(userId) === member_data['user_id']) {
298 328 allowed = false;
299 329 return false // breaks the loop
@@ -303,35 +333,38 b' ReviewersController = function () {'
303 333 };
304 334
305 335 var userAllowed = userAllowedReview(id);
306 if (!userAllowed){
307 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
336 if (!userAllowed) {
337 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
308 338 } else {
309 339 // only add if it's not there
310 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
340 var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
311 341
312 342 if (alreadyReviewer) {
313 343 alert(_gettext('User `{0}` already in reviewers').format(username));
314 344 } else {
315 345 members.innerHTML += renderTemplate('reviewMemberEntry', {
316 'member': reviewer_obj,
317 'mandatory': mandatory,
318 'allowed_to_update': true,
319 'review_status': 'not_reviewed',
320 'review_status_label': _gettext('Not Reviewed'),
321 'reasons': reasons,
322 'create': true
323 });
346 'member': reviewer_obj,
347 'mandatory': mandatory,
348 'allowed_to_update': true,
349 'review_status': 'not_reviewed',
350 'review_status_label': _gettext('Not Reviewed'),
351 'reasons': reasons,
352 'create': true
353 });
324 354 tooltipActivate();
325 355 }
326 356 }
327 357
328 358 };
329 359
330 this.updateReviewers = function(repo_name, pull_request_id){
360 this.updateReviewers = function (repo_name, pull_request_id) {
331 361 var postData = $('#reviewers input').serialize();
332 362 _updatePullRequest(repo_name, pull_request_id, postData);
333 363 };
334 364
365 this.handleDiffData = function (data) {
366 self.diffDataHandler(data)
367 }
335 368 };
336 369
337 370
@@ -393,6 +393,7 b' html[dir="rtl"] .select2-results {'
393 393 -moz-user-select: none;
394 394 -ms-user-select: none;
395 395 user-select: none;
396 white-space: nowrap;
396 397 }
397 398
398 399 .select2-results-dept-1 .select2-result-label { padding-left: 20px }
@@ -2,10 +2,8 b''
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 %if c.ancestor:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">
7 ${h.short_id(c.ancestor)}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
9 7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
10 8 </div>
11 9 %endif
@@ -241,7 +241,93 b''
241 241 // custom code mirror
242 242 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
243 243
244 var diffDataHandler = function(data) {
245
246 $('#pull_request_overview').html(data);
247
248 var commitElements = data['commits'];
249 var files = data['files'];
250 var added = data['stats'][0]
251 var deleted = data['stats'][1]
252 var commonAncestorId = data['ancestor'];
253
254 var prTitleAndDesc = getTitleAndDescription(
255 sourceRef()[1], commitElements, 5);
256
257 var title = prTitleAndDesc[0];
258 var proposedDescription = prTitleAndDesc[1];
259
260 var useGeneratedTitle = (
261 $('#pullrequest_title').hasClass('autogenerated-title') ||
262 $('#pullrequest_title').val() === "");
263
264 if (title && useGeneratedTitle) {
265 // use generated title if we haven't specified our own
266 $('#pullrequest_title').val(title);
267 $('#pullrequest_title').addClass('autogenerated-title');
268
269 }
270
271 var useGeneratedDescription = (
272 !codeMirrorInstance._userDefinedValue ||
273 codeMirrorInstance.getValue() === "");
274
275 if (proposedDescription && useGeneratedDescription) {
276 // set proposed content, if we haven't defined our own,
277 // or we don't have description written
278 codeMirrorInstance._userDefinedValue = false; // reset state
279 codeMirrorInstance.setValue(proposedDescription);
280 }
281
282 // refresh our codeMirror so events kicks in and it's change aware
283 codeMirrorInstance.refresh();
284
285 var url_data = {
286 'repo_name': targetRepo(),
287 'target_repo': sourceRepo(),
288 'source_ref': targetRef()[2],
289 'source_ref_type': 'rev',
290 'target_ref': sourceRef()[2],
291 'target_ref_type': 'rev',
292 'merge': true,
293 '_': Date.now() // bypass browser caching
294 }; // gather the source/target ref and repo here
295 var url = pyroutes.url('repo_compare', url_data);
296
297 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
298 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
299
300 $.each(commitElements, function(idx, value) {
301 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
302 });
303
304 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
305 msg += _ngettext(
306 'This pull requests will consist of <strong>{0} commit</strong>.',
307 'This pull requests will consist of <strong>{0} commits</strong>.',
308 commitElements.length).format(commitElements.length)
309
310 msg += '\n';
311 msg += _ngettext(
312 '<strong>{0} file</strong> changed, ',
313 '<strong>{0} files</strong> changed, ',
314 files.length).format(files.length)
315 msg += '<span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted</span>.'.format(added, deleted)
316
317 msg += '\n\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
318
319 if (commitElements.length) {
320 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
321 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
322 }
323 else {
324 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
325 }
326
327 };
328
244 329 reviewersController = new ReviewersController();
330 reviewersController.diffDataHandler = diffDataHandler;
245 331
246 332 var queryTargetRepo = function(self, query) {
247 333 // cache ALL results if query is empty
@@ -286,99 +372,6 b''
286 372 query.callback({results: data.results});
287 373 };
288 374
289 var loadRepoRefDiffPreview = function() {
290
291 var url_data = {
292 'repo_name': targetRepo(),
293 'target_repo': sourceRepo(),
294 'source_ref': targetRef()[2],
295 'source_ref_type': 'rev',
296 'target_ref': sourceRef()[2],
297 'target_ref_type': 'rev',
298 'merge': true,
299 '_': Date.now() // bypass browser caching
300 }; // gather the source/target ref and repo here
301
302 if (sourceRef().length !== 3 || targetRef().length !== 3) {
303 prButtonLock(true, "${_('Please select source and target')}");
304 return;
305 }
306 var url = pyroutes.url('repo_compare', url_data);
307
308 // lock PR button, so we cannot send PR before it's calculated
309 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
310
311 if (loadRepoRefDiffPreview._currentRequest) {
312 loadRepoRefDiffPreview._currentRequest.abort();
313 }
314
315 loadRepoRefDiffPreview._currentRequest = $.get(url)
316 .error(function(jqXHR, textStatus, errorThrown) {
317 if (textStatus !== 'abort') {
318 var prefix = "Error while processing request.\n"
319 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
320 ajaxErrorSwal(message);
321 }
322
323 })
324 .done(function(data) {
325 loadRepoRefDiffPreview._currentRequest = null;
326 $('#pull_request_overview').html(data);
327
328 var commitElements = $(data).find('tr[commit_id]');
329
330 var prTitleAndDesc = getTitleAndDescription(
331 sourceRef()[1], commitElements, 5);
332
333 var title = prTitleAndDesc[0];
334 var proposedDescription = prTitleAndDesc[1];
335
336 var useGeneratedTitle = (
337 $('#pullrequest_title').hasClass('autogenerated-title') ||
338 $('#pullrequest_title').val() === "");
339
340 if (title && useGeneratedTitle) {
341 // use generated title if we haven't specified our own
342 $('#pullrequest_title').val(title);
343 $('#pullrequest_title').addClass('autogenerated-title');
344
345 }
346
347 var useGeneratedDescription = (
348 !codeMirrorInstance._userDefinedValue ||
349 codeMirrorInstance.getValue() === "");
350
351 if (proposedDescription && useGeneratedDescription) {
352 // set proposed content, if we haven't defined our own,
353 // or we don't have description written
354 codeMirrorInstance._userDefinedValue = false; // reset state
355 codeMirrorInstance.setValue(proposedDescription);
356 }
357
358 // refresh our codeMirror so events kicks in and it's change aware
359 codeMirrorInstance.refresh();
360
361 var msg = '';
362 if (commitElements.length === 1) {
363 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
364 } else {
365 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
366 }
367
368 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
369
370 if (commitElements.length) {
371 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
372 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
373 }
374 else {
375 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
376 }
377
378
379 });
380 };
381
382 375 var Select2Box = function(element, overrides) {
383 376 var globalDefaults = {
384 377 dropdownAutoWidth: true,
@@ -476,13 +469,11 b''
476 469 targetRepoSelect2.initRepo(defaultTargetRepo, false);
477 470
478 471 $sourceRef.on('change', function(e){
479 loadRepoRefDiffPreview();
480 472 reviewersController.loadDefaultReviewers(
481 473 sourceRepo(), sourceRef(), targetRepo(), targetRef());
482 474 });
483 475
484 476 $targetRef.on('change', function(e){
485 loadRepoRefDiffPreview();
486 477 reviewersController.loadDefaultReviewers(
487 478 sourceRepo(), sourceRef(), targetRepo(), targetRef());
488 479 });
@@ -502,7 +493,6 b''
502 493 success: function(data) {
503 494 $('#target_ref_loading').hide();
504 495 targetRepoChanged(data);
505 loadRepoRefDiffPreview();
506 496 },
507 497 error: function(jqXHR, textStatus, errorThrown) {
508 498 var prefix = "Error while fetching entries.\n"
@@ -533,8 +523,8 b''
533 523 % if c.default_source_ref:
534 524 // in case we have a pre-selected value, use it now
535 525 $sourceRef.select2('val', '${c.default_source_ref}');
536 // diff preview load
537 loadRepoRefDiffPreview();
526
527
538 528 // default reviewers
539 529 reviewersController.loadDefaultReviewers(
540 530 sourceRepo(), sourceRef(), targetRepo(), targetRef());
General Comments 0
You need to be logged in to leave comments. Login now