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