##// END OF EJS Templates
pull-requests: security, check for permissions on exposure of repo-refs
ergo -
r2251:4ded942f stable
parent child Browse files
Show More
@@ -1,1236 +1,1243 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 pull_request_id = self.request.matchdict['pull_request_id']
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user,
367 367 translator=self.request.translate)
368 368 c.pr_merge_errors = _merge_check.error_details
369 369 c.pr_merge_possible = not _merge_check.failed
370 370 c.pr_merge_message = _merge_check.merge_msg
371 371
372 372 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 373 pull_request_latest, translator=self.request.translate)
374 374
375 375 c.pull_request_review_status = _merge_check.review_status
376 376 if merge_checks:
377 377 self.request.override_renderer = \
378 378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 379 return self._get_template_context(c)
380 380
381 381 comments_model = CommentsModel()
382 382
383 383 # reviewers and statuses
384 384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 386
387 387 # GENERAL COMMENTS with versions #
388 388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 389 q = q.order_by(ChangesetComment.comment_id.asc())
390 390 general_comments = q
391 391
392 392 # pick comments we want to render at current version
393 393 c.comment_versions = comments_model.aggregate_comments(
394 394 general_comments, versions, c.at_version_num)
395 395 c.comments = c.comment_versions[c.at_version_num]['until']
396 396
397 397 # INLINE COMMENTS with versions #
398 398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 399 q = q.order_by(ChangesetComment.comment_id.asc())
400 400 inline_comments = q
401 401
402 402 c.inline_versions = comments_model.aggregate_comments(
403 403 inline_comments, versions, c.at_version_num, inline=True)
404 404
405 405 # inject latest version
406 406 latest_ver = PullRequest.get_pr_display_object(
407 407 pull_request_latest, pull_request_latest)
408 408
409 409 c.versions = versions + [latest_ver]
410 410
411 411 # if we use version, then do not show later comments
412 412 # than current version
413 413 display_inline_comments = collections.defaultdict(
414 414 lambda: collections.defaultdict(list))
415 415 for co in inline_comments:
416 416 if c.at_version_num:
417 417 # pick comments that are at least UPTO given version, so we
418 418 # don't render comments for higher version
419 419 should_render = co.pull_request_version_id and \
420 420 co.pull_request_version_id <= c.at_version_num
421 421 else:
422 422 # showing all, for 'latest'
423 423 should_render = True
424 424
425 425 if should_render:
426 426 display_inline_comments[co.f_path][co.line_no].append(co)
427 427
428 428 # load diff data into template context, if we use compare mode then
429 429 # diff is calculated based on changes between versions of PR
430 430
431 431 source_repo = pull_request_at_ver.source_repo
432 432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 433
434 434 target_repo = pull_request_at_ver.target_repo
435 435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 436
437 437 if compare:
438 438 # in compare switch the diff base to latest commit from prev version
439 439 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 440
441 441 # despite opening commits for bookmarks/branches/tags, we always
442 442 # convert this to rev to prevent changes after bookmark or branch change
443 443 c.source_ref_type = 'rev'
444 444 c.source_ref = source_ref_id
445 445
446 446 c.target_ref_type = 'rev'
447 447 c.target_ref = target_ref_id
448 448
449 449 c.source_repo = source_repo
450 450 c.target_repo = target_repo
451 451
452 452 c.commit_ranges = []
453 453 source_commit = EmptyCommit()
454 454 target_commit = EmptyCommit()
455 455 c.missing_requirements = False
456 456
457 457 source_scm = source_repo.scm_instance()
458 458 target_scm = target_repo.scm_instance()
459 459
460 460 # try first shadow repo, fallback to regular repo
461 461 try:
462 462 commits_source_repo = pull_request_latest.get_shadow_repo()
463 463 except Exception:
464 464 log.debug('Failed to get shadow repo', exc_info=True)
465 465 commits_source_repo = source_scm
466 466
467 467 c.commits_source_repo = commits_source_repo
468 468 commit_cache = {}
469 469 try:
470 470 pre_load = ["author", "branch", "date", "message"]
471 471 show_revs = pull_request_at_ver.revisions
472 472 for rev in show_revs:
473 473 comm = commits_source_repo.get_commit(
474 474 commit_id=rev, pre_load=pre_load)
475 475 c.commit_ranges.append(comm)
476 476 commit_cache[comm.raw_id] = comm
477 477
478 478 # Order here matters, we first need to get target, and then
479 479 # the source
480 480 target_commit = commits_source_repo.get_commit(
481 481 commit_id=safe_str(target_ref_id))
482 482
483 483 source_commit = commits_source_repo.get_commit(
484 484 commit_id=safe_str(source_ref_id))
485 485
486 486 except CommitDoesNotExistError:
487 487 log.warning(
488 488 'Failed to get commit from `{}` repo'.format(
489 489 commits_source_repo), exc_info=True)
490 490 except RepositoryRequirementError:
491 491 log.warning(
492 492 'Failed to get all required data from repo', exc_info=True)
493 493 c.missing_requirements = True
494 494
495 495 c.ancestor = None # set it to None, to hide it from PR view
496 496
497 497 try:
498 498 ancestor_id = source_scm.get_common_ancestor(
499 499 source_commit.raw_id, target_commit.raw_id, target_scm)
500 500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 501 except Exception:
502 502 c.ancestor_commit = None
503 503
504 504 c.statuses = source_repo.statuses(
505 505 [x.raw_id for x in c.commit_ranges])
506 506
507 507 # auto collapse if we have more than limit
508 508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 510 c.compare_mode = compare
511 511
512 512 # diff_limit is the old behavior, will cut off the whole diff
513 513 # if the limit is applied otherwise will just hide the
514 514 # big files from the front-end
515 515 diff_limit = c.visual.cut_off_limit_diff
516 516 file_limit = c.visual.cut_off_limit_file
517 517
518 518 c.missing_commits = False
519 519 if (c.missing_requirements
520 520 or isinstance(source_commit, EmptyCommit)
521 521 or source_commit == target_commit):
522 522
523 523 c.missing_commits = True
524 524 else:
525 525
526 526 c.diffset = self._get_diffset(
527 527 c.source_repo.repo_name, commits_source_repo,
528 528 source_ref_id, target_ref_id,
529 529 target_commit, source_commit,
530 530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 531
532 532 c.limited_diff = c.diffset.limited_diff
533 533
534 534 # calculate removed files that are bound to comments
535 535 comment_deleted_files = [
536 536 fname for fname in display_inline_comments
537 537 if fname not in c.diffset.file_stats]
538 538
539 539 c.deleted_files_comments = collections.defaultdict(dict)
540 540 for fname, per_line_comments in display_inline_comments.items():
541 541 if fname in comment_deleted_files:
542 542 c.deleted_files_comments[fname]['stats'] = 0
543 543 c.deleted_files_comments[fname]['comments'] = list()
544 544 for lno, comments in per_line_comments.items():
545 545 c.deleted_files_comments[fname]['comments'].extend(
546 546 comments)
547 547
548 548 # this is a hack to properly display links, when creating PR, the
549 549 # compare view and others uses different notation, and
550 550 # compare_commits.mako renders links based on the target_repo.
551 551 # We need to swap that here to generate it properly on the html side
552 552 c.target_repo = c.source_repo
553 553
554 554 c.commit_statuses = ChangesetStatus.STATUSES
555 555
556 556 c.show_version_changes = not pr_closed
557 557 if c.show_version_changes:
558 558 cur_obj = pull_request_at_ver
559 559 prev_obj = prev_pull_request_at_ver
560 560
561 561 old_commit_ids = prev_obj.revisions
562 562 new_commit_ids = cur_obj.revisions
563 563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 564 old_commit_ids, new_commit_ids)
565 565 c.commit_changes_summary = commit_changes
566 566
567 567 # calculate the diff for commits between versions
568 568 c.commit_changes = []
569 569 mark = lambda cs, fw: list(
570 570 h.itertools.izip_longest([], cs, fillvalue=fw))
571 571 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 572 + mark(commit_changes.removed, 'r') \
573 573 + mark(commit_changes.common, 'c'):
574 574
575 575 if raw_id in commit_cache:
576 576 commit = commit_cache[raw_id]
577 577 else:
578 578 try:
579 579 commit = commits_source_repo.get_commit(raw_id)
580 580 except CommitDoesNotExistError:
581 581 # in case we fail extracting still use "dummy" commit
582 582 # for display in commit diff
583 583 commit = h.AttributeDict(
584 584 {'raw_id': raw_id,
585 585 'message': 'EMPTY or MISSING COMMIT'})
586 586 c.commit_changes.append([c_type, commit])
587 587
588 588 # current user review statuses for each version
589 589 c.review_versions = {}
590 590 if self._rhodecode_user.user_id in allowed_reviewers:
591 591 for co in general_comments:
592 592 if co.author.user_id == self._rhodecode_user.user_id:
593 593 # each comment has a status change
594 594 status = co.status_change
595 595 if status:
596 596 _ver_pr = status[0].comment.pull_request_version_id
597 597 c.review_versions[_ver_pr] = status[0]
598 598
599 599 return self._get_template_context(c)
600 600
601 601 def assure_not_empty_repo(self):
602 602 _ = self.request.translate
603 603
604 604 try:
605 605 self.db_repo.scm_instance().get_commit()
606 606 except EmptyRepositoryError:
607 607 h.flash(h.literal(_('There are no commits yet')),
608 608 category='warning')
609 609 raise HTTPFound(
610 610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 611
612 612 @LoginRequired()
613 613 @NotAnonymous()
614 614 @HasRepoPermissionAnyDecorator(
615 615 'repository.read', 'repository.write', 'repository.admin')
616 616 @view_config(
617 617 route_name='pullrequest_new', request_method='GET',
618 618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 619 def pull_request_new(self):
620 620 _ = self.request.translate
621 621 c = self.load_default_context()
622 622
623 623 self.assure_not_empty_repo()
624 624 source_repo = self.db_repo
625 625
626 626 commit_id = self.request.GET.get('commit')
627 627 branch_ref = self.request.GET.get('branch')
628 628 bookmark_ref = self.request.GET.get('bookmark')
629 629
630 630 try:
631 631 source_repo_data = PullRequestModel().generate_repo_data(
632 632 source_repo, commit_id=commit_id,
633 633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 634 except CommitDoesNotExistError as e:
635 635 log.exception(e)
636 636 h.flash(_('Commit does not exist'), 'error')
637 637 raise HTTPFound(
638 638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 639
640 640 default_target_repo = source_repo
641 641
642 642 if source_repo.parent:
643 643 parent_vcs_obj = source_repo.parent.scm_instance()
644 644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 645 # change default if we have a parent repo
646 646 default_target_repo = source_repo.parent
647 647
648 648 target_repo_data = PullRequestModel().generate_repo_data(
649 649 default_target_repo, translator=self.request.translate)
650 650
651 651 selected_source_ref = source_repo_data['refs']['selected_ref']
652 652
653 653 title_source_ref = selected_source_ref.split(':', 2)[1]
654 654 c.default_title = PullRequestModel().generate_pullrequest_title(
655 655 source=source_repo.repo_name,
656 656 source_ref=title_source_ref,
657 657 target=default_target_repo.repo_name
658 658 )
659 659
660 660 c.default_repo_data = {
661 661 'source_repo_name': source_repo.repo_name,
662 662 'source_refs_json': json.dumps(source_repo_data),
663 663 'target_repo_name': default_target_repo.repo_name,
664 664 'target_refs_json': json.dumps(target_repo_data),
665 665 }
666 666 c.default_source_ref = selected_source_ref
667 667
668 668 return self._get_template_context(c)
669 669
670 670 @LoginRequired()
671 671 @NotAnonymous()
672 672 @HasRepoPermissionAnyDecorator(
673 673 'repository.read', 'repository.write', 'repository.admin')
674 674 @view_config(
675 675 route_name='pullrequest_repo_refs', request_method='GET',
676 676 renderer='json_ext', xhr=True)
677 677 def pull_request_repo_refs(self):
678 678 target_repo_name = self.request.matchdict['target_repo_name']
679 679 repo = Repository.get_by_repo_name(target_repo_name)
680 680 if not repo:
681 681 raise HTTPNotFound()
682
683 target_perm = HasRepoPermissionAny(
684 'repository.read', 'repository.write', 'repository.admin')(
685 target_repo_name)
686 if not target_perm:
687 raise HTTPNotFound()
688
682 689 return PullRequestModel().generate_repo_data(
683 690 repo, translator=self.request.translate)
684 691
685 692 @LoginRequired()
686 693 @NotAnonymous()
687 694 @HasRepoPermissionAnyDecorator(
688 695 'repository.read', 'repository.write', 'repository.admin')
689 696 @view_config(
690 697 route_name='pullrequest_repo_destinations', request_method='GET',
691 698 renderer='json_ext', xhr=True)
692 699 def pull_request_repo_destinations(self):
693 700 _ = self.request.translate
694 701 filter_query = self.request.GET.get('query')
695 702
696 703 query = Repository.query() \
697 704 .order_by(func.length(Repository.repo_name)) \
698 705 .filter(
699 706 or_(Repository.repo_name == self.db_repo.repo_name,
700 707 Repository.fork_id == self.db_repo.repo_id))
701 708
702 709 if filter_query:
703 710 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
704 711 query = query.filter(
705 712 Repository.repo_name.ilike(ilike_expression))
706 713
707 714 add_parent = False
708 715 if self.db_repo.parent:
709 716 if filter_query in self.db_repo.parent.repo_name:
710 717 parent_vcs_obj = self.db_repo.parent.scm_instance()
711 718 if parent_vcs_obj and not parent_vcs_obj.is_empty():
712 719 add_parent = True
713 720
714 721 limit = 20 - 1 if add_parent else 20
715 722 all_repos = query.limit(limit).all()
716 723 if add_parent:
717 724 all_repos += [self.db_repo.parent]
718 725
719 726 repos = []
720 727 for obj in ScmModel().get_repos(all_repos):
721 728 repos.append({
722 729 'id': obj['name'],
723 730 'text': obj['name'],
724 731 'type': 'repo',
725 732 'obj': obj['dbrepo']
726 733 })
727 734
728 735 data = {
729 736 'more': False,
730 737 'results': [{
731 738 'text': _('Repositories'),
732 739 'children': repos
733 740 }] if repos else []
734 741 }
735 742 return data
736 743
737 744 @LoginRequired()
738 745 @NotAnonymous()
739 746 @HasRepoPermissionAnyDecorator(
740 747 'repository.read', 'repository.write', 'repository.admin')
741 748 @CSRFRequired()
742 749 @view_config(
743 750 route_name='pullrequest_create', request_method='POST',
744 751 renderer=None)
745 752 def pull_request_create(self):
746 753 _ = self.request.translate
747 754 self.assure_not_empty_repo()
748 755
749 756 controls = peppercorn.parse(self.request.POST.items())
750 757
751 758 try:
752 759 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
753 760 except formencode.Invalid as errors:
754 761 if errors.error_dict.get('revisions'):
755 762 msg = 'Revisions: %s' % errors.error_dict['revisions']
756 763 elif errors.error_dict.get('pullrequest_title'):
757 764 msg = _('Pull request requires a title with min. 3 chars')
758 765 else:
759 766 msg = _('Error creating pull request: {}').format(errors)
760 767 log.exception(msg)
761 768 h.flash(msg, 'error')
762 769
763 770 # would rather just go back to form ...
764 771 raise HTTPFound(
765 772 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
766 773
767 774 source_repo = _form['source_repo']
768 775 source_ref = _form['source_ref']
769 776 target_repo = _form['target_repo']
770 777 target_ref = _form['target_ref']
771 778 commit_ids = _form['revisions'][::-1]
772 779
773 780 # find the ancestor for this pr
774 781 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
775 782 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
776 783
777 784 # re-check permissions again here
778 785 # source_repo we must have read permissions
779 786
780 787 source_perm = HasRepoPermissionAny(
781 788 'repository.read',
782 789 'repository.write', 'repository.admin')(source_db_repo.repo_name)
783 790 if not source_perm:
784 791 msg = _('Not Enough permissions to source repo `{}`.'.format(
785 792 source_db_repo.repo_name))
786 793 h.flash(msg, category='error')
787 794 # copy the args back to redirect
788 795 org_query = self.request.GET.mixed()
789 796 raise HTTPFound(
790 797 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
791 798 _query=org_query))
792 799
793 800 # target repo we must have read permissions, and also later on
794 801 # we want to check branch permissions here
795 802 target_perm = HasRepoPermissionAny(
796 803 'repository.read',
797 804 'repository.write', 'repository.admin')(target_db_repo.repo_name)
798 805 if not target_perm:
799 806 msg = _('Not Enough permissions to target repo `{}`.'.format(
800 807 target_db_repo.repo_name))
801 808 h.flash(msg, category='error')
802 809 # copy the args back to redirect
803 810 org_query = self.request.GET.mixed()
804 811 raise HTTPFound(
805 812 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
806 813 _query=org_query))
807 814
808 815 source_scm = source_db_repo.scm_instance()
809 816 target_scm = target_db_repo.scm_instance()
810 817
811 818 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
812 819 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
813 820
814 821 ancestor = source_scm.get_common_ancestor(
815 822 source_commit.raw_id, target_commit.raw_id, target_scm)
816 823
817 824 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
818 825 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
819 826
820 827 pullrequest_title = _form['pullrequest_title']
821 828 title_source_ref = source_ref.split(':', 2)[1]
822 829 if not pullrequest_title:
823 830 pullrequest_title = PullRequestModel().generate_pullrequest_title(
824 831 source=source_repo,
825 832 source_ref=title_source_ref,
826 833 target=target_repo
827 834 )
828 835
829 836 description = _form['pullrequest_desc']
830 837
831 838 get_default_reviewers_data, validate_default_reviewers = \
832 839 PullRequestModel().get_reviewer_functions()
833 840
834 841 # recalculate reviewers logic, to make sure we can validate this
835 842 reviewer_rules = get_default_reviewers_data(
836 843 self._rhodecode_db_user, source_db_repo,
837 844 source_commit, target_db_repo, target_commit)
838 845
839 846 given_reviewers = _form['review_members']
840 847 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
841 848
842 849 try:
843 850 pull_request = PullRequestModel().create(
844 851 self._rhodecode_user.user_id, source_repo, source_ref,
845 852 target_repo, target_ref, commit_ids, reviewers,
846 853 pullrequest_title, description, reviewer_rules
847 854 )
848 855 Session().commit()
849 856
850 857 h.flash(_('Successfully opened new pull request'),
851 858 category='success')
852 859 except Exception:
853 860 msg = _('Error occurred during creation of this pull request.')
854 861 log.exception(msg)
855 862 h.flash(msg, category='error')
856 863
857 864 # copy the args back to redirect
858 865 org_query = self.request.GET.mixed()
859 866 raise HTTPFound(
860 867 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
861 868 _query=org_query))
862 869
863 870 raise HTTPFound(
864 871 h.route_path('pullrequest_show', repo_name=target_repo,
865 872 pull_request_id=pull_request.pull_request_id))
866 873
867 874 @LoginRequired()
868 875 @NotAnonymous()
869 876 @HasRepoPermissionAnyDecorator(
870 877 'repository.read', 'repository.write', 'repository.admin')
871 878 @CSRFRequired()
872 879 @view_config(
873 880 route_name='pullrequest_update', request_method='POST',
874 881 renderer='json_ext')
875 882 def pull_request_update(self):
876 883 pull_request = PullRequest.get_or_404(
877 884 self.request.matchdict['pull_request_id'])
878 885
879 886 # only owner or admin can update it
880 887 allowed_to_update = PullRequestModel().check_user_update(
881 888 pull_request, self._rhodecode_user)
882 889 if allowed_to_update:
883 890 controls = peppercorn.parse(self.request.POST.items())
884 891
885 892 if 'review_members' in controls:
886 893 self._update_reviewers(
887 894 pull_request, controls['review_members'],
888 895 pull_request.reviewer_data)
889 896 elif str2bool(self.request.POST.get('update_commits', 'false')):
890 897 self._update_commits(pull_request)
891 898 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
892 899 self._edit_pull_request(pull_request)
893 900 else:
894 901 raise HTTPBadRequest()
895 902 return True
896 903 raise HTTPForbidden()
897 904
898 905 def _edit_pull_request(self, pull_request):
899 906 _ = self.request.translate
900 907 try:
901 908 PullRequestModel().edit(
902 909 pull_request, self.request.POST.get('title'),
903 910 self.request.POST.get('description'), self._rhodecode_user)
904 911 except ValueError:
905 912 msg = _(u'Cannot update closed pull requests.')
906 913 h.flash(msg, category='error')
907 914 return
908 915 else:
909 916 Session().commit()
910 917
911 918 msg = _(u'Pull request title & description updated.')
912 919 h.flash(msg, category='success')
913 920 return
914 921
915 922 def _update_commits(self, pull_request):
916 923 _ = self.request.translate
917 924 resp = PullRequestModel().update_commits(pull_request)
918 925
919 926 if resp.executed:
920 927
921 928 if resp.target_changed and resp.source_changed:
922 929 changed = 'target and source repositories'
923 930 elif resp.target_changed and not resp.source_changed:
924 931 changed = 'target repository'
925 932 elif not resp.target_changed and resp.source_changed:
926 933 changed = 'source repository'
927 934 else:
928 935 changed = 'nothing'
929 936
930 937 msg = _(
931 938 u'Pull request updated to "{source_commit_id}" with '
932 939 u'{count_added} added, {count_removed} removed commits. '
933 940 u'Source of changes: {change_source}')
934 941 msg = msg.format(
935 942 source_commit_id=pull_request.source_ref_parts.commit_id,
936 943 count_added=len(resp.changes.added),
937 944 count_removed=len(resp.changes.removed),
938 945 change_source=changed)
939 946 h.flash(msg, category='success')
940 947
941 948 channel = '/repo${}$/pr/{}'.format(
942 949 pull_request.target_repo.repo_name,
943 950 pull_request.pull_request_id)
944 951 message = msg + (
945 952 ' - <a onclick="window.location.reload()">'
946 953 '<strong>{}</strong></a>'.format(_('Reload page')))
947 954 channelstream.post_message(
948 955 channel, message, self._rhodecode_user.username,
949 956 registry=self.request.registry)
950 957 else:
951 958 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
952 959 warning_reasons = [
953 960 UpdateFailureReason.NO_CHANGE,
954 961 UpdateFailureReason.WRONG_REF_TYPE,
955 962 ]
956 963 category = 'warning' if resp.reason in warning_reasons else 'error'
957 964 h.flash(msg, category=category)
958 965
959 966 @LoginRequired()
960 967 @NotAnonymous()
961 968 @HasRepoPermissionAnyDecorator(
962 969 'repository.read', 'repository.write', 'repository.admin')
963 970 @CSRFRequired()
964 971 @view_config(
965 972 route_name='pullrequest_merge', request_method='POST',
966 973 renderer='json_ext')
967 974 def pull_request_merge(self):
968 975 """
969 976 Merge will perform a server-side merge of the specified
970 977 pull request, if the pull request is approved and mergeable.
971 978 After successful merging, the pull request is automatically
972 979 closed, with a relevant comment.
973 980 """
974 981 pull_request = PullRequest.get_or_404(
975 982 self.request.matchdict['pull_request_id'])
976 983
977 984 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
978 985 translator=self.request.translate)
979 986 merge_possible = not check.failed
980 987
981 988 for err_type, error_msg in check.errors:
982 989 h.flash(error_msg, category=err_type)
983 990
984 991 if merge_possible:
985 992 log.debug("Pre-conditions checked, trying to merge.")
986 993 extras = vcs_operation_context(
987 994 self.request.environ, repo_name=pull_request.target_repo.repo_name,
988 995 username=self._rhodecode_db_user.username, action='push',
989 996 scm=pull_request.target_repo.repo_type)
990 997 self._merge_pull_request(
991 998 pull_request, self._rhodecode_db_user, extras)
992 999 else:
993 1000 log.debug("Pre-conditions failed, NOT merging.")
994 1001
995 1002 raise HTTPFound(
996 1003 h.route_path('pullrequest_show',
997 1004 repo_name=pull_request.target_repo.repo_name,
998 1005 pull_request_id=pull_request.pull_request_id))
999 1006
1000 1007 def _merge_pull_request(self, pull_request, user, extras):
1001 1008 _ = self.request.translate
1002 1009 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1003 1010
1004 1011 if merge_resp.executed:
1005 1012 log.debug("The merge was successful, closing the pull request.")
1006 1013 PullRequestModel().close_pull_request(
1007 1014 pull_request.pull_request_id, user)
1008 1015 Session().commit()
1009 1016 msg = _('Pull request was successfully merged and closed.')
1010 1017 h.flash(msg, category='success')
1011 1018 else:
1012 1019 log.debug(
1013 1020 "The merge was not successful. Merge response: %s",
1014 1021 merge_resp)
1015 1022 msg = PullRequestModel().merge_status_message(
1016 1023 merge_resp.failure_reason)
1017 1024 h.flash(msg, category='error')
1018 1025
1019 1026 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1020 1027 _ = self.request.translate
1021 1028 get_default_reviewers_data, validate_default_reviewers = \
1022 1029 PullRequestModel().get_reviewer_functions()
1023 1030
1024 1031 try:
1025 1032 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1026 1033 except ValueError as e:
1027 1034 log.error('Reviewers Validation: {}'.format(e))
1028 1035 h.flash(e, category='error')
1029 1036 return
1030 1037
1031 1038 PullRequestModel().update_reviewers(
1032 1039 pull_request, reviewers, self._rhodecode_user)
1033 1040 h.flash(_('Pull request reviewers updated.'), category='success')
1034 1041 Session().commit()
1035 1042
1036 1043 @LoginRequired()
1037 1044 @NotAnonymous()
1038 1045 @HasRepoPermissionAnyDecorator(
1039 1046 'repository.read', 'repository.write', 'repository.admin')
1040 1047 @CSRFRequired()
1041 1048 @view_config(
1042 1049 route_name='pullrequest_delete', request_method='POST',
1043 1050 renderer='json_ext')
1044 1051 def pull_request_delete(self):
1045 1052 _ = self.request.translate
1046 1053
1047 1054 pull_request = PullRequest.get_or_404(
1048 1055 self.request.matchdict['pull_request_id'])
1049 1056
1050 1057 pr_closed = pull_request.is_closed()
1051 1058 allowed_to_delete = PullRequestModel().check_user_delete(
1052 1059 pull_request, self._rhodecode_user) and not pr_closed
1053 1060
1054 1061 # only owner can delete it !
1055 1062 if allowed_to_delete:
1056 1063 PullRequestModel().delete(pull_request, self._rhodecode_user)
1057 1064 Session().commit()
1058 1065 h.flash(_('Successfully deleted pull request'),
1059 1066 category='success')
1060 1067 raise HTTPFound(h.route_path('pullrequest_show_all',
1061 1068 repo_name=self.db_repo_name))
1062 1069
1063 1070 log.warning('user %s tried to delete pull request without access',
1064 1071 self._rhodecode_user)
1065 1072 raise HTTPNotFound()
1066 1073
1067 1074 @LoginRequired()
1068 1075 @NotAnonymous()
1069 1076 @HasRepoPermissionAnyDecorator(
1070 1077 'repository.read', 'repository.write', 'repository.admin')
1071 1078 @CSRFRequired()
1072 1079 @view_config(
1073 1080 route_name='pullrequest_comment_create', request_method='POST',
1074 1081 renderer='json_ext')
1075 1082 def pull_request_comment_create(self):
1076 1083 _ = self.request.translate
1077 1084
1078 1085 pull_request = PullRequest.get_or_404(
1079 1086 self.request.matchdict['pull_request_id'])
1080 1087 pull_request_id = pull_request.pull_request_id
1081 1088
1082 1089 if pull_request.is_closed():
1083 1090 log.debug('comment: forbidden because pull request is closed')
1084 1091 raise HTTPForbidden()
1085 1092
1086 1093 allowed_to_comment = PullRequestModel().check_user_comment(
1087 1094 pull_request, self._rhodecode_user)
1088 1095 if not allowed_to_comment:
1089 1096 log.debug(
1090 1097 'comment: forbidden because pull request is from forbidden repo')
1091 1098 raise HTTPForbidden()
1092 1099
1093 1100 c = self.load_default_context()
1094 1101
1095 1102 status = self.request.POST.get('changeset_status', None)
1096 1103 text = self.request.POST.get('text')
1097 1104 comment_type = self.request.POST.get('comment_type')
1098 1105 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1099 1106 close_pull_request = self.request.POST.get('close_pull_request')
1100 1107
1101 1108 # the logic here should work like following, if we submit close
1102 1109 # pr comment, use `close_pull_request_with_comment` function
1103 1110 # else handle regular comment logic
1104 1111
1105 1112 if close_pull_request:
1106 1113 # only owner or admin or person with write permissions
1107 1114 allowed_to_close = PullRequestModel().check_user_update(
1108 1115 pull_request, self._rhodecode_user)
1109 1116 if not allowed_to_close:
1110 1117 log.debug('comment: forbidden because not allowed to close '
1111 1118 'pull request %s', pull_request_id)
1112 1119 raise HTTPForbidden()
1113 1120 comment, status = PullRequestModel().close_pull_request_with_comment(
1114 1121 pull_request, self._rhodecode_user, self.db_repo, message=text)
1115 1122 Session().flush()
1116 1123 events.trigger(
1117 1124 events.PullRequestCommentEvent(pull_request, comment))
1118 1125
1119 1126 else:
1120 1127 # regular comment case, could be inline, or one with status.
1121 1128 # for that one we check also permissions
1122 1129
1123 1130 allowed_to_change_status = PullRequestModel().check_user_change_status(
1124 1131 pull_request, self._rhodecode_user)
1125 1132
1126 1133 if status and allowed_to_change_status:
1127 1134 message = (_('Status change %(transition_icon)s %(status)s')
1128 1135 % {'transition_icon': '>',
1129 1136 'status': ChangesetStatus.get_status_lbl(status)})
1130 1137 text = text or message
1131 1138
1132 1139 comment = CommentsModel().create(
1133 1140 text=text,
1134 1141 repo=self.db_repo.repo_id,
1135 1142 user=self._rhodecode_user.user_id,
1136 1143 pull_request=pull_request,
1137 1144 f_path=self.request.POST.get('f_path'),
1138 1145 line_no=self.request.POST.get('line'),
1139 1146 status_change=(ChangesetStatus.get_status_lbl(status)
1140 1147 if status and allowed_to_change_status else None),
1141 1148 status_change_type=(status
1142 1149 if status and allowed_to_change_status else None),
1143 1150 comment_type=comment_type,
1144 1151 resolves_comment_id=resolves_comment_id
1145 1152 )
1146 1153
1147 1154 if allowed_to_change_status:
1148 1155 # calculate old status before we change it
1149 1156 old_calculated_status = pull_request.calculated_review_status()
1150 1157
1151 1158 # get status if set !
1152 1159 if status:
1153 1160 ChangesetStatusModel().set_status(
1154 1161 self.db_repo.repo_id,
1155 1162 status,
1156 1163 self._rhodecode_user.user_id,
1157 1164 comment,
1158 1165 pull_request=pull_request
1159 1166 )
1160 1167
1161 1168 Session().flush()
1162 1169 events.trigger(
1163 1170 events.PullRequestCommentEvent(pull_request, comment))
1164 1171
1165 1172 # we now calculate the status of pull request, and based on that
1166 1173 # calculation we set the commits status
1167 1174 calculated_status = pull_request.calculated_review_status()
1168 1175 if old_calculated_status != calculated_status:
1169 1176 PullRequestModel()._trigger_pull_request_hook(
1170 1177 pull_request, self._rhodecode_user, 'review_status_change')
1171 1178
1172 1179 Session().commit()
1173 1180
1174 1181 data = {
1175 1182 'target_id': h.safeid(h.safe_unicode(
1176 1183 self.request.POST.get('f_path'))),
1177 1184 }
1178 1185 if comment:
1179 1186 c.co = comment
1180 1187 rendered_comment = render(
1181 1188 'rhodecode:templates/changeset/changeset_comment_block.mako',
1182 1189 self._get_template_context(c), self.request)
1183 1190
1184 1191 data.update(comment.get_dict())
1185 1192 data.update({'rendered_text': rendered_comment})
1186 1193
1187 1194 return data
1188 1195
1189 1196 @LoginRequired()
1190 1197 @NotAnonymous()
1191 1198 @HasRepoPermissionAnyDecorator(
1192 1199 'repository.read', 'repository.write', 'repository.admin')
1193 1200 @CSRFRequired()
1194 1201 @view_config(
1195 1202 route_name='pullrequest_comment_delete', request_method='POST',
1196 1203 renderer='json_ext')
1197 1204 def pull_request_comment_delete(self):
1198 1205 pull_request = PullRequest.get_or_404(
1199 1206 self.request.matchdict['pull_request_id'])
1200 1207
1201 1208 comment = ChangesetComment.get_or_404(
1202 1209 self.request.matchdict['comment_id'])
1203 1210 comment_id = comment.comment_id
1204 1211
1205 1212 if pull_request.is_closed():
1206 1213 log.debug('comment: forbidden because pull request is closed')
1207 1214 raise HTTPForbidden()
1208 1215
1209 1216 if not comment:
1210 1217 log.debug('Comment with id:%s not found, skipping', comment_id)
1211 1218 # comment already deleted in another call probably
1212 1219 return True
1213 1220
1214 1221 if comment.pull_request.is_closed():
1215 1222 # don't allow deleting comments on closed pull request
1216 1223 raise HTTPForbidden()
1217 1224
1218 1225 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1219 1226 super_admin = h.HasPermissionAny('hg.admin')()
1220 1227 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1221 1228 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1222 1229 comment_repo_admin = is_repo_admin and is_repo_comment
1223 1230
1224 1231 if super_admin or comment_owner or comment_repo_admin:
1225 1232 old_calculated_status = comment.pull_request.calculated_review_status()
1226 1233 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1227 1234 Session().commit()
1228 1235 calculated_status = comment.pull_request.calculated_review_status()
1229 1236 if old_calculated_status != calculated_status:
1230 1237 PullRequestModel()._trigger_pull_request_hook(
1231 1238 comment.pull_request, self._rhodecode_user, 'review_status_change')
1232 1239 return True
1233 1240 else:
1234 1241 log.warning('No permissions for user %s to delete comment_id: %s',
1235 1242 self._rhodecode_db_user, comment_id)
1236 1243 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now