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