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