##// END OF EJS Templates
pull-requests: comments don't order by versions since we anyway order by unique number.
marcink -
r1706:2260b99c default
parent child Browse files
Show More
@@ -1,1095 +1,1096 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_TYPE,
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 579
580 580 pr_closed = pull_request.is_closed()
581 581 allowed_to_delete = PullRequestModel().check_user_delete(
582 582 pull_request, c.rhodecode_user) and not pr_closed
583 583
584 584 # only owner can delete it !
585 585 if allowed_to_delete:
586 586 PullRequestModel().delete(pull_request)
587 587 Session().commit()
588 588 h.flash(_('Successfully deleted pull request'),
589 589 category='success')
590 590 return redirect(url('my_account_pullrequests'))
591 591
592 592 h.flash(_('Your are not allowed to delete this pull request'),
593 593 category='error')
594 594 raise HTTPForbidden()
595 595
596 596 def _get_pr_version(self, pull_request_id, version=None):
597 597 pull_request_id = safe_int(pull_request_id)
598 598 at_version = None
599 599
600 600 if version and version == 'latest':
601 601 pull_request_ver = PullRequest.get(pull_request_id)
602 602 pull_request_obj = pull_request_ver
603 603 _org_pull_request_obj = pull_request_obj
604 604 at_version = 'latest'
605 605 elif version:
606 606 pull_request_ver = PullRequestVersion.get_or_404(version)
607 607 pull_request_obj = pull_request_ver
608 608 _org_pull_request_obj = pull_request_ver.pull_request
609 609 at_version = pull_request_ver.pull_request_version_id
610 610 else:
611 611 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
612 612
613 613 pull_request_display_obj = PullRequest.get_pr_display_object(
614 614 pull_request_obj, _org_pull_request_obj)
615 615
616 616 return _org_pull_request_obj, pull_request_obj, \
617 617 pull_request_display_obj, at_version
618 618
619 619 def _get_diffset(
620 620 self, source_repo, source_ref_id, target_ref_id, target_commit,
621 621 source_commit, diff_limit, file_limit, display_inline_comments):
622 622 vcs_diff = PullRequestModel().get_diff(
623 623 source_repo, source_ref_id, target_ref_id)
624 624
625 625 diff_processor = diffs.DiffProcessor(
626 626 vcs_diff, format='newdiff', diff_limit=diff_limit,
627 627 file_limit=file_limit, show_full_diff=c.fulldiff)
628 628
629 629 _parsed = diff_processor.prepare()
630 630
631 631 def _node_getter(commit):
632 632 def get_node(fname):
633 633 try:
634 634 return commit.get_node(fname)
635 635 except NodeDoesNotExistError:
636 636 return None
637 637
638 638 return get_node
639 639
640 640 diffset = codeblocks.DiffSet(
641 641 repo_name=c.repo_name,
642 642 source_repo_name=c.source_repo.repo_name,
643 643 source_node_getter=_node_getter(target_commit),
644 644 target_node_getter=_node_getter(source_commit),
645 645 comments=display_inline_comments
646 646 )
647 647 diffset = diffset.render_patchset(
648 648 _parsed, target_commit.raw_id, source_commit.raw_id)
649 649
650 650 return diffset
651 651
652 652 @LoginRequired()
653 653 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
654 654 'repository.admin')
655 655 def show(self, repo_name, pull_request_id):
656 656 pull_request_id = safe_int(pull_request_id)
657 657 version = request.GET.get('version')
658 658 from_version = request.GET.get('from_version') or version
659 659 merge_checks = request.GET.get('merge_checks')
660 660 c.fulldiff = str2bool(request.GET.get('fulldiff'))
661 661
662 662 (pull_request_latest,
663 663 pull_request_at_ver,
664 664 pull_request_display_obj,
665 665 at_version) = self._get_pr_version(
666 666 pull_request_id, version=version)
667 667 pr_closed = pull_request_latest.is_closed()
668 668
669 669 if pr_closed and (version or from_version):
670 670 # not allow to browse versions
671 671 return redirect(h.url('pullrequest_show', repo_name=repo_name,
672 672 pull_request_id=pull_request_id))
673 673
674 674 versions = pull_request_display_obj.versions()
675 675
676 676 c.at_version = at_version
677 677 c.at_version_num = (at_version
678 678 if at_version and at_version != 'latest'
679 679 else None)
680 680 c.at_version_pos = ChangesetComment.get_index_from_version(
681 681 c.at_version_num, versions)
682 682
683 683 (prev_pull_request_latest,
684 684 prev_pull_request_at_ver,
685 685 prev_pull_request_display_obj,
686 686 prev_at_version) = self._get_pr_version(
687 687 pull_request_id, version=from_version)
688 688
689 689 c.from_version = prev_at_version
690 690 c.from_version_num = (prev_at_version
691 691 if prev_at_version and prev_at_version != 'latest'
692 692 else None)
693 693 c.from_version_pos = ChangesetComment.get_index_from_version(
694 694 c.from_version_num, versions)
695 695
696 696 # define if we're in COMPARE mode or VIEW at version mode
697 697 compare = at_version != prev_at_version
698 698
699 699 # pull_requests repo_name we opened it against
700 700 # ie. target_repo must match
701 701 if repo_name != pull_request_at_ver.target_repo.repo_name:
702 702 raise HTTPNotFound
703 703
704 704 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
705 705 pull_request_at_ver)
706 706
707 707 c.pull_request = pull_request_display_obj
708 708 c.pull_request_latest = pull_request_latest
709 709
710 710 if compare or (at_version and not at_version == 'latest'):
711 711 c.allowed_to_change_status = False
712 712 c.allowed_to_update = False
713 713 c.allowed_to_merge = False
714 714 c.allowed_to_delete = False
715 715 c.allowed_to_comment = False
716 716 c.allowed_to_close = False
717 717 else:
718 718 c.allowed_to_change_status = PullRequestModel(). \
719 719 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
720 720 and not pr_closed
721 721
722 722 c.allowed_to_update = PullRequestModel().check_user_update(
723 723 pull_request_latest, c.rhodecode_user) and not pr_closed
724 724 c.allowed_to_merge = PullRequestModel().check_user_merge(
725 725 pull_request_latest, c.rhodecode_user) and not pr_closed
726 726 c.allowed_to_delete = PullRequestModel().check_user_delete(
727 727 pull_request_latest, c.rhodecode_user) and not pr_closed
728 728 c.allowed_to_comment = not pr_closed
729 729 c.allowed_to_close = c.allowed_to_merge and not pr_closed
730 730
731 731 # check merge capabilities
732 732 _merge_check = MergeCheck.validate(
733 733 pull_request_latest, user=c.rhodecode_user)
734 734 c.pr_merge_errors = _merge_check.error_details
735 735 c.pr_merge_possible = not _merge_check.failed
736 736 c.pr_merge_message = _merge_check.merge_msg
737 737
738 738 c.pull_request_review_status = _merge_check.review_status
739 739 if merge_checks:
740 740 return render('/pullrequests/pullrequest_merge_checks.mako')
741 741
742 742 comments_model = CommentsModel()
743 743
744 744 # reviewers and statuses
745 745 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
746 746 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
747 747
748 748 # GENERAL COMMENTS with versions #
749 749 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
750 750 q = q.order_by(ChangesetComment.comment_id.asc())
751 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
751 general_comments = q
752 752
753 753 # pick comments we want to render at current version
754 754 c.comment_versions = comments_model.aggregate_comments(
755 755 general_comments, versions, c.at_version_num)
756 756 c.comments = c.comment_versions[c.at_version_num]['until']
757 757
758 758 # INLINE COMMENTS with versions #
759 759 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
760 760 q = q.order_by(ChangesetComment.comment_id.asc())
761 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
761 inline_comments = q
762
762 763 c.inline_versions = comments_model.aggregate_comments(
763 764 inline_comments, versions, c.at_version_num, inline=True)
764 765
765 766 # inject latest version
766 767 latest_ver = PullRequest.get_pr_display_object(
767 768 pull_request_latest, pull_request_latest)
768 769
769 770 c.versions = versions + [latest_ver]
770 771
771 772 # if we use version, then do not show later comments
772 773 # than current version
773 774 display_inline_comments = collections.defaultdict(
774 775 lambda: collections.defaultdict(list))
775 776 for co in inline_comments:
776 777 if c.at_version_num:
777 778 # pick comments that are at least UPTO given version, so we
778 779 # don't render comments for higher version
779 780 should_render = co.pull_request_version_id and \
780 781 co.pull_request_version_id <= c.at_version_num
781 782 else:
782 783 # showing all, for 'latest'
783 784 should_render = True
784 785
785 786 if should_render:
786 787 display_inline_comments[co.f_path][co.line_no].append(co)
787 788
788 789 # load diff data into template context, if we use compare mode then
789 790 # diff is calculated based on changes between versions of PR
790 791
791 792 source_repo = pull_request_at_ver.source_repo
792 793 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
793 794
794 795 target_repo = pull_request_at_ver.target_repo
795 796 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
796 797
797 798 if compare:
798 799 # in compare switch the diff base to latest commit from prev version
799 800 target_ref_id = prev_pull_request_display_obj.revisions[0]
800 801
801 802 # despite opening commits for bookmarks/branches/tags, we always
802 803 # convert this to rev to prevent changes after bookmark or branch change
803 804 c.source_ref_type = 'rev'
804 805 c.source_ref = source_ref_id
805 806
806 807 c.target_ref_type = 'rev'
807 808 c.target_ref = target_ref_id
808 809
809 810 c.source_repo = source_repo
810 811 c.target_repo = target_repo
811 812
812 813 # diff_limit is the old behavior, will cut off the whole diff
813 814 # if the limit is applied otherwise will just hide the
814 815 # big files from the front-end
815 816 diff_limit = self.cut_off_limit_diff
816 817 file_limit = self.cut_off_limit_file
817 818
818 819 c.commit_ranges = []
819 820 source_commit = EmptyCommit()
820 821 target_commit = EmptyCommit()
821 822 c.missing_requirements = False
822 823
823 824 source_scm = source_repo.scm_instance()
824 825 target_scm = target_repo.scm_instance()
825 826
826 827 # try first shadow repo, fallback to regular repo
827 828 try:
828 829 commits_source_repo = pull_request_latest.get_shadow_repo()
829 830 except Exception:
830 831 log.debug('Failed to get shadow repo', exc_info=True)
831 832 commits_source_repo = source_scm
832 833
833 834 c.commits_source_repo = commits_source_repo
834 835 commit_cache = {}
835 836 try:
836 837 pre_load = ["author", "branch", "date", "message"]
837 838 show_revs = pull_request_at_ver.revisions
838 839 for rev in show_revs:
839 840 comm = commits_source_repo.get_commit(
840 841 commit_id=rev, pre_load=pre_load)
841 842 c.commit_ranges.append(comm)
842 843 commit_cache[comm.raw_id] = comm
843 844
844 845 target_commit = commits_source_repo.get_commit(
845 846 commit_id=safe_str(target_ref_id))
846 847 source_commit = commits_source_repo.get_commit(
847 848 commit_id=safe_str(source_ref_id))
848 849 except CommitDoesNotExistError:
849 850 pass
850 851 except RepositoryRequirementError:
851 852 log.warning(
852 853 'Failed to get all required data from repo', exc_info=True)
853 854 c.missing_requirements = True
854 855
855 856 c.ancestor = None # set it to None, to hide it from PR view
856 857
857 858 try:
858 859 ancestor_id = source_scm.get_common_ancestor(
859 860 source_commit.raw_id, target_commit.raw_id, target_scm)
860 861 c.ancestor_commit = source_scm.get_commit(ancestor_id)
861 862 except Exception:
862 863 c.ancestor_commit = None
863 864
864 865 c.statuses = source_repo.statuses(
865 866 [x.raw_id for x in c.commit_ranges])
866 867
867 868 # auto collapse if we have more than limit
868 869 collapse_limit = diffs.DiffProcessor._collapse_commits_over
869 870 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
870 871 c.compare_mode = compare
871 872
872 873 c.missing_commits = False
873 874 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
874 875 or source_commit == target_commit):
875 876
876 877 c.missing_commits = True
877 878 else:
878 879
879 880 c.diffset = self._get_diffset(
880 881 commits_source_repo, source_ref_id, target_ref_id,
881 882 target_commit, source_commit,
882 883 diff_limit, file_limit, display_inline_comments)
883 884
884 885 c.limited_diff = c.diffset.limited_diff
885 886
886 887 # calculate removed files that are bound to comments
887 888 comment_deleted_files = [
888 889 fname for fname in display_inline_comments
889 890 if fname not in c.diffset.file_stats]
890 891
891 892 c.deleted_files_comments = collections.defaultdict(dict)
892 893 for fname, per_line_comments in display_inline_comments.items():
893 894 if fname in comment_deleted_files:
894 895 c.deleted_files_comments[fname]['stats'] = 0
895 896 c.deleted_files_comments[fname]['comments'] = list()
896 897 for lno, comments in per_line_comments.items():
897 898 c.deleted_files_comments[fname]['comments'].extend(
898 899 comments)
899 900
900 901 # this is a hack to properly display links, when creating PR, the
901 902 # compare view and others uses different notation, and
902 903 # compare_commits.mako renders links based on the target_repo.
903 904 # We need to swap that here to generate it properly on the html side
904 905 c.target_repo = c.source_repo
905 906
906 907 c.commit_statuses = ChangesetStatus.STATUSES
907 908
908 909 c.show_version_changes = not pr_closed
909 910 if c.show_version_changes:
910 911 cur_obj = pull_request_at_ver
911 912 prev_obj = prev_pull_request_at_ver
912 913
913 914 old_commit_ids = prev_obj.revisions
914 915 new_commit_ids = cur_obj.revisions
915 916 commit_changes = PullRequestModel()._calculate_commit_id_changes(
916 917 old_commit_ids, new_commit_ids)
917 918 c.commit_changes_summary = commit_changes
918 919
919 920 # calculate the diff for commits between versions
920 921 c.commit_changes = []
921 922 mark = lambda cs, fw: list(
922 923 h.itertools.izip_longest([], cs, fillvalue=fw))
923 924 for c_type, raw_id in mark(commit_changes.added, 'a') \
924 925 + mark(commit_changes.removed, 'r') \
925 926 + mark(commit_changes.common, 'c'):
926 927
927 928 if raw_id in commit_cache:
928 929 commit = commit_cache[raw_id]
929 930 else:
930 931 try:
931 932 commit = commits_source_repo.get_commit(raw_id)
932 933 except CommitDoesNotExistError:
933 934 # in case we fail extracting still use "dummy" commit
934 935 # for display in commit diff
935 936 commit = h.AttributeDict(
936 937 {'raw_id': raw_id,
937 938 'message': 'EMPTY or MISSING COMMIT'})
938 939 c.commit_changes.append([c_type, commit])
939 940
940 941 # current user review statuses for each version
941 942 c.review_versions = {}
942 943 if c.rhodecode_user.user_id in allowed_reviewers:
943 944 for co in general_comments:
944 945 if co.author.user_id == c.rhodecode_user.user_id:
945 946 # each comment has a status change
946 947 status = co.status_change
947 948 if status:
948 949 _ver_pr = status[0].comment.pull_request_version_id
949 950 c.review_versions[_ver_pr] = status[0]
950 951
951 952 return render('/pullrequests/pullrequest_show.mako')
952 953
953 954 @LoginRequired()
954 955 @NotAnonymous()
955 956 @HasRepoPermissionAnyDecorator(
956 957 'repository.read', 'repository.write', 'repository.admin')
957 958 @auth.CSRFRequired()
958 959 @jsonify
959 960 def comment(self, repo_name, pull_request_id):
960 961 pull_request_id = safe_int(pull_request_id)
961 962 pull_request = PullRequest.get_or_404(pull_request_id)
962 963 if pull_request.is_closed():
963 964 raise HTTPForbidden()
964 965
965 966 status = request.POST.get('changeset_status', None)
966 967 text = request.POST.get('text')
967 968 comment_type = request.POST.get('comment_type')
968 969 resolves_comment_id = request.POST.get('resolves_comment_id', None)
969 970 close_pull_request = request.POST.get('close_pull_request')
970 971
971 972 close_pr = False
972 973 # only owner or admin or person with write permissions
973 974 allowed_to_close = PullRequestModel().check_user_update(
974 975 pull_request, c.rhodecode_user)
975 976
976 977 if close_pull_request and allowed_to_close:
977 978 close_pr = True
978 979 pull_request_review_status = pull_request.calculated_review_status()
979 980 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
980 981 # approved only if we have voting consent
981 982 status = ChangesetStatus.STATUS_APPROVED
982 983 else:
983 984 status = ChangesetStatus.STATUS_REJECTED
984 985
985 986 allowed_to_change_status = PullRequestModel().check_user_change_status(
986 987 pull_request, c.rhodecode_user)
987 988
988 989 if status and allowed_to_change_status:
989 990 message = (_('Status change %(transition_icon)s %(status)s')
990 991 % {'transition_icon': '>',
991 992 'status': ChangesetStatus.get_status_lbl(status)})
992 993 if close_pr:
993 994 message = _('Closing with') + ' ' + message
994 995 text = text or message
995 996 comm = CommentsModel().create(
996 997 text=text,
997 998 repo=c.rhodecode_db_repo.repo_id,
998 999 user=c.rhodecode_user.user_id,
999 1000 pull_request=pull_request_id,
1000 1001 f_path=request.POST.get('f_path'),
1001 1002 line_no=request.POST.get('line'),
1002 1003 status_change=(ChangesetStatus.get_status_lbl(status)
1003 1004 if status and allowed_to_change_status else None),
1004 1005 status_change_type=(status
1005 1006 if status and allowed_to_change_status else None),
1006 1007 closing_pr=close_pr,
1007 1008 comment_type=comment_type,
1008 1009 resolves_comment_id=resolves_comment_id
1009 1010 )
1010 1011
1011 1012 if allowed_to_change_status:
1012 1013 old_calculated_status = pull_request.calculated_review_status()
1013 1014 # get status if set !
1014 1015 if status:
1015 1016 ChangesetStatusModel().set_status(
1016 1017 c.rhodecode_db_repo.repo_id,
1017 1018 status,
1018 1019 c.rhodecode_user.user_id,
1019 1020 comm,
1020 1021 pull_request=pull_request_id
1021 1022 )
1022 1023
1023 1024 Session().flush()
1024 1025 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1025 1026 # we now calculate the status of pull request, and based on that
1026 1027 # calculation we set the commits status
1027 1028 calculated_status = pull_request.calculated_review_status()
1028 1029 if old_calculated_status != calculated_status:
1029 1030 PullRequestModel()._trigger_pull_request_hook(
1030 1031 pull_request, c.rhodecode_user, 'review_status_change')
1031 1032
1032 1033 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1033 1034 calculated_status)
1034 1035
1035 1036 if close_pr:
1036 1037 status_completed = (
1037 1038 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1038 1039 ChangesetStatus.STATUS_REJECTED])
1039 1040 if close_pull_request or status_completed:
1040 1041 PullRequestModel().close_pull_request(
1041 1042 pull_request_id, c.rhodecode_user)
1042 1043 else:
1043 1044 h.flash(_('Closing pull request on other statuses than '
1044 1045 'rejected or approved is forbidden. '
1045 1046 'Calculated status from all reviewers '
1046 1047 'is currently: %s') % calculated_status_lbl,
1047 1048 category='warning')
1048 1049
1049 1050 Session().commit()
1050 1051
1051 1052 if not request.is_xhr:
1052 1053 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1053 1054 pull_request_id=pull_request_id))
1054 1055
1055 1056 data = {
1056 1057 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1057 1058 }
1058 1059 if comm:
1059 1060 c.co = comm
1060 1061 c.inline_comment = True if comm.line_no else False
1061 1062 data.update(comm.get_dict())
1062 1063 data.update({'rendered_text':
1063 1064 render('changeset/changeset_comment_block.mako')})
1064 1065
1065 1066 return data
1066 1067
1067 1068 @LoginRequired()
1068 1069 @NotAnonymous()
1069 1070 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1070 1071 'repository.admin')
1071 1072 @auth.CSRFRequired()
1072 1073 @jsonify
1073 1074 def delete_comment(self, repo_name, comment_id):
1074 1075 return self._delete_comment(comment_id)
1075 1076
1076 1077 def _delete_comment(self, comment_id):
1077 1078 comment_id = safe_int(comment_id)
1078 1079 co = ChangesetComment.get_or_404(comment_id)
1079 1080 if co.pull_request.is_closed():
1080 1081 # don't allow deleting comments on closed pull request
1081 1082 raise HTTPForbidden()
1082 1083
1083 1084 is_owner = co.author.user_id == c.rhodecode_user.user_id
1084 1085 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1085 1086 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1086 1087 old_calculated_status = co.pull_request.calculated_review_status()
1087 1088 CommentsModel().delete(comment=co)
1088 1089 Session().commit()
1089 1090 calculated_status = co.pull_request.calculated_review_status()
1090 1091 if old_calculated_status != calculated_status:
1091 1092 PullRequestModel()._trigger_pull_request_hook(
1092 1093 co.pull_request, c.rhodecode_user, 'review_status_change')
1093 1094 return True
1094 1095 else:
1095 1096 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now