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