##// END OF EJS Templates
review-rules: extend code to support the forbid commit authors.
marcink -
r1787:bb077306 default
parent child Browse files
Show More

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

@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_commit_author_to_review = Column(
21 "forbid_commit_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_commit_author_to_review.create(table=repo_review_rule_table)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 """
22 22
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 77 # defines current db version for migrations
54 __dbversion__ = 78 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'https://code.rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,1018 +1,1023 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(PullrequestsController, self).__before__()
72 72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74 74
75 75 @LoginRequired()
76 76 @NotAnonymous()
77 77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 78 'repository.admin')
79 79 @HasAcceptedRepoType('git', 'hg')
80 80 def index(self):
81 81 source_repo = c.rhodecode_db_repo
82 82
83 83 try:
84 84 source_repo.scm_instance().get_commit()
85 85 except EmptyRepositoryError:
86 86 h.flash(h.literal(_('There are no commits yet')),
87 87 category='warning')
88 88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
89 89
90 90 commit_id = request.GET.get('commit')
91 91 branch_ref = request.GET.get('branch')
92 92 bookmark_ref = request.GET.get('bookmark')
93 93
94 94 try:
95 95 source_repo_data = PullRequestModel().generate_repo_data(
96 96 source_repo, commit_id=commit_id,
97 97 branch=branch_ref, bookmark=bookmark_ref)
98 98 except CommitDoesNotExistError as e:
99 99 log.exception(e)
100 100 h.flash(_('Commit does not exist'), 'error')
101 101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102 102
103 103 default_target_repo = source_repo
104 104
105 105 if source_repo.parent:
106 106 parent_vcs_obj = source_repo.parent.scm_instance()
107 107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 108 # change default if we have a parent repo
109 109 default_target_repo = source_repo.parent
110 110
111 111 target_repo_data = PullRequestModel().generate_repo_data(
112 112 default_target_repo)
113 113
114 114 selected_source_ref = source_repo_data['refs']['selected_ref']
115 115
116 116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 118 source=source_repo.repo_name,
119 119 source_ref=title_source_ref,
120 120 target=default_target_repo.repo_name
121 121 )
122 122
123 123 c.default_repo_data = {
124 124 'source_repo_name': source_repo.repo_name,
125 125 'source_refs_json': json.dumps(source_repo_data),
126 126 'target_repo_name': default_target_repo.repo_name,
127 127 'target_refs_json': json.dumps(target_repo_data),
128 128 }
129 129 c.default_source_ref = selected_source_ref
130 130
131 131 return render('/pullrequests/pullrequest.mako')
132 132
133 133 @LoginRequired()
134 134 @NotAnonymous()
135 135 @XHRRequired()
136 136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 137 'repository.admin')
138 138 @jsonify
139 139 def get_repo_refs(self, repo_name, target_repo_name):
140 140 repo = Repository.get_by_repo_name(target_repo_name)
141 141 if not repo:
142 142 raise HTTPNotFound
143 143 return PullRequestModel().generate_repo_data(repo)
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @XHRRequired()
148 148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 149 'repository.admin')
150 150 @jsonify
151 151 def get_repo_destinations(self, repo_name):
152 152 repo = Repository.get_by_repo_name(repo_name)
153 153 if not repo:
154 154 raise HTTPNotFound
155 155 filter_query = request.GET.get('query')
156 156
157 157 query = Repository.query() \
158 158 .order_by(func.length(Repository.repo_name)) \
159 159 .filter(or_(
160 160 Repository.repo_name == repo.repo_name,
161 161 Repository.fork_id == repo.repo_id))
162 162
163 163 if filter_query:
164 164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 165 query = query.filter(
166 166 Repository.repo_name.ilike(ilike_expression))
167 167
168 168 add_parent = False
169 169 if repo.parent:
170 170 if filter_query in repo.parent.repo_name:
171 171 parent_vcs_obj = repo.parent.scm_instance()
172 172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 173 add_parent = True
174 174
175 175 limit = 20 - 1 if add_parent else 20
176 176 all_repos = query.limit(limit).all()
177 177 if add_parent:
178 178 all_repos += [repo.parent]
179 179
180 180 repos = []
181 181 for obj in self.scm_model.get_repos(all_repos):
182 182 repos.append({
183 183 'id': obj['name'],
184 184 'text': obj['name'],
185 185 'type': 'repo',
186 186 'obj': obj['dbrepo']
187 187 })
188 188
189 189 data = {
190 190 'more': False,
191 191 'results': [{
192 192 'text': _('Repositories'),
193 193 'children': repos
194 194 }] if repos else []
195 195 }
196 196 return data
197 197
198 198 @LoginRequired()
199 199 @NotAnonymous()
200 200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 201 'repository.admin')
202 202 @HasAcceptedRepoType('git', 'hg')
203 203 @auth.CSRFRequired()
204 204 def create(self, repo_name):
205 205 repo = Repository.get_by_repo_name(repo_name)
206 206 if not repo:
207 207 raise HTTPNotFound
208 208
209 209 controls = peppercorn.parse(request.POST.items())
210 210
211 211 try:
212 212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 213 except formencode.Invalid as errors:
214 214 if errors.error_dict.get('revisions'):
215 215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 216 elif errors.error_dict.get('pullrequest_title'):
217 217 msg = _('Pull request requires a title with min. 3 chars')
218 218 else:
219 219 msg = _('Error creating pull request: {}').format(errors)
220 220 log.exception(msg)
221 221 h.flash(msg, 'error')
222 222
223 223 # would rather just go back to form ...
224 224 return redirect(url('pullrequest_home', repo_name=repo_name))
225 225
226 226 source_repo = _form['source_repo']
227 227 source_ref = _form['source_ref']
228 228 target_repo = _form['target_repo']
229 229 target_ref = _form['target_ref']
230 230 commit_ids = _form['revisions'][::-1]
231 231
232 232 # find the ancestor for this pr
233 233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
234 234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
235 235
236 236 source_scm = source_db_repo.scm_instance()
237 237 target_scm = target_db_repo.scm_instance()
238 238
239 239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
240 240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
241 241
242 242 ancestor = source_scm.get_common_ancestor(
243 243 source_commit.raw_id, target_commit.raw_id, target_scm)
244 244
245 245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
246 246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
247 247
248 248 pullrequest_title = _form['pullrequest_title']
249 249 title_source_ref = source_ref.split(':', 2)[1]
250 250 if not pullrequest_title:
251 251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
252 252 source=source_repo,
253 253 source_ref=title_source_ref,
254 254 target=target_repo
255 255 )
256 256
257 257 description = _form['pullrequest_desc']
258 258
259 259 get_default_reviewers_data, validate_default_reviewers = \
260 260 PullRequestModel().get_reviewer_functions()
261 261
262 262 # recalculate reviewers logic, to make sure we can validate this
263 263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user, source_db_repo, source_commit, target_db_repo,
265 target_commit)
264 c.rhodecode_user.get_instance(), source_db_repo,
265 source_commit, target_db_repo, target_commit)
266 266
267 267 reviewers = validate_default_reviewers(
268 268 _form['review_members'], reviewer_rules)
269 269
270 270 try:
271 271 pull_request = PullRequestModel().create(
272 272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
273 273 target_ref, commit_ids, reviewers, pullrequest_title,
274 274 description, reviewer_rules
275 275 )
276 276 Session().commit()
277 277 h.flash(_('Successfully opened new pull request'),
278 278 category='success')
279 279 except Exception as e:
280 280 msg = _('Error occurred during creation of this pull request.')
281 281 log.exception(msg)
282 282 h.flash(msg, category='error')
283 283 return redirect(url('pullrequest_home', repo_name=repo_name))
284 284
285 285 return redirect(url('pullrequest_show', repo_name=target_repo,
286 286 pull_request_id=pull_request.pull_request_id))
287 287
288 288 @LoginRequired()
289 289 @NotAnonymous()
290 290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 291 'repository.admin')
292 292 @auth.CSRFRequired()
293 293 @jsonify
294 294 def update(self, repo_name, pull_request_id):
295 295 pull_request_id = safe_int(pull_request_id)
296 296 pull_request = PullRequest.get_or_404(pull_request_id)
297 297 # only owner or admin can update it
298 298 allowed_to_update = PullRequestModel().check_user_update(
299 299 pull_request, c.rhodecode_user)
300 300 if allowed_to_update:
301 301 controls = peppercorn.parse(request.POST.items())
302 302
303 303 if 'review_members' in controls:
304 304 self._update_reviewers(
305 305 pull_request_id, controls['review_members'],
306 306 pull_request.reviewer_data)
307 307 elif str2bool(request.POST.get('update_commits', 'false')):
308 308 self._update_commits(pull_request)
309 309 elif str2bool(request.POST.get('close_pull_request', 'false')):
310 310 self._reject_close(pull_request)
311 311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
312 312 self._edit_pull_request(pull_request)
313 313 else:
314 314 raise HTTPBadRequest()
315 315 return True
316 316 raise HTTPForbidden()
317 317
318 318 def _edit_pull_request(self, pull_request):
319 319 try:
320 320 PullRequestModel().edit(
321 321 pull_request, request.POST.get('title'),
322 322 request.POST.get('description'))
323 323 except ValueError:
324 324 msg = _(u'Cannot update closed pull requests.')
325 325 h.flash(msg, category='error')
326 326 return
327 327 else:
328 328 Session().commit()
329 329
330 330 msg = _(u'Pull request title & description updated.')
331 331 h.flash(msg, category='success')
332 332 return
333 333
334 334 def _update_commits(self, pull_request):
335 335 resp = PullRequestModel().update_commits(pull_request)
336 336
337 337 if resp.executed:
338 338
339 339 if resp.target_changed and resp.source_changed:
340 340 changed = 'target and source repositories'
341 341 elif resp.target_changed and not resp.source_changed:
342 342 changed = 'target repository'
343 343 elif not resp.target_changed and resp.source_changed:
344 344 changed = 'source repository'
345 345 else:
346 346 changed = 'nothing'
347 347
348 348 msg = _(
349 349 u'Pull request updated to "{source_commit_id}" with '
350 350 u'{count_added} added, {count_removed} removed commits. '
351 351 u'Source of changes: {change_source}')
352 352 msg = msg.format(
353 353 source_commit_id=pull_request.source_ref_parts.commit_id,
354 354 count_added=len(resp.changes.added),
355 355 count_removed=len(resp.changes.removed),
356 356 change_source=changed)
357 357 h.flash(msg, category='success')
358 358
359 359 registry = get_current_registry()
360 360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
361 361 channelstream_config = rhodecode_plugins.get('channelstream', {})
362 362 if channelstream_config.get('enabled'):
363 363 message = msg + (
364 364 ' - <a onclick="window.location.reload()">'
365 365 '<strong>{}</strong></a>'.format(_('Reload page')))
366 366 channel = '/repo${}$/pr/{}'.format(
367 367 pull_request.target_repo.repo_name,
368 368 pull_request.pull_request_id
369 369 )
370 370 payload = {
371 371 'type': 'message',
372 372 'user': 'system',
373 373 'exclude_users': [request.user.username],
374 374 'channel': channel,
375 375 'message': {
376 376 'message': message,
377 377 'level': 'success',
378 378 'topic': '/notifications'
379 379 }
380 380 }
381 381 channelstream_request(
382 382 channelstream_config, [payload], '/message',
383 383 raise_exc=False)
384 384 else:
385 385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
386 386 warning_reasons = [
387 387 UpdateFailureReason.NO_CHANGE,
388 388 UpdateFailureReason.WRONG_REF_TYPE,
389 389 ]
390 390 category = 'warning' if resp.reason in warning_reasons else 'error'
391 391 h.flash(msg, category=category)
392 392
393 393 @auth.CSRFRequired()
394 394 @LoginRequired()
395 395 @NotAnonymous()
396 396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 397 'repository.admin')
398 398 def merge(self, repo_name, pull_request_id):
399 399 """
400 400 POST /{repo_name}/pull-request/{pull_request_id}
401 401
402 402 Merge will perform a server-side merge of the specified
403 403 pull request, if the pull request is approved and mergeable.
404 404 After successful merging, the pull request is automatically
405 405 closed, with a relevant comment.
406 406 """
407 407 pull_request_id = safe_int(pull_request_id)
408 408 pull_request = PullRequest.get_or_404(pull_request_id)
409 409 user = c.rhodecode_user
410 410
411 411 check = MergeCheck.validate(pull_request, user)
412 412 merge_possible = not check.failed
413 413
414 414 for err_type, error_msg in check.errors:
415 415 h.flash(error_msg, category=err_type)
416 416
417 417 if merge_possible:
418 418 log.debug("Pre-conditions checked, trying to merge.")
419 419 extras = vcs_operation_context(
420 420 request.environ, repo_name=pull_request.target_repo.repo_name,
421 421 username=user.username, action='push',
422 422 scm=pull_request.target_repo.repo_type)
423 423 self._merge_pull_request(pull_request, user, extras)
424 424
425 425 return redirect(url(
426 426 'pullrequest_show',
427 427 repo_name=pull_request.target_repo.repo_name,
428 428 pull_request_id=pull_request.pull_request_id))
429 429
430 430 def _merge_pull_request(self, pull_request, user, extras):
431 431 merge_resp = PullRequestModel().merge(
432 432 pull_request, user, extras=extras)
433 433
434 434 if merge_resp.executed:
435 435 log.debug("The merge was successful, closing the pull request.")
436 436 PullRequestModel().close_pull_request(
437 437 pull_request.pull_request_id, user)
438 438 Session().commit()
439 439 msg = _('Pull request was successfully merged and closed.')
440 440 h.flash(msg, category='success')
441 441 else:
442 442 log.debug(
443 443 "The merge was not successful. Merge response: %s",
444 444 merge_resp)
445 445 msg = PullRequestModel().merge_status_message(
446 446 merge_resp.failure_reason)
447 447 h.flash(msg, category='error')
448 448
449 449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
450 450
451 451 get_default_reviewers_data, validate_default_reviewers = \
452 452 PullRequestModel().get_reviewer_functions()
453 453
454 454 try:
455 455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 456 except ValueError as e:
457 457 log.error('Reviewers Validation:{}'.format(e))
458 458 h.flash(e, category='error')
459 459 return
460 460
461 461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 462 h.flash(_('Pull request reviewers updated.'), category='success')
463 463 Session().commit()
464 464
465 465 def _reject_close(self, pull_request):
466 466 if pull_request.is_closed():
467 467 raise HTTPForbidden()
468 468
469 469 PullRequestModel().close_pull_request_with_comment(
470 470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
471 471 Session().commit()
472 472
473 473 @LoginRequired()
474 474 @NotAnonymous()
475 475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 476 'repository.admin')
477 477 @auth.CSRFRequired()
478 478 @jsonify
479 479 def delete(self, repo_name, pull_request_id):
480 480 pull_request_id = safe_int(pull_request_id)
481 481 pull_request = PullRequest.get_or_404(pull_request_id)
482 482
483 483 pr_closed = pull_request.is_closed()
484 484 allowed_to_delete = PullRequestModel().check_user_delete(
485 485 pull_request, c.rhodecode_user) and not pr_closed
486 486
487 487 # only owner can delete it !
488 488 if allowed_to_delete:
489 489 PullRequestModel().delete(pull_request)
490 490 Session().commit()
491 491 h.flash(_('Successfully deleted pull request'),
492 492 category='success')
493 493 return redirect(url('my_account_pullrequests'))
494 494
495 495 h.flash(_('Your are not allowed to delete this pull request'),
496 496 category='error')
497 497 raise HTTPForbidden()
498 498
499 499 def _get_pr_version(self, pull_request_id, version=None):
500 500 pull_request_id = safe_int(pull_request_id)
501 501 at_version = None
502 502
503 503 if version and version == 'latest':
504 504 pull_request_ver = PullRequest.get(pull_request_id)
505 505 pull_request_obj = pull_request_ver
506 506 _org_pull_request_obj = pull_request_obj
507 507 at_version = 'latest'
508 508 elif version:
509 509 pull_request_ver = PullRequestVersion.get_or_404(version)
510 510 pull_request_obj = pull_request_ver
511 511 _org_pull_request_obj = pull_request_ver.pull_request
512 512 at_version = pull_request_ver.pull_request_version_id
513 513 else:
514 514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 515 pull_request_id)
516 516
517 517 pull_request_display_obj = PullRequest.get_pr_display_object(
518 518 pull_request_obj, _org_pull_request_obj)
519 519
520 520 return _org_pull_request_obj, pull_request_obj, \
521 521 pull_request_display_obj, at_version
522 522
523 523 def _get_diffset(
524 524 self, source_repo, source_ref_id, target_ref_id, target_commit,
525 525 source_commit, diff_limit, file_limit, display_inline_comments):
526 526 vcs_diff = PullRequestModel().get_diff(
527 527 source_repo, source_ref_id, target_ref_id)
528 528
529 529 diff_processor = diffs.DiffProcessor(
530 530 vcs_diff, format='newdiff', diff_limit=diff_limit,
531 531 file_limit=file_limit, show_full_diff=c.fulldiff)
532 532
533 533 _parsed = diff_processor.prepare()
534 534
535 535 def _node_getter(commit):
536 536 def get_node(fname):
537 537 try:
538 538 return commit.get_node(fname)
539 539 except NodeDoesNotExistError:
540 540 return None
541 541
542 542 return get_node
543 543
544 544 diffset = codeblocks.DiffSet(
545 545 repo_name=c.repo_name,
546 546 source_repo_name=c.source_repo.repo_name,
547 547 source_node_getter=_node_getter(target_commit),
548 548 target_node_getter=_node_getter(source_commit),
549 549 comments=display_inline_comments
550 550 )
551 551 diffset = diffset.render_patchset(
552 552 _parsed, target_commit.raw_id, source_commit.raw_id)
553 553
554 554 return diffset
555 555
556 556 @LoginRequired()
557 557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
558 558 'repository.admin')
559 559 def show(self, repo_name, pull_request_id):
560 560 pull_request_id = safe_int(pull_request_id)
561 561 version = request.GET.get('version')
562 562 from_version = request.GET.get('from_version') or version
563 563 merge_checks = request.GET.get('merge_checks')
564 564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
565 565
566 566 (pull_request_latest,
567 567 pull_request_at_ver,
568 568 pull_request_display_obj,
569 569 at_version) = self._get_pr_version(
570 570 pull_request_id, version=version)
571 571 pr_closed = pull_request_latest.is_closed()
572 572
573 573 if pr_closed and (version or from_version):
574 574 # not allow to browse versions
575 575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
576 576 pull_request_id=pull_request_id))
577 577
578 578 versions = pull_request_display_obj.versions()
579 579
580 580 c.at_version = at_version
581 581 c.at_version_num = (at_version
582 582 if at_version and at_version != 'latest'
583 583 else None)
584 584 c.at_version_pos = ChangesetComment.get_index_from_version(
585 585 c.at_version_num, versions)
586 586
587 587 (prev_pull_request_latest,
588 588 prev_pull_request_at_ver,
589 589 prev_pull_request_display_obj,
590 590 prev_at_version) = self._get_pr_version(
591 591 pull_request_id, version=from_version)
592 592
593 593 c.from_version = prev_at_version
594 594 c.from_version_num = (prev_at_version
595 595 if prev_at_version and prev_at_version != 'latest'
596 596 else None)
597 597 c.from_version_pos = ChangesetComment.get_index_from_version(
598 598 c.from_version_num, versions)
599 599
600 600 # define if we're in COMPARE mode or VIEW at version mode
601 601 compare = at_version != prev_at_version
602 602
603 603 # pull_requests repo_name we opened it against
604 604 # ie. target_repo must match
605 605 if repo_name != pull_request_at_ver.target_repo.repo_name:
606 606 raise HTTPNotFound
607 607
608 608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
609 609 pull_request_at_ver)
610 610
611 611 c.pull_request = pull_request_display_obj
612 612 c.pull_request_latest = pull_request_latest
613 613
614 614 if compare or (at_version and not at_version == 'latest'):
615 615 c.allowed_to_change_status = False
616 616 c.allowed_to_update = False
617 617 c.allowed_to_merge = False
618 618 c.allowed_to_delete = False
619 619 c.allowed_to_comment = False
620 620 c.allowed_to_close = False
621 621 else:
622 622 can_change_status = PullRequestModel().check_user_change_status(
623 623 pull_request_at_ver, c.rhodecode_user)
624 624 c.allowed_to_change_status = can_change_status and not pr_closed
625 625
626 626 c.allowed_to_update = PullRequestModel().check_user_update(
627 627 pull_request_latest, c.rhodecode_user) and not pr_closed
628 628 c.allowed_to_merge = PullRequestModel().check_user_merge(
629 629 pull_request_latest, c.rhodecode_user) and not pr_closed
630 630 c.allowed_to_delete = PullRequestModel().check_user_delete(
631 631 pull_request_latest, c.rhodecode_user) and not pr_closed
632 632 c.allowed_to_comment = not pr_closed
633 633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
634 634
635 635 c.forbid_adding_reviewers = False
636 636 c.forbid_author_to_review = False
637 c.forbid_commit_author_to_review = False
637 638
638 639 if pull_request_latest.reviewer_data and \
639 640 'rules' in pull_request_latest.reviewer_data:
640 641 rules = pull_request_latest.reviewer_data['rules'] or {}
641 642 try:
642 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
643 c.forbid_author_to_review = rules.get('forbid_author_to_review')
643 c.forbid_adding_reviewers = rules.get(
644 'forbid_adding_reviewers')
645 c.forbid_author_to_review = rules.get(
646 'forbid_author_to_review')
647 c.forbid_commit_author_to_review = rules.get(
648 'forbid_commit_author_to_review')
644 649 except Exception:
645 650 pass
646 651
647 652 # check merge capabilities
648 653 _merge_check = MergeCheck.validate(
649 654 pull_request_latest, user=c.rhodecode_user)
650 655 c.pr_merge_errors = _merge_check.error_details
651 656 c.pr_merge_possible = not _merge_check.failed
652 657 c.pr_merge_message = _merge_check.merge_msg
653 658
654 659 c.pull_request_review_status = _merge_check.review_status
655 660 if merge_checks:
656 661 return render('/pullrequests/pullrequest_merge_checks.mako')
657 662
658 663 comments_model = CommentsModel()
659 664
660 665 # reviewers and statuses
661 666 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
662 667 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
663 668
664 669 # GENERAL COMMENTS with versions #
665 670 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
666 671 q = q.order_by(ChangesetComment.comment_id.asc())
667 672 general_comments = q
668 673
669 674 # pick comments we want to render at current version
670 675 c.comment_versions = comments_model.aggregate_comments(
671 676 general_comments, versions, c.at_version_num)
672 677 c.comments = c.comment_versions[c.at_version_num]['until']
673 678
674 679 # INLINE COMMENTS with versions #
675 680 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
676 681 q = q.order_by(ChangesetComment.comment_id.asc())
677 682 inline_comments = q
678 683
679 684 c.inline_versions = comments_model.aggregate_comments(
680 685 inline_comments, versions, c.at_version_num, inline=True)
681 686
682 687 # inject latest version
683 688 latest_ver = PullRequest.get_pr_display_object(
684 689 pull_request_latest, pull_request_latest)
685 690
686 691 c.versions = versions + [latest_ver]
687 692
688 693 # if we use version, then do not show later comments
689 694 # than current version
690 695 display_inline_comments = collections.defaultdict(
691 696 lambda: collections.defaultdict(list))
692 697 for co in inline_comments:
693 698 if c.at_version_num:
694 699 # pick comments that are at least UPTO given version, so we
695 700 # don't render comments for higher version
696 701 should_render = co.pull_request_version_id and \
697 702 co.pull_request_version_id <= c.at_version_num
698 703 else:
699 704 # showing all, for 'latest'
700 705 should_render = True
701 706
702 707 if should_render:
703 708 display_inline_comments[co.f_path][co.line_no].append(co)
704 709
705 710 # load diff data into template context, if we use compare mode then
706 711 # diff is calculated based on changes between versions of PR
707 712
708 713 source_repo = pull_request_at_ver.source_repo
709 714 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
710 715
711 716 target_repo = pull_request_at_ver.target_repo
712 717 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
713 718
714 719 if compare:
715 720 # in compare switch the diff base to latest commit from prev version
716 721 target_ref_id = prev_pull_request_display_obj.revisions[0]
717 722
718 723 # despite opening commits for bookmarks/branches/tags, we always
719 724 # convert this to rev to prevent changes after bookmark or branch change
720 725 c.source_ref_type = 'rev'
721 726 c.source_ref = source_ref_id
722 727
723 728 c.target_ref_type = 'rev'
724 729 c.target_ref = target_ref_id
725 730
726 731 c.source_repo = source_repo
727 732 c.target_repo = target_repo
728 733
729 734 # diff_limit is the old behavior, will cut off the whole diff
730 735 # if the limit is applied otherwise will just hide the
731 736 # big files from the front-end
732 737 diff_limit = self.cut_off_limit_diff
733 738 file_limit = self.cut_off_limit_file
734 739
735 740 c.commit_ranges = []
736 741 source_commit = EmptyCommit()
737 742 target_commit = EmptyCommit()
738 743 c.missing_requirements = False
739 744
740 745 source_scm = source_repo.scm_instance()
741 746 target_scm = target_repo.scm_instance()
742 747
743 748 # try first shadow repo, fallback to regular repo
744 749 try:
745 750 commits_source_repo = pull_request_latest.get_shadow_repo()
746 751 except Exception:
747 752 log.debug('Failed to get shadow repo', exc_info=True)
748 753 commits_source_repo = source_scm
749 754
750 755 c.commits_source_repo = commits_source_repo
751 756 commit_cache = {}
752 757 try:
753 758 pre_load = ["author", "branch", "date", "message"]
754 759 show_revs = pull_request_at_ver.revisions
755 760 for rev in show_revs:
756 761 comm = commits_source_repo.get_commit(
757 762 commit_id=rev, pre_load=pre_load)
758 763 c.commit_ranges.append(comm)
759 764 commit_cache[comm.raw_id] = comm
760 765
761 766 # Order here matters, we first need to get target, and then
762 767 # the source
763 768 target_commit = commits_source_repo.get_commit(
764 769 commit_id=safe_str(target_ref_id))
765 770
766 771 source_commit = commits_source_repo.get_commit(
767 772 commit_id=safe_str(source_ref_id))
768 773
769 774 except CommitDoesNotExistError:
770 775 log.warning(
771 776 'Failed to get commit from `{}` repo'.format(
772 777 commits_source_repo), exc_info=True)
773 778 except RepositoryRequirementError:
774 779 log.warning(
775 780 'Failed to get all required data from repo', exc_info=True)
776 781 c.missing_requirements = True
777 782
778 783 c.ancestor = None # set it to None, to hide it from PR view
779 784
780 785 try:
781 786 ancestor_id = source_scm.get_common_ancestor(
782 787 source_commit.raw_id, target_commit.raw_id, target_scm)
783 788 c.ancestor_commit = source_scm.get_commit(ancestor_id)
784 789 except Exception:
785 790 c.ancestor_commit = None
786 791
787 792 c.statuses = source_repo.statuses(
788 793 [x.raw_id for x in c.commit_ranges])
789 794
790 795 # auto collapse if we have more than limit
791 796 collapse_limit = diffs.DiffProcessor._collapse_commits_over
792 797 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
793 798 c.compare_mode = compare
794 799
795 800 c.missing_commits = False
796 801 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
797 802 or source_commit == target_commit):
798 803
799 804 c.missing_commits = True
800 805 else:
801 806
802 807 c.diffset = self._get_diffset(
803 808 commits_source_repo, source_ref_id, target_ref_id,
804 809 target_commit, source_commit,
805 810 diff_limit, file_limit, display_inline_comments)
806 811
807 812 c.limited_diff = c.diffset.limited_diff
808 813
809 814 # calculate removed files that are bound to comments
810 815 comment_deleted_files = [
811 816 fname for fname in display_inline_comments
812 817 if fname not in c.diffset.file_stats]
813 818
814 819 c.deleted_files_comments = collections.defaultdict(dict)
815 820 for fname, per_line_comments in display_inline_comments.items():
816 821 if fname in comment_deleted_files:
817 822 c.deleted_files_comments[fname]['stats'] = 0
818 823 c.deleted_files_comments[fname]['comments'] = list()
819 824 for lno, comments in per_line_comments.items():
820 825 c.deleted_files_comments[fname]['comments'].extend(
821 826 comments)
822 827
823 828 # this is a hack to properly display links, when creating PR, the
824 829 # compare view and others uses different notation, and
825 830 # compare_commits.mako renders links based on the target_repo.
826 831 # We need to swap that here to generate it properly on the html side
827 832 c.target_repo = c.source_repo
828 833
829 834 c.commit_statuses = ChangesetStatus.STATUSES
830 835
831 836 c.show_version_changes = not pr_closed
832 837 if c.show_version_changes:
833 838 cur_obj = pull_request_at_ver
834 839 prev_obj = prev_pull_request_at_ver
835 840
836 841 old_commit_ids = prev_obj.revisions
837 842 new_commit_ids = cur_obj.revisions
838 843 commit_changes = PullRequestModel()._calculate_commit_id_changes(
839 844 old_commit_ids, new_commit_ids)
840 845 c.commit_changes_summary = commit_changes
841 846
842 847 # calculate the diff for commits between versions
843 848 c.commit_changes = []
844 849 mark = lambda cs, fw: list(
845 850 h.itertools.izip_longest([], cs, fillvalue=fw))
846 851 for c_type, raw_id in mark(commit_changes.added, 'a') \
847 852 + mark(commit_changes.removed, 'r') \
848 853 + mark(commit_changes.common, 'c'):
849 854
850 855 if raw_id in commit_cache:
851 856 commit = commit_cache[raw_id]
852 857 else:
853 858 try:
854 859 commit = commits_source_repo.get_commit(raw_id)
855 860 except CommitDoesNotExistError:
856 861 # in case we fail extracting still use "dummy" commit
857 862 # for display in commit diff
858 863 commit = h.AttributeDict(
859 864 {'raw_id': raw_id,
860 865 'message': 'EMPTY or MISSING COMMIT'})
861 866 c.commit_changes.append([c_type, commit])
862 867
863 868 # current user review statuses for each version
864 869 c.review_versions = {}
865 870 if c.rhodecode_user.user_id in allowed_reviewers:
866 871 for co in general_comments:
867 872 if co.author.user_id == c.rhodecode_user.user_id:
868 873 # each comment has a status change
869 874 status = co.status_change
870 875 if status:
871 876 _ver_pr = status[0].comment.pull_request_version_id
872 877 c.review_versions[_ver_pr] = status[0]
873 878
874 879 return render('/pullrequests/pullrequest_show.mako')
875 880
876 881 @LoginRequired()
877 882 @NotAnonymous()
878 883 @HasRepoPermissionAnyDecorator(
879 884 'repository.read', 'repository.write', 'repository.admin')
880 885 @auth.CSRFRequired()
881 886 @jsonify
882 887 def comment(self, repo_name, pull_request_id):
883 888 pull_request_id = safe_int(pull_request_id)
884 889 pull_request = PullRequest.get_or_404(pull_request_id)
885 890 if pull_request.is_closed():
886 891 raise HTTPForbidden()
887 892
888 893 status = request.POST.get('changeset_status', None)
889 894 text = request.POST.get('text')
890 895 comment_type = request.POST.get('comment_type')
891 896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
892 897 close_pull_request = request.POST.get('close_pull_request')
893 898
894 899 close_pr = False
895 900 # only owner or admin or person with write permissions
896 901 allowed_to_close = PullRequestModel().check_user_update(
897 902 pull_request, c.rhodecode_user)
898 903
899 904 if close_pull_request and allowed_to_close:
900 905 close_pr = True
901 906 pull_request_review_status = pull_request.calculated_review_status()
902 907 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
903 908 # approved only if we have voting consent
904 909 status = ChangesetStatus.STATUS_APPROVED
905 910 else:
906 911 status = ChangesetStatus.STATUS_REJECTED
907 912
908 913 allowed_to_change_status = PullRequestModel().check_user_change_status(
909 914 pull_request, c.rhodecode_user)
910 915
911 916 if status and allowed_to_change_status:
912 917 message = (_('Status change %(transition_icon)s %(status)s')
913 918 % {'transition_icon': '>',
914 919 'status': ChangesetStatus.get_status_lbl(status)})
915 920 if close_pr:
916 921 message = _('Closing with') + ' ' + message
917 922 text = text or message
918 923 comm = CommentsModel().create(
919 924 text=text,
920 925 repo=c.rhodecode_db_repo.repo_id,
921 926 user=c.rhodecode_user.user_id,
922 927 pull_request=pull_request_id,
923 928 f_path=request.POST.get('f_path'),
924 929 line_no=request.POST.get('line'),
925 930 status_change=(ChangesetStatus.get_status_lbl(status)
926 931 if status and allowed_to_change_status else None),
927 932 status_change_type=(status
928 933 if status and allowed_to_change_status else None),
929 934 closing_pr=close_pr,
930 935 comment_type=comment_type,
931 936 resolves_comment_id=resolves_comment_id
932 937 )
933 938
934 939 if allowed_to_change_status:
935 940 old_calculated_status = pull_request.calculated_review_status()
936 941 # get status if set !
937 942 if status:
938 943 ChangesetStatusModel().set_status(
939 944 c.rhodecode_db_repo.repo_id,
940 945 status,
941 946 c.rhodecode_user.user_id,
942 947 comm,
943 948 pull_request=pull_request_id
944 949 )
945 950
946 951 Session().flush()
947 952 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
948 953 # we now calculate the status of pull request, and based on that
949 954 # calculation we set the commits status
950 955 calculated_status = pull_request.calculated_review_status()
951 956 if old_calculated_status != calculated_status:
952 957 PullRequestModel()._trigger_pull_request_hook(
953 958 pull_request, c.rhodecode_user, 'review_status_change')
954 959
955 960 calculated_status_lbl = ChangesetStatus.get_status_lbl(
956 961 calculated_status)
957 962
958 963 if close_pr:
959 964 status_completed = (
960 965 calculated_status in [ChangesetStatus.STATUS_APPROVED,
961 966 ChangesetStatus.STATUS_REJECTED])
962 967 if close_pull_request or status_completed:
963 968 PullRequestModel().close_pull_request(
964 969 pull_request_id, c.rhodecode_user)
965 970 else:
966 971 h.flash(_('Closing pull request on other statuses than '
967 972 'rejected or approved is forbidden. '
968 973 'Calculated status from all reviewers '
969 974 'is currently: %s') % calculated_status_lbl,
970 975 category='warning')
971 976
972 977 Session().commit()
973 978
974 979 if not request.is_xhr:
975 980 return redirect(h.url('pullrequest_show', repo_name=repo_name,
976 981 pull_request_id=pull_request_id))
977 982
978 983 data = {
979 984 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
980 985 }
981 986 if comm:
982 987 c.co = comm
983 988 c.inline_comment = True if comm.line_no else False
984 989 data.update(comm.get_dict())
985 990 data.update({'rendered_text':
986 991 render('changeset/changeset_comment_block.mako')})
987 992
988 993 return data
989 994
990 995 @LoginRequired()
991 996 @NotAnonymous()
992 997 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
993 998 'repository.admin')
994 999 @auth.CSRFRequired()
995 1000 @jsonify
996 1001 def delete_comment(self, repo_name, comment_id):
997 1002 return self._delete_comment(comment_id)
998 1003
999 1004 def _delete_comment(self, comment_id):
1000 1005 comment_id = safe_int(comment_id)
1001 1006 co = ChangesetComment.get_or_404(comment_id)
1002 1007 if co.pull_request.is_closed():
1003 1008 # don't allow deleting comments on closed pull request
1004 1009 raise HTTPForbidden()
1005 1010
1006 1011 is_owner = co.author.user_id == c.rhodecode_user.user_id
1007 1012 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1008 1013 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1009 1014 old_calculated_status = co.pull_request.calculated_review_status()
1010 1015 CommentsModel().delete(comment=co)
1011 1016 Session().commit()
1012 1017 calculated_status = co.pull_request.calculated_review_status()
1013 1018 if old_calculated_status != calculated_status:
1014 1019 PullRequestModel()._trigger_pull_request_hook(
1015 1020 co.pull_request, c.rhodecode_user, 'review_status_change')
1016 1021 return True
1017 1022 else:
1018 1023 raise HTTPForbidden()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,600 +1,615 b''
1 1 // # Copyright (C) 2010-2017 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 $('#save').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#save').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(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).find('td.td-description .message').data('messageRaw');
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 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0){
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158 if (data.rules.use_code_authors_for_review) {
159 159 self.$rulesList.append(
160 160 self.addRule(
161 161 _gettext('Reviewers picked from source code changes.'))
162 162 )
163 163 }
164 164 if (data.rules.forbid_adding_reviewers) {
165 165 $('#add_reviewer_input').remove();
166 166 self.$rulesList.append(
167 167 self.addRule(
168 168 _gettext('Adding new reviewers is forbidden.'))
169 169 )
170 170 }
171 171 if (data.rules.forbid_author_to_review) {
172 172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 173 self.$rulesList.append(
174 174 self.addRule(
175 175 _gettext('Author is not allowed to be a reviewer.'))
176 176 )
177 177 }
178 if (data.rules.forbid_commit_author_to_review) {
179
180 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
183 });
184
185 }
186
187 self.$rulesList.append(
188 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
191 }
192
178 193 return self.forbidReviewUsers
179 194 };
180 195
181 196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182 197
183 198 if (self.currentRequest) {
184 199 // make sure we cleanup old running requests before triggering this
185 200 // again
186 201 self.currentRequest.abort();
187 202 }
188 203
189 204 $('.calculate-reviewers').show();
190 205 // reset reviewer members
191 206 self.$reviewMembers.empty();
192 207
193 208 prButtonLock(true, null, 'reviewers');
194 209 $('#user').hide(); // hide user autocomplete before load
195 210
196 211 var url = pyroutes.url('repo_default_reviewers_data',
197 212 {
198 213 'repo_name': templateContext.repo_name,
199 214 'source_repo': sourceRepo,
200 215 'source_ref': sourceRef[2],
201 216 'target_repo': targetRepo,
202 217 'target_ref': targetRef[2]
203 218 });
204 219
205 220 self.currentRequest = $.get(url)
206 221 .done(function(data) {
207 222 self.currentRequest = null;
208 223
209 224 // review rules
210 225 self.loadReviewRules(data);
211 226
212 227 for (var i = 0; i < data.reviewers.length; i++) {
213 228 var reviewer = data.reviewers[i];
214 229 self.addReviewMember(
215 230 reviewer.user_id, reviewer.firstname,
216 231 reviewer.lastname, reviewer.username,
217 232 reviewer.gravatar_link, reviewer.reasons,
218 233 reviewer.mandatory);
219 234 }
220 235 $('.calculate-reviewers').hide();
221 236 prButtonLock(false, null, 'reviewers');
222 237 $('#user').show(); // show user autocomplete after load
223 238 });
224 239 };
225 240
226 241 // check those, refactor
227 242 this.removeReviewMember = function(reviewer_id, mark_delete) {
228 243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
229 244
230 245 if(typeof(mark_delete) === undefined){
231 246 mark_delete = false;
232 247 }
233 248
234 249 if(mark_delete === true){
235 250 if (reviewer){
236 251 // now delete the input
237 252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
238 253 // mark as to-delete
239 254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
240 255 obj.addClass('to-delete');
241 256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
242 257 }
243 258 }
244 259 else{
245 260 $('#reviewer_{0}'.format(reviewer_id)).remove();
246 261 }
247 262 };
248 263
249 264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
250 265 var members = self.$reviewMembers.get(0);
251 266 var reasons_html = '';
252 267 var reasons_inputs = '';
253 268 var reasons = reasons || [];
254 269 var mandatory = mandatory || false;
255 270
256 271 if (reasons) {
257 272 for (var i = 0; i < reasons.length; i++) {
258 273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
259 274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
260 275 }
261 276 }
262 277 var tmpl = '' +
263 278 '<li id="reviewer_{2}" class="reviewer_entry">'+
264 279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
265 280 '<div class="reviewer_status">'+
266 281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
267 282 '</div>'+
268 283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
269 284 '<span class="reviewer_name user">{1}</span>'+
270 285 reasons_html +
271 286 '<input type="hidden" name="user_id" value="{2}">'+
272 287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
273 288 '{3}'+
274 289 '<input type="hidden" name="__end__" value="reasons:sequence">';
275 290
276 291 if (mandatory) {
277 292 tmpl += ''+
278 293 '<div class="reviewer_member_mandatory_remove">' +
279 294 '<i class="icon-remove-sign"></i>'+
280 295 '</div>' +
281 296 '<input type="hidden" name="mandatory" value="true">'+
282 297 '<div class="reviewer_member_mandatory">' +
283 298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 299 '</div>';
285 300
286 301 } else {
287 302 tmpl += ''+
288 303 '<input type="hidden" name="mandatory" value="false">'+
289 304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 305 '<i class="icon-remove-sign"></i>'+
291 306 '</div>';
292 307 }
293 308 // continue template
294 309 tmpl += ''+
295 310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
296 311 '</li>' ;
297 312
298 313 var displayname = "{0} ({1} {2})".format(
299 314 nname, escapeHtml(fname), escapeHtml(lname));
300 315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
301 316 // check if we don't have this ID already in
302 317 var ids = [];
303 318 var _els = self.$reviewMembers.find('li').toArray();
304 319 for (el in _els){
305 320 ids.push(_els[el].id)
306 321 }
307 322
308 323 var userAllowedReview = function(userId) {
309 324 var allowed = true;
310 325 $.each(self.forbidReviewUsers, function(index, member_data) {
311 326 if (parseInt(userId) === member_data['user_id']) {
312 327 allowed = false;
313 328 return false // breaks the loop
314 329 }
315 330 });
316 331 return allowed
317 332 };
318 333
319 334 var userAllowed = userAllowedReview(id);
320 335 if (!userAllowed){
321 336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 337 }
323 338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324 339
325 340 if(shouldAdd) {
326 341 // only add if it's not there
327 342 members.innerHTML += element;
328 343 }
329 344
330 345 };
331 346
332 347 this.updateReviewers = function(repo_name, pull_request_id){
333 348 var postData = '_method=put&' + $('#reviewers input').serialize();
334 349 _updatePullRequest(repo_name, pull_request_id, postData);
335 350 };
336 351
337 352 };
338 353
339 354
340 355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
341 356 var url = pyroutes.url(
342 357 'pullrequest_update',
343 358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
344 359 if (typeof postData === 'string' ) {
345 360 postData += '&csrf_token=' + CSRF_TOKEN;
346 361 } else {
347 362 postData.csrf_token = CSRF_TOKEN;
348 363 }
349 364 var success = function(o) {
350 365 window.location.reload();
351 366 };
352 367 ajaxPOST(url, postData, success);
353 368 };
354 369
355 370 /**
356 371 * PULL REQUEST reject & close
357 372 */
358 373 var closePullRequest = function(repo_name, pull_request_id) {
359 374 var postData = {
360 375 '_method': 'put',
361 376 'close_pull_request': true};
362 377 _updatePullRequest(repo_name, pull_request_id, postData);
363 378 };
364 379
365 380 /**
366 381 * PULL REQUEST update commits
367 382 */
368 383 var updateCommits = function(repo_name, pull_request_id) {
369 384 var postData = {
370 385 '_method': 'put',
371 386 'update_commits': true};
372 387 _updatePullRequest(repo_name, pull_request_id, postData);
373 388 };
374 389
375 390
376 391 /**
377 392 * PULL REQUEST edit info
378 393 */
379 394 var editPullRequest = function(repo_name, pull_request_id, title, description) {
380 395 var url = pyroutes.url(
381 396 'pullrequest_update',
382 397 {"repo_name": repo_name, "pull_request_id": pull_request_id});
383 398
384 399 var postData = {
385 400 '_method': 'put',
386 401 'title': title,
387 402 'description': description,
388 403 'edit_pull_request': true,
389 404 'csrf_token': CSRF_TOKEN
390 405 };
391 406 var success = function(o) {
392 407 window.location.reload();
393 408 };
394 409 ajaxPOST(url, postData, success);
395 410 };
396 411
397 412 var initPullRequestsCodeMirror = function (textAreaId) {
398 413 var ta = $(textAreaId).get(0);
399 414 var initialHeight = '100px';
400 415
401 416 // default options
402 417 var codeMirrorOptions = {
403 418 mode: "text",
404 419 lineNumbers: false,
405 420 indentUnit: 4,
406 421 theme: 'rc-input'
407 422 };
408 423
409 424 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
410 425 // marker for manually set description
411 426 codeMirrorInstance._userDefinedDesc = false;
412 427 codeMirrorInstance.setSize(null, initialHeight);
413 428 codeMirrorInstance.on("change", function(instance, changeObj) {
414 429 var height = initialHeight;
415 430 var lines = instance.lineCount();
416 431 if (lines > 6 && lines < 20) {
417 432 height = "auto"
418 433 }
419 434 else if (lines >= 20) {
420 435 height = 20 * 15;
421 436 }
422 437 instance.setSize(null, height);
423 438
424 439 // detect if the change was trigger by auto desc, or user input
425 440 changeOrigin = changeObj.origin;
426 441
427 442 if (changeOrigin === "setValue") {
428 443 cmLog.debug('Change triggered by setValue');
429 444 }
430 445 else {
431 446 cmLog.debug('user triggered change !');
432 447 // set special marker to indicate user has created an input.
433 448 instance._userDefinedDesc = true;
434 449 }
435 450
436 451 });
437 452
438 453 return codeMirrorInstance
439 454 };
440 455
441 456 /**
442 457 * Reviewer autocomplete
443 458 */
444 459 var ReviewerAutoComplete = function(inputId) {
445 460 $(inputId).autocomplete({
446 461 serviceUrl: pyroutes.url('user_autocomplete_data'),
447 462 minChars:2,
448 463 maxHeight:400,
449 464 deferRequestBy: 300, //miliseconds
450 465 showNoSuggestionNotice: true,
451 466 tabDisabled: true,
452 467 autoSelectFirst: true,
453 468 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
454 469 formatResult: autocompleteFormatResult,
455 470 lookupFilter: autocompleteFilterResult,
456 471 onSelect: function(element, data) {
457 472
458 473 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
459 474 if (data.value_type == 'user_group') {
460 475 reasons.push(_gettext('member of "{0}"').format(data.value_display));
461 476
462 477 $.each(data.members, function(index, member_data) {
463 478 reviewersController.addReviewMember(
464 479 member_data.id, member_data.first_name, member_data.last_name,
465 480 member_data.username, member_data.icon_link, reasons);
466 481 })
467 482
468 483 } else {
469 484 reviewersController.addReviewMember(
470 485 data.id, data.first_name, data.last_name,
471 486 data.username, data.icon_link, reasons);
472 487 }
473 488
474 489 $(inputId).val('');
475 490 }
476 491 });
477 492 };
478 493
479 494
480 495 VersionController = function () {
481 496 var self = this;
482 497 this.$verSource = $('input[name=ver_source]');
483 498 this.$verTarget = $('input[name=ver_target]');
484 499 this.$showVersionDiff = $('#show-version-diff');
485 500
486 501 this.adjustRadioSelectors = function (curNode) {
487 502 var getVal = function (item) {
488 503 if (item == 'latest') {
489 504 return Number.MAX_SAFE_INTEGER
490 505 }
491 506 else {
492 507 return parseInt(item)
493 508 }
494 509 };
495 510
496 511 var curVal = getVal($(curNode).val());
497 512 var cleared = false;
498 513
499 514 $.each(self.$verSource, function (index, value) {
500 515 var elVal = getVal($(value).val());
501 516
502 517 if (elVal > curVal) {
503 518 if ($(value).is(':checked')) {
504 519 cleared = true;
505 520 }
506 521 $(value).attr('disabled', 'disabled');
507 522 $(value).removeAttr('checked');
508 523 $(value).css({'opacity': 0.1});
509 524 }
510 525 else {
511 526 $(value).css({'opacity': 1});
512 527 $(value).removeAttr('disabled');
513 528 }
514 529 });
515 530
516 531 if (cleared) {
517 532 // if we unchecked an active, set the next one to same loc.
518 533 $(this.$verSource).filter('[value={0}]'.format(
519 534 curVal)).attr('checked', 'checked');
520 535 }
521 536
522 537 self.setLockAction(false,
523 538 $(curNode).data('verPos'),
524 539 $(this.$verSource).filter(':checked').data('verPos')
525 540 );
526 541 };
527 542
528 543
529 544 this.attachVersionListener = function () {
530 545 self.$verTarget.change(function (e) {
531 546 self.adjustRadioSelectors(this)
532 547 });
533 548 self.$verSource.change(function (e) {
534 549 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
535 550 });
536 551 };
537 552
538 553 this.init = function () {
539 554
540 555 var curNode = self.$verTarget.filter(':checked');
541 556 self.adjustRadioSelectors(curNode);
542 557 self.setLockAction(true);
543 558 self.attachVersionListener();
544 559
545 560 };
546 561
547 562 this.setLockAction = function (state, selectedVersion, otherVersion) {
548 563 var $showVersionDiff = this.$showVersionDiff;
549 564
550 565 if (state) {
551 566 $showVersionDiff.attr('disabled', 'disabled');
552 567 $showVersionDiff.addClass('disabled');
553 568 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
554 569 }
555 570 else {
556 571 $showVersionDiff.removeAttr('disabled');
557 572 $showVersionDiff.removeClass('disabled');
558 573
559 574 if (selectedVersion == otherVersion) {
560 575 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
561 576 } else {
562 577 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
563 578 }
564 579 }
565 580
566 581 };
567 582
568 583 this.showVersionDiff = function () {
569 584 var target = self.$verTarget.filter(':checked');
570 585 var source = self.$verSource.filter(':checked');
571 586
572 587 if (target.val() && source.val()) {
573 588 var params = {
574 589 'pull_request_id': templateContext.pull_request_data.pull_request_id,
575 590 'repo_name': templateContext.repo_name,
576 591 'version': target.val(),
577 592 'from_version': source.val()
578 593 };
579 594 window.location = pyroutes.url('pullrequest_show', params)
580 595 }
581 596
582 597 return false;
583 598 };
584 599
585 600 this.toggleVersionView = function (elem) {
586 601
587 602 if (this.$showVersionDiff.is(':visible')) {
588 603 $('.version-pr').hide();
589 604 this.$showVersionDiff.hide();
590 605 $(elem).html($(elem).data('toggleOn'))
591 606 } else {
592 607 $('.version-pr').show();
593 608 this.$showVersionDiff.show();
594 609 $(elem).html($(elem).data('toggleOff'))
595 610 }
596 611
597 612 return false
598 613 }
599 614
600 615 }; No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now