##// END OF EJS Templates
pr: Use new update response object from pr model. #3950
Martin Bornhold -
r1076:ab17456b default
parent child Browse files
Show More
@@ -1,896 +1,890 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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
25 25 import peppercorn
26 26 import formencode
27 27 import logging
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 30 from pylons import request, tmpl_context as c, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from pyramid.threadlocal import get_current_registry
34 34 from sqlalchemy.sql import func
35 35 from sqlalchemy.sql.expression import or_
36 36
37 37 from rhodecode import events
38 38 from rhodecode.lib import auth, diffs, helpers as h
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.base import (
41 41 BaseRepoController, render, vcs_operation_context)
42 42 from rhodecode.lib.auth import (
43 43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 44 HasAcceptedRepoType, XHRRequired)
45 45 from rhodecode.lib.channelstream import channelstream_request
46 46 from rhodecode.lib.utils import jsonify
47 47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 49 from rhodecode.lib.vcs.exceptions import (
50 50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 51 from rhodecode.lib.diffs import LimitedDiffContainer
52 52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 53 from rhodecode.model.comment import ChangesetCommentsModel
54 54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 55 Repository
56 56 from rhodecode.model.forms import PullRequestForm
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.pull_request import PullRequestModel
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class PullrequestsController(BaseRepoController):
64 64 def __before__(self):
65 65 super(PullrequestsController, self).__before__()
66 66
67 67 def _load_compare_data(self, pull_request, enable_comments=True):
68 68 """
69 69 Load context data needed for generating compare diff
70 70
71 71 :param pull_request: object related to the request
72 72 :param enable_comments: flag to determine if comments are included
73 73 """
74 74 source_repo = pull_request.source_repo
75 75 source_ref_id = pull_request.source_ref_parts.commit_id
76 76
77 77 target_repo = pull_request.target_repo
78 78 target_ref_id = pull_request.target_ref_parts.commit_id
79 79
80 80 # despite opening commits for bookmarks/branches/tags, we always
81 81 # convert this to rev to prevent changes after bookmark or branch change
82 82 c.source_ref_type = 'rev'
83 83 c.source_ref = source_ref_id
84 84
85 85 c.target_ref_type = 'rev'
86 86 c.target_ref = target_ref_id
87 87
88 88 c.source_repo = source_repo
89 89 c.target_repo = target_repo
90 90
91 91 c.fulldiff = bool(request.GET.get('fulldiff'))
92 92
93 93 # diff_limit is the old behavior, will cut off the whole diff
94 94 # if the limit is applied otherwise will just hide the
95 95 # big files from the front-end
96 96 diff_limit = self.cut_off_limit_diff
97 97 file_limit = self.cut_off_limit_file
98 98
99 99 pre_load = ["author", "branch", "date", "message"]
100 100
101 101 c.commit_ranges = []
102 102 source_commit = EmptyCommit()
103 103 target_commit = EmptyCommit()
104 104 c.missing_requirements = False
105 105 try:
106 106 c.commit_ranges = [
107 107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 108 for rev in pull_request.revisions]
109 109
110 110 c.statuses = source_repo.statuses(
111 111 [x.raw_id for x in c.commit_ranges])
112 112
113 113 target_commit = source_repo.get_commit(
114 114 commit_id=safe_str(target_ref_id))
115 115 source_commit = source_repo.get_commit(
116 116 commit_id=safe_str(source_ref_id))
117 117 except RepositoryRequirementError:
118 118 c.missing_requirements = True
119 119
120 120 c.missing_commits = False
121 121 if (c.missing_requirements or
122 122 isinstance(source_commit, EmptyCommit) or
123 123 source_commit == target_commit):
124 124 _parsed = []
125 125 c.missing_commits = True
126 126 else:
127 127 vcs_diff = PullRequestModel().get_diff(pull_request)
128 128 diff_processor = diffs.DiffProcessor(
129 129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
130 130 file_limit=file_limit, show_full_diff=c.fulldiff)
131 131 _parsed = diff_processor.prepare()
132 132
133 133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
134 134
135 135 c.files = []
136 136 c.changes = {}
137 137 c.lines_added = 0
138 138 c.lines_deleted = 0
139 139 c.included_files = []
140 140 c.deleted_files = []
141 141
142 142 for f in _parsed:
143 143 st = f['stats']
144 144 c.lines_added += st['added']
145 145 c.lines_deleted += st['deleted']
146 146
147 147 fid = h.FID('', f['filename'])
148 148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 149 c.included_files.append(f['filename'])
150 150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 151 parsed_lines=[f])
152 152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153 153
154 154 def _extract_ordering(self, request):
155 155 column_index = safe_int(request.GET.get('order[0][column]'))
156 156 order_dir = request.GET.get('order[0][dir]', 'desc')
157 157 order_by = request.GET.get(
158 158 'columns[%s][data][sort]' % column_index, 'name_raw')
159 159 return order_by, order_dir
160 160
161 161 @LoginRequired()
162 162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 163 'repository.admin')
164 164 @HasAcceptedRepoType('git', 'hg')
165 165 def show_all(self, repo_name):
166 166 # filter types
167 167 c.active = 'open'
168 168 c.source = str2bool(request.GET.get('source'))
169 169 c.closed = str2bool(request.GET.get('closed'))
170 170 c.my = str2bool(request.GET.get('my'))
171 171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 173 c.repo_name = repo_name
174 174
175 175 opened_by = None
176 176 if c.my:
177 177 c.active = 'my'
178 178 opened_by = [c.rhodecode_user.user_id]
179 179
180 180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 181 if c.closed:
182 182 c.active = 'closed'
183 183 statuses = [PullRequest.STATUS_CLOSED]
184 184
185 185 if c.awaiting_review and not c.source:
186 186 c.active = 'awaiting'
187 187 if c.source and not c.awaiting_review:
188 188 c.active = 'source'
189 189 if c.awaiting_my_review:
190 190 c.active = 'awaiting_my'
191 191
192 192 data = self._get_pull_requests_list(
193 193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 194 if not request.is_xhr:
195 195 c.data = json.dumps(data['data'])
196 196 c.records_total = data['recordsTotal']
197 197 return render('/pullrequests/pullrequests.html')
198 198 else:
199 199 return json.dumps(data)
200 200
201 201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 202 # pagination
203 203 start = safe_int(request.GET.get('start'), 0)
204 204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 205 order_by, order_dir = self._extract_ordering(request)
206 206
207 207 if c.awaiting_review:
208 208 pull_requests = PullRequestModel().get_awaiting_review(
209 209 repo_name, source=c.source, opened_by=opened_by,
210 210 statuses=statuses, offset=start, length=length,
211 211 order_by=order_by, order_dir=order_dir)
212 212 pull_requests_total_count = PullRequestModel(
213 213 ).count_awaiting_review(
214 214 repo_name, source=c.source, statuses=statuses,
215 215 opened_by=opened_by)
216 216 elif c.awaiting_my_review:
217 217 pull_requests = PullRequestModel().get_awaiting_my_review(
218 218 repo_name, source=c.source, opened_by=opened_by,
219 219 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 220 offset=start, length=length, order_by=order_by,
221 221 order_dir=order_dir)
222 222 pull_requests_total_count = PullRequestModel(
223 223 ).count_awaiting_my_review(
224 224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 225 statuses=statuses, opened_by=opened_by)
226 226 else:
227 227 pull_requests = PullRequestModel().get_all(
228 228 repo_name, source=c.source, opened_by=opened_by,
229 229 statuses=statuses, offset=start, length=length,
230 230 order_by=order_by, order_dir=order_dir)
231 231 pull_requests_total_count = PullRequestModel().count_all(
232 232 repo_name, source=c.source, statuses=statuses,
233 233 opened_by=opened_by)
234 234
235 235 from rhodecode.lib.utils import PartialRenderer
236 236 _render = PartialRenderer('data_table/_dt_elements.html')
237 237 data = []
238 238 for pr in pull_requests:
239 239 comments = ChangesetCommentsModel().get_all_comments(
240 240 c.rhodecode_db_repo.repo_id, pull_request=pr)
241 241
242 242 data.append({
243 243 'name': _render('pullrequest_name',
244 244 pr.pull_request_id, pr.target_repo.repo_name),
245 245 'name_raw': pr.pull_request_id,
246 246 'status': _render('pullrequest_status',
247 247 pr.calculated_review_status()),
248 248 'title': _render(
249 249 'pullrequest_title', pr.title, pr.description),
250 250 'description': h.escape(pr.description),
251 251 'updated_on': _render('pullrequest_updated_on',
252 252 h.datetime_to_time(pr.updated_on)),
253 253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 254 'created_on': _render('pullrequest_updated_on',
255 255 h.datetime_to_time(pr.created_on)),
256 256 'created_on_raw': h.datetime_to_time(pr.created_on),
257 257 'author': _render('pullrequest_author',
258 258 pr.author.full_contact, ),
259 259 'author_raw': pr.author.full_name,
260 260 'comments': _render('pullrequest_comments', len(comments)),
261 261 'comments_raw': len(comments),
262 262 'closed': pr.is_closed(),
263 263 })
264 264 # json used to render the grid
265 265 data = ({
266 266 'data': data,
267 267 'recordsTotal': pull_requests_total_count,
268 268 'recordsFiltered': pull_requests_total_count,
269 269 })
270 270 return data
271 271
272 272 @LoginRequired()
273 273 @NotAnonymous()
274 274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 275 'repository.admin')
276 276 @HasAcceptedRepoType('git', 'hg')
277 277 def index(self):
278 278 source_repo = c.rhodecode_db_repo
279 279
280 280 try:
281 281 source_repo.scm_instance().get_commit()
282 282 except EmptyRepositoryError:
283 283 h.flash(h.literal(_('There are no commits yet')),
284 284 category='warning')
285 285 redirect(url('summary_home', repo_name=source_repo.repo_name))
286 286
287 287 commit_id = request.GET.get('commit')
288 288 branch_ref = request.GET.get('branch')
289 289 bookmark_ref = request.GET.get('bookmark')
290 290
291 291 try:
292 292 source_repo_data = PullRequestModel().generate_repo_data(
293 293 source_repo, commit_id=commit_id,
294 294 branch=branch_ref, bookmark=bookmark_ref)
295 295 except CommitDoesNotExistError as e:
296 296 log.exception(e)
297 297 h.flash(_('Commit does not exist'), 'error')
298 298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299 299
300 300 default_target_repo = source_repo
301 301
302 302 if source_repo.parent:
303 303 parent_vcs_obj = source_repo.parent.scm_instance()
304 304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 305 # change default if we have a parent repo
306 306 default_target_repo = source_repo.parent
307 307
308 308 target_repo_data = PullRequestModel().generate_repo_data(
309 309 default_target_repo)
310 310
311 311 selected_source_ref = source_repo_data['refs']['selected_ref']
312 312
313 313 title_source_ref = selected_source_ref.split(':', 2)[1]
314 314 c.default_title = PullRequestModel().generate_pullrequest_title(
315 315 source=source_repo.repo_name,
316 316 source_ref=title_source_ref,
317 317 target=default_target_repo.repo_name
318 318 )
319 319
320 320 c.default_repo_data = {
321 321 'source_repo_name': source_repo.repo_name,
322 322 'source_refs_json': json.dumps(source_repo_data),
323 323 'target_repo_name': default_target_repo.repo_name,
324 324 'target_refs_json': json.dumps(target_repo_data),
325 325 }
326 326 c.default_source_ref = selected_source_ref
327 327
328 328 return render('/pullrequests/pullrequest.html')
329 329
330 330 @LoginRequired()
331 331 @NotAnonymous()
332 332 @XHRRequired()
333 333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 334 'repository.admin')
335 335 @jsonify
336 336 def get_repo_refs(self, repo_name, target_repo_name):
337 337 repo = Repository.get_by_repo_name(target_repo_name)
338 338 if not repo:
339 339 raise HTTPNotFound
340 340 return PullRequestModel().generate_repo_data(repo)
341 341
342 342 @LoginRequired()
343 343 @NotAnonymous()
344 344 @XHRRequired()
345 345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 346 'repository.admin')
347 347 @jsonify
348 348 def get_repo_destinations(self, repo_name):
349 349 repo = Repository.get_by_repo_name(repo_name)
350 350 if not repo:
351 351 raise HTTPNotFound
352 352 filter_query = request.GET.get('query')
353 353
354 354 query = Repository.query() \
355 355 .order_by(func.length(Repository.repo_name)) \
356 356 .filter(or_(
357 357 Repository.repo_name == repo.repo_name,
358 358 Repository.fork_id == repo.repo_id))
359 359
360 360 if filter_query:
361 361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 362 query = query.filter(
363 363 Repository.repo_name.ilike(ilike_expression))
364 364
365 365 add_parent = False
366 366 if repo.parent:
367 367 if filter_query in repo.parent.repo_name:
368 368 parent_vcs_obj = repo.parent.scm_instance()
369 369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 370 add_parent = True
371 371
372 372 limit = 20 - 1 if add_parent else 20
373 373 all_repos = query.limit(limit).all()
374 374 if add_parent:
375 375 all_repos += [repo.parent]
376 376
377 377 repos = []
378 378 for obj in self.scm_model.get_repos(all_repos):
379 379 repos.append({
380 380 'id': obj['name'],
381 381 'text': obj['name'],
382 382 'type': 'repo',
383 383 'obj': obj['dbrepo']
384 384 })
385 385
386 386 data = {
387 387 'more': False,
388 388 'results': [{
389 389 'text': _('Repositories'),
390 390 'children': repos
391 391 }] if repos else []
392 392 }
393 393 return data
394 394
395 395 @LoginRequired()
396 396 @NotAnonymous()
397 397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 398 'repository.admin')
399 399 @HasAcceptedRepoType('git', 'hg')
400 400 @auth.CSRFRequired()
401 401 def create(self, repo_name):
402 402 repo = Repository.get_by_repo_name(repo_name)
403 403 if not repo:
404 404 raise HTTPNotFound
405 405
406 406 controls = peppercorn.parse(request.POST.items())
407 407
408 408 try:
409 409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 410 except formencode.Invalid as errors:
411 411 if errors.error_dict.get('revisions'):
412 412 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 413 elif errors.error_dict.get('pullrequest_title'):
414 414 msg = _('Pull request requires a title with min. 3 chars')
415 415 else:
416 416 msg = _('Error creating pull request: {}').format(errors)
417 417 log.exception(msg)
418 418 h.flash(msg, 'error')
419 419
420 420 # would rather just go back to form ...
421 421 return redirect(url('pullrequest_home', repo_name=repo_name))
422 422
423 423 source_repo = _form['source_repo']
424 424 source_ref = _form['source_ref']
425 425 target_repo = _form['target_repo']
426 426 target_ref = _form['target_ref']
427 427 commit_ids = _form['revisions'][::-1]
428 428 reviewers = [
429 429 (r['user_id'], r['reasons']) for r in _form['review_members']]
430 430
431 431 # find the ancestor for this pr
432 432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434 434
435 435 source_scm = source_db_repo.scm_instance()
436 436 target_scm = target_db_repo.scm_instance()
437 437
438 438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440 440
441 441 ancestor = source_scm.get_common_ancestor(
442 442 source_commit.raw_id, target_commit.raw_id, target_scm)
443 443
444 444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446 446
447 447 pullrequest_title = _form['pullrequest_title']
448 448 title_source_ref = source_ref.split(':', 2)[1]
449 449 if not pullrequest_title:
450 450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 451 source=source_repo,
452 452 source_ref=title_source_ref,
453 453 target=target_repo
454 454 )
455 455
456 456 description = _form['pullrequest_desc']
457 457 try:
458 458 pull_request = PullRequestModel().create(
459 459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 460 target_ref, commit_ids, reviewers, pullrequest_title,
461 461 description
462 462 )
463 463 Session().commit()
464 464 h.flash(_('Successfully opened new pull request'),
465 465 category='success')
466 466 except Exception as e:
467 467 msg = _('Error occurred during sending pull request')
468 468 log.exception(msg)
469 469 h.flash(msg, category='error')
470 470 return redirect(url('pullrequest_home', repo_name=repo_name))
471 471
472 472 return redirect(url('pullrequest_show', repo_name=target_repo,
473 473 pull_request_id=pull_request.pull_request_id))
474 474
475 475 @LoginRequired()
476 476 @NotAnonymous()
477 477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 478 'repository.admin')
479 479 @auth.CSRFRequired()
480 480 @jsonify
481 481 def update(self, repo_name, pull_request_id):
482 482 pull_request_id = safe_int(pull_request_id)
483 483 pull_request = PullRequest.get_or_404(pull_request_id)
484 484 # only owner or admin can update it
485 485 allowed_to_update = PullRequestModel().check_user_update(
486 486 pull_request, c.rhodecode_user)
487 487 if allowed_to_update:
488 488 controls = peppercorn.parse(request.POST.items())
489 489
490 490 if 'review_members' in controls:
491 491 self._update_reviewers(
492 492 pull_request_id, controls['review_members'])
493 493 elif str2bool(request.POST.get('update_commits', 'false')):
494 494 self._update_commits(pull_request)
495 495 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 496 self._reject_close(pull_request)
497 497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 498 self._edit_pull_request(pull_request)
499 499 else:
500 500 raise HTTPBadRequest()
501 501 return True
502 502 raise HTTPForbidden()
503 503
504 504 def _edit_pull_request(self, pull_request):
505 505 try:
506 506 PullRequestModel().edit(
507 507 pull_request, request.POST.get('title'),
508 508 request.POST.get('description'))
509 509 except ValueError:
510 510 msg = _(u'Cannot update closed pull requests.')
511 511 h.flash(msg, category='error')
512 512 return
513 513 else:
514 514 Session().commit()
515 515
516 516 msg = _(u'Pull request title & description updated.')
517 517 h.flash(msg, category='success')
518 518 return
519 519
520 520 def _update_commits(self, pull_request):
521 try:
522 if PullRequestModel().has_valid_update_type(pull_request):
523 updated_version, changes = PullRequestModel().update_commits(
524 pull_request)
525 if updated_version:
526 msg = _(
527 u'Pull request updated to "{source_commit_id}" with '
528 u'{count_added} added, {count_removed} removed '
529 u'commits.'
530 ).format(
531 source_commit_id=pull_request.source_ref_parts.commit_id,
532 count_added=len(changes.added),
533 count_removed=len(changes.removed))
534 h.flash(msg, category='success')
535 registry = get_current_registry()
536 rhodecode_plugins = getattr(registry,
537 'rhodecode_plugins', {})
538 channelstream_config = rhodecode_plugins.get(
539 'channelstream', {})
540 if channelstream_config.get('enabled'):
541 message = msg + ' - <a onclick="' \
542 'window.location.reload()">' \
543 '<strong>{}</strong></a>'.format(
544 _('Reload page')
545 )
546 channel = '/repo${}$/pr/{}'.format(
547 pull_request.target_repo.repo_name,
548 pull_request.pull_request_id
549 )
550 payload = {
551 'type': 'message',
552 'user': 'system',
553 'exclude_users': [request.user.username],
554 'channel': channel,
555 'message': {
556 'message': message,
557 'level': 'success',
558 'topic': '/notifications'
559 }
560 }
561 channelstream_request(channelstream_config, [payload],
562 '/message', raise_exc=False)
563 else:
564 h.flash(_("Nothing changed in pull request."),
565 category='warning')
566 else:
567 msg = _(
568 u"Skipping update of pull request due to reference "
569 u"type: {reference_type}"
570 ).format(reference_type=pull_request.source_ref_parts.type)
571 h.flash(msg, category='warning')
572 except CommitDoesNotExistError:
573 h.flash(
574 _(u'Update failed due to missing commits.'), category='error')
521 resp = PullRequestModel().update_commits(pull_request)
522 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
523
524 # Abort if pull request update failed.
525 if not resp.success:
526 h.flash(msg, category='error')
527 return
528
529 if resp.reason == UpdateFailureReason.NONE:
530 msg = _(
531 u'Pull request updated to "{source_commit_id}" with '
532 u'{count_added} added, {count_removed} removed commits.')
533 msg = msg.format(
534 source_commit_id=pull_request.source_ref_parts.commit_id,
535 count_added=len(resp.changes.added),
536 count_removed=len(resp.changes.removed))
537 h.flash(msg, category='success')
538
539 registry = get_current_registry()
540 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
541 channelstream_config = rhodecode_plugins.get('channelstream', {})
542 if channelstream_config.get('enabled'):
543 message = msg + (
544 ' - <a onclick="window.location.reload()">'
545 '<strong>{}</strong></a>'.format(_('Reload page')))
546 channel = '/repo${}$/pr/{}'.format(
547 pull_request.target_repo.repo_name,
548 pull_request.pull_request_id
549 )
550 payload = {
551 'type': 'message',
552 'user': 'system',
553 'exclude_users': [request.user.username],
554 'channel': channel,
555 'message': {
556 'message': message,
557 'level': 'success',
558 'topic': '/notifications'
559 }
560 }
561 channelstream_request(
562 channelstream_config, [payload], '/message',
563 raise_exc=False)
564 elif resp.reason == UpdateFailureReason.NO_CHANGE:
565 # Display a warning if no update is needed.
566 h.flash(msg, category='warning')
567 else:
568 h.flash(msg, category='error')
575 569
576 570 @auth.CSRFRequired()
577 571 @LoginRequired()
578 572 @NotAnonymous()
579 573 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
580 574 'repository.admin')
581 575 def merge(self, repo_name, pull_request_id):
582 576 """
583 577 POST /{repo_name}/pull-request/{pull_request_id}
584 578
585 579 Merge will perform a server-side merge of the specified
586 580 pull request, if the pull request is approved and mergeable.
587 581 After succesfull merging, the pull request is automatically
588 582 closed, with a relevant comment.
589 583 """
590 584 pull_request_id = safe_int(pull_request_id)
591 585 pull_request = PullRequest.get_or_404(pull_request_id)
592 586 user = c.rhodecode_user
593 587
594 588 if self._meets_merge_pre_conditions(pull_request, user):
595 589 log.debug("Pre-conditions checked, trying to merge.")
596 590 extras = vcs_operation_context(
597 591 request.environ, repo_name=pull_request.target_repo.repo_name,
598 592 username=user.username, action='push',
599 593 scm=pull_request.target_repo.repo_type)
600 594 self._merge_pull_request(pull_request, user, extras)
601 595
602 596 return redirect(url(
603 597 'pullrequest_show',
604 598 repo_name=pull_request.target_repo.repo_name,
605 599 pull_request_id=pull_request.pull_request_id))
606 600
607 601 def _meets_merge_pre_conditions(self, pull_request, user):
608 602 if not PullRequestModel().check_user_merge(pull_request, user):
609 603 raise HTTPForbidden()
610 604
611 605 merge_status, msg = PullRequestModel().merge_status(pull_request)
612 606 if not merge_status:
613 607 log.debug("Cannot merge, not mergeable.")
614 608 h.flash(msg, category='error')
615 609 return False
616 610
617 611 if (pull_request.calculated_review_status()
618 612 is not ChangesetStatus.STATUS_APPROVED):
619 613 log.debug("Cannot merge, approval is pending.")
620 614 msg = _('Pull request reviewer approval is pending.')
621 615 h.flash(msg, category='error')
622 616 return False
623 617 return True
624 618
625 619 def _merge_pull_request(self, pull_request, user, extras):
626 620 merge_resp = PullRequestModel().merge(
627 621 pull_request, user, extras=extras)
628 622
629 623 if merge_resp.executed:
630 624 log.debug("The merge was successful, closing the pull request.")
631 625 PullRequestModel().close_pull_request(
632 626 pull_request.pull_request_id, user)
633 627 Session().commit()
634 628 msg = _('Pull request was successfully merged and closed.')
635 629 h.flash(msg, category='success')
636 630 else:
637 631 log.debug(
638 632 "The merge was not successful. Merge response: %s",
639 633 merge_resp)
640 634 msg = PullRequestModel().merge_status_message(
641 635 merge_resp.failure_reason)
642 636 h.flash(msg, category='error')
643 637
644 638 def _update_reviewers(self, pull_request_id, review_members):
645 639 reviewers = [
646 640 (int(r['user_id']), r['reasons']) for r in review_members]
647 641 PullRequestModel().update_reviewers(pull_request_id, reviewers)
648 642 Session().commit()
649 643
650 644 def _reject_close(self, pull_request):
651 645 if pull_request.is_closed():
652 646 raise HTTPForbidden()
653 647
654 648 PullRequestModel().close_pull_request_with_comment(
655 649 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
656 650 Session().commit()
657 651
658 652 @LoginRequired()
659 653 @NotAnonymous()
660 654 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
661 655 'repository.admin')
662 656 @auth.CSRFRequired()
663 657 @jsonify
664 658 def delete(self, repo_name, pull_request_id):
665 659 pull_request_id = safe_int(pull_request_id)
666 660 pull_request = PullRequest.get_or_404(pull_request_id)
667 661 # only owner can delete it !
668 662 if pull_request.author.user_id == c.rhodecode_user.user_id:
669 663 PullRequestModel().delete(pull_request)
670 664 Session().commit()
671 665 h.flash(_('Successfully deleted pull request'),
672 666 category='success')
673 667 return redirect(url('my_account_pullrequests'))
674 668 raise HTTPForbidden()
675 669
676 670 @LoginRequired()
677 671 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
678 672 'repository.admin')
679 673 def show(self, repo_name, pull_request_id):
680 674 pull_request_id = safe_int(pull_request_id)
681 675 c.pull_request = PullRequest.get_or_404(pull_request_id)
682 676
683 677 c.template_context['pull_request_data']['pull_request_id'] = \
684 678 pull_request_id
685 679
686 680 # pull_requests repo_name we opened it against
687 681 # ie. target_repo must match
688 682 if repo_name != c.pull_request.target_repo.repo_name:
689 683 raise HTTPNotFound
690 684
691 685 c.allowed_to_change_status = PullRequestModel(). \
692 686 check_user_change_status(c.pull_request, c.rhodecode_user)
693 687 c.allowed_to_update = PullRequestModel().check_user_update(
694 688 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
695 689 c.allowed_to_merge = PullRequestModel().check_user_merge(
696 690 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
697 691 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
698 692 c.pull_request)
699 693
700 694 cc_model = ChangesetCommentsModel()
701 695
702 696 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
703 697
704 698 c.pull_request_review_status = c.pull_request.calculated_review_status()
705 699 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
706 700 c.pull_request)
707 701 c.approval_msg = None
708 702 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
709 703 c.approval_msg = _('Reviewer approval is pending.')
710 704 c.pr_merge_status = False
711 705 # load compare data into template context
712 706 enable_comments = not c.pull_request.is_closed()
713 707 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
714 708
715 709 # this is a hack to properly display links, when creating PR, the
716 710 # compare view and others uses different notation, and
717 711 # compare_commits.html renders links based on the target_repo.
718 712 # We need to swap that here to generate it properly on the html side
719 713 c.target_repo = c.source_repo
720 714
721 715 # inline comments
722 716 c.inline_cnt = 0
723 717 c.inline_comments = cc_model.get_inline_comments(
724 718 c.rhodecode_db_repo.repo_id,
725 719 pull_request=pull_request_id).items()
726 720 # count inline comments
727 721 for __, lines in c.inline_comments:
728 722 for comments in lines.values():
729 723 c.inline_cnt += len(comments)
730 724
731 725 # outdated comments
732 726 c.outdated_cnt = 0
733 727 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
734 728 c.outdated_comments = cc_model.get_outdated_comments(
735 729 c.rhodecode_db_repo.repo_id,
736 730 pull_request=c.pull_request)
737 731 # Count outdated comments and check for deleted files
738 732 for file_name, lines in c.outdated_comments.iteritems():
739 733 for comments in lines.values():
740 734 c.outdated_cnt += len(comments)
741 735 if file_name not in c.included_files:
742 736 c.deleted_files.append(file_name)
743 737 else:
744 738 c.outdated_comments = {}
745 739
746 740 # comments
747 741 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
748 742 pull_request=pull_request_id)
749 743
750 744 if c.allowed_to_update:
751 745 force_close = ('forced_closed', _('Close Pull Request'))
752 746 statuses = ChangesetStatus.STATUSES + [force_close]
753 747 else:
754 748 statuses = ChangesetStatus.STATUSES
755 749 c.commit_statuses = statuses
756 750
757 751 c.ancestor = None # TODO: add ancestor here
758 752
759 753 return render('/pullrequests/pullrequest_show.html')
760 754
761 755 @LoginRequired()
762 756 @NotAnonymous()
763 757 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
764 758 'repository.admin')
765 759 @auth.CSRFRequired()
766 760 @jsonify
767 761 def comment(self, repo_name, pull_request_id):
768 762 pull_request_id = safe_int(pull_request_id)
769 763 pull_request = PullRequest.get_or_404(pull_request_id)
770 764 if pull_request.is_closed():
771 765 raise HTTPForbidden()
772 766
773 767 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
774 768 # as a changeset status, still we want to send it in one value.
775 769 status = request.POST.get('changeset_status', None)
776 770 text = request.POST.get('text')
777 771 if status and '_closed' in status:
778 772 close_pr = True
779 773 status = status.replace('_closed', '')
780 774 else:
781 775 close_pr = False
782 776
783 777 forced = (status == 'forced')
784 778 if forced:
785 779 status = 'rejected'
786 780
787 781 allowed_to_change_status = PullRequestModel().check_user_change_status(
788 782 pull_request, c.rhodecode_user)
789 783
790 784 if status and allowed_to_change_status:
791 785 message = (_('Status change %(transition_icon)s %(status)s')
792 786 % {'transition_icon': '>',
793 787 'status': ChangesetStatus.get_status_lbl(status)})
794 788 if close_pr:
795 789 message = _('Closing with') + ' ' + message
796 790 text = text or message
797 791 comm = ChangesetCommentsModel().create(
798 792 text=text,
799 793 repo=c.rhodecode_db_repo.repo_id,
800 794 user=c.rhodecode_user.user_id,
801 795 pull_request=pull_request_id,
802 796 f_path=request.POST.get('f_path'),
803 797 line_no=request.POST.get('line'),
804 798 status_change=(ChangesetStatus.get_status_lbl(status)
805 799 if status and allowed_to_change_status else None),
806 800 status_change_type=(status
807 801 if status and allowed_to_change_status else None),
808 802 closing_pr=close_pr
809 803 )
810 804
811 805
812 806
813 807 if allowed_to_change_status:
814 808 old_calculated_status = pull_request.calculated_review_status()
815 809 # get status if set !
816 810 if status:
817 811 ChangesetStatusModel().set_status(
818 812 c.rhodecode_db_repo.repo_id,
819 813 status,
820 814 c.rhodecode_user.user_id,
821 815 comm,
822 816 pull_request=pull_request_id
823 817 )
824 818
825 819 Session().flush()
826 820 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
827 821 # we now calculate the status of pull request, and based on that
828 822 # calculation we set the commits status
829 823 calculated_status = pull_request.calculated_review_status()
830 824 if old_calculated_status != calculated_status:
831 825 PullRequestModel()._trigger_pull_request_hook(
832 826 pull_request, c.rhodecode_user, 'review_status_change')
833 827
834 828 calculated_status_lbl = ChangesetStatus.get_status_lbl(
835 829 calculated_status)
836 830
837 831 if close_pr:
838 832 status_completed = (
839 833 calculated_status in [ChangesetStatus.STATUS_APPROVED,
840 834 ChangesetStatus.STATUS_REJECTED])
841 835 if forced or status_completed:
842 836 PullRequestModel().close_pull_request(
843 837 pull_request_id, c.rhodecode_user)
844 838 else:
845 839 h.flash(_('Closing pull request on other statuses than '
846 840 'rejected or approved is forbidden. '
847 841 'Calculated status from all reviewers '
848 842 'is currently: %s') % calculated_status_lbl,
849 843 category='warning')
850 844
851 845 Session().commit()
852 846
853 847 if not request.is_xhr:
854 848 return redirect(h.url('pullrequest_show', repo_name=repo_name,
855 849 pull_request_id=pull_request_id))
856 850
857 851 data = {
858 852 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
859 853 }
860 854 if comm:
861 855 c.co = comm
862 856 data.update(comm.get_dict())
863 857 data.update({'rendered_text':
864 858 render('changeset/changeset_comment_block.html')})
865 859
866 860 return data
867 861
868 862 @LoginRequired()
869 863 @NotAnonymous()
870 864 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
871 865 'repository.admin')
872 866 @auth.CSRFRequired()
873 867 @jsonify
874 868 def delete_comment(self, repo_name, comment_id):
875 869 return self._delete_comment(comment_id)
876 870
877 871 def _delete_comment(self, comment_id):
878 872 comment_id = safe_int(comment_id)
879 873 co = ChangesetComment.get_or_404(comment_id)
880 874 if co.pull_request.is_closed():
881 875 # don't allow deleting comments on closed pull request
882 876 raise HTTPForbidden()
883 877
884 878 is_owner = co.author.user_id == c.rhodecode_user.user_id
885 879 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
886 880 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
887 881 old_calculated_status = co.pull_request.calculated_review_status()
888 882 ChangesetCommentsModel().delete(comment=co)
889 883 Session().commit()
890 884 calculated_status = co.pull_request.calculated_review_status()
891 885 if old_calculated_status != calculated_status:
892 886 PullRequestModel()._trigger_pull_request_hook(
893 887 co.pull_request, c.rhodecode_user, 'review_status_change')
894 888 return True
895 889 else:
896 890 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now