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