##// END OF EJS Templates
pull-requests: use consistent check who is allowed to delete a pull request.
marcink -
r1607:5a387f60 default
parent child Browse files
Show More
@@ -1,1083 +1,1091 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 def _extract_ordering(self, request):
76 76 column_index = safe_int(request.GET.get('order[0][column]'))
77 77 order_dir = request.GET.get('order[0][dir]', 'desc')
78 78 order_by = request.GET.get(
79 79 'columns[%s][data][sort]' % column_index, 'name_raw')
80 80 return order_by, order_dir
81 81
82 82 @LoginRequired()
83 83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
84 84 'repository.admin')
85 85 @HasAcceptedRepoType('git', 'hg')
86 86 def show_all(self, repo_name):
87 87 # filter types
88 88 c.active = 'open'
89 89 c.source = str2bool(request.GET.get('source'))
90 90 c.closed = str2bool(request.GET.get('closed'))
91 91 c.my = str2bool(request.GET.get('my'))
92 92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
93 93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
94 94 c.repo_name = repo_name
95 95
96 96 opened_by = None
97 97 if c.my:
98 98 c.active = 'my'
99 99 opened_by = [c.rhodecode_user.user_id]
100 100
101 101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
102 102 if c.closed:
103 103 c.active = 'closed'
104 104 statuses = [PullRequest.STATUS_CLOSED]
105 105
106 106 if c.awaiting_review and not c.source:
107 107 c.active = 'awaiting'
108 108 if c.source and not c.awaiting_review:
109 109 c.active = 'source'
110 110 if c.awaiting_my_review:
111 111 c.active = 'awaiting_my'
112 112
113 113 data = self._get_pull_requests_list(
114 114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
115 115 if not request.is_xhr:
116 116 c.data = json.dumps(data['data'])
117 117 c.records_total = data['recordsTotal']
118 118 return render('/pullrequests/pullrequests.mako')
119 119 else:
120 120 return json.dumps(data)
121 121
122 122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
123 123 # pagination
124 124 start = safe_int(request.GET.get('start'), 0)
125 125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
126 126 order_by, order_dir = self._extract_ordering(request)
127 127
128 128 if c.awaiting_review:
129 129 pull_requests = PullRequestModel().get_awaiting_review(
130 130 repo_name, source=c.source, opened_by=opened_by,
131 131 statuses=statuses, offset=start, length=length,
132 132 order_by=order_by, order_dir=order_dir)
133 133 pull_requests_total_count = PullRequestModel(
134 134 ).count_awaiting_review(
135 135 repo_name, source=c.source, statuses=statuses,
136 136 opened_by=opened_by)
137 137 elif c.awaiting_my_review:
138 138 pull_requests = PullRequestModel().get_awaiting_my_review(
139 139 repo_name, source=c.source, opened_by=opened_by,
140 140 user_id=c.rhodecode_user.user_id, statuses=statuses,
141 141 offset=start, length=length, order_by=order_by,
142 142 order_dir=order_dir)
143 143 pull_requests_total_count = PullRequestModel(
144 144 ).count_awaiting_my_review(
145 145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
146 146 statuses=statuses, opened_by=opened_by)
147 147 else:
148 148 pull_requests = PullRequestModel().get_all(
149 149 repo_name, source=c.source, opened_by=opened_by,
150 150 statuses=statuses, offset=start, length=length,
151 151 order_by=order_by, order_dir=order_dir)
152 152 pull_requests_total_count = PullRequestModel().count_all(
153 153 repo_name, source=c.source, statuses=statuses,
154 154 opened_by=opened_by)
155 155
156 156 from rhodecode.lib.utils import PartialRenderer
157 157 _render = PartialRenderer('data_table/_dt_elements.mako')
158 158 data = []
159 159 for pr in pull_requests:
160 160 comments = CommentsModel().get_all_comments(
161 161 c.rhodecode_db_repo.repo_id, pull_request=pr)
162 162
163 163 data.append({
164 164 'name': _render('pullrequest_name',
165 165 pr.pull_request_id, pr.target_repo.repo_name),
166 166 'name_raw': pr.pull_request_id,
167 167 'status': _render('pullrequest_status',
168 168 pr.calculated_review_status()),
169 169 'title': _render(
170 170 'pullrequest_title', pr.title, pr.description),
171 171 'description': h.escape(pr.description),
172 172 'updated_on': _render('pullrequest_updated_on',
173 173 h.datetime_to_time(pr.updated_on)),
174 174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
175 175 'created_on': _render('pullrequest_updated_on',
176 176 h.datetime_to_time(pr.created_on)),
177 177 'created_on_raw': h.datetime_to_time(pr.created_on),
178 178 'author': _render('pullrequest_author',
179 179 pr.author.full_contact, ),
180 180 'author_raw': pr.author.full_name,
181 181 'comments': _render('pullrequest_comments', len(comments)),
182 182 'comments_raw': len(comments),
183 183 'closed': pr.is_closed(),
184 184 })
185 185 # json used to render the grid
186 186 data = ({
187 187 'data': data,
188 188 'recordsTotal': pull_requests_total_count,
189 189 'recordsFiltered': pull_requests_total_count,
190 190 })
191 191 return data
192 192
193 193 @LoginRequired()
194 194 @NotAnonymous()
195 195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 196 'repository.admin')
197 197 @HasAcceptedRepoType('git', 'hg')
198 198 def index(self):
199 199 source_repo = c.rhodecode_db_repo
200 200
201 201 try:
202 202 source_repo.scm_instance().get_commit()
203 203 except EmptyRepositoryError:
204 204 h.flash(h.literal(_('There are no commits yet')),
205 205 category='warning')
206 206 redirect(url('summary_home', repo_name=source_repo.repo_name))
207 207
208 208 commit_id = request.GET.get('commit')
209 209 branch_ref = request.GET.get('branch')
210 210 bookmark_ref = request.GET.get('bookmark')
211 211
212 212 try:
213 213 source_repo_data = PullRequestModel().generate_repo_data(
214 214 source_repo, commit_id=commit_id,
215 215 branch=branch_ref, bookmark=bookmark_ref)
216 216 except CommitDoesNotExistError as e:
217 217 log.exception(e)
218 218 h.flash(_('Commit does not exist'), 'error')
219 219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
220 220
221 221 default_target_repo = source_repo
222 222
223 223 if source_repo.parent:
224 224 parent_vcs_obj = source_repo.parent.scm_instance()
225 225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
226 226 # change default if we have a parent repo
227 227 default_target_repo = source_repo.parent
228 228
229 229 target_repo_data = PullRequestModel().generate_repo_data(
230 230 default_target_repo)
231 231
232 232 selected_source_ref = source_repo_data['refs']['selected_ref']
233 233
234 234 title_source_ref = selected_source_ref.split(':', 2)[1]
235 235 c.default_title = PullRequestModel().generate_pullrequest_title(
236 236 source=source_repo.repo_name,
237 237 source_ref=title_source_ref,
238 238 target=default_target_repo.repo_name
239 239 )
240 240
241 241 c.default_repo_data = {
242 242 'source_repo_name': source_repo.repo_name,
243 243 'source_refs_json': json.dumps(source_repo_data),
244 244 'target_repo_name': default_target_repo.repo_name,
245 245 'target_refs_json': json.dumps(target_repo_data),
246 246 }
247 247 c.default_source_ref = selected_source_ref
248 248
249 249 return render('/pullrequests/pullrequest.mako')
250 250
251 251 @LoginRequired()
252 252 @NotAnonymous()
253 253 @XHRRequired()
254 254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 255 'repository.admin')
256 256 @jsonify
257 257 def get_repo_refs(self, repo_name, target_repo_name):
258 258 repo = Repository.get_by_repo_name(target_repo_name)
259 259 if not repo:
260 260 raise HTTPNotFound
261 261 return PullRequestModel().generate_repo_data(repo)
262 262
263 263 @LoginRequired()
264 264 @NotAnonymous()
265 265 @XHRRequired()
266 266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
267 267 'repository.admin')
268 268 @jsonify
269 269 def get_repo_destinations(self, repo_name):
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271 if not repo:
272 272 raise HTTPNotFound
273 273 filter_query = request.GET.get('query')
274 274
275 275 query = Repository.query() \
276 276 .order_by(func.length(Repository.repo_name)) \
277 277 .filter(or_(
278 278 Repository.repo_name == repo.repo_name,
279 279 Repository.fork_id == repo.repo_id))
280 280
281 281 if filter_query:
282 282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
283 283 query = query.filter(
284 284 Repository.repo_name.ilike(ilike_expression))
285 285
286 286 add_parent = False
287 287 if repo.parent:
288 288 if filter_query in repo.parent.repo_name:
289 289 parent_vcs_obj = repo.parent.scm_instance()
290 290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
291 291 add_parent = True
292 292
293 293 limit = 20 - 1 if add_parent else 20
294 294 all_repos = query.limit(limit).all()
295 295 if add_parent:
296 296 all_repos += [repo.parent]
297 297
298 298 repos = []
299 299 for obj in self.scm_model.get_repos(all_repos):
300 300 repos.append({
301 301 'id': obj['name'],
302 302 'text': obj['name'],
303 303 'type': 'repo',
304 304 'obj': obj['dbrepo']
305 305 })
306 306
307 307 data = {
308 308 'more': False,
309 309 'results': [{
310 310 'text': _('Repositories'),
311 311 'children': repos
312 312 }] if repos else []
313 313 }
314 314 return data
315 315
316 316 @LoginRequired()
317 317 @NotAnonymous()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 @HasAcceptedRepoType('git', 'hg')
321 321 @auth.CSRFRequired()
322 322 def create(self, repo_name):
323 323 repo = Repository.get_by_repo_name(repo_name)
324 324 if not repo:
325 325 raise HTTPNotFound
326 326
327 327 controls = peppercorn.parse(request.POST.items())
328 328
329 329 try:
330 330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
331 331 except formencode.Invalid as errors:
332 332 if errors.error_dict.get('revisions'):
333 333 msg = 'Revisions: %s' % errors.error_dict['revisions']
334 334 elif errors.error_dict.get('pullrequest_title'):
335 335 msg = _('Pull request requires a title with min. 3 chars')
336 336 else:
337 337 msg = _('Error creating pull request: {}').format(errors)
338 338 log.exception(msg)
339 339 h.flash(msg, 'error')
340 340
341 341 # would rather just go back to form ...
342 342 return redirect(url('pullrequest_home', repo_name=repo_name))
343 343
344 344 source_repo = _form['source_repo']
345 345 source_ref = _form['source_ref']
346 346 target_repo = _form['target_repo']
347 347 target_ref = _form['target_ref']
348 348 commit_ids = _form['revisions'][::-1]
349 349 reviewers = [
350 350 (r['user_id'], r['reasons']) for r in _form['review_members']]
351 351
352 352 # find the ancestor for this pr
353 353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
354 354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
355 355
356 356 source_scm = source_db_repo.scm_instance()
357 357 target_scm = target_db_repo.scm_instance()
358 358
359 359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
360 360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
361 361
362 362 ancestor = source_scm.get_common_ancestor(
363 363 source_commit.raw_id, target_commit.raw_id, target_scm)
364 364
365 365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
366 366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
367 367
368 368 pullrequest_title = _form['pullrequest_title']
369 369 title_source_ref = source_ref.split(':', 2)[1]
370 370 if not pullrequest_title:
371 371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
372 372 source=source_repo,
373 373 source_ref=title_source_ref,
374 374 target=target_repo
375 375 )
376 376
377 377 description = _form['pullrequest_desc']
378 378 try:
379 379 pull_request = PullRequestModel().create(
380 380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
381 381 target_ref, commit_ids, reviewers, pullrequest_title,
382 382 description
383 383 )
384 384 Session().commit()
385 385 h.flash(_('Successfully opened new pull request'),
386 386 category='success')
387 387 except Exception as e:
388 388 msg = _('Error occurred during sending pull request')
389 389 log.exception(msg)
390 390 h.flash(msg, category='error')
391 391 return redirect(url('pullrequest_home', repo_name=repo_name))
392 392
393 393 return redirect(url('pullrequest_show', repo_name=target_repo,
394 394 pull_request_id=pull_request.pull_request_id))
395 395
396 396 @LoginRequired()
397 397 @NotAnonymous()
398 398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 399 'repository.admin')
400 400 @auth.CSRFRequired()
401 401 @jsonify
402 402 def update(self, repo_name, pull_request_id):
403 403 pull_request_id = safe_int(pull_request_id)
404 404 pull_request = PullRequest.get_or_404(pull_request_id)
405 405 # only owner or admin can update it
406 406 allowed_to_update = PullRequestModel().check_user_update(
407 407 pull_request, c.rhodecode_user)
408 408 if allowed_to_update:
409 409 controls = peppercorn.parse(request.POST.items())
410 410
411 411 if 'review_members' in controls:
412 412 self._update_reviewers(
413 413 pull_request_id, controls['review_members'])
414 414 elif str2bool(request.POST.get('update_commits', 'false')):
415 415 self._update_commits(pull_request)
416 416 elif str2bool(request.POST.get('close_pull_request', 'false')):
417 417 self._reject_close(pull_request)
418 418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
419 419 self._edit_pull_request(pull_request)
420 420 else:
421 421 raise HTTPBadRequest()
422 422 return True
423 423 raise HTTPForbidden()
424 424
425 425 def _edit_pull_request(self, pull_request):
426 426 try:
427 427 PullRequestModel().edit(
428 428 pull_request, request.POST.get('title'),
429 429 request.POST.get('description'))
430 430 except ValueError:
431 431 msg = _(u'Cannot update closed pull requests.')
432 432 h.flash(msg, category='error')
433 433 return
434 434 else:
435 435 Session().commit()
436 436
437 437 msg = _(u'Pull request title & description updated.')
438 438 h.flash(msg, category='success')
439 439 return
440 440
441 441 def _update_commits(self, pull_request):
442 442 resp = PullRequestModel().update_commits(pull_request)
443 443
444 444 if resp.executed:
445 445
446 446 if resp.target_changed and resp.source_changed:
447 447 changed = 'target and source repositories'
448 448 elif resp.target_changed and not resp.source_changed:
449 449 changed = 'target repository'
450 450 elif not resp.target_changed and resp.source_changed:
451 451 changed = 'source repository'
452 452 else:
453 453 changed = 'nothing'
454 454
455 455 msg = _(
456 456 u'Pull request updated to "{source_commit_id}" with '
457 457 u'{count_added} added, {count_removed} removed commits. '
458 458 u'Source of changes: {change_source}')
459 459 msg = msg.format(
460 460 source_commit_id=pull_request.source_ref_parts.commit_id,
461 461 count_added=len(resp.changes.added),
462 462 count_removed=len(resp.changes.removed),
463 463 change_source=changed)
464 464 h.flash(msg, category='success')
465 465
466 466 registry = get_current_registry()
467 467 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
468 468 channelstream_config = rhodecode_plugins.get('channelstream', {})
469 469 if channelstream_config.get('enabled'):
470 470 message = msg + (
471 471 ' - <a onclick="window.location.reload()">'
472 472 '<strong>{}</strong></a>'.format(_('Reload page')))
473 473 channel = '/repo${}$/pr/{}'.format(
474 474 pull_request.target_repo.repo_name,
475 475 pull_request.pull_request_id
476 476 )
477 477 payload = {
478 478 'type': 'message',
479 479 'user': 'system',
480 480 'exclude_users': [request.user.username],
481 481 'channel': channel,
482 482 'message': {
483 483 'message': message,
484 484 'level': 'success',
485 485 'topic': '/notifications'
486 486 }
487 487 }
488 488 channelstream_request(
489 489 channelstream_config, [payload], '/message',
490 490 raise_exc=False)
491 491 else:
492 492 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
493 493 warning_reasons = [
494 494 UpdateFailureReason.NO_CHANGE,
495 495 UpdateFailureReason.WRONG_REF_TPYE,
496 496 ]
497 497 category = 'warning' if resp.reason in warning_reasons else 'error'
498 498 h.flash(msg, category=category)
499 499
500 500 @auth.CSRFRequired()
501 501 @LoginRequired()
502 502 @NotAnonymous()
503 503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
504 504 'repository.admin')
505 505 def merge(self, repo_name, pull_request_id):
506 506 """
507 507 POST /{repo_name}/pull-request/{pull_request_id}
508 508
509 509 Merge will perform a server-side merge of the specified
510 510 pull request, if the pull request is approved and mergeable.
511 511 After successful merging, the pull request is automatically
512 512 closed, with a relevant comment.
513 513 """
514 514 pull_request_id = safe_int(pull_request_id)
515 515 pull_request = PullRequest.get_or_404(pull_request_id)
516 516 user = c.rhodecode_user
517 517
518 518 check = MergeCheck.validate(pull_request, user)
519 519 merge_possible = not check.failed
520 520
521 521 for err_type, error_msg in check.errors:
522 522 h.flash(error_msg, category=err_type)
523 523
524 524 if merge_possible:
525 525 log.debug("Pre-conditions checked, trying to merge.")
526 526 extras = vcs_operation_context(
527 527 request.environ, repo_name=pull_request.target_repo.repo_name,
528 528 username=user.username, action='push',
529 529 scm=pull_request.target_repo.repo_type)
530 530 self._merge_pull_request(pull_request, user, extras)
531 531
532 532 return redirect(url(
533 533 'pullrequest_show',
534 534 repo_name=pull_request.target_repo.repo_name,
535 535 pull_request_id=pull_request.pull_request_id))
536 536
537 537 def _merge_pull_request(self, pull_request, user, extras):
538 538 merge_resp = PullRequestModel().merge(
539 539 pull_request, user, extras=extras)
540 540
541 541 if merge_resp.executed:
542 542 log.debug("The merge was successful, closing the pull request.")
543 543 PullRequestModel().close_pull_request(
544 544 pull_request.pull_request_id, user)
545 545 Session().commit()
546 546 msg = _('Pull request was successfully merged and closed.')
547 547 h.flash(msg, category='success')
548 548 else:
549 549 log.debug(
550 550 "The merge was not successful. Merge response: %s",
551 551 merge_resp)
552 552 msg = PullRequestModel().merge_status_message(
553 553 merge_resp.failure_reason)
554 554 h.flash(msg, category='error')
555 555
556 556 def _update_reviewers(self, pull_request_id, review_members):
557 557 reviewers = [
558 558 (int(r['user_id']), r['reasons']) for r in review_members]
559 559 PullRequestModel().update_reviewers(pull_request_id, reviewers)
560 560 Session().commit()
561 561
562 562 def _reject_close(self, pull_request):
563 563 if pull_request.is_closed():
564 564 raise HTTPForbidden()
565 565
566 566 PullRequestModel().close_pull_request_with_comment(
567 567 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
568 568 Session().commit()
569 569
570 570 @LoginRequired()
571 571 @NotAnonymous()
572 572 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
573 573 'repository.admin')
574 574 @auth.CSRFRequired()
575 575 @jsonify
576 576 def delete(self, repo_name, pull_request_id):
577 577 pull_request_id = safe_int(pull_request_id)
578 578 pull_request = PullRequest.get_or_404(pull_request_id)
579
580 pr_closed = pull_request.is_closed()
581 allowed_to_delete = PullRequestModel().check_user_delete(
582 pull_request, c.rhodecode_user) and not pr_closed
583
579 584 # only owner can delete it !
580 if pull_request.author.user_id == c.rhodecode_user.user_id:
585 if allowed_to_delete:
581 586 PullRequestModel().delete(pull_request)
582 587 Session().commit()
583 588 h.flash(_('Successfully deleted pull request'),
584 589 category='success')
585 590 return redirect(url('my_account_pullrequests'))
591
592 h.flash(_('Your are not allowed to delete this pull request'),
593 category='error')
586 594 raise HTTPForbidden()
587 595
588 596 def _get_pr_version(self, pull_request_id, version=None):
589 597 pull_request_id = safe_int(pull_request_id)
590 598 at_version = None
591 599
592 600 if version and version == 'latest':
593 601 pull_request_ver = PullRequest.get(pull_request_id)
594 602 pull_request_obj = pull_request_ver
595 603 _org_pull_request_obj = pull_request_obj
596 604 at_version = 'latest'
597 605 elif version:
598 606 pull_request_ver = PullRequestVersion.get_or_404(version)
599 607 pull_request_obj = pull_request_ver
600 608 _org_pull_request_obj = pull_request_ver.pull_request
601 609 at_version = pull_request_ver.pull_request_version_id
602 610 else:
603 611 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
604 612
605 613 pull_request_display_obj = PullRequest.get_pr_display_object(
606 614 pull_request_obj, _org_pull_request_obj)
607 615
608 616 return _org_pull_request_obj, pull_request_obj, \
609 617 pull_request_display_obj, at_version
610 618
611 619 def _get_diffset(
612 620 self, source_repo, source_ref_id, target_ref_id, target_commit,
613 621 source_commit, diff_limit, file_limit, display_inline_comments):
614 622 vcs_diff = PullRequestModel().get_diff(
615 623 source_repo, source_ref_id, target_ref_id)
616 624
617 625 diff_processor = diffs.DiffProcessor(
618 626 vcs_diff, format='newdiff', diff_limit=diff_limit,
619 627 file_limit=file_limit, show_full_diff=c.fulldiff)
620 628
621 629 _parsed = diff_processor.prepare()
622 630
623 631 def _node_getter(commit):
624 632 def get_node(fname):
625 633 try:
626 634 return commit.get_node(fname)
627 635 except NodeDoesNotExistError:
628 636 return None
629 637
630 638 return get_node
631 639
632 640 diffset = codeblocks.DiffSet(
633 641 repo_name=c.repo_name,
634 642 source_repo_name=c.source_repo.repo_name,
635 643 source_node_getter=_node_getter(target_commit),
636 644 target_node_getter=_node_getter(source_commit),
637 645 comments=display_inline_comments
638 646 )
639 647 diffset = diffset.render_patchset(
640 648 _parsed, target_commit.raw_id, source_commit.raw_id)
641 649
642 650 return diffset
643 651
644 652 @LoginRequired()
645 653 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
646 654 'repository.admin')
647 655 def show(self, repo_name, pull_request_id):
648 656 pull_request_id = safe_int(pull_request_id)
649 657 version = request.GET.get('version')
650 658 from_version = request.GET.get('from_version') or version
651 659 merge_checks = request.GET.get('merge_checks')
652 660 c.fulldiff = str2bool(request.GET.get('fulldiff'))
653 661
654 662 (pull_request_latest,
655 663 pull_request_at_ver,
656 664 pull_request_display_obj,
657 665 at_version) = self._get_pr_version(
658 666 pull_request_id, version=version)
659 667 pr_closed = pull_request_latest.is_closed()
660 668
661 669 if pr_closed and (version or from_version):
662 670 # not allow to browse versions
663 671 return redirect(h.url('pullrequest_show', repo_name=repo_name,
664 672 pull_request_id=pull_request_id))
665 673
666 674 versions = pull_request_display_obj.versions()
667 675
668 676 c.at_version = at_version
669 677 c.at_version_num = (at_version
670 678 if at_version and at_version != 'latest'
671 679 else None)
672 680 c.at_version_pos = ChangesetComment.get_index_from_version(
673 681 c.at_version_num, versions)
674 682
675 683 (prev_pull_request_latest,
676 684 prev_pull_request_at_ver,
677 685 prev_pull_request_display_obj,
678 686 prev_at_version) = self._get_pr_version(
679 687 pull_request_id, version=from_version)
680 688
681 689 c.from_version = prev_at_version
682 690 c.from_version_num = (prev_at_version
683 691 if prev_at_version and prev_at_version != 'latest'
684 692 else None)
685 693 c.from_version_pos = ChangesetComment.get_index_from_version(
686 694 c.from_version_num, versions)
687 695
688 696 # define if we're in COMPARE mode or VIEW at version mode
689 697 compare = at_version != prev_at_version
690 698
691 699 # pull_requests repo_name we opened it against
692 700 # ie. target_repo must match
693 701 if repo_name != pull_request_at_ver.target_repo.repo_name:
694 702 raise HTTPNotFound
695 703
696 704 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
697 705 pull_request_at_ver)
698 706
699 707 c.pull_request = pull_request_display_obj
700 708 c.pull_request_latest = pull_request_latest
701 709
702 710 if compare or (at_version and not at_version == 'latest'):
703 711 c.allowed_to_change_status = False
704 712 c.allowed_to_update = False
705 713 c.allowed_to_merge = False
706 714 c.allowed_to_delete = False
707 715 c.allowed_to_comment = False
708 716 c.allowed_to_close = False
709 717 else:
710 718 c.allowed_to_change_status = PullRequestModel(). \
711 719 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
712 720 and not pr_closed
713 721
714 722 c.allowed_to_update = PullRequestModel().check_user_update(
715 723 pull_request_latest, c.rhodecode_user) and not pr_closed
716 724 c.allowed_to_merge = PullRequestModel().check_user_merge(
717 725 pull_request_latest, c.rhodecode_user) and not pr_closed
718 726 c.allowed_to_delete = PullRequestModel().check_user_delete(
719 727 pull_request_latest, c.rhodecode_user) and not pr_closed
720 728 c.allowed_to_comment = not pr_closed
721 729 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
722 730
723 731 # check merge capabilities
724 732 _merge_check = MergeCheck.validate(
725 733 pull_request_latest, user=c.rhodecode_user)
726 734 c.pr_merge_errors = _merge_check.error_details
727 735 c.pr_merge_possible = not _merge_check.failed
728 736 c.pr_merge_message = _merge_check.merge_msg
729 737
730 738 c.pull_request_review_status = _merge_check.review_status
731 739 if merge_checks:
732 740 return render('/pullrequests/pullrequest_merge_checks.mako')
733 741
734 742 comments_model = CommentsModel()
735 743
736 744 # reviewers and statuses
737 745 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
738 746 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
739 747
740 748 # GENERAL COMMENTS with versions #
741 749 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
742 750 q = q.order_by(ChangesetComment.comment_id.asc())
743 751 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
744 752
745 753 # pick comments we want to render at current version
746 754 c.comment_versions = comments_model.aggregate_comments(
747 755 general_comments, versions, c.at_version_num)
748 756 c.comments = c.comment_versions[c.at_version_num]['until']
749 757
750 758 # INLINE COMMENTS with versions #
751 759 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
752 760 q = q.order_by(ChangesetComment.comment_id.asc())
753 761 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
754 762 c.inline_versions = comments_model.aggregate_comments(
755 763 inline_comments, versions, c.at_version_num, inline=True)
756 764
757 765 # inject latest version
758 766 latest_ver = PullRequest.get_pr_display_object(
759 767 pull_request_latest, pull_request_latest)
760 768
761 769 c.versions = versions + [latest_ver]
762 770
763 771 # if we use version, then do not show later comments
764 772 # than current version
765 773 display_inline_comments = collections.defaultdict(
766 774 lambda: collections.defaultdict(list))
767 775 for co in inline_comments:
768 776 if c.at_version_num:
769 777 # pick comments that are at least UPTO given version, so we
770 778 # don't render comments for higher version
771 779 should_render = co.pull_request_version_id and \
772 780 co.pull_request_version_id <= c.at_version_num
773 781 else:
774 782 # showing all, for 'latest'
775 783 should_render = True
776 784
777 785 if should_render:
778 786 display_inline_comments[co.f_path][co.line_no].append(co)
779 787
780 788 # load diff data into template context, if we use compare mode then
781 789 # diff is calculated based on changes between versions of PR
782 790
783 791 source_repo = pull_request_at_ver.source_repo
784 792 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
785 793
786 794 target_repo = pull_request_at_ver.target_repo
787 795 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
788 796
789 797 if compare:
790 798 # in compare switch the diff base to latest commit from prev version
791 799 target_ref_id = prev_pull_request_display_obj.revisions[0]
792 800
793 801 # despite opening commits for bookmarks/branches/tags, we always
794 802 # convert this to rev to prevent changes after bookmark or branch change
795 803 c.source_ref_type = 'rev'
796 804 c.source_ref = source_ref_id
797 805
798 806 c.target_ref_type = 'rev'
799 807 c.target_ref = target_ref_id
800 808
801 809 c.source_repo = source_repo
802 810 c.target_repo = target_repo
803 811
804 812 # diff_limit is the old behavior, will cut off the whole diff
805 813 # if the limit is applied otherwise will just hide the
806 814 # big files from the front-end
807 815 diff_limit = self.cut_off_limit_diff
808 816 file_limit = self.cut_off_limit_file
809 817
810 818 c.commit_ranges = []
811 819 source_commit = EmptyCommit()
812 820 target_commit = EmptyCommit()
813 821 c.missing_requirements = False
814 822
815 823 source_scm = source_repo.scm_instance()
816 824 target_scm = target_repo.scm_instance()
817 825
818 826 # try first shadow repo, fallback to regular repo
819 827 try:
820 828 commits_source_repo = pull_request_latest.get_shadow_repo()
821 829 except Exception:
822 830 log.debug('Failed to get shadow repo', exc_info=True)
823 831 commits_source_repo = source_scm
824 832
825 833 c.commits_source_repo = commits_source_repo
826 834 commit_cache = {}
827 835 try:
828 836 pre_load = ["author", "branch", "date", "message"]
829 837 show_revs = pull_request_at_ver.revisions
830 838 for rev in show_revs:
831 839 comm = commits_source_repo.get_commit(
832 840 commit_id=rev, pre_load=pre_load)
833 841 c.commit_ranges.append(comm)
834 842 commit_cache[comm.raw_id] = comm
835 843
836 844 target_commit = commits_source_repo.get_commit(
837 845 commit_id=safe_str(target_ref_id))
838 846 source_commit = commits_source_repo.get_commit(
839 847 commit_id=safe_str(source_ref_id))
840 848 except CommitDoesNotExistError:
841 849 pass
842 850 except RepositoryRequirementError:
843 851 log.warning(
844 852 'Failed to get all required data from repo', exc_info=True)
845 853 c.missing_requirements = True
846 854
847 855 c.ancestor = None # set it to None, to hide it from PR view
848 856
849 857 try:
850 858 ancestor_id = source_scm.get_common_ancestor(
851 859 source_commit.raw_id, target_commit.raw_id, target_scm)
852 860 c.ancestor_commit = source_scm.get_commit(ancestor_id)
853 861 except Exception:
854 862 c.ancestor_commit = None
855 863
856 864 c.statuses = source_repo.statuses(
857 865 [x.raw_id for x in c.commit_ranges])
858 866
859 867 # auto collapse if we have more than limit
860 868 collapse_limit = diffs.DiffProcessor._collapse_commits_over
861 869 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
862 870 c.compare_mode = compare
863 871
864 872 c.missing_commits = False
865 873 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
866 874 or source_commit == target_commit):
867 875
868 876 c.missing_commits = True
869 877 else:
870 878
871 879 c.diffset = self._get_diffset(
872 880 commits_source_repo, source_ref_id, target_ref_id,
873 881 target_commit, source_commit,
874 882 diff_limit, file_limit, display_inline_comments)
875 883
876 884 c.limited_diff = c.diffset.limited_diff
877 885
878 886 # calculate removed files that are bound to comments
879 887 comment_deleted_files = [
880 888 fname for fname in display_inline_comments
881 889 if fname not in c.diffset.file_stats]
882 890
883 891 c.deleted_files_comments = collections.defaultdict(dict)
884 892 for fname, per_line_comments in display_inline_comments.items():
885 893 if fname in comment_deleted_files:
886 894 c.deleted_files_comments[fname]['stats'] = 0
887 895 c.deleted_files_comments[fname]['comments'] = list()
888 896 for lno, comments in per_line_comments.items():
889 897 c.deleted_files_comments[fname]['comments'].extend(
890 898 comments)
891 899
892 900 # this is a hack to properly display links, when creating PR, the
893 901 # compare view and others uses different notation, and
894 902 # compare_commits.mako renders links based on the target_repo.
895 903 # We need to swap that here to generate it properly on the html side
896 904 c.target_repo = c.source_repo
897 905
898 906 c.commit_statuses = ChangesetStatus.STATUSES
899 907
900 908 c.show_version_changes = not pr_closed
901 909 if c.show_version_changes:
902 910 cur_obj = pull_request_at_ver
903 911 prev_obj = prev_pull_request_at_ver
904 912
905 913 old_commit_ids = prev_obj.revisions
906 914 new_commit_ids = cur_obj.revisions
907 915 commit_changes = PullRequestModel()._calculate_commit_id_changes(
908 916 old_commit_ids, new_commit_ids)
909 917 c.commit_changes_summary = commit_changes
910 918
911 919 # calculate the diff for commits between versions
912 920 c.commit_changes = []
913 921 mark = lambda cs, fw: list(
914 922 h.itertools.izip_longest([], cs, fillvalue=fw))
915 923 for c_type, raw_id in mark(commit_changes.added, 'a') \
916 924 + mark(commit_changes.removed, 'r') \
917 925 + mark(commit_changes.common, 'c'):
918 926
919 927 if raw_id in commit_cache:
920 928 commit = commit_cache[raw_id]
921 929 else:
922 930 try:
923 931 commit = commits_source_repo.get_commit(raw_id)
924 932 except CommitDoesNotExistError:
925 933 # in case we fail extracting still use "dummy" commit
926 934 # for display in commit diff
927 935 commit = h.AttributeDict(
928 936 {'raw_id': raw_id,
929 937 'message': 'EMPTY or MISSING COMMIT'})
930 938 c.commit_changes.append([c_type, commit])
931 939
932 940 # current user review statuses for each version
933 941 c.review_versions = {}
934 942 if c.rhodecode_user.user_id in allowed_reviewers:
935 943 for co in general_comments:
936 944 if co.author.user_id == c.rhodecode_user.user_id:
937 945 # each comment has a status change
938 946 status = co.status_change
939 947 if status:
940 948 _ver_pr = status[0].comment.pull_request_version_id
941 949 c.review_versions[_ver_pr] = status[0]
942 950
943 951 return render('/pullrequests/pullrequest_show.mako')
944 952
945 953 @LoginRequired()
946 954 @NotAnonymous()
947 955 @HasRepoPermissionAnyDecorator(
948 956 'repository.read', 'repository.write', 'repository.admin')
949 957 @auth.CSRFRequired()
950 958 @jsonify
951 959 def comment(self, repo_name, pull_request_id):
952 960 pull_request_id = safe_int(pull_request_id)
953 961 pull_request = PullRequest.get_or_404(pull_request_id)
954 962 if pull_request.is_closed():
955 963 raise HTTPForbidden()
956 964
957 965 status = request.POST.get('changeset_status', None)
958 966 text = request.POST.get('text')
959 967 comment_type = request.POST.get('comment_type')
960 968 resolves_comment_id = request.POST.get('resolves_comment_id', None)
961 969 close_pull_request = request.POST.get('close_pull_request')
962 970
963 971 close_pr = False
964 972 if close_pull_request:
965 973 close_pr = True
966 974 pull_request_review_status = pull_request.calculated_review_status()
967 975 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
968 976 # approved only if we have voting consent
969 977 status = ChangesetStatus.STATUS_APPROVED
970 978 else:
971 979 status = ChangesetStatus.STATUS_REJECTED
972 980
973 981 allowed_to_change_status = PullRequestModel().check_user_change_status(
974 982 pull_request, c.rhodecode_user)
975 983
976 984 if status and allowed_to_change_status:
977 985 message = (_('Status change %(transition_icon)s %(status)s')
978 986 % {'transition_icon': '>',
979 987 'status': ChangesetStatus.get_status_lbl(status)})
980 988 if close_pr:
981 989 message = _('Closing with') + ' ' + message
982 990 text = text or message
983 991 comm = CommentsModel().create(
984 992 text=text,
985 993 repo=c.rhodecode_db_repo.repo_id,
986 994 user=c.rhodecode_user.user_id,
987 995 pull_request=pull_request_id,
988 996 f_path=request.POST.get('f_path'),
989 997 line_no=request.POST.get('line'),
990 998 status_change=(ChangesetStatus.get_status_lbl(status)
991 999 if status and allowed_to_change_status else None),
992 1000 status_change_type=(status
993 1001 if status and allowed_to_change_status else None),
994 1002 closing_pr=close_pr,
995 1003 comment_type=comment_type,
996 1004 resolves_comment_id=resolves_comment_id
997 1005 )
998 1006
999 1007 if allowed_to_change_status:
1000 1008 old_calculated_status = pull_request.calculated_review_status()
1001 1009 # get status if set !
1002 1010 if status:
1003 1011 ChangesetStatusModel().set_status(
1004 1012 c.rhodecode_db_repo.repo_id,
1005 1013 status,
1006 1014 c.rhodecode_user.user_id,
1007 1015 comm,
1008 1016 pull_request=pull_request_id
1009 1017 )
1010 1018
1011 1019 Session().flush()
1012 1020 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1013 1021 # we now calculate the status of pull request, and based on that
1014 1022 # calculation we set the commits status
1015 1023 calculated_status = pull_request.calculated_review_status()
1016 1024 if old_calculated_status != calculated_status:
1017 1025 PullRequestModel()._trigger_pull_request_hook(
1018 1026 pull_request, c.rhodecode_user, 'review_status_change')
1019 1027
1020 1028 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1021 1029 calculated_status)
1022 1030
1023 1031 if close_pr:
1024 1032 status_completed = (
1025 1033 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1026 1034 ChangesetStatus.STATUS_REJECTED])
1027 1035 if close_pull_request or status_completed:
1028 1036 PullRequestModel().close_pull_request(
1029 1037 pull_request_id, c.rhodecode_user)
1030 1038 else:
1031 1039 h.flash(_('Closing pull request on other statuses than '
1032 1040 'rejected or approved is forbidden. '
1033 1041 'Calculated status from all reviewers '
1034 1042 'is currently: %s') % calculated_status_lbl,
1035 1043 category='warning')
1036 1044
1037 1045 Session().commit()
1038 1046
1039 1047 if not request.is_xhr:
1040 1048 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1041 1049 pull_request_id=pull_request_id))
1042 1050
1043 1051 data = {
1044 1052 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1045 1053 }
1046 1054 if comm:
1047 1055 c.co = comm
1048 1056 c.inline_comment = True if comm.line_no else False
1049 1057 data.update(comm.get_dict())
1050 1058 data.update({'rendered_text':
1051 1059 render('changeset/changeset_comment_block.mako')})
1052 1060
1053 1061 return data
1054 1062
1055 1063 @LoginRequired()
1056 1064 @NotAnonymous()
1057 1065 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1058 1066 'repository.admin')
1059 1067 @auth.CSRFRequired()
1060 1068 @jsonify
1061 1069 def delete_comment(self, repo_name, comment_id):
1062 1070 return self._delete_comment(comment_id)
1063 1071
1064 1072 def _delete_comment(self, comment_id):
1065 1073 comment_id = safe_int(comment_id)
1066 1074 co = ChangesetComment.get_or_404(comment_id)
1067 1075 if co.pull_request.is_closed():
1068 1076 # don't allow deleting comments on closed pull request
1069 1077 raise HTTPForbidden()
1070 1078
1071 1079 is_owner = co.author.user_id == c.rhodecode_user.user_id
1072 1080 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1073 1081 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1074 1082 old_calculated_status = co.pull_request.calculated_review_status()
1075 1083 CommentsModel().delete(comment=co)
1076 1084 Session().commit()
1077 1085 calculated_status = co.pull_request.calculated_review_status()
1078 1086 if old_calculated_status != calculated_status:
1079 1087 PullRequestModel()._trigger_pull_request_hook(
1080 1088 co.pull_request, c.rhodecode_user, 'review_status_change')
1081 1089 return True
1082 1090 else:
1083 1091 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now