##// END OF EJS Templates
reviewers: changes for new author/commit author logic....
milka -
r4559:36905c9f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,78 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
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRule.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31
32 new_column = Column('pr_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
33 batch_op.add_column(new_column)
34
35 new_column = Column('commit_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
36 batch_op.add_column(new_column)
37
38 _migrate_review_flags_to_new_cols(op, meta.Session)
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
49
50 def _migrate_review_flags_to_new_cols(op, session):
51
52 # set defaults for pr_author
53 query = text(
54 'UPDATE repo_review_rules SET pr_author = :val'
55 ).bindparams(val='no_rule')
56 op.execute(query)
57
58 # set defaults for commit_author
59 query = text(
60 'UPDATE repo_review_rules SET commit_author = :val'
61 ).bindparams(val='no_rule')
62 op.execute(query)
63
64 session().commit()
65
66 # now change the flags to forbid based on
67 # forbid_author_to_review, forbid_commit_author_to_review
68 query = text(
69 'UPDATE repo_review_rules SET pr_author = :val WHERE forbid_author_to_review = TRUE'
70 ).bindparams(val='forbid_pr_author')
71 op.execute(query)
72
73 query = text(
74 'UPDATE repo_review_rules SET commit_author = :val WHERE forbid_commit_author_to_review = TRUE'
75 ).bindparams(val='forbid_commit_author')
76 op.execute(query)
77
78 session().commit()
@@ -1,60 +1,60 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 from collections import OrderedDict
23 23
24 24 import sys
25 25 import platform
26 26
27 27 VERSION = tuple(open(os.path.join(
28 28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29 29
30 30 BACKENDS = OrderedDict()
31 31
32 32 BACKENDS['hg'] = 'Mercurial repository'
33 33 BACKENDS['git'] = 'Git repository'
34 34 BACKENDS['svn'] = 'Subversion repository'
35 35
36 36
37 37 CELERY_ENABLED = False
38 38 CELERY_EAGER = False
39 39
40 40 # link to config for pyramid
41 41 CONFIG = {}
42 42
43 43 # Populated with the settings dictionary from application init in
44 44 # rhodecode.conf.environment.load_pyramid_environment
45 45 PYRAMID_SETTINGS = {}
46 46
47 47 # Linked module for extensions
48 48 EXTENSIONS = {}
49 49
50 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 111 # defines current db version for migrations
51 __dbversion__ = 112 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
55 55 __url__ = 'https://code.rhodecode.com'
56 56
57 57 is_windows = __platform__ in ['Windows']
58 58 is_unix = not is_windows
59 59 is_test = False
60 60 disable_error_handler = False
@@ -1,111 +1,113 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from rhodecode.lib import helpers as h, rc_cache
22 22 from rhodecode.lib.utils2 import safe_int
23 23 from rhodecode.model.pull_request import get_diff_info
24 24 from rhodecode.model.db import PullRequestReviewers
25 25 # V3 - Reviewers, with default rules data
26 26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
27 # v5 - pr_author/commit_author include/exclude logic
28 REVIEWER_API_VERSION = 'V5'
28 29
29 30
30 31 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
31 32 """
32 33 Returns json struct of a reviewer for frontend
33 34
34 35 :param user: the reviewer
35 36 :param reasons: list of strings of why they are reviewers
36 37 :param mandatory: bool, to set user as mandatory
37 38 """
38 39 role = role or PullRequestReviewers.ROLE_REVIEWER
39 40 if role not in PullRequestReviewers.ROLES:
40 41 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
41 42
42 43 return {
43 44 'user_id': user.user_id,
44 45 'reasons': reasons or [],
45 46 'rules': rules or [],
46 47 'role': role,
47 48 'mandatory': mandatory,
48 49 'user_group': user_group,
49 50 'username': user.username,
50 51 'first_name': user.first_name,
51 52 'last_name': user.last_name,
52 53 'user_link': h.link_to_user(user),
53 54 'gravatar_link': h.gravatar_url(user.email, 14),
54 55 }
55 56
56 57
57 58 def to_reviewers(e):
58 59 if isinstance(e, (tuple, list)):
59 60 return map(reviewer_as_json, e)
60 61 else:
61 62 return reviewer_as_json(e)
62 63
63 64
64 65 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
65 66 include_diff_info=True):
66 67 """
67 68 Return json for default reviewers of a repository
68 69 """
69 70
70 71 diff_info = {}
71 72 if include_diff_info:
72 73 diff_info = get_diff_info(
73 74 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
74 75
75 76 reasons = ['Default reviewer', 'Repository owner']
76 77 json_reviewers = [reviewer_as_json(
77 78 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
78 79
79 80 compute_key = rc_cache.utils.compute_key_from_params(
80 81 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
81 82 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
82 83 target_ref.commit_id)
83 84
84 85 return {
85 86 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
86 87 'compute_key': compute_key,
87 88 'diff_info': diff_info,
88 89 'reviewers': json_reviewers,
89 90 'rules': {},
90 91 'rules_data': {},
92 'rules_humanized': [],
91 93 }
92 94
93 95
94 96 def validate_default_reviewers(review_members, reviewer_rules):
95 97 """
96 98 Function to validate submitted reviewers against the saved rules
97 99 """
98 100 reviewers = []
99 101 reviewer_by_id = {}
100 102 for r in review_members:
101 103 reviewer_user_id = safe_int(r['user_id'])
102 104 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
103 105
104 106 reviewer_by_id[reviewer_user_id] = entry
105 107 reviewers.append(entry)
106 108
107 109 return reviewers
108 110
109 111
110 112 def validate_observers(observer_members, reviewer_rules):
111 113 return {}
@@ -1,1855 +1,1851 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 110 self.db_repo.repo_id, pull_request=pr,
111 111 include_drafts=False, count_only=True)
112 112
113 113 data.append({
114 114 'name': _render('pullrequest_name',
115 115 pr.pull_request_id, pr.pull_request_state,
116 116 pr.work_in_progress, pr.target_repo.repo_name,
117 117 short=True),
118 118 'name_raw': pr.pull_request_id,
119 119 'status': _render('pullrequest_status',
120 120 pr.calculated_review_status()),
121 121 'title': _render('pullrequest_title', pr.title, pr.description),
122 122 'description': h.escape(pr.description),
123 123 'updated_on': _render('pullrequest_updated_on',
124 124 h.datetime_to_time(pr.updated_on),
125 125 pr.versions_count),
126 126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 127 'created_on': _render('pullrequest_updated_on',
128 128 h.datetime_to_time(pr.created_on)),
129 129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 130 'state': pr.pull_request_state,
131 131 'author': _render('pullrequest_author',
132 132 pr.author.full_contact, ),
133 133 'author_raw': pr.author.full_name,
134 134 'comments': _render('pullrequest_comments', comments_count),
135 135 'comments_raw': comments_count,
136 136 'closed': pr.is_closed(),
137 137 })
138 138
139 139 data = ({
140 140 'draw': draw,
141 141 'data': data,
142 142 'recordsTotal': pull_requests_total_count,
143 143 'recordsFiltered': pull_requests_total_count,
144 144 })
145 145 return data
146 146
147 147 @LoginRequired()
148 148 @HasRepoPermissionAnyDecorator(
149 149 'repository.read', 'repository.write', 'repository.admin')
150 150 @view_config(
151 151 route_name='pullrequest_show_all', request_method='GET',
152 152 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
153 153 def pull_request_list(self):
154 154 c = self.load_default_context()
155 155
156 156 req_get = self.request.GET
157 157 c.source = str2bool(req_get.get('source'))
158 158 c.closed = str2bool(req_get.get('closed'))
159 159 c.my = str2bool(req_get.get('my'))
160 160 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
161 161 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
162 162
163 163 c.active = 'open'
164 164 if c.my:
165 165 c.active = 'my'
166 166 if c.closed:
167 167 c.active = 'closed'
168 168 if c.awaiting_review and not c.source:
169 169 c.active = 'awaiting'
170 170 if c.source and not c.awaiting_review:
171 171 c.active = 'source'
172 172 if c.awaiting_my_review:
173 173 c.active = 'awaiting_my'
174 174
175 175 return self._get_template_context(c)
176 176
177 177 @LoginRequired()
178 178 @HasRepoPermissionAnyDecorator(
179 179 'repository.read', 'repository.write', 'repository.admin')
180 180 @view_config(
181 181 route_name='pullrequest_show_all_data', request_method='GET',
182 182 renderer='json_ext', xhr=True)
183 183 def pull_request_list_data(self):
184 184 self.load_default_context()
185 185
186 186 # additional filters
187 187 req_get = self.request.GET
188 188 source = str2bool(req_get.get('source'))
189 189 closed = str2bool(req_get.get('closed'))
190 190 my = str2bool(req_get.get('my'))
191 191 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 193
194 194 filter_type = 'awaiting_review' if awaiting_review \
195 195 else 'awaiting_my_review' if awaiting_my_review \
196 196 else None
197 197
198 198 opened_by = None
199 199 if my:
200 200 opened_by = [self._rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if closed:
204 204 statuses = [PullRequest.STATUS_CLOSED]
205 205
206 206 data = self._get_pull_requests_list(
207 207 repo_name=self.db_repo_name, source=source,
208 208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 209
210 210 return data
211 211
212 212 def _is_diff_cache_enabled(self, target_repo):
213 213 caching_enabled = self._get_general_setting(
214 214 target_repo, 'rhodecode_diff_cache')
215 215 log.debug('Diff caching enabled: %s', caching_enabled)
216 216 return caching_enabled
217 217
218 218 def _get_diffset(self, source_repo_name, source_repo,
219 219 ancestor_commit,
220 220 source_ref_id, target_ref_id,
221 221 target_commit, source_commit, diff_limit, file_limit,
222 222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 223
224 224 if use_ancestor:
225 225 # we might want to not use it for versions
226 226 target_ref_id = ancestor_commit.raw_id
227 227
228 228 vcs_diff = PullRequestModel().get_diff(
229 229 source_repo, source_ref_id, target_ref_id,
230 230 hide_whitespace_changes, diff_context)
231 231
232 232 diff_processor = diffs.DiffProcessor(
233 233 vcs_diff, format='newdiff', diff_limit=diff_limit,
234 234 file_limit=file_limit, show_full_diff=fulldiff)
235 235
236 236 _parsed = diff_processor.prepare()
237 237
238 238 diffset = codeblocks.DiffSet(
239 239 repo_name=self.db_repo_name,
240 240 source_repo_name=source_repo_name,
241 241 source_node_getter=codeblocks.diffset_node_getter(target_commit),
242 242 target_node_getter=codeblocks.diffset_node_getter(source_commit),
243 243 )
244 244 diffset = self.path_filter.render_patchset_filtered(
245 245 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
246 246
247 247 return diffset
248 248
249 249 def _get_range_diffset(self, source_scm, source_repo,
250 250 commit1, commit2, diff_limit, file_limit,
251 251 fulldiff, hide_whitespace_changes, diff_context):
252 252 vcs_diff = source_scm.get_diff(
253 253 commit1, commit2,
254 254 ignore_whitespace=hide_whitespace_changes,
255 255 context=diff_context)
256 256
257 257 diff_processor = diffs.DiffProcessor(
258 258 vcs_diff, format='newdiff', diff_limit=diff_limit,
259 259 file_limit=file_limit, show_full_diff=fulldiff)
260 260
261 261 _parsed = diff_processor.prepare()
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=source_repo.repo_name,
265 265 source_node_getter=codeblocks.diffset_node_getter(commit1),
266 266 target_node_getter=codeblocks.diffset_node_getter(commit2))
267 267
268 268 diffset = self.path_filter.render_patchset_filtered(
269 269 diffset, _parsed, commit1.raw_id, commit2.raw_id)
270 270
271 271 return diffset
272 272
273 273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
274 274 comments_model = CommentsModel()
275 275
276 276 # GENERAL COMMENTS with versions #
277 277 q = comments_model._all_general_comments_of_pull_request(pull_request)
278 278 q = q.order_by(ChangesetComment.comment_id.asc())
279 279 if not include_drafts:
280 280 q = q.filter(ChangesetComment.draft == false())
281 281 general_comments = q
282 282
283 283 # pick comments we want to render at current version
284 284 c.comment_versions = comments_model.aggregate_comments(
285 285 general_comments, versions, c.at_version_num)
286 286
287 287 # INLINE COMMENTS with versions #
288 288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
289 289 q = q.order_by(ChangesetComment.comment_id.asc())
290 290 if not include_drafts:
291 291 q = q.filter(ChangesetComment.draft == false())
292 292 inline_comments = q
293 293
294 294 c.inline_versions = comments_model.aggregate_comments(
295 295 inline_comments, versions, c.at_version_num, inline=True)
296 296
297 297 # Comments inline+general
298 298 if c.at_version:
299 299 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
300 300 c.comments = c.comment_versions[c.at_version_num]['display']
301 301 else:
302 302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
303 303 c.comments = c.comment_versions[c.at_version_num]['until']
304 304
305 305 return general_comments, inline_comments
306 306
307 307 @LoginRequired()
308 308 @HasRepoPermissionAnyDecorator(
309 309 'repository.read', 'repository.write', 'repository.admin')
310 310 @view_config(
311 311 route_name='pullrequest_show', request_method='GET',
312 312 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
313 313 def pull_request_show(self):
314 314 _ = self.request.translate
315 315 c = self.load_default_context()
316 316
317 317 pull_request = PullRequest.get_or_404(
318 318 self.request.matchdict['pull_request_id'])
319 319 pull_request_id = pull_request.pull_request_id
320 320
321 321 c.state_progressing = pull_request.is_state_changing()
322 322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
323 323
324 324 _new_state = {
325 325 'created': PullRequest.STATE_CREATED,
326 326 }.get(self.request.GET.get('force_state'))
327 327
328 328 if c.is_super_admin and _new_state:
329 329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
330 330 h.flash(
331 331 _('Pull Request state was force changed to `{}`').format(_new_state),
332 332 category='success')
333 333 Session().commit()
334 334
335 335 raise HTTPFound(h.route_path(
336 336 'pullrequest_show', repo_name=self.db_repo_name,
337 337 pull_request_id=pull_request_id))
338 338
339 339 version = self.request.GET.get('version')
340 340 from_version = self.request.GET.get('from_version') or version
341 341 merge_checks = self.request.GET.get('merge_checks')
342 342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
343 343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
344 344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
345 345
346 346 # fetch global flags of ignore ws or context lines
347 347 diff_context = diffs.get_diff_context(self.request)
348 348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
349 349
350 350 (pull_request_latest,
351 351 pull_request_at_ver,
352 352 pull_request_display_obj,
353 353 at_version) = PullRequestModel().get_pr_version(
354 354 pull_request_id, version=version)
355 355
356 356 pr_closed = pull_request_latest.is_closed()
357 357
358 358 if pr_closed and (version or from_version):
359 359 # not allow to browse versions for closed PR
360 360 raise HTTPFound(h.route_path(
361 361 'pullrequest_show', repo_name=self.db_repo_name,
362 362 pull_request_id=pull_request_id))
363 363
364 364 versions = pull_request_display_obj.versions()
365 365 # used to store per-commit range diffs
366 366 c.changes = collections.OrderedDict()
367 367
368 368 c.at_version = at_version
369 369 c.at_version_num = (at_version
370 370 if at_version and at_version != PullRequest.LATEST_VER
371 371 else None)
372 372
373 373 c.at_version_index = ChangesetComment.get_index_from_version(
374 374 c.at_version_num, versions)
375 375
376 376 (prev_pull_request_latest,
377 377 prev_pull_request_at_ver,
378 378 prev_pull_request_display_obj,
379 379 prev_at_version) = PullRequestModel().get_pr_version(
380 380 pull_request_id, version=from_version)
381 381
382 382 c.from_version = prev_at_version
383 383 c.from_version_num = (prev_at_version
384 384 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
385 385 else None)
386 386 c.from_version_index = ChangesetComment.get_index_from_version(
387 387 c.from_version_num, versions)
388 388
389 389 # define if we're in COMPARE mode or VIEW at version mode
390 390 compare = at_version != prev_at_version
391 391
392 392 # pull_requests repo_name we opened it against
393 393 # ie. target_repo must match
394 394 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
395 395 log.warning('Mismatch between the current repo: %s, and target %s',
396 396 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
397 397 raise HTTPNotFound()
398 398
399 399 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
400 400
401 401 c.pull_request = pull_request_display_obj
402 402 c.renderer = pull_request_at_ver.description_renderer or c.renderer
403 403 c.pull_request_latest = pull_request_latest
404 404
405 405 # inject latest version
406 406 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
407 407 c.versions = versions + [latest_ver]
408 408
409 409 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
410 410 c.allowed_to_change_status = False
411 411 c.allowed_to_update = False
412 412 c.allowed_to_merge = False
413 413 c.allowed_to_delete = False
414 414 c.allowed_to_comment = False
415 415 c.allowed_to_close = False
416 416 else:
417 417 can_change_status = PullRequestModel().check_user_change_status(
418 418 pull_request_at_ver, self._rhodecode_user)
419 419 c.allowed_to_change_status = can_change_status and not pr_closed
420 420
421 421 c.allowed_to_update = PullRequestModel().check_user_update(
422 422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 423 c.allowed_to_merge = PullRequestModel().check_user_merge(
424 424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 425 c.allowed_to_delete = PullRequestModel().check_user_delete(
426 426 pull_request_latest, self._rhodecode_user) and not pr_closed
427 427 c.allowed_to_comment = not pr_closed
428 428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
429 429
430 430 c.forbid_adding_reviewers = False
431 c.forbid_author_to_review = False
432 c.forbid_commit_author_to_review = False
433 431
434 432 if pull_request_latest.reviewer_data and \
435 433 'rules' in pull_request_latest.reviewer_data:
436 434 rules = pull_request_latest.reviewer_data['rules'] or {}
437 435 try:
438 436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
439 c.forbid_author_to_review = rules.get('forbid_author_to_review')
440 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
441 437 except Exception:
442 438 pass
443 439
444 440 # check merge capabilities
445 441 _merge_check = MergeCheck.validate(
446 442 pull_request_latest, auth_user=self._rhodecode_user,
447 443 translator=self.request.translate,
448 444 force_shadow_repo_refresh=force_refresh)
449 445
450 446 c.pr_merge_errors = _merge_check.error_details
451 447 c.pr_merge_possible = not _merge_check.failed
452 448 c.pr_merge_message = _merge_check.merge_msg
453 449 c.pr_merge_source_commit = _merge_check.source_commit
454 450 c.pr_merge_target_commit = _merge_check.target_commit
455 451
456 452 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 453 pull_request_latest, translator=self.request.translate)
458 454
459 455 c.pull_request_review_status = _merge_check.review_status
460 456 if merge_checks:
461 457 self.request.override_renderer = \
462 458 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 459 return self._get_template_context(c)
464 460
465 461 c.reviewers_count = pull_request.reviewers_count
466 462 c.observers_count = pull_request.observers_count
467 463
468 464 # reviewers and statuses
469 465 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
470 466 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 467 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 468
473 469 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 470 member_reviewer = h.reviewer_as_json(
475 471 member, reasons=reasons, mandatory=mandatory,
476 472 role=review_obj.role,
477 473 user_group=review_obj.rule_user_group_data()
478 474 )
479 475
480 476 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 477 member_reviewer['review_status'] = current_review_status
482 478 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 479 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 480 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 481
486 482 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
487 483
488 484 for observer_obj, member in pull_request_at_ver.observers():
489 485 member_observer = h.reviewer_as_json(
490 486 member, reasons=[], mandatory=False,
491 487 role=observer_obj.role,
492 488 user_group=observer_obj.rule_user_group_data()
493 489 )
494 490 member_observer['allowed_to_update'] = c.allowed_to_update
495 491 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 492
497 493 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
498 494
499 495 general_comments, inline_comments = \
500 496 self.register_comments_vars(c, pull_request_latest, versions)
501 497
502 498 # TODOs
503 499 c.unresolved_comments = CommentsModel() \
504 500 .get_pull_request_unresolved_todos(pull_request_latest)
505 501 c.resolved_comments = CommentsModel() \
506 502 .get_pull_request_resolved_todos(pull_request_latest)
507 503
508 504 # if we use version, then do not show later comments
509 505 # than current version
510 506 display_inline_comments = collections.defaultdict(
511 507 lambda: collections.defaultdict(list))
512 508 for co in inline_comments:
513 509 if c.at_version_num:
514 510 # pick comments that are at least UPTO given version, so we
515 511 # don't render comments for higher version
516 512 should_render = co.pull_request_version_id and \
517 513 co.pull_request_version_id <= c.at_version_num
518 514 else:
519 515 # showing all, for 'latest'
520 516 should_render = True
521 517
522 518 if should_render:
523 519 display_inline_comments[co.f_path][co.line_no].append(co)
524 520
525 521 # load diff data into template context, if we use compare mode then
526 522 # diff is calculated based on changes between versions of PR
527 523
528 524 source_repo = pull_request_at_ver.source_repo
529 525 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
530 526
531 527 target_repo = pull_request_at_ver.target_repo
532 528 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
533 529
534 530 if compare:
535 531 # in compare switch the diff base to latest commit from prev version
536 532 target_ref_id = prev_pull_request_display_obj.revisions[0]
537 533
538 534 # despite opening commits for bookmarks/branches/tags, we always
539 535 # convert this to rev to prevent changes after bookmark or branch change
540 536 c.source_ref_type = 'rev'
541 537 c.source_ref = source_ref_id
542 538
543 539 c.target_ref_type = 'rev'
544 540 c.target_ref = target_ref_id
545 541
546 542 c.source_repo = source_repo
547 543 c.target_repo = target_repo
548 544
549 545 c.commit_ranges = []
550 546 source_commit = EmptyCommit()
551 547 target_commit = EmptyCommit()
552 548 c.missing_requirements = False
553 549
554 550 source_scm = source_repo.scm_instance()
555 551 target_scm = target_repo.scm_instance()
556 552
557 553 shadow_scm = None
558 554 try:
559 555 shadow_scm = pull_request_latest.get_shadow_repo()
560 556 except Exception:
561 557 log.debug('Failed to get shadow repo', exc_info=True)
562 558 # try first the existing source_repo, and then shadow
563 559 # repo if we can obtain one
564 560 commits_source_repo = source_scm
565 561 if shadow_scm:
566 562 commits_source_repo = shadow_scm
567 563
568 564 c.commits_source_repo = commits_source_repo
569 565 c.ancestor = None # set it to None, to hide it from PR view
570 566
571 567 # empty version means latest, so we keep this to prevent
572 568 # double caching
573 569 version_normalized = version or PullRequest.LATEST_VER
574 570 from_version_normalized = from_version or PullRequest.LATEST_VER
575 571
576 572 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
577 573 cache_file_path = diff_cache_exist(
578 574 cache_path, 'pull_request', pull_request_id, version_normalized,
579 575 from_version_normalized, source_ref_id, target_ref_id,
580 576 hide_whitespace_changes, diff_context, c.fulldiff)
581 577
582 578 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
583 579 force_recache = self.get_recache_flag()
584 580
585 581 cached_diff = None
586 582 if caching_enabled:
587 583 cached_diff = load_cached_diff(cache_file_path)
588 584
589 585 has_proper_commit_cache = (
590 586 cached_diff and cached_diff.get('commits')
591 587 and len(cached_diff.get('commits', [])) == 5
592 588 and cached_diff.get('commits')[0]
593 589 and cached_diff.get('commits')[3])
594 590
595 591 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
596 592 diff_commit_cache = \
597 593 (ancestor_commit, commit_cache, missing_requirements,
598 594 source_commit, target_commit) = cached_diff['commits']
599 595 else:
600 596 # NOTE(marcink): we reach potentially unreachable errors when a PR has
601 597 # merge errors resulting in potentially hidden commits in the shadow repo.
602 598 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
603 599 and _merge_check.merge_response
604 600 maybe_unreachable = maybe_unreachable \
605 601 and _merge_check.merge_response.metadata.get('unresolved_files')
606 602 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
607 603 diff_commit_cache = \
608 604 (ancestor_commit, commit_cache, missing_requirements,
609 605 source_commit, target_commit) = self.get_commits(
610 606 commits_source_repo,
611 607 pull_request_at_ver,
612 608 source_commit,
613 609 source_ref_id,
614 610 source_scm,
615 611 target_commit,
616 612 target_ref_id,
617 613 target_scm,
618 614 maybe_unreachable=maybe_unreachable)
619 615
620 616 # register our commit range
621 617 for comm in commit_cache.values():
622 618 c.commit_ranges.append(comm)
623 619
624 620 c.missing_requirements = missing_requirements
625 621 c.ancestor_commit = ancestor_commit
626 622 c.statuses = source_repo.statuses(
627 623 [x.raw_id for x in c.commit_ranges])
628 624
629 625 # auto collapse if we have more than limit
630 626 collapse_limit = diffs.DiffProcessor._collapse_commits_over
631 627 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
632 628 c.compare_mode = compare
633 629
634 630 # diff_limit is the old behavior, will cut off the whole diff
635 631 # if the limit is applied otherwise will just hide the
636 632 # big files from the front-end
637 633 diff_limit = c.visual.cut_off_limit_diff
638 634 file_limit = c.visual.cut_off_limit_file
639 635
640 636 c.missing_commits = False
641 637 if (c.missing_requirements
642 638 or isinstance(source_commit, EmptyCommit)
643 639 or source_commit == target_commit):
644 640
645 641 c.missing_commits = True
646 642 else:
647 643 c.inline_comments = display_inline_comments
648 644
649 645 use_ancestor = True
650 646 if from_version_normalized != version_normalized:
651 647 use_ancestor = False
652 648
653 649 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
654 650 if not force_recache and has_proper_diff_cache:
655 651 c.diffset = cached_diff['diff']
656 652 else:
657 653 try:
658 654 c.diffset = self._get_diffset(
659 655 c.source_repo.repo_name, commits_source_repo,
660 656 c.ancestor_commit,
661 657 source_ref_id, target_ref_id,
662 658 target_commit, source_commit,
663 659 diff_limit, file_limit, c.fulldiff,
664 660 hide_whitespace_changes, diff_context,
665 661 use_ancestor=use_ancestor
666 662 )
667 663
668 664 # save cached diff
669 665 if caching_enabled:
670 666 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
671 667 except CommitDoesNotExistError:
672 668 log.exception('Failed to generate diffset')
673 669 c.missing_commits = True
674 670
675 671 if not c.missing_commits:
676 672
677 673 c.limited_diff = c.diffset.limited_diff
678 674
679 675 # calculate removed files that are bound to comments
680 676 comment_deleted_files = [
681 677 fname for fname in display_inline_comments
682 678 if fname not in c.diffset.file_stats]
683 679
684 680 c.deleted_files_comments = collections.defaultdict(dict)
685 681 for fname, per_line_comments in display_inline_comments.items():
686 682 if fname in comment_deleted_files:
687 683 c.deleted_files_comments[fname]['stats'] = 0
688 684 c.deleted_files_comments[fname]['comments'] = list()
689 685 for lno, comments in per_line_comments.items():
690 686 c.deleted_files_comments[fname]['comments'].extend(comments)
691 687
692 688 # maybe calculate the range diff
693 689 if c.range_diff_on:
694 690 # TODO(marcink): set whitespace/context
695 691 context_lcl = 3
696 692 ign_whitespace_lcl = False
697 693
698 694 for commit in c.commit_ranges:
699 695 commit2 = commit
700 696 commit1 = commit.first_parent
701 697
702 698 range_diff_cache_file_path = diff_cache_exist(
703 699 cache_path, 'diff', commit.raw_id,
704 700 ign_whitespace_lcl, context_lcl, c.fulldiff)
705 701
706 702 cached_diff = None
707 703 if caching_enabled:
708 704 cached_diff = load_cached_diff(range_diff_cache_file_path)
709 705
710 706 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
711 707 if not force_recache and has_proper_diff_cache:
712 708 diffset = cached_diff['diff']
713 709 else:
714 710 diffset = self._get_range_diffset(
715 711 commits_source_repo, source_repo,
716 712 commit1, commit2, diff_limit, file_limit,
717 713 c.fulldiff, ign_whitespace_lcl, context_lcl
718 714 )
719 715
720 716 # save cached diff
721 717 if caching_enabled:
722 718 cache_diff(range_diff_cache_file_path, diffset, None)
723 719
724 720 c.changes[commit.raw_id] = diffset
725 721
726 722 # this is a hack to properly display links, when creating PR, the
727 723 # compare view and others uses different notation, and
728 724 # compare_commits.mako renders links based on the target_repo.
729 725 # We need to swap that here to generate it properly on the html side
730 726 c.target_repo = c.source_repo
731 727
732 728 c.commit_statuses = ChangesetStatus.STATUSES
733 729
734 730 c.show_version_changes = not pr_closed
735 731 if c.show_version_changes:
736 732 cur_obj = pull_request_at_ver
737 733 prev_obj = prev_pull_request_at_ver
738 734
739 735 old_commit_ids = prev_obj.revisions
740 736 new_commit_ids = cur_obj.revisions
741 737 commit_changes = PullRequestModel()._calculate_commit_id_changes(
742 738 old_commit_ids, new_commit_ids)
743 739 c.commit_changes_summary = commit_changes
744 740
745 741 # calculate the diff for commits between versions
746 742 c.commit_changes = []
747 743
748 744 def mark(cs, fw):
749 745 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
750 746
751 747 for c_type, raw_id in mark(commit_changes.added, 'a') \
752 748 + mark(commit_changes.removed, 'r') \
753 749 + mark(commit_changes.common, 'c'):
754 750
755 751 if raw_id in commit_cache:
756 752 commit = commit_cache[raw_id]
757 753 else:
758 754 try:
759 755 commit = commits_source_repo.get_commit(raw_id)
760 756 except CommitDoesNotExistError:
761 757 # in case we fail extracting still use "dummy" commit
762 758 # for display in commit diff
763 759 commit = h.AttributeDict(
764 760 {'raw_id': raw_id,
765 761 'message': 'EMPTY or MISSING COMMIT'})
766 762 c.commit_changes.append([c_type, commit])
767 763
768 764 # current user review statuses for each version
769 765 c.review_versions = {}
770 766 is_reviewer = PullRequestModel().is_user_reviewer(
771 767 pull_request, self._rhodecode_user)
772 768 if is_reviewer:
773 769 for co in general_comments:
774 770 if co.author.user_id == self._rhodecode_user.user_id:
775 771 status = co.status_change
776 772 if status:
777 773 _ver_pr = status[0].comment.pull_request_version_id
778 774 c.review_versions[_ver_pr] = status[0]
779 775
780 776 return self._get_template_context(c)
781 777
782 778 def get_commits(
783 779 self, commits_source_repo, pull_request_at_ver, source_commit,
784 780 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
785 781 maybe_unreachable=False):
786 782
787 783 commit_cache = collections.OrderedDict()
788 784 missing_requirements = False
789 785
790 786 try:
791 787 pre_load = ["author", "date", "message", "branch", "parents"]
792 788
793 789 pull_request_commits = pull_request_at_ver.revisions
794 790 log.debug('Loading %s commits from %s',
795 791 len(pull_request_commits), commits_source_repo)
796 792
797 793 for rev in pull_request_commits:
798 794 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
799 795 maybe_unreachable=maybe_unreachable)
800 796 commit_cache[comm.raw_id] = comm
801 797
802 798 # Order here matters, we first need to get target, and then
803 799 # the source
804 800 target_commit = commits_source_repo.get_commit(
805 801 commit_id=safe_str(target_ref_id))
806 802
807 803 source_commit = commits_source_repo.get_commit(
808 804 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
809 805 except CommitDoesNotExistError:
810 806 log.warning('Failed to get commit from `{}` repo'.format(
811 807 commits_source_repo), exc_info=True)
812 808 except RepositoryRequirementError:
813 809 log.warning('Failed to get all required data from repo', exc_info=True)
814 810 missing_requirements = True
815 811
816 812 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
817 813
818 814 try:
819 815 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
820 816 except Exception:
821 817 ancestor_commit = None
822 818
823 819 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
824 820
825 821 def assure_not_empty_repo(self):
826 822 _ = self.request.translate
827 823
828 824 try:
829 825 self.db_repo.scm_instance().get_commit()
830 826 except EmptyRepositoryError:
831 827 h.flash(h.literal(_('There are no commits yet')),
832 828 category='warning')
833 829 raise HTTPFound(
834 830 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
835 831
836 832 @LoginRequired()
837 833 @NotAnonymous()
838 834 @HasRepoPermissionAnyDecorator(
839 835 'repository.read', 'repository.write', 'repository.admin')
840 836 @view_config(
841 837 route_name='pullrequest_new', request_method='GET',
842 838 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
843 839 def pull_request_new(self):
844 840 _ = self.request.translate
845 841 c = self.load_default_context()
846 842
847 843 self.assure_not_empty_repo()
848 844 source_repo = self.db_repo
849 845
850 846 commit_id = self.request.GET.get('commit')
851 847 branch_ref = self.request.GET.get('branch')
852 848 bookmark_ref = self.request.GET.get('bookmark')
853 849
854 850 try:
855 851 source_repo_data = PullRequestModel().generate_repo_data(
856 852 source_repo, commit_id=commit_id,
857 853 branch=branch_ref, bookmark=bookmark_ref,
858 854 translator=self.request.translate)
859 855 except CommitDoesNotExistError as e:
860 856 log.exception(e)
861 857 h.flash(_('Commit does not exist'), 'error')
862 858 raise HTTPFound(
863 859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
864 860
865 861 default_target_repo = source_repo
866 862
867 863 if source_repo.parent and c.has_origin_repo_read_perm:
868 864 parent_vcs_obj = source_repo.parent.scm_instance()
869 865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
870 866 # change default if we have a parent repo
871 867 default_target_repo = source_repo.parent
872 868
873 869 target_repo_data = PullRequestModel().generate_repo_data(
874 870 default_target_repo, translator=self.request.translate)
875 871
876 872 selected_source_ref = source_repo_data['refs']['selected_ref']
877 873 title_source_ref = ''
878 874 if selected_source_ref:
879 875 title_source_ref = selected_source_ref.split(':', 2)[1]
880 876 c.default_title = PullRequestModel().generate_pullrequest_title(
881 877 source=source_repo.repo_name,
882 878 source_ref=title_source_ref,
883 879 target=default_target_repo.repo_name
884 880 )
885 881
886 882 c.default_repo_data = {
887 883 'source_repo_name': source_repo.repo_name,
888 884 'source_refs_json': json.dumps(source_repo_data),
889 885 'target_repo_name': default_target_repo.repo_name,
890 886 'target_refs_json': json.dumps(target_repo_data),
891 887 }
892 888 c.default_source_ref = selected_source_ref
893 889
894 890 return self._get_template_context(c)
895 891
896 892 @LoginRequired()
897 893 @NotAnonymous()
898 894 @HasRepoPermissionAnyDecorator(
899 895 'repository.read', 'repository.write', 'repository.admin')
900 896 @view_config(
901 897 route_name='pullrequest_repo_refs', request_method='GET',
902 898 renderer='json_ext', xhr=True)
903 899 def pull_request_repo_refs(self):
904 900 self.load_default_context()
905 901 target_repo_name = self.request.matchdict['target_repo_name']
906 902 repo = Repository.get_by_repo_name(target_repo_name)
907 903 if not repo:
908 904 raise HTTPNotFound()
909 905
910 906 target_perm = HasRepoPermissionAny(
911 907 'repository.read', 'repository.write', 'repository.admin')(
912 908 target_repo_name)
913 909 if not target_perm:
914 910 raise HTTPNotFound()
915 911
916 912 return PullRequestModel().generate_repo_data(
917 913 repo, translator=self.request.translate)
918 914
919 915 @LoginRequired()
920 916 @NotAnonymous()
921 917 @HasRepoPermissionAnyDecorator(
922 918 'repository.read', 'repository.write', 'repository.admin')
923 919 @view_config(
924 920 route_name='pullrequest_repo_targets', request_method='GET',
925 921 renderer='json_ext', xhr=True)
926 922 def pullrequest_repo_targets(self):
927 923 _ = self.request.translate
928 924 filter_query = self.request.GET.get('query')
929 925
930 926 # get the parents
931 927 parent_target_repos = []
932 928 if self.db_repo.parent:
933 929 parents_query = Repository.query() \
934 930 .order_by(func.length(Repository.repo_name)) \
935 931 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
936 932
937 933 if filter_query:
938 934 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
939 935 parents_query = parents_query.filter(
940 936 Repository.repo_name.ilike(ilike_expression))
941 937 parents = parents_query.limit(20).all()
942 938
943 939 for parent in parents:
944 940 parent_vcs_obj = parent.scm_instance()
945 941 if parent_vcs_obj and not parent_vcs_obj.is_empty():
946 942 parent_target_repos.append(parent)
947 943
948 944 # get other forks, and repo itself
949 945 query = Repository.query() \
950 946 .order_by(func.length(Repository.repo_name)) \
951 947 .filter(
952 948 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
953 949 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
954 950 ) \
955 951 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
956 952
957 953 if filter_query:
958 954 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
959 955 query = query.filter(Repository.repo_name.ilike(ilike_expression))
960 956
961 957 limit = max(20 - len(parent_target_repos), 5) # not less then 5
962 958 target_repos = query.limit(limit).all()
963 959
964 960 all_target_repos = target_repos + parent_target_repos
965 961
966 962 repos = []
967 963 # This checks permissions to the repositories
968 964 for obj in ScmModel().get_repos(all_target_repos):
969 965 repos.append({
970 966 'id': obj['name'],
971 967 'text': obj['name'],
972 968 'type': 'repo',
973 969 'repo_id': obj['dbrepo']['repo_id'],
974 970 'repo_type': obj['dbrepo']['repo_type'],
975 971 'private': obj['dbrepo']['private'],
976 972
977 973 })
978 974
979 975 data = {
980 976 'more': False,
981 977 'results': [{
982 978 'text': _('Repositories'),
983 979 'children': repos
984 980 }] if repos else []
985 981 }
986 982 return data
987 983
988 984 @classmethod
989 985 def get_comment_ids(cls, post_data):
990 986 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
991 987
992 988 @LoginRequired()
993 989 @NotAnonymous()
994 990 @HasRepoPermissionAnyDecorator(
995 991 'repository.read', 'repository.write', 'repository.admin')
996 992 @view_config(
997 993 route_name='pullrequest_comments', request_method='POST',
998 994 renderer='string_html', xhr=True)
999 995 def pullrequest_comments(self):
1000 996 self.load_default_context()
1001 997
1002 998 pull_request = PullRequest.get_or_404(
1003 999 self.request.matchdict['pull_request_id'])
1004 1000 pull_request_id = pull_request.pull_request_id
1005 1001 version = self.request.GET.get('version')
1006 1002
1007 1003 _render = self.request.get_partial_renderer(
1008 1004 'rhodecode:templates/base/sidebar.mako')
1009 1005 c = _render.get_call_context()
1010 1006
1011 1007 (pull_request_latest,
1012 1008 pull_request_at_ver,
1013 1009 pull_request_display_obj,
1014 1010 at_version) = PullRequestModel().get_pr_version(
1015 1011 pull_request_id, version=version)
1016 1012 versions = pull_request_display_obj.versions()
1017 1013 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1018 1014 c.versions = versions + [latest_ver]
1019 1015
1020 1016 c.at_version = at_version
1021 1017 c.at_version_num = (at_version
1022 1018 if at_version and at_version != PullRequest.LATEST_VER
1023 1019 else None)
1024 1020
1025 1021 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1026 1022 all_comments = c.inline_comments_flat + c.comments
1027 1023
1028 1024 existing_ids = self.get_comment_ids(self.request.POST)
1029 1025 return _render('comments_table', all_comments, len(all_comments),
1030 1026 existing_ids=existing_ids)
1031 1027
1032 1028 @LoginRequired()
1033 1029 @NotAnonymous()
1034 1030 @HasRepoPermissionAnyDecorator(
1035 1031 'repository.read', 'repository.write', 'repository.admin')
1036 1032 @view_config(
1037 1033 route_name='pullrequest_todos', request_method='POST',
1038 1034 renderer='string_html', xhr=True)
1039 1035 def pullrequest_todos(self):
1040 1036 self.load_default_context()
1041 1037
1042 1038 pull_request = PullRequest.get_or_404(
1043 1039 self.request.matchdict['pull_request_id'])
1044 1040 pull_request_id = pull_request.pull_request_id
1045 1041 version = self.request.GET.get('version')
1046 1042
1047 1043 _render = self.request.get_partial_renderer(
1048 1044 'rhodecode:templates/base/sidebar.mako')
1049 1045 c = _render.get_call_context()
1050 1046 (pull_request_latest,
1051 1047 pull_request_at_ver,
1052 1048 pull_request_display_obj,
1053 1049 at_version) = PullRequestModel().get_pr_version(
1054 1050 pull_request_id, version=version)
1055 1051 versions = pull_request_display_obj.versions()
1056 1052 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1057 1053 c.versions = versions + [latest_ver]
1058 1054
1059 1055 c.at_version = at_version
1060 1056 c.at_version_num = (at_version
1061 1057 if at_version and at_version != PullRequest.LATEST_VER
1062 1058 else None)
1063 1059
1064 1060 c.unresolved_comments = CommentsModel() \
1065 1061 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1066 1062 c.resolved_comments = CommentsModel() \
1067 1063 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1068 1064
1069 1065 all_comments = c.unresolved_comments + c.resolved_comments
1070 1066 existing_ids = self.get_comment_ids(self.request.POST)
1071 1067 return _render('comments_table', all_comments, len(c.unresolved_comments),
1072 1068 todo_comments=True, existing_ids=existing_ids)
1073 1069
1074 1070 @LoginRequired()
1075 1071 @NotAnonymous()
1076 1072 @HasRepoPermissionAnyDecorator(
1077 1073 'repository.read', 'repository.write', 'repository.admin')
1078 1074 @CSRFRequired()
1079 1075 @view_config(
1080 1076 route_name='pullrequest_create', request_method='POST',
1081 1077 renderer=None)
1082 1078 def pull_request_create(self):
1083 1079 _ = self.request.translate
1084 1080 self.assure_not_empty_repo()
1085 1081 self.load_default_context()
1086 1082
1087 1083 controls = peppercorn.parse(self.request.POST.items())
1088 1084
1089 1085 try:
1090 1086 form = PullRequestForm(
1091 1087 self.request.translate, self.db_repo.repo_id)()
1092 1088 _form = form.to_python(controls)
1093 1089 except formencode.Invalid as errors:
1094 1090 if errors.error_dict.get('revisions'):
1095 1091 msg = 'Revisions: %s' % errors.error_dict['revisions']
1096 1092 elif errors.error_dict.get('pullrequest_title'):
1097 1093 msg = errors.error_dict.get('pullrequest_title')
1098 1094 else:
1099 1095 msg = _('Error creating pull request: {}').format(errors)
1100 1096 log.exception(msg)
1101 1097 h.flash(msg, 'error')
1102 1098
1103 1099 # would rather just go back to form ...
1104 1100 raise HTTPFound(
1105 1101 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1106 1102
1107 1103 source_repo = _form['source_repo']
1108 1104 source_ref = _form['source_ref']
1109 1105 target_repo = _form['target_repo']
1110 1106 target_ref = _form['target_ref']
1111 1107 commit_ids = _form['revisions'][::-1]
1112 1108 common_ancestor_id = _form['common_ancestor']
1113 1109
1114 1110 # find the ancestor for this pr
1115 1111 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1116 1112 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1117 1113
1118 1114 if not (source_db_repo or target_db_repo):
1119 1115 h.flash(_('source_repo or target repo not found'), category='error')
1120 1116 raise HTTPFound(
1121 1117 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1122 1118
1123 1119 # re-check permissions again here
1124 1120 # source_repo we must have read permissions
1125 1121
1126 1122 source_perm = HasRepoPermissionAny(
1127 1123 'repository.read', 'repository.write', 'repository.admin')(
1128 1124 source_db_repo.repo_name)
1129 1125 if not source_perm:
1130 1126 msg = _('Not Enough permissions to source repo `{}`.'.format(
1131 1127 source_db_repo.repo_name))
1132 1128 h.flash(msg, category='error')
1133 1129 # copy the args back to redirect
1134 1130 org_query = self.request.GET.mixed()
1135 1131 raise HTTPFound(
1136 1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1137 1133 _query=org_query))
1138 1134
1139 1135 # target repo we must have read permissions, and also later on
1140 1136 # we want to check branch permissions here
1141 1137 target_perm = HasRepoPermissionAny(
1142 1138 'repository.read', 'repository.write', 'repository.admin')(
1143 1139 target_db_repo.repo_name)
1144 1140 if not target_perm:
1145 1141 msg = _('Not Enough permissions to target repo `{}`.'.format(
1146 1142 target_db_repo.repo_name))
1147 1143 h.flash(msg, category='error')
1148 1144 # copy the args back to redirect
1149 1145 org_query = self.request.GET.mixed()
1150 1146 raise HTTPFound(
1151 1147 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1152 1148 _query=org_query))
1153 1149
1154 1150 source_scm = source_db_repo.scm_instance()
1155 1151 target_scm = target_db_repo.scm_instance()
1156 1152
1157 1153 source_ref_obj = unicode_to_reference(source_ref)
1158 1154 target_ref_obj = unicode_to_reference(target_ref)
1159 1155
1160 1156 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1161 1157 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1162 1158
1163 1159 ancestor = source_scm.get_common_ancestor(
1164 1160 source_commit.raw_id, target_commit.raw_id, target_scm)
1165 1161
1166 1162 # recalculate target ref based on ancestor
1167 1163 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1168 1164
1169 1165 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1170 1166 PullRequestModel().get_reviewer_functions()
1171 1167
1172 1168 # recalculate reviewers logic, to make sure we can validate this
1173 1169 reviewer_rules = get_default_reviewers_data(
1174 1170 self._rhodecode_db_user,
1175 1171 source_db_repo,
1176 1172 source_ref_obj,
1177 1173 target_db_repo,
1178 1174 target_ref_obj,
1179 1175 include_diff_info=False)
1180 1176
1181 1177 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1182 1178 observers = validate_observers(_form['observer_members'], reviewer_rules)
1183 1179
1184 1180 pullrequest_title = _form['pullrequest_title']
1185 1181 title_source_ref = source_ref_obj.name
1186 1182 if not pullrequest_title:
1187 1183 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1188 1184 source=source_repo,
1189 1185 source_ref=title_source_ref,
1190 1186 target=target_repo
1191 1187 )
1192 1188
1193 1189 description = _form['pullrequest_desc']
1194 1190 description_renderer = _form['description_renderer']
1195 1191
1196 1192 try:
1197 1193 pull_request = PullRequestModel().create(
1198 1194 created_by=self._rhodecode_user.user_id,
1199 1195 source_repo=source_repo,
1200 1196 source_ref=source_ref,
1201 1197 target_repo=target_repo,
1202 1198 target_ref=target_ref,
1203 1199 revisions=commit_ids,
1204 1200 common_ancestor_id=common_ancestor_id,
1205 1201 reviewers=reviewers,
1206 1202 observers=observers,
1207 1203 title=pullrequest_title,
1208 1204 description=description,
1209 1205 description_renderer=description_renderer,
1210 1206 reviewer_data=reviewer_rules,
1211 1207 auth_user=self._rhodecode_user
1212 1208 )
1213 1209 Session().commit()
1214 1210
1215 1211 h.flash(_('Successfully opened new pull request'),
1216 1212 category='success')
1217 1213 except Exception:
1218 1214 msg = _('Error occurred during creation of this pull request.')
1219 1215 log.exception(msg)
1220 1216 h.flash(msg, category='error')
1221 1217
1222 1218 # copy the args back to redirect
1223 1219 org_query = self.request.GET.mixed()
1224 1220 raise HTTPFound(
1225 1221 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1226 1222 _query=org_query))
1227 1223
1228 1224 raise HTTPFound(
1229 1225 h.route_path('pullrequest_show', repo_name=target_repo,
1230 1226 pull_request_id=pull_request.pull_request_id))
1231 1227
1232 1228 @LoginRequired()
1233 1229 @NotAnonymous()
1234 1230 @HasRepoPermissionAnyDecorator(
1235 1231 'repository.read', 'repository.write', 'repository.admin')
1236 1232 @CSRFRequired()
1237 1233 @view_config(
1238 1234 route_name='pullrequest_update', request_method='POST',
1239 1235 renderer='json_ext')
1240 1236 def pull_request_update(self):
1241 1237 pull_request = PullRequest.get_or_404(
1242 1238 self.request.matchdict['pull_request_id'])
1243 1239 _ = self.request.translate
1244 1240
1245 1241 c = self.load_default_context()
1246 1242 redirect_url = None
1247 1243
1248 1244 if pull_request.is_closed():
1249 1245 log.debug('update: forbidden because pull request is closed')
1250 1246 msg = _(u'Cannot update closed pull requests.')
1251 1247 h.flash(msg, category='error')
1252 1248 return {'response': True,
1253 1249 'redirect_url': redirect_url}
1254 1250
1255 1251 is_state_changing = pull_request.is_state_changing()
1256 1252 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1257 1253
1258 1254 # only owner or admin can update it
1259 1255 allowed_to_update = PullRequestModel().check_user_update(
1260 1256 pull_request, self._rhodecode_user)
1261 1257
1262 1258 if allowed_to_update:
1263 1259 controls = peppercorn.parse(self.request.POST.items())
1264 1260 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1265 1261
1266 1262 if 'review_members' in controls:
1267 1263 self._update_reviewers(
1268 1264 c,
1269 1265 pull_request, controls['review_members'],
1270 1266 pull_request.reviewer_data,
1271 1267 PullRequestReviewers.ROLE_REVIEWER)
1272 1268 elif 'observer_members' in controls:
1273 1269 self._update_reviewers(
1274 1270 c,
1275 1271 pull_request, controls['observer_members'],
1276 1272 pull_request.reviewer_data,
1277 1273 PullRequestReviewers.ROLE_OBSERVER)
1278 1274 elif str2bool(self.request.POST.get('update_commits', 'false')):
1279 1275 if is_state_changing:
1280 1276 log.debug('commits update: forbidden because pull request is in state %s',
1281 1277 pull_request.pull_request_state)
1282 1278 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1283 1279 u'Current state is: `{}`').format(
1284 1280 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1285 1281 h.flash(msg, category='error')
1286 1282 return {'response': True,
1287 1283 'redirect_url': redirect_url}
1288 1284
1289 1285 self._update_commits(c, pull_request)
1290 1286 if force_refresh:
1291 1287 redirect_url = h.route_path(
1292 1288 'pullrequest_show', repo_name=self.db_repo_name,
1293 1289 pull_request_id=pull_request.pull_request_id,
1294 1290 _query={"force_refresh": 1})
1295 1291 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1296 1292 self._edit_pull_request(pull_request)
1297 1293 else:
1298 1294 log.error('Unhandled update data.')
1299 1295 raise HTTPBadRequest()
1300 1296
1301 1297 return {'response': True,
1302 1298 'redirect_url': redirect_url}
1303 1299 raise HTTPForbidden()
1304 1300
1305 1301 def _edit_pull_request(self, pull_request):
1306 1302 """
1307 1303 Edit title and description
1308 1304 """
1309 1305 _ = self.request.translate
1310 1306
1311 1307 try:
1312 1308 PullRequestModel().edit(
1313 1309 pull_request,
1314 1310 self.request.POST.get('title'),
1315 1311 self.request.POST.get('description'),
1316 1312 self.request.POST.get('description_renderer'),
1317 1313 self._rhodecode_user)
1318 1314 except ValueError:
1319 1315 msg = _(u'Cannot update closed pull requests.')
1320 1316 h.flash(msg, category='error')
1321 1317 return
1322 1318 else:
1323 1319 Session().commit()
1324 1320
1325 1321 msg = _(u'Pull request title & description updated.')
1326 1322 h.flash(msg, category='success')
1327 1323 return
1328 1324
1329 1325 def _update_commits(self, c, pull_request):
1330 1326 _ = self.request.translate
1331 1327
1332 1328 with pull_request.set_state(PullRequest.STATE_UPDATING):
1333 1329 resp = PullRequestModel().update_commits(
1334 1330 pull_request, self._rhodecode_db_user)
1335 1331
1336 1332 if resp.executed:
1337 1333
1338 1334 if resp.target_changed and resp.source_changed:
1339 1335 changed = 'target and source repositories'
1340 1336 elif resp.target_changed and not resp.source_changed:
1341 1337 changed = 'target repository'
1342 1338 elif not resp.target_changed and resp.source_changed:
1343 1339 changed = 'source repository'
1344 1340 else:
1345 1341 changed = 'nothing'
1346 1342
1347 1343 msg = _(u'Pull request updated to "{source_commit_id}" with '
1348 1344 u'{count_added} added, {count_removed} removed commits. '
1349 1345 u'Source of changes: {change_source}.')
1350 1346 msg = msg.format(
1351 1347 source_commit_id=pull_request.source_ref_parts.commit_id,
1352 1348 count_added=len(resp.changes.added),
1353 1349 count_removed=len(resp.changes.removed),
1354 1350 change_source=changed)
1355 1351 h.flash(msg, category='success')
1356 1352 channelstream.pr_update_channelstream_push(
1357 1353 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1358 1354 else:
1359 1355 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1360 1356 warning_reasons = [
1361 1357 UpdateFailureReason.NO_CHANGE,
1362 1358 UpdateFailureReason.WRONG_REF_TYPE,
1363 1359 ]
1364 1360 category = 'warning' if resp.reason in warning_reasons else 'error'
1365 1361 h.flash(msg, category=category)
1366 1362
1367 1363 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1368 1364 _ = self.request.translate
1369 1365
1370 1366 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1371 1367 PullRequestModel().get_reviewer_functions()
1372 1368
1373 1369 if role == PullRequestReviewers.ROLE_REVIEWER:
1374 1370 try:
1375 1371 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1376 1372 except ValueError as e:
1377 1373 log.error('Reviewers Validation: {}'.format(e))
1378 1374 h.flash(e, category='error')
1379 1375 return
1380 1376
1381 1377 old_calculated_status = pull_request.calculated_review_status()
1382 1378 PullRequestModel().update_reviewers(
1383 1379 pull_request, reviewers, self._rhodecode_db_user)
1384 1380
1385 1381 Session().commit()
1386 1382
1387 1383 msg = _('Pull request reviewers updated.')
1388 1384 h.flash(msg, category='success')
1389 1385 channelstream.pr_update_channelstream_push(
1390 1386 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 1387
1392 1388 # trigger status changed if change in reviewers changes the status
1393 1389 calculated_status = pull_request.calculated_review_status()
1394 1390 if old_calculated_status != calculated_status:
1395 1391 PullRequestModel().trigger_pull_request_hook(
1396 1392 pull_request, self._rhodecode_user, 'review_status_change',
1397 1393 data={'status': calculated_status})
1398 1394
1399 1395 elif role == PullRequestReviewers.ROLE_OBSERVER:
1400 1396 try:
1401 1397 observers = validate_observers(review_members, reviewer_rules)
1402 1398 except ValueError as e:
1403 1399 log.error('Observers Validation: {}'.format(e))
1404 1400 h.flash(e, category='error')
1405 1401 return
1406 1402
1407 1403 PullRequestModel().update_observers(
1408 1404 pull_request, observers, self._rhodecode_db_user)
1409 1405
1410 1406 Session().commit()
1411 1407 msg = _('Pull request observers updated.')
1412 1408 h.flash(msg, category='success')
1413 1409 channelstream.pr_update_channelstream_push(
1414 1410 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1415 1411
1416 1412 @LoginRequired()
1417 1413 @NotAnonymous()
1418 1414 @HasRepoPermissionAnyDecorator(
1419 1415 'repository.read', 'repository.write', 'repository.admin')
1420 1416 @CSRFRequired()
1421 1417 @view_config(
1422 1418 route_name='pullrequest_merge', request_method='POST',
1423 1419 renderer='json_ext')
1424 1420 def pull_request_merge(self):
1425 1421 """
1426 1422 Merge will perform a server-side merge of the specified
1427 1423 pull request, if the pull request is approved and mergeable.
1428 1424 After successful merging, the pull request is automatically
1429 1425 closed, with a relevant comment.
1430 1426 """
1431 1427 pull_request = PullRequest.get_or_404(
1432 1428 self.request.matchdict['pull_request_id'])
1433 1429 _ = self.request.translate
1434 1430
1435 1431 if pull_request.is_state_changing():
1436 1432 log.debug('show: forbidden because pull request is in state %s',
1437 1433 pull_request.pull_request_state)
1438 1434 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1439 1435 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1440 1436 pull_request.pull_request_state)
1441 1437 h.flash(msg, category='error')
1442 1438 raise HTTPFound(
1443 1439 h.route_path('pullrequest_show',
1444 1440 repo_name=pull_request.target_repo.repo_name,
1445 1441 pull_request_id=pull_request.pull_request_id))
1446 1442
1447 1443 self.load_default_context()
1448 1444
1449 1445 with pull_request.set_state(PullRequest.STATE_UPDATING):
1450 1446 check = MergeCheck.validate(
1451 1447 pull_request, auth_user=self._rhodecode_user,
1452 1448 translator=self.request.translate)
1453 1449 merge_possible = not check.failed
1454 1450
1455 1451 for err_type, error_msg in check.errors:
1456 1452 h.flash(error_msg, category=err_type)
1457 1453
1458 1454 if merge_possible:
1459 1455 log.debug("Pre-conditions checked, trying to merge.")
1460 1456 extras = vcs_operation_context(
1461 1457 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1462 1458 username=self._rhodecode_db_user.username, action='push',
1463 1459 scm=pull_request.target_repo.repo_type)
1464 1460 with pull_request.set_state(PullRequest.STATE_UPDATING):
1465 1461 self._merge_pull_request(
1466 1462 pull_request, self._rhodecode_db_user, extras)
1467 1463 else:
1468 1464 log.debug("Pre-conditions failed, NOT merging.")
1469 1465
1470 1466 raise HTTPFound(
1471 1467 h.route_path('pullrequest_show',
1472 1468 repo_name=pull_request.target_repo.repo_name,
1473 1469 pull_request_id=pull_request.pull_request_id))
1474 1470
1475 1471 def _merge_pull_request(self, pull_request, user, extras):
1476 1472 _ = self.request.translate
1477 1473 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1478 1474
1479 1475 if merge_resp.executed:
1480 1476 log.debug("The merge was successful, closing the pull request.")
1481 1477 PullRequestModel().close_pull_request(
1482 1478 pull_request.pull_request_id, user)
1483 1479 Session().commit()
1484 1480 msg = _('Pull request was successfully merged and closed.')
1485 1481 h.flash(msg, category='success')
1486 1482 else:
1487 1483 log.debug(
1488 1484 "The merge was not successful. Merge response: %s", merge_resp)
1489 1485 msg = merge_resp.merge_status_message
1490 1486 h.flash(msg, category='error')
1491 1487
1492 1488 @LoginRequired()
1493 1489 @NotAnonymous()
1494 1490 @HasRepoPermissionAnyDecorator(
1495 1491 'repository.read', 'repository.write', 'repository.admin')
1496 1492 @CSRFRequired()
1497 1493 @view_config(
1498 1494 route_name='pullrequest_delete', request_method='POST',
1499 1495 renderer='json_ext')
1500 1496 def pull_request_delete(self):
1501 1497 _ = self.request.translate
1502 1498
1503 1499 pull_request = PullRequest.get_or_404(
1504 1500 self.request.matchdict['pull_request_id'])
1505 1501 self.load_default_context()
1506 1502
1507 1503 pr_closed = pull_request.is_closed()
1508 1504 allowed_to_delete = PullRequestModel().check_user_delete(
1509 1505 pull_request, self._rhodecode_user) and not pr_closed
1510 1506
1511 1507 # only owner can delete it !
1512 1508 if allowed_to_delete:
1513 1509 PullRequestModel().delete(pull_request, self._rhodecode_user)
1514 1510 Session().commit()
1515 1511 h.flash(_('Successfully deleted pull request'),
1516 1512 category='success')
1517 1513 raise HTTPFound(h.route_path('pullrequest_show_all',
1518 1514 repo_name=self.db_repo_name))
1519 1515
1520 1516 log.warning('user %s tried to delete pull request without access',
1521 1517 self._rhodecode_user)
1522 1518 raise HTTPNotFound()
1523 1519
1524 1520 def _pull_request_comments_create(self, pull_request, comments):
1525 1521 _ = self.request.translate
1526 1522 data = {}
1527 1523 if not comments:
1528 1524 return
1529 1525 pull_request_id = pull_request.pull_request_id
1530 1526
1531 1527 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1532 1528
1533 1529 for entry in comments:
1534 1530 c = self.load_default_context()
1535 1531 comment_type = entry['comment_type']
1536 1532 text = entry['text']
1537 1533 status = entry['status']
1538 1534 is_draft = str2bool(entry['is_draft'])
1539 1535 resolves_comment_id = entry['resolves_comment_id']
1540 1536 close_pull_request = entry['close_pull_request']
1541 1537 f_path = entry['f_path']
1542 1538 line_no = entry['line']
1543 1539 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1544 1540
1545 1541 # the logic here should work like following, if we submit close
1546 1542 # pr comment, use `close_pull_request_with_comment` function
1547 1543 # else handle regular comment logic
1548 1544
1549 1545 if close_pull_request:
1550 1546 # only owner or admin or person with write permissions
1551 1547 allowed_to_close = PullRequestModel().check_user_update(
1552 1548 pull_request, self._rhodecode_user)
1553 1549 if not allowed_to_close:
1554 1550 log.debug('comment: forbidden because not allowed to close '
1555 1551 'pull request %s', pull_request_id)
1556 1552 raise HTTPForbidden()
1557 1553
1558 1554 # This also triggers `review_status_change`
1559 1555 comment, status = PullRequestModel().close_pull_request_with_comment(
1560 1556 pull_request, self._rhodecode_user, self.db_repo, message=text,
1561 1557 auth_user=self._rhodecode_user)
1562 1558 Session().flush()
1563 1559 is_inline = comment.is_inline
1564 1560
1565 1561 PullRequestModel().trigger_pull_request_hook(
1566 1562 pull_request, self._rhodecode_user, 'comment',
1567 1563 data={'comment': comment})
1568 1564
1569 1565 else:
1570 1566 # regular comment case, could be inline, or one with status.
1571 1567 # for that one we check also permissions
1572 1568 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1573 1569 allowed_to_change_status = PullRequestModel().check_user_change_status(
1574 1570 pull_request, self._rhodecode_user) and not is_draft
1575 1571
1576 1572 if status and allowed_to_change_status:
1577 1573 message = (_('Status change %(transition_icon)s %(status)s')
1578 1574 % {'transition_icon': '>',
1579 1575 'status': ChangesetStatus.get_status_lbl(status)})
1580 1576 text = text or message
1581 1577
1582 1578 comment = CommentsModel().create(
1583 1579 text=text,
1584 1580 repo=self.db_repo.repo_id,
1585 1581 user=self._rhodecode_user.user_id,
1586 1582 pull_request=pull_request,
1587 1583 f_path=f_path,
1588 1584 line_no=line_no,
1589 1585 status_change=(ChangesetStatus.get_status_lbl(status)
1590 1586 if status and allowed_to_change_status else None),
1591 1587 status_change_type=(status
1592 1588 if status and allowed_to_change_status else None),
1593 1589 comment_type=comment_type,
1594 1590 is_draft=is_draft,
1595 1591 resolves_comment_id=resolves_comment_id,
1596 1592 auth_user=self._rhodecode_user,
1597 1593 send_email=not is_draft, # skip notification for draft comments
1598 1594 )
1599 1595 is_inline = comment.is_inline
1600 1596
1601 1597 if allowed_to_change_status:
1602 1598 # calculate old status before we change it
1603 1599 old_calculated_status = pull_request.calculated_review_status()
1604 1600
1605 1601 # get status if set !
1606 1602 if status:
1607 1603 ChangesetStatusModel().set_status(
1608 1604 self.db_repo.repo_id,
1609 1605 status,
1610 1606 self._rhodecode_user.user_id,
1611 1607 comment,
1612 1608 pull_request=pull_request
1613 1609 )
1614 1610
1615 1611 Session().flush()
1616 1612 # this is somehow required to get access to some relationship
1617 1613 # loaded on comment
1618 1614 Session().refresh(comment)
1619 1615
1620 1616 # skip notifications for drafts
1621 1617 if not is_draft:
1622 1618 PullRequestModel().trigger_pull_request_hook(
1623 1619 pull_request, self._rhodecode_user, 'comment',
1624 1620 data={'comment': comment})
1625 1621
1626 1622 # we now calculate the status of pull request, and based on that
1627 1623 # calculation we set the commits status
1628 1624 calculated_status = pull_request.calculated_review_status()
1629 1625 if old_calculated_status != calculated_status:
1630 1626 PullRequestModel().trigger_pull_request_hook(
1631 1627 pull_request, self._rhodecode_user, 'review_status_change',
1632 1628 data={'status': calculated_status})
1633 1629
1634 1630 comment_id = comment.comment_id
1635 1631 data[comment_id] = {
1636 1632 'target_id': target_elem_id
1637 1633 }
1638 1634 Session().flush()
1639 1635
1640 1636 c.co = comment
1641 1637 c.at_version_num = None
1642 1638 c.is_new = True
1643 1639 rendered_comment = render(
1644 1640 'rhodecode:templates/changeset/changeset_comment_block.mako',
1645 1641 self._get_template_context(c), self.request)
1646 1642
1647 1643 data[comment_id].update(comment.get_dict())
1648 1644 data[comment_id].update({'rendered_text': rendered_comment})
1649 1645
1650 1646 Session().commit()
1651 1647
1652 1648 # skip channelstream for draft comments
1653 1649 if not all_drafts:
1654 1650 comment_broadcast_channel = channelstream.comment_channel(
1655 1651 self.db_repo_name, pull_request_obj=pull_request)
1656 1652
1657 1653 comment_data = data
1658 1654 posted_comment_type = 'inline' if is_inline else 'general'
1659 1655 if len(data) == 1:
1660 1656 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1661 1657 else:
1662 1658 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1663 1659
1664 1660 channelstream.comment_channelstream_push(
1665 1661 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1666 1662 comment_data=comment_data)
1667 1663
1668 1664 return data
1669 1665
1670 1666 @LoginRequired()
1671 1667 @NotAnonymous()
1672 1668 @HasRepoPermissionAnyDecorator(
1673 1669 'repository.read', 'repository.write', 'repository.admin')
1674 1670 @CSRFRequired()
1675 1671 @view_config(
1676 1672 route_name='pullrequest_comment_create', request_method='POST',
1677 1673 renderer='json_ext')
1678 1674 def pull_request_comment_create(self):
1679 1675 _ = self.request.translate
1680 1676
1681 1677 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1682 1678
1683 1679 if pull_request.is_closed():
1684 1680 log.debug('comment: forbidden because pull request is closed')
1685 1681 raise HTTPForbidden()
1686 1682
1687 1683 allowed_to_comment = PullRequestModel().check_user_comment(
1688 1684 pull_request, self._rhodecode_user)
1689 1685 if not allowed_to_comment:
1690 1686 log.debug('comment: forbidden because pull request is from forbidden repo')
1691 1687 raise HTTPForbidden()
1692 1688
1693 1689 comment_data = {
1694 1690 'comment_type': self.request.POST.get('comment_type'),
1695 1691 'text': self.request.POST.get('text'),
1696 1692 'status': self.request.POST.get('changeset_status', None),
1697 1693 'is_draft': self.request.POST.get('draft'),
1698 1694 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1699 1695 'close_pull_request': self.request.POST.get('close_pull_request'),
1700 1696 'f_path': self.request.POST.get('f_path'),
1701 1697 'line': self.request.POST.get('line'),
1702 1698 }
1703 1699 data = self._pull_request_comments_create(pull_request, [comment_data])
1704 1700
1705 1701 return data
1706 1702
1707 1703 @LoginRequired()
1708 1704 @NotAnonymous()
1709 1705 @HasRepoPermissionAnyDecorator(
1710 1706 'repository.read', 'repository.write', 'repository.admin')
1711 1707 @CSRFRequired()
1712 1708 @view_config(
1713 1709 route_name='pullrequest_comment_delete', request_method='POST',
1714 1710 renderer='json_ext')
1715 1711 def pull_request_comment_delete(self):
1716 1712 pull_request = PullRequest.get_or_404(
1717 1713 self.request.matchdict['pull_request_id'])
1718 1714
1719 1715 comment = ChangesetComment.get_or_404(
1720 1716 self.request.matchdict['comment_id'])
1721 1717 comment_id = comment.comment_id
1722 1718
1723 1719 if comment.immutable:
1724 1720 # don't allow deleting comments that are immutable
1725 1721 raise HTTPForbidden()
1726 1722
1727 1723 if pull_request.is_closed():
1728 1724 log.debug('comment: forbidden because pull request is closed')
1729 1725 raise HTTPForbidden()
1730 1726
1731 1727 if not comment:
1732 1728 log.debug('Comment with id:%s not found, skipping', comment_id)
1733 1729 # comment already deleted in another call probably
1734 1730 return True
1735 1731
1736 1732 if comment.pull_request.is_closed():
1737 1733 # don't allow deleting comments on closed pull request
1738 1734 raise HTTPForbidden()
1739 1735
1740 1736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1741 1737 super_admin = h.HasPermissionAny('hg.admin')()
1742 1738 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1743 1739 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1744 1740 comment_repo_admin = is_repo_admin and is_repo_comment
1745 1741
1746 1742 if super_admin or comment_owner or comment_repo_admin:
1747 1743 old_calculated_status = comment.pull_request.calculated_review_status()
1748 1744 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1749 1745 Session().commit()
1750 1746 calculated_status = comment.pull_request.calculated_review_status()
1751 1747 if old_calculated_status != calculated_status:
1752 1748 PullRequestModel().trigger_pull_request_hook(
1753 1749 comment.pull_request, self._rhodecode_user, 'review_status_change',
1754 1750 data={'status': calculated_status})
1755 1751 return True
1756 1752 else:
1757 1753 log.warning('No permissions for user %s to delete comment_id: %s',
1758 1754 self._rhodecode_db_user, comment_id)
1759 1755 raise HTTPNotFound()
1760 1756
1761 1757 @LoginRequired()
1762 1758 @NotAnonymous()
1763 1759 @HasRepoPermissionAnyDecorator(
1764 1760 'repository.read', 'repository.write', 'repository.admin')
1765 1761 @CSRFRequired()
1766 1762 @view_config(
1767 1763 route_name='pullrequest_comment_edit', request_method='POST',
1768 1764 renderer='json_ext')
1769 1765 def pull_request_comment_edit(self):
1770 1766 self.load_default_context()
1771 1767
1772 1768 pull_request = PullRequest.get_or_404(
1773 1769 self.request.matchdict['pull_request_id']
1774 1770 )
1775 1771 comment = ChangesetComment.get_or_404(
1776 1772 self.request.matchdict['comment_id']
1777 1773 )
1778 1774 comment_id = comment.comment_id
1779 1775
1780 1776 if comment.immutable:
1781 1777 # don't allow deleting comments that are immutable
1782 1778 raise HTTPForbidden()
1783 1779
1784 1780 if pull_request.is_closed():
1785 1781 log.debug('comment: forbidden because pull request is closed')
1786 1782 raise HTTPForbidden()
1787 1783
1788 1784 if comment.pull_request.is_closed():
1789 1785 # don't allow deleting comments on closed pull request
1790 1786 raise HTTPForbidden()
1791 1787
1792 1788 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1793 1789 super_admin = h.HasPermissionAny('hg.admin')()
1794 1790 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1795 1791 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1796 1792 comment_repo_admin = is_repo_admin and is_repo_comment
1797 1793
1798 1794 if super_admin or comment_owner or comment_repo_admin:
1799 1795 text = self.request.POST.get('text')
1800 1796 version = self.request.POST.get('version')
1801 1797 if text == comment.text:
1802 1798 log.warning(
1803 1799 'Comment(PR): '
1804 1800 'Trying to create new version '
1805 1801 'with the same comment body {}'.format(
1806 1802 comment_id,
1807 1803 )
1808 1804 )
1809 1805 raise HTTPNotFound()
1810 1806
1811 1807 if version.isdigit():
1812 1808 version = int(version)
1813 1809 else:
1814 1810 log.warning(
1815 1811 'Comment(PR): Wrong version type {} {} '
1816 1812 'for comment {}'.format(
1817 1813 version,
1818 1814 type(version),
1819 1815 comment_id,
1820 1816 )
1821 1817 )
1822 1818 raise HTTPNotFound()
1823 1819
1824 1820 try:
1825 1821 comment_history = CommentsModel().edit(
1826 1822 comment_id=comment_id,
1827 1823 text=text,
1828 1824 auth_user=self._rhodecode_user,
1829 1825 version=version,
1830 1826 )
1831 1827 except CommentVersionMismatch:
1832 1828 raise HTTPConflict()
1833 1829
1834 1830 if not comment_history:
1835 1831 raise HTTPNotFound()
1836 1832
1837 1833 Session().commit()
1838 1834 if not comment.draft:
1839 1835 PullRequestModel().trigger_pull_request_hook(
1840 1836 pull_request, self._rhodecode_user, 'comment_edit',
1841 1837 data={'comment': comment})
1842 1838
1843 1839 return {
1844 1840 'comment_history_id': comment_history.comment_history_id,
1845 1841 'comment_id': comment.comment_id,
1846 1842 'comment_version': comment_history.version,
1847 1843 'comment_author_username': comment_history.author.username,
1848 1844 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1849 1845 'comment_created_on': h.age_component(comment_history.created_on,
1850 1846 time_is_local=True),
1851 1847 }
1852 1848 else:
1853 1849 log.warning('No permissions for user %s to edit comment_id: %s',
1854 1850 self._rhodecode_db_user, comment_id)
1855 1851 raise HTTPNotFound()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1195 +1,1146 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 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 83 var rawMessage = elements[0]['message'];
84 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
87 87 // use reference name
88 88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 89 var refType = sourceRefType;
90 90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 91 }
92 92
93 93 return [title, desc]
94 94 };
95 95
96 96
97 97 window.ReviewersController = function () {
98 98 var self = this;
99 99 this.$loadingIndicator = $('.calculate-reviewers');
100 100 this.$reviewRulesContainer = $('#review_rules');
101 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
102 102 this.$userRule = $('.pr-user-rule-container');
103 103 this.$reviewMembers = $('#review_members');
104 104 this.$observerMembers = $('#observer_members');
105 105
106 106 this.currentRequest = null;
107 107 this.diffData = null;
108 108 this.enabledRules = [];
109 109 // sync with db.py entries
110 110 this.ROLE_REVIEWER = 'reviewer';
111 111 this.ROLE_OBSERVER = 'observer'
112 112
113 113 //dummy handler, we might register our own later
114 114 this.diffDataHandler = function (data) {};
115 115
116 116 this.defaultForbidUsers = function () {
117 117 return [
118 118 {
119 119 'username': 'default',
120 120 'user_id': templateContext.default_user.user_id
121 121 }
122 122 ];
123 123 };
124 124
125 125 // init default forbidden users
126 126 this.forbidUsers = this.defaultForbidUsers();
127 127
128 128 this.hideReviewRules = function () {
129 129 self.$reviewRulesContainer.hide();
130 130 $(self.$userRule.selector).hide();
131 131 };
132 132
133 133 this.showReviewRules = function () {
134 134 self.$reviewRulesContainer.show();
135 135 $(self.$userRule.selector).show();
136 136 };
137 137
138 138 this.addRule = function (ruleText) {
139 139 self.showReviewRules();
140 140 self.enabledRules.push(ruleText);
141 141 return '<div>- {0}</div>'.format(ruleText)
142 142 };
143 143
144 144 this.increaseCounter = function(role) {
145 145 if (role === self.ROLE_REVIEWER) {
146 146 var $elem = $('#reviewers-cnt')
147 147 var cnt = parseInt($elem.data('count') || 0)
148 148 cnt +=1
149 149 $elem.html(cnt);
150 150 $elem.data('count', cnt);
151 151 }
152 152 else if (role === self.ROLE_OBSERVER) {
153 153 var $elem = $('#observers-cnt');
154 154 var cnt = parseInt($elem.data('count') || 0)
155 155 cnt +=1
156 156 $elem.html(cnt);
157 157 $elem.data('count', cnt);
158 158 }
159 159 }
160 160
161 161 this.resetCounter = function () {
162 162 var $elem = $('#reviewers-cnt');
163 163
164 164 $elem.data('count', 0);
165 165 $elem.html(0);
166 166
167 167 var $elem = $('#observers-cnt');
168 168
169 169 $elem.data('count', 0);
170 170 $elem.html(0);
171 171 }
172 172
173 173 this.loadReviewRules = function (data) {
174 174 self.diffData = data;
175 175
176 176 // reset forbidden Users
177 177 this.forbidUsers = self.defaultForbidUsers();
178 178
179 179 // reset state of review rules
180 180 self.$rulesList.html('');
181 181
182 182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 183 // default rule, case for older repo that don't have any rules stored
184 184 self.$rulesList.append(
185 self.addRule(
186 _gettext('All reviewers must vote.'))
185 self.addRule(_gettext('All reviewers must vote.'))
187 186 );
188 187 return self.forbidUsers
189 188 }
190 189
191 if (data.rules.voting !== undefined) {
192 if (data.rules.voting < 0) {
193 self.$rulesList.append(
194 self.addRule(
195 _gettext('All individual reviewers must vote.'))
196 )
197 } else if (data.rules.voting === 1) {
198 self.$rulesList.append(
199 self.addRule(
200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
201 )
202
203 } else {
204 self.$rulesList.append(
205 self.addRule(
206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
207 )
208 }
190 if (data.rules.forbid_adding_reviewers) {
191 $('#add_reviewer_input').remove();
209 192 }
210 193
211 if (data.rules.voting_groups !== undefined) {
212 $.each(data.rules.voting_groups, function (index, rule_data) {
213 self.$rulesList.append(
214 self.addRule(rule_data.text)
215 )
216 });
217 }
218
219 if (data.rules.use_code_authors_for_review) {
220 self.$rulesList.append(
221 self.addRule(
222 _gettext('Reviewers picked from source code changes.'))
223 )
194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
195 $.each(data.rules_data.forbidden_users, function(idx, val){
196 self.forbidUsers.push(val)
197 })
224 198 }
225 199
226 if (data.rules.forbid_adding_reviewers) {
227 $('#add_reviewer_input').remove();
200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
201 $.each(data.rules_humanized, function(idx, val) {
202 self.$rulesList.append(
203 self.addRule(val)
204 )
205 })
206 } else {
207 // we don't have any rules set, so we inform users about it
228 208 self.$rulesList.append(
229 self.addRule(
230 _gettext('Adding new reviewers is forbidden.'))
231 )
232 }
233
234 if (data.rules.forbid_author_to_review) {
235 self.forbidUsers.push(data.rules_data.pr_author);
236 self.$rulesList.append(
237 self.addRule(
238 _gettext('Author is not allowed to be a reviewer.'))
209 self.addRule(_gettext('No additional review rules set.'))
239 210 )
240 211 }
241 212
242 if (data.rules.forbid_commit_author_to_review) {
243
244 if (data.rules_data.forbidden_users) {
245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
246 self.forbidUsers.push(member_data)
247 });
248 }
249
250 self.$rulesList.append(
251 self.addRule(
252 _gettext('Commit Authors are not allowed to be a reviewer.'))
253 )
254 }
255
256 // we don't have any rules set, so we inform users about it
257 if (self.enabledRules.length === 0) {
258 self.addRule(
259 _gettext('No review rules set.'))
260 }
261
262 213 return self.forbidUsers
263 214 };
264 215
265 216 this.emptyTables = function () {
266 217 self.emptyReviewersTable();
267 218 self.emptyObserversTable();
268 219
269 220 // Also reset counters.
270 221 self.resetCounter();
271 222 }
272 223
273 224 this.emptyReviewersTable = function (withText) {
274 225 self.$reviewMembers.empty();
275 226 if (withText !== undefined) {
276 227 self.$reviewMembers.html(withText)
277 228 }
278 229 };
279 230
280 231 this.emptyObserversTable = function (withText) {
281 232 self.$observerMembers.empty();
282 233 if (withText !== undefined) {
283 234 self.$observerMembers.html(withText)
284 235 }
285 236 }
286 237
287 238 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
288 239
289 240 if (self.currentRequest) {
290 241 // make sure we cleanup old running requests before triggering this again
291 242 self.currentRequest.abort();
292 243 }
293 244
294 245 self.$loadingIndicator.show();
295 246
296 247 // reset reviewer/observe members
297 248 self.emptyTables();
298 249
299 250 prButtonLock(true, null, 'reviewers');
300 251 $('#user').hide(); // hide user autocomplete before load
301 252 $('#observer').hide(); //hide observer autocomplete before load
302 253
303 254 // lock PR button, so we cannot send PR before it's calculated
304 255 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
305 256
306 257 if (sourceRef.length !== 3 || targetRef.length !== 3) {
307 258 // don't load defaults in case we're missing some refs...
308 259 self.$loadingIndicator.hide();
309 260 return
310 261 }
311 262
312 263 var url = pyroutes.url('repo_default_reviewers_data',
313 264 {
314 265 'repo_name': templateContext.repo_name,
315 266 'source_repo': sourceRepo,
316 267 'source_ref_type': sourceRef[0],
317 268 'source_ref_name': sourceRef[1],
318 269 'source_ref': sourceRef[2],
319 270 'target_repo': targetRepo,
320 271 'target_ref': targetRef[2],
321 272 'target_ref_type': sourceRef[0],
322 273 'target_ref_name': sourceRef[1]
323 274 });
324 275
325 276 self.currentRequest = $.ajax({
326 277 url: url,
327 278 headers: {'X-PARTIAL-XHR': true},
328 279 type: 'GET',
329 280 success: function (data) {
330 281
331 282 self.currentRequest = null;
332 283
333 284 // review rules
334 285 self.loadReviewRules(data);
335 286 var diffHandled = self.handleDiffData(data["diff_info"]);
336 287 if (diffHandled === false) {
337 288 return
338 289 }
339 290
340 291 for (var i = 0; i < data.reviewers.length; i++) {
341 292 var reviewer = data.reviewers[i];
342 293 // load reviewer rules from the repo data
343 294 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
344 295 }
345 296
346 297
347 298 self.$loadingIndicator.hide();
348 299 prButtonLock(false, null, 'reviewers');
349 300
350 301 $('#user').show(); // show user autocomplete before load
351 302 $('#observer').show(); // show observer autocomplete before load
352 303
353 304 var commitElements = data["diff_info"]['commits'];
354 305
355 306 if (commitElements.length === 0) {
356 307 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
357 308 _gettext('There are no commits to merge.'));
358 309 prButtonLock(true, noCommitsMsg, 'all');
359 310
360 311 } else {
361 312 // un-lock PR button, so we cannot send PR before it's calculated
362 313 prButtonLock(false, null, 'compare');
363 314 }
364 315
365 316 },
366 317 error: function (jqXHR, textStatus, errorThrown) {
367 318 var prefix = "Loading diff and reviewers/observers failed\n"
368 319 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
369 320 ajaxErrorSwal(message);
370 321 }
371 322 });
372 323
373 324 };
374 325
375 326 // check those, refactor
376 327 this.removeMember = function (reviewer_id, mark_delete) {
377 328 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
378 329
379 330 if (typeof (mark_delete) === undefined) {
380 331 mark_delete = false;
381 332 }
382 333
383 334 if (mark_delete === true) {
384 335 if (reviewer) {
385 336 // now delete the input
386 337 $('#reviewer_{0} input'.format(reviewer_id)).remove();
387 338 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
388 339 // mark as to-delete
389 340 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
390 341 obj.addClass('to-delete');
391 342 obj.css({"text-decoration": "line-through", "opacity": 0.5});
392 343 }
393 344 } else {
394 345 $('#reviewer_{0}'.format(reviewer_id)).remove();
395 346 }
396 347 };
397 348
398 349 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
399 350
400 351 var id = reviewer_obj.user_id;
401 352 var username = reviewer_obj.username;
402 353
403 354 reasons = reasons || [];
404 355 mandatory = mandatory || false;
405 356 role = role || self.ROLE_REVIEWER
406 357
407 358 // register current set IDS to check if we don't have this ID already in
408 359 // and prevent duplicates
409 360 var currentIds = [];
410 361
411 362 $.each($('.reviewer_entry'), function (index, value) {
412 363 currentIds.push($(value).data('reviewerUserId'))
413 364 })
414 365
415 366 var userAllowedReview = function (userId) {
416 367 var allowed = true;
417 368 $.each(self.forbidUsers, function (index, member_data) {
418 369 if (parseInt(userId) === member_data['user_id']) {
419 370 allowed = false;
420 371 return false // breaks the loop
421 372 }
422 373 });
423 374 return allowed
424 375 };
425 376
426 377 var userAllowed = userAllowedReview(id);
427 378
428 379 if (!userAllowed) {
429 380 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
430 381 } else {
431 382 // only add if it's not there
432 383 var alreadyReviewer = currentIds.indexOf(id) != -1;
433 384
434 385 if (alreadyReviewer) {
435 386 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
436 387 } else {
437 388
438 389 var reviewerEntry = renderTemplate('reviewMemberEntry', {
439 390 'member': reviewer_obj,
440 391 'mandatory': mandatory,
441 392 'role': role,
442 393 'reasons': reasons,
443 394 'allowed_to_update': true,
444 395 'review_status': 'not_reviewed',
445 396 'review_status_label': _gettext('Not Reviewed'),
446 397 'user_group': reviewer_obj.user_group,
447 398 'create': true,
448 399 'rule_show': true,
449 400 })
450 401
451 402 if (role === self.ROLE_REVIEWER) {
452 403 $(self.$reviewMembers.selector).append(reviewerEntry);
453 404 self.increaseCounter(self.ROLE_REVIEWER);
454 405 $('#reviewer-empty-msg').remove()
455 406 }
456 407 else if (role === self.ROLE_OBSERVER) {
457 408 $(self.$observerMembers.selector).append(reviewerEntry);
458 409 self.increaseCounter(self.ROLE_OBSERVER);
459 410 $('#observer-empty-msg').remove();
460 411 }
461 412
462 413 tooltipActivate();
463 414 }
464 415 }
465 416
466 417 };
467 418
468 419 this.updateReviewers = function (repo_name, pull_request_id, role) {
469 420 if (role === 'reviewer') {
470 421 var postData = $('#reviewers input').serialize();
471 422 _updatePullRequest(repo_name, pull_request_id, postData);
472 423 } else if (role === 'observer') {
473 424 var postData = $('#observers input').serialize();
474 425 _updatePullRequest(repo_name, pull_request_id, postData);
475 426 }
476 427 };
477 428
478 429 this.handleDiffData = function (data) {
479 430 return self.diffDataHandler(data)
480 431 }
481 432 };
482 433
483 434
484 435 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
485 436 var url = pyroutes.url(
486 437 'pullrequest_update',
487 438 {"repo_name": repo_name, "pull_request_id": pull_request_id});
488 439 if (typeof postData === 'string' ) {
489 440 postData += '&csrf_token=' + CSRF_TOKEN;
490 441 } else {
491 442 postData.csrf_token = CSRF_TOKEN;
492 443 }
493 444
494 445 var success = function(o) {
495 446 var redirectUrl = o['redirect_url'];
496 447 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
497 448 window.location = redirectUrl;
498 449 } else {
499 450 window.location.reload();
500 451 }
501 452 };
502 453
503 454 ajaxPOST(url, postData, success);
504 455 };
505 456
506 457 /**
507 458 * PULL REQUEST update commits
508 459 */
509 460 var updateCommits = function(repo_name, pull_request_id, force) {
510 461 var postData = {
511 462 'update_commits': true
512 463 };
513 464 if (force !== undefined && force === true) {
514 465 postData['force_refresh'] = true
515 466 }
516 467 _updatePullRequest(repo_name, pull_request_id, postData);
517 468 };
518 469
519 470
520 471 /**
521 472 * PULL REQUEST edit info
522 473 */
523 474 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
524 475 var url = pyroutes.url(
525 476 'pullrequest_update',
526 477 {"repo_name": repo_name, "pull_request_id": pull_request_id});
527 478
528 479 var postData = {
529 480 'title': title,
530 481 'description': description,
531 482 'description_renderer': renderer,
532 483 'edit_pull_request': true,
533 484 'csrf_token': CSRF_TOKEN
534 485 };
535 486 var success = function(o) {
536 487 window.location.reload();
537 488 };
538 489 ajaxPOST(url, postData, success);
539 490 };
540 491
541 492
542 493 /**
543 494 * autocomplete handler for reviewers/observers
544 495 */
545 496 var autoCompleteHandler = function (inputId, controller, role) {
546 497
547 498 return function (element, data) {
548 499 var mandatory = false;
549 500 var reasons = [_gettext('added manually by "{0}"').format(
550 501 templateContext.rhodecode_user.username)];
551 502
552 503 // add whole user groups
553 504 if (data.value_type == 'user_group') {
554 505 reasons.push(_gettext('member of "{0}"').format(data.value_display));
555 506
556 507 $.each(data.members, function (index, member_data) {
557 508 var reviewer = member_data;
558 509 reviewer['user_id'] = member_data['id'];
559 510 reviewer['gravatar_link'] = member_data['icon_link'];
560 511 reviewer['user_link'] = member_data['profile_link'];
561 512 reviewer['rules'] = [];
562 513 controller.addMember(reviewer, reasons, mandatory, role);
563 514 })
564 515 }
565 516 // add single user
566 517 else {
567 518 var reviewer = data;
568 519 reviewer['user_id'] = data['id'];
569 520 reviewer['gravatar_link'] = data['icon_link'];
570 521 reviewer['user_link'] = data['profile_link'];
571 522 reviewer['rules'] = [];
572 523 controller.addMember(reviewer, reasons, mandatory, role);
573 524 }
574 525
575 526 $(inputId).val('');
576 527 }
577 528 }
578 529
579 530 /**
580 531 * Reviewer autocomplete
581 532 */
582 533 var ReviewerAutoComplete = function (inputId, controller) {
583 534 var self = this;
584 535 self.controller = controller;
585 536 self.inputId = inputId;
586 537 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
587 538
588 539 $(inputId).autocomplete({
589 540 serviceUrl: pyroutes.url('user_autocomplete_data'),
590 541 minChars: 2,
591 542 maxHeight: 400,
592 543 deferRequestBy: 300, //miliseconds
593 544 showNoSuggestionNotice: true,
594 545 tabDisabled: true,
595 546 autoSelectFirst: true,
596 547 params: {
597 548 user_id: templateContext.rhodecode_user.user_id,
598 549 user_groups: true,
599 550 user_groups_expand: true,
600 551 skip_default_user: true
601 552 },
602 553 formatResult: autocompleteFormatResult,
603 554 lookupFilter: autocompleteFilterResult,
604 555 onSelect: handler
605 556 });
606 557 };
607 558
608 559 /**
609 560 * Observers autocomplete
610 561 */
611 562 var ObserverAutoComplete = function(inputId, controller) {
612 563 var self = this;
613 564 self.controller = controller;
614 565 self.inputId = inputId;
615 566 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
616 567
617 568 $(inputId).autocomplete({
618 569 serviceUrl: pyroutes.url('user_autocomplete_data'),
619 570 minChars: 2,
620 571 maxHeight: 400,
621 572 deferRequestBy: 300, //miliseconds
622 573 showNoSuggestionNotice: true,
623 574 tabDisabled: true,
624 575 autoSelectFirst: true,
625 576 params: {
626 577 user_id: templateContext.rhodecode_user.user_id,
627 578 user_groups: true,
628 579 user_groups_expand: true,
629 580 skip_default_user: true
630 581 },
631 582 formatResult: autocompleteFormatResult,
632 583 lookupFilter: autocompleteFilterResult,
633 584 onSelect: handler
634 585 });
635 586 }
636 587
637 588
638 589 window.VersionController = function () {
639 590 var self = this;
640 591 this.$verSource = $('input[name=ver_source]');
641 592 this.$verTarget = $('input[name=ver_target]');
642 593 this.$showVersionDiff = $('#show-version-diff');
643 594
644 595 this.adjustRadioSelectors = function (curNode) {
645 596 var getVal = function (item) {
646 597 if (item === 'latest') {
647 598 return Number.MAX_SAFE_INTEGER
648 599 }
649 600 else {
650 601 return parseInt(item)
651 602 }
652 603 };
653 604
654 605 var curVal = getVal($(curNode).val());
655 606 var cleared = false;
656 607
657 608 $.each(self.$verSource, function (index, value) {
658 609 var elVal = getVal($(value).val());
659 610
660 611 if (elVal > curVal) {
661 612 if ($(value).is(':checked')) {
662 613 cleared = true;
663 614 }
664 615 $(value).attr('disabled', 'disabled');
665 616 $(value).removeAttr('checked');
666 617 $(value).css({'opacity': 0.1});
667 618 }
668 619 else {
669 620 $(value).css({'opacity': 1});
670 621 $(value).removeAttr('disabled');
671 622 }
672 623 });
673 624
674 625 if (cleared) {
675 626 // if we unchecked an active, set the next one to same loc.
676 627 $(this.$verSource).filter('[value={0}]'.format(
677 628 curVal)).attr('checked', 'checked');
678 629 }
679 630
680 631 self.setLockAction(false,
681 632 $(curNode).data('verPos'),
682 633 $(this.$verSource).filter(':checked').data('verPos')
683 634 );
684 635 };
685 636
686 637
687 638 this.attachVersionListener = function () {
688 639 self.$verTarget.change(function (e) {
689 640 self.adjustRadioSelectors(this)
690 641 });
691 642 self.$verSource.change(function (e) {
692 643 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
693 644 });
694 645 };
695 646
696 647 this.init = function () {
697 648
698 649 var curNode = self.$verTarget.filter(':checked');
699 650 self.adjustRadioSelectors(curNode);
700 651 self.setLockAction(true);
701 652 self.attachVersionListener();
702 653
703 654 };
704 655
705 656 this.setLockAction = function (state, selectedVersion, otherVersion) {
706 657 var $showVersionDiff = this.$showVersionDiff;
707 658
708 659 if (state) {
709 660 $showVersionDiff.attr('disabled', 'disabled');
710 661 $showVersionDiff.addClass('disabled');
711 662 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
712 663 }
713 664 else {
714 665 $showVersionDiff.removeAttr('disabled');
715 666 $showVersionDiff.removeClass('disabled');
716 667
717 668 if (selectedVersion == otherVersion) {
718 669 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
719 670 } else {
720 671 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
721 672 }
722 673 }
723 674
724 675 };
725 676
726 677 this.showVersionDiff = function () {
727 678 var target = self.$verTarget.filter(':checked');
728 679 var source = self.$verSource.filter(':checked');
729 680
730 681 if (target.val() && source.val()) {
731 682 var params = {
732 683 'pull_request_id': templateContext.pull_request_data.pull_request_id,
733 684 'repo_name': templateContext.repo_name,
734 685 'version': target.val(),
735 686 'from_version': source.val()
736 687 };
737 688 window.location = pyroutes.url('pullrequest_show', params)
738 689 }
739 690
740 691 return false;
741 692 };
742 693
743 694 this.toggleVersionView = function (elem) {
744 695
745 696 if (this.$showVersionDiff.is(':visible')) {
746 697 $('.version-pr').hide();
747 698 this.$showVersionDiff.hide();
748 699 $(elem).html($(elem).data('toggleOn'))
749 700 } else {
750 701 $('.version-pr').show();
751 702 this.$showVersionDiff.show();
752 703 $(elem).html($(elem).data('toggleOff'))
753 704 }
754 705
755 706 return false
756 707 };
757 708
758 709 };
759 710
760 711
761 712 window.UpdatePrController = function () {
762 713 var self = this;
763 714 this.$updateCommits = $('#update_commits');
764 715 this.$updateCommitsSwitcher = $('#update_commits_switcher');
765 716
766 717 this.lockUpdateButton = function (label) {
767 718 self.$updateCommits.attr('disabled', 'disabled');
768 719 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
769 720
770 721 self.$updateCommits.addClass('disabled');
771 722 self.$updateCommitsSwitcher.addClass('disabled');
772 723
773 724 self.$updateCommits.removeClass('btn-primary');
774 725 self.$updateCommitsSwitcher.removeClass('btn-primary');
775 726
776 727 self.$updateCommits.text(_gettext(label));
777 728 };
778 729
779 730 this.isUpdateLocked = function () {
780 731 return self.$updateCommits.attr('disabled') !== undefined;
781 732 };
782 733
783 734 this.updateCommits = function (curNode) {
784 735 if (self.isUpdateLocked()) {
785 736 return
786 737 }
787 738 self.lockUpdateButton(_gettext('Updating...'));
788 739 updateCommits(
789 740 templateContext.repo_name,
790 741 templateContext.pull_request_data.pull_request_id);
791 742 };
792 743
793 744 this.forceUpdateCommits = function () {
794 745 if (self.isUpdateLocked()) {
795 746 return
796 747 }
797 748 self.lockUpdateButton(_gettext('Force updating...'));
798 749 var force = true;
799 750 updateCommits(
800 751 templateContext.repo_name,
801 752 templateContext.pull_request_data.pull_request_id, force);
802 753 };
803 754 };
804 755
805 756
806 757 /**
807 758 * Reviewer display panel
808 759 */
809 760 window.ReviewersPanel = {
810 761 editButton: null,
811 762 closeButton: null,
812 763 addButton: null,
813 764 removeButtons: null,
814 765 reviewRules: null,
815 766 setReviewers: null,
816 767 controller: null,
817 768
818 769 setSelectors: function () {
819 770 var self = this;
820 771 self.editButton = $('#open_edit_reviewers');
821 772 self.closeButton =$('#close_edit_reviewers');
822 773 self.addButton = $('#add_reviewer');
823 774 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
824 775 },
825 776
826 777 init: function (controller, reviewRules, setReviewers) {
827 778 var self = this;
828 779 self.setSelectors();
829 780
830 781 self.controller = controller;
831 782 self.reviewRules = reviewRules;
832 783 self.setReviewers = setReviewers;
833 784
834 785 self.editButton.on('click', function (e) {
835 786 self.edit();
836 787 });
837 788 self.closeButton.on('click', function (e) {
838 789 self.close();
839 790 self.renderReviewers();
840 791 });
841 792
842 793 self.renderReviewers();
843 794
844 795 },
845 796
846 797 renderReviewers: function () {
847 798 var self = this;
848 799
849 800 if (self.setReviewers.reviewers === undefined) {
850 801 return
851 802 }
852 803 if (self.setReviewers.reviewers.length === 0) {
853 804 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
854 805 return
855 806 }
856 807
857 808 self.controller.emptyReviewersTable();
858 809
859 810 $.each(self.setReviewers.reviewers, function (key, val) {
860 811
861 812 var member = val;
862 813 if (member.role === self.controller.ROLE_REVIEWER) {
863 814 var entry = renderTemplate('reviewMemberEntry', {
864 815 'member': member,
865 816 'mandatory': member.mandatory,
866 817 'role': member.role,
867 818 'reasons': member.reasons,
868 819 'allowed_to_update': member.allowed_to_update,
869 820 'review_status': member.review_status,
870 821 'review_status_label': member.review_status_label,
871 822 'user_group': member.user_group,
872 823 'create': false
873 824 });
874 825
875 826 $(self.controller.$reviewMembers.selector).append(entry)
876 827 }
877 828 });
878 829
879 830 tooltipActivate();
880 831 },
881 832
882 833 edit: function (event) {
883 834 var self = this;
884 835 self.editButton.hide();
885 836 self.closeButton.show();
886 837 self.addButton.show();
887 838 $(self.removeButtons.selector).css('visibility', 'visible');
888 839 // review rules
889 840 self.controller.loadReviewRules(this.reviewRules);
890 841 },
891 842
892 843 close: function (event) {
893 844 var self = this;
894 845 this.editButton.show();
895 846 this.closeButton.hide();
896 847 this.addButton.hide();
897 848 $(this.removeButtons.selector).css('visibility', 'hidden');
898 849 // hide review rules
899 850 self.controller.hideReviewRules();
900 851 }
901 852 };
902 853
903 854 /**
904 855 * Reviewer display panel
905 856 */
906 857 window.ObserversPanel = {
907 858 editButton: null,
908 859 closeButton: null,
909 860 addButton: null,
910 861 removeButtons: null,
911 862 reviewRules: null,
912 863 setReviewers: null,
913 864 controller: null,
914 865
915 866 setSelectors: function () {
916 867 var self = this;
917 868 self.editButton = $('#open_edit_observers');
918 869 self.closeButton =$('#close_edit_observers');
919 870 self.addButton = $('#add_observer');
920 871 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
921 872 },
922 873
923 874 init: function (controller, reviewRules, setReviewers) {
924 875 var self = this;
925 876 self.setSelectors();
926 877
927 878 self.controller = controller;
928 879 self.reviewRules = reviewRules;
929 880 self.setReviewers = setReviewers;
930 881
931 882 self.editButton.on('click', function (e) {
932 883 self.edit();
933 884 });
934 885 self.closeButton.on('click', function (e) {
935 886 self.close();
936 887 self.renderObservers();
937 888 });
938 889
939 890 self.renderObservers();
940 891
941 892 },
942 893
943 894 renderObservers: function () {
944 895 var self = this;
945 896 if (self.setReviewers.observers === undefined) {
946 897 return
947 898 }
948 899 if (self.setReviewers.observers.length === 0) {
949 900 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
950 901 return
951 902 }
952 903
953 904 self.controller.emptyObserversTable();
954 905
955 906 $.each(self.setReviewers.observers, function (key, val) {
956 907 var member = val;
957 908 if (member.role === self.controller.ROLE_OBSERVER) {
958 909 var entry = renderTemplate('reviewMemberEntry', {
959 910 'member': member,
960 911 'mandatory': member.mandatory,
961 912 'role': member.role,
962 913 'reasons': member.reasons,
963 914 'allowed_to_update': member.allowed_to_update,
964 915 'review_status': member.review_status,
965 916 'review_status_label': member.review_status_label,
966 917 'user_group': member.user_group,
967 918 'create': false
968 919 });
969 920
970 921 $(self.controller.$observerMembers.selector).append(entry)
971 922 }
972 923 });
973 924
974 925 tooltipActivate();
975 926 },
976 927
977 928 edit: function (event) {
978 929 this.editButton.hide();
979 930 this.closeButton.show();
980 931 this.addButton.show();
981 932 $(this.removeButtons.selector).css('visibility', 'visible');
982 933 },
983 934
984 935 close: function (event) {
985 936 this.editButton.show();
986 937 this.closeButton.hide();
987 938 this.addButton.hide();
988 939 $(this.removeButtons.selector).css('visibility', 'hidden');
989 940 }
990 941
991 942 };
992 943
993 944 window.PRDetails = {
994 945 editButton: null,
995 946 closeButton: null,
996 947 deleteButton: null,
997 948 viewFields: null,
998 949 editFields: null,
999 950
1000 951 setSelectors: function () {
1001 952 var self = this;
1002 953 self.editButton = $('#open_edit_pullrequest')
1003 954 self.closeButton = $('#close_edit_pullrequest')
1004 955 self.deleteButton = $('#delete_pullrequest')
1005 956 self.viewFields = $('#pr-desc, #pr-title')
1006 957 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
1007 958 },
1008 959
1009 960 init: function () {
1010 961 var self = this;
1011 962 self.setSelectors();
1012 963 self.editButton.on('click', function (e) {
1013 964 self.edit();
1014 965 });
1015 966 self.closeButton.on('click', function (e) {
1016 967 self.view();
1017 968 });
1018 969 },
1019 970
1020 971 edit: function (event) {
1021 972 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1022 973 this.viewFields.hide();
1023 974 this.editButton.hide();
1024 975 this.deleteButton.hide();
1025 976 this.closeButton.show();
1026 977 this.editFields.show();
1027 978 cmInstance.refresh();
1028 979 },
1029 980
1030 981 view: function (event) {
1031 982 this.editButton.show();
1032 983 this.deleteButton.show();
1033 984 this.editFields.hide();
1034 985 this.closeButton.hide();
1035 986 this.viewFields.show();
1036 987 }
1037 988 };
1038 989
1039 990 /**
1040 991 * OnLine presence using channelstream
1041 992 */
1042 993 window.ReviewerPresenceController = function (channel) {
1043 994 var self = this;
1044 995 this.channel = channel;
1045 996 this.users = {};
1046 997
1047 998 this.storeUsers = function (users) {
1048 999 self.users = {}
1049 1000 $.each(users, function (index, value) {
1050 1001 var userId = value.state.id;
1051 1002 self.users[userId] = value.state;
1052 1003 })
1053 1004 }
1054 1005
1055 1006 this.render = function () {
1056 1007 $.each($('.reviewer_entry'), function (index, value) {
1057 1008 var userData = $(value).data();
1058 1009 if (self.users[userData.reviewerUserId] !== undefined) {
1059 1010 $(value).find('.presence-state').show();
1060 1011 } else {
1061 1012 $(value).find('.presence-state').hide();
1062 1013 }
1063 1014 })
1064 1015 };
1065 1016
1066 1017 this.handlePresence = function (data) {
1067 1018 if (data.type == 'presence' && data.channel === self.channel) {
1068 1019 this.storeUsers(data.users);
1069 1020 this.render();
1070 1021 }
1071 1022 };
1072 1023
1073 1024 this.handleChannelUpdate = function (data) {
1074 1025 if (data.channel === this.channel) {
1075 1026 this.storeUsers(data.state.users);
1076 1027 this.render();
1077 1028 }
1078 1029
1079 1030 };
1080 1031
1081 1032 /* subscribe to the current presence */
1082 1033 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1083 1034 /* subscribe to updates e.g connect/disconnect */
1084 1035 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1085 1036
1086 1037 };
1087 1038
1088 1039 window.refreshComments = function (version) {
1089 1040 version = version || templateContext.pull_request_data.pull_request_version || '';
1090 1041
1091 1042 // Pull request case
1092 1043 if (templateContext.pull_request_data.pull_request_id !== null) {
1093 1044 var params = {
1094 1045 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1095 1046 'repo_name': templateContext.repo_name,
1096 1047 'version': version,
1097 1048 };
1098 1049 var loadUrl = pyroutes.url('pullrequest_comments', params);
1099 1050 } // commit case
1100 1051 else {
1101 1052 return
1102 1053 }
1103 1054
1104 1055 var currentIDs = []
1105 1056 $.each($('.comment'), function (idx, element) {
1106 1057 currentIDs.push($(element).data('commentId'));
1107 1058 });
1108 1059 var data = {"comments": currentIDs};
1109 1060
1110 1061 var $targetElem = $('.comments-content-table');
1111 1062 $targetElem.css('opacity', 0.3);
1112 1063
1113 1064 var success = function (data) {
1114 1065 var $counterElem = $('#comments-count');
1115 1066 var newCount = $(data).data('counter');
1116 1067 if (newCount !== undefined) {
1117 1068 var callback = function () {
1118 1069 $counterElem.animate({'opacity': 1.00}, 200)
1119 1070 $counterElem.html(newCount);
1120 1071 };
1121 1072 $counterElem.animate({'opacity': 0.15}, 200, callback);
1122 1073 }
1123 1074
1124 1075 $targetElem.css('opacity', 1);
1125 1076 $targetElem.html(data);
1126 1077 tooltipActivate();
1127 1078 }
1128 1079
1129 1080 ajaxPOST(loadUrl, data, success, null, {})
1130 1081
1131 1082 }
1132 1083
1133 1084 window.refreshTODOs = function (version) {
1134 1085 version = version || templateContext.pull_request_data.pull_request_version || '';
1135 1086 // Pull request case
1136 1087 if (templateContext.pull_request_data.pull_request_id !== null) {
1137 1088 var params = {
1138 1089 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1139 1090 'repo_name': templateContext.repo_name,
1140 1091 'version': version,
1141 1092 };
1142 1093 var loadUrl = pyroutes.url('pullrequest_todos', params);
1143 1094 } // commit case
1144 1095 else {
1145 1096 return
1146 1097 }
1147 1098
1148 1099 var currentIDs = []
1149 1100 $.each($('.comment'), function (idx, element) {
1150 1101 currentIDs.push($(element).data('commentId'));
1151 1102 });
1152 1103
1153 1104 var data = {"comments": currentIDs};
1154 1105 var $targetElem = $('.todos-content-table');
1155 1106 $targetElem.css('opacity', 0.3);
1156 1107
1157 1108 var success = function (data) {
1158 1109 var $counterElem = $('#todos-count')
1159 1110 var newCount = $(data).data('counter');
1160 1111 if (newCount !== undefined) {
1161 1112 var callback = function () {
1162 1113 $counterElem.animate({'opacity': 1.00}, 200)
1163 1114 $counterElem.html(newCount);
1164 1115 };
1165 1116 $counterElem.animate({'opacity': 0.15}, 200, callback);
1166 1117 }
1167 1118
1168 1119 $targetElem.css('opacity', 1);
1169 1120 $targetElem.html(data);
1170 1121 tooltipActivate();
1171 1122 }
1172 1123
1173 1124 ajaxPOST(loadUrl, data, success, null, {})
1174 1125
1175 1126 }
1176 1127
1177 1128 window.refreshAllComments = function (version) {
1178 1129 version = version || templateContext.pull_request_data.pull_request_version || '';
1179 1130
1180 1131 refreshComments(version);
1181 1132 refreshTODOs(version);
1182 1133 };
1183 1134
1184 1135 window.refreshDraftComments = function () {
1185 1136 alert('TODO: refresh Draft Comments needs implementation')
1186 1137 };
1187 1138
1188 1139 window.sidebarComment = function (commentId) {
1189 1140 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1190 1141 if (!jsonData) {
1191 1142 return 'Failed to load comment {0}'.format(commentId)
1192 1143 }
1193 1144 var funcData = JSON.parse(atob(jsonData));
1194 1145 return renderTemplate('sideBarCommentHovercard', funcData)
1195 1146 };
General Comments 0
You need to be logged in to leave comments. Login now