##// END OF EJS Templates
comments: fix existing ids discovery
marcink -
r4501:c3a98398 stable
parent child Browse files
Show More
@@ -1,1794 +1,1794 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (
49 49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 50 PullRequestReviewers)
51 51 from rhodecode.model.forms import PullRequestForm
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 54 from rhodecode.model.scm import ScmModel
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 60
61 61 def load_default_context(self):
62 62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 65 # backward compat., we use for OLD PRs a plain renderer
66 66 c.renderer = 'plain'
67 67 return c
68 68
69 69 def _get_pull_requests_list(
70 70 self, repo_name, source, filter_type, opened_by, statuses):
71 71
72 72 draw, start, limit = self._extract_chunk(self.request)
73 73 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 74 _render = self.request.get_partial_renderer(
75 75 'rhodecode:templates/data_table/_dt_elements.mako')
76 76
77 77 # pagination
78 78
79 79 if filter_type == 'awaiting_review':
80 80 pull_requests = PullRequestModel().get_awaiting_review(
81 81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 82 statuses=statuses, offset=start, length=limit,
83 83 order_by=order_by, order_dir=order_dir)
84 84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 85 repo_name, search_q=search_q, source=source, statuses=statuses,
86 86 opened_by=opened_by)
87 87 elif filter_type == 'awaiting_my_review':
88 88 pull_requests = PullRequestModel().get_awaiting_my_review(
89 89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 90 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 91 offset=start, length=limit, order_by=order_by,
92 92 order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 95 statuses=statuses, opened_by=opened_by)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr)
110 110
111 111 data.append({
112 112 'name': _render('pullrequest_name',
113 113 pr.pull_request_id, pr.pull_request_state,
114 114 pr.work_in_progress, pr.target_repo.repo_name),
115 115 'name_raw': pr.pull_request_id,
116 116 'status': _render('pullrequest_status',
117 117 pr.calculated_review_status()),
118 118 'title': _render('pullrequest_title', pr.title, pr.description),
119 119 'description': h.escape(pr.description),
120 120 'updated_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.updated_on)),
122 122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
123 123 'created_on': _render('pullrequest_updated_on',
124 124 h.datetime_to_time(pr.created_on)),
125 125 'created_on_raw': h.datetime_to_time(pr.created_on),
126 126 'state': pr.pull_request_state,
127 127 'author': _render('pullrequest_author',
128 128 pr.author.full_contact, ),
129 129 'author_raw': pr.author.full_name,
130 130 'comments': _render('pullrequest_comments', len(comments)),
131 131 'comments_raw': len(comments),
132 132 'closed': pr.is_closed(),
133 133 })
134 134
135 135 data = ({
136 136 'draw': draw,
137 137 'data': data,
138 138 'recordsTotal': pull_requests_total_count,
139 139 'recordsFiltered': pull_requests_total_count,
140 140 })
141 141 return data
142 142
143 143 @LoginRequired()
144 144 @HasRepoPermissionAnyDecorator(
145 145 'repository.read', 'repository.write', 'repository.admin')
146 146 @view_config(
147 147 route_name='pullrequest_show_all', request_method='GET',
148 148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
149 149 def pull_request_list(self):
150 150 c = self.load_default_context()
151 151
152 152 req_get = self.request.GET
153 153 c.source = str2bool(req_get.get('source'))
154 154 c.closed = str2bool(req_get.get('closed'))
155 155 c.my = str2bool(req_get.get('my'))
156 156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
157 157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
158 158
159 159 c.active = 'open'
160 160 if c.my:
161 161 c.active = 'my'
162 162 if c.closed:
163 163 c.active = 'closed'
164 164 if c.awaiting_review and not c.source:
165 165 c.active = 'awaiting'
166 166 if c.source and not c.awaiting_review:
167 167 c.active = 'source'
168 168 if c.awaiting_my_review:
169 169 c.active = 'awaiting_my'
170 170
171 171 return self._get_template_context(c)
172 172
173 173 @LoginRequired()
174 174 @HasRepoPermissionAnyDecorator(
175 175 'repository.read', 'repository.write', 'repository.admin')
176 176 @view_config(
177 177 route_name='pullrequest_show_all_data', request_method='GET',
178 178 renderer='json_ext', xhr=True)
179 179 def pull_request_list_data(self):
180 180 self.load_default_context()
181 181
182 182 # additional filters
183 183 req_get = self.request.GET
184 184 source = str2bool(req_get.get('source'))
185 185 closed = str2bool(req_get.get('closed'))
186 186 my = str2bool(req_get.get('my'))
187 187 awaiting_review = str2bool(req_get.get('awaiting_review'))
188 188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
189 189
190 190 filter_type = 'awaiting_review' if awaiting_review \
191 191 else 'awaiting_my_review' if awaiting_my_review \
192 192 else None
193 193
194 194 opened_by = None
195 195 if my:
196 196 opened_by = [self._rhodecode_user.user_id]
197 197
198 198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
199 199 if closed:
200 200 statuses = [PullRequest.STATUS_CLOSED]
201 201
202 202 data = self._get_pull_requests_list(
203 203 repo_name=self.db_repo_name, source=source,
204 204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
205 205
206 206 return data
207 207
208 208 def _is_diff_cache_enabled(self, target_repo):
209 209 caching_enabled = self._get_general_setting(
210 210 target_repo, 'rhodecode_diff_cache')
211 211 log.debug('Diff caching enabled: %s', caching_enabled)
212 212 return caching_enabled
213 213
214 214 def _get_diffset(self, source_repo_name, source_repo,
215 215 ancestor_commit,
216 216 source_ref_id, target_ref_id,
217 217 target_commit, source_commit, diff_limit, file_limit,
218 218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
219 219
220 220 if use_ancestor:
221 221 # we might want to not use it for versions
222 222 target_ref_id = ancestor_commit.raw_id
223 223
224 224 vcs_diff = PullRequestModel().get_diff(
225 225 source_repo, source_ref_id, target_ref_id,
226 226 hide_whitespace_changes, diff_context)
227 227
228 228 diff_processor = diffs.DiffProcessor(
229 229 vcs_diff, format='newdiff', diff_limit=diff_limit,
230 230 file_limit=file_limit, show_full_diff=fulldiff)
231 231
232 232 _parsed = diff_processor.prepare()
233 233
234 234 diffset = codeblocks.DiffSet(
235 235 repo_name=self.db_repo_name,
236 236 source_repo_name=source_repo_name,
237 237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
238 238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
239 239 )
240 240 diffset = self.path_filter.render_patchset_filtered(
241 241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
242 242
243 243 return diffset
244 244
245 245 def _get_range_diffset(self, source_scm, source_repo,
246 246 commit1, commit2, diff_limit, file_limit,
247 247 fulldiff, hide_whitespace_changes, diff_context):
248 248 vcs_diff = source_scm.get_diff(
249 249 commit1, commit2,
250 250 ignore_whitespace=hide_whitespace_changes,
251 251 context=diff_context)
252 252
253 253 diff_processor = diffs.DiffProcessor(
254 254 vcs_diff, format='newdiff', diff_limit=diff_limit,
255 255 file_limit=file_limit, show_full_diff=fulldiff)
256 256
257 257 _parsed = diff_processor.prepare()
258 258
259 259 diffset = codeblocks.DiffSet(
260 260 repo_name=source_repo.repo_name,
261 261 source_node_getter=codeblocks.diffset_node_getter(commit1),
262 262 target_node_getter=codeblocks.diffset_node_getter(commit2))
263 263
264 264 diffset = self.path_filter.render_patchset_filtered(
265 265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
266 266
267 267 return diffset
268 268
269 269 def register_comments_vars(self, c, pull_request, versions):
270 270 comments_model = CommentsModel()
271 271
272 272 # GENERAL COMMENTS with versions #
273 273 q = comments_model._all_general_comments_of_pull_request(pull_request)
274 274 q = q.order_by(ChangesetComment.comment_id.asc())
275 275 general_comments = q
276 276
277 277 # pick comments we want to render at current version
278 278 c.comment_versions = comments_model.aggregate_comments(
279 279 general_comments, versions, c.at_version_num)
280 280
281 281 # INLINE COMMENTS with versions #
282 282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
283 283 q = q.order_by(ChangesetComment.comment_id.asc())
284 284 inline_comments = q
285 285
286 286 c.inline_versions = comments_model.aggregate_comments(
287 287 inline_comments, versions, c.at_version_num, inline=True)
288 288
289 289 # Comments inline+general
290 290 if c.at_version:
291 291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
292 292 c.comments = c.comment_versions[c.at_version_num]['display']
293 293 else:
294 294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
295 295 c.comments = c.comment_versions[c.at_version_num]['until']
296 296
297 297 return general_comments, inline_comments
298 298
299 299 @LoginRequired()
300 300 @HasRepoPermissionAnyDecorator(
301 301 'repository.read', 'repository.write', 'repository.admin')
302 302 @view_config(
303 303 route_name='pullrequest_show', request_method='GET',
304 304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
305 305 def pull_request_show(self):
306 306 _ = self.request.translate
307 307 c = self.load_default_context()
308 308
309 309 pull_request = PullRequest.get_or_404(
310 310 self.request.matchdict['pull_request_id'])
311 311 pull_request_id = pull_request.pull_request_id
312 312
313 313 c.state_progressing = pull_request.is_state_changing()
314 314 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
315 315 pull_request.target_repo.repo_name, pull_request.pull_request_id)
316 316
317 317 _new_state = {
318 318 'created': PullRequest.STATE_CREATED,
319 319 }.get(self.request.GET.get('force_state'))
320 320
321 321 if c.is_super_admin and _new_state:
322 322 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
323 323 h.flash(
324 324 _('Pull Request state was force changed to `{}`').format(_new_state),
325 325 category='success')
326 326 Session().commit()
327 327
328 328 raise HTTPFound(h.route_path(
329 329 'pullrequest_show', repo_name=self.db_repo_name,
330 330 pull_request_id=pull_request_id))
331 331
332 332 version = self.request.GET.get('version')
333 333 from_version = self.request.GET.get('from_version') or version
334 334 merge_checks = self.request.GET.get('merge_checks')
335 335 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
336 336 force_refresh = str2bool(self.request.GET.get('force_refresh'))
337 337 c.range_diff_on = self.request.GET.get('range-diff') == "1"
338 338
339 339 # fetch global flags of ignore ws or context lines
340 340 diff_context = diffs.get_diff_context(self.request)
341 341 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
342 342
343 343 (pull_request_latest,
344 344 pull_request_at_ver,
345 345 pull_request_display_obj,
346 346 at_version) = PullRequestModel().get_pr_version(
347 347 pull_request_id, version=version)
348 348
349 349 pr_closed = pull_request_latest.is_closed()
350 350
351 351 if pr_closed and (version or from_version):
352 352 # not allow to browse versions for closed PR
353 353 raise HTTPFound(h.route_path(
354 354 'pullrequest_show', repo_name=self.db_repo_name,
355 355 pull_request_id=pull_request_id))
356 356
357 357 versions = pull_request_display_obj.versions()
358 358 # used to store per-commit range diffs
359 359 c.changes = collections.OrderedDict()
360 360
361 361 c.at_version = at_version
362 362 c.at_version_num = (at_version
363 363 if at_version and at_version != PullRequest.LATEST_VER
364 364 else None)
365 365
366 366 c.at_version_index = ChangesetComment.get_index_from_version(
367 367 c.at_version_num, versions)
368 368
369 369 (prev_pull_request_latest,
370 370 prev_pull_request_at_ver,
371 371 prev_pull_request_display_obj,
372 372 prev_at_version) = PullRequestModel().get_pr_version(
373 373 pull_request_id, version=from_version)
374 374
375 375 c.from_version = prev_at_version
376 376 c.from_version_num = (prev_at_version
377 377 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
378 378 else None)
379 379 c.from_version_index = ChangesetComment.get_index_from_version(
380 380 c.from_version_num, versions)
381 381
382 382 # define if we're in COMPARE mode or VIEW at version mode
383 383 compare = at_version != prev_at_version
384 384
385 385 # pull_requests repo_name we opened it against
386 386 # ie. target_repo must match
387 387 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
388 388 log.warning('Mismatch between the current repo: %s, and target %s',
389 389 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
390 390 raise HTTPNotFound()
391 391
392 392 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
393 393
394 394 c.pull_request = pull_request_display_obj
395 395 c.renderer = pull_request_at_ver.description_renderer or c.renderer
396 396 c.pull_request_latest = pull_request_latest
397 397
398 398 # inject latest version
399 399 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
400 400 c.versions = versions + [latest_ver]
401 401
402 402 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
403 403 c.allowed_to_change_status = False
404 404 c.allowed_to_update = False
405 405 c.allowed_to_merge = False
406 406 c.allowed_to_delete = False
407 407 c.allowed_to_comment = False
408 408 c.allowed_to_close = False
409 409 else:
410 410 can_change_status = PullRequestModel().check_user_change_status(
411 411 pull_request_at_ver, self._rhodecode_user)
412 412 c.allowed_to_change_status = can_change_status and not pr_closed
413 413
414 414 c.allowed_to_update = PullRequestModel().check_user_update(
415 415 pull_request_latest, self._rhodecode_user) and not pr_closed
416 416 c.allowed_to_merge = PullRequestModel().check_user_merge(
417 417 pull_request_latest, self._rhodecode_user) and not pr_closed
418 418 c.allowed_to_delete = PullRequestModel().check_user_delete(
419 419 pull_request_latest, self._rhodecode_user) and not pr_closed
420 420 c.allowed_to_comment = not pr_closed
421 421 c.allowed_to_close = c.allowed_to_merge and not pr_closed
422 422
423 423 c.forbid_adding_reviewers = False
424 424 c.forbid_author_to_review = False
425 425 c.forbid_commit_author_to_review = False
426 426
427 427 if pull_request_latest.reviewer_data and \
428 428 'rules' in pull_request_latest.reviewer_data:
429 429 rules = pull_request_latest.reviewer_data['rules'] or {}
430 430 try:
431 431 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 432 c.forbid_author_to_review = rules.get('forbid_author_to_review')
433 433 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
434 434 except Exception:
435 435 pass
436 436
437 437 # check merge capabilities
438 438 _merge_check = MergeCheck.validate(
439 439 pull_request_latest, auth_user=self._rhodecode_user,
440 440 translator=self.request.translate,
441 441 force_shadow_repo_refresh=force_refresh)
442 442
443 443 c.pr_merge_errors = _merge_check.error_details
444 444 c.pr_merge_possible = not _merge_check.failed
445 445 c.pr_merge_message = _merge_check.merge_msg
446 446 c.pr_merge_source_commit = _merge_check.source_commit
447 447 c.pr_merge_target_commit = _merge_check.target_commit
448 448
449 449 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 450 pull_request_latest, translator=self.request.translate)
451 451
452 452 c.pull_request_review_status = _merge_check.review_status
453 453 if merge_checks:
454 454 self.request.override_renderer = \
455 455 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 456 return self._get_template_context(c)
457 457
458 458 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 459 c.reviewers_count = pull_request.reviewers_count
460 460 c.observers_count = pull_request.observers_count
461 461
462 462 # reviewers and statuses
463 463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466 466
467 467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 468 member_reviewer = h.reviewer_as_json(
469 469 member, reasons=reasons, mandatory=mandatory,
470 470 role=review_obj.role,
471 471 user_group=review_obj.rule_user_group_data()
472 472 )
473 473
474 474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 475 member_reviewer['review_status'] = current_review_status
476 476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 477 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479 479
480 480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481 481
482 482 for observer_obj, member in pull_request_at_ver.observers():
483 483 member_observer = h.reviewer_as_json(
484 484 member, reasons=[], mandatory=False,
485 485 role=observer_obj.role,
486 486 user_group=observer_obj.rule_user_group_data()
487 487 )
488 488 member_observer['allowed_to_update'] = c.allowed_to_update
489 489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490 490
491 491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492 492
493 493 general_comments, inline_comments = \
494 494 self.register_comments_vars(c, pull_request_latest, versions)
495 495
496 496 # TODOs
497 497 c.unresolved_comments = CommentsModel() \
498 498 .get_pull_request_unresolved_todos(pull_request_latest)
499 499 c.resolved_comments = CommentsModel() \
500 500 .get_pull_request_resolved_todos(pull_request_latest)
501 501
502 502 # if we use version, then do not show later comments
503 503 # than current version
504 504 display_inline_comments = collections.defaultdict(
505 505 lambda: collections.defaultdict(list))
506 506 for co in inline_comments:
507 507 if c.at_version_num:
508 508 # pick comments that are at least UPTO given version, so we
509 509 # don't render comments for higher version
510 510 should_render = co.pull_request_version_id and \
511 511 co.pull_request_version_id <= c.at_version_num
512 512 else:
513 513 # showing all, for 'latest'
514 514 should_render = True
515 515
516 516 if should_render:
517 517 display_inline_comments[co.f_path][co.line_no].append(co)
518 518
519 519 # load diff data into template context, if we use compare mode then
520 520 # diff is calculated based on changes between versions of PR
521 521
522 522 source_repo = pull_request_at_ver.source_repo
523 523 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
524 524
525 525 target_repo = pull_request_at_ver.target_repo
526 526 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
527 527
528 528 if compare:
529 529 # in compare switch the diff base to latest commit from prev version
530 530 target_ref_id = prev_pull_request_display_obj.revisions[0]
531 531
532 532 # despite opening commits for bookmarks/branches/tags, we always
533 533 # convert this to rev to prevent changes after bookmark or branch change
534 534 c.source_ref_type = 'rev'
535 535 c.source_ref = source_ref_id
536 536
537 537 c.target_ref_type = 'rev'
538 538 c.target_ref = target_ref_id
539 539
540 540 c.source_repo = source_repo
541 541 c.target_repo = target_repo
542 542
543 543 c.commit_ranges = []
544 544 source_commit = EmptyCommit()
545 545 target_commit = EmptyCommit()
546 546 c.missing_requirements = False
547 547
548 548 source_scm = source_repo.scm_instance()
549 549 target_scm = target_repo.scm_instance()
550 550
551 551 shadow_scm = None
552 552 try:
553 553 shadow_scm = pull_request_latest.get_shadow_repo()
554 554 except Exception:
555 555 log.debug('Failed to get shadow repo', exc_info=True)
556 556 # try first the existing source_repo, and then shadow
557 557 # repo if we can obtain one
558 558 commits_source_repo = source_scm
559 559 if shadow_scm:
560 560 commits_source_repo = shadow_scm
561 561
562 562 c.commits_source_repo = commits_source_repo
563 563 c.ancestor = None # set it to None, to hide it from PR view
564 564
565 565 # empty version means latest, so we keep this to prevent
566 566 # double caching
567 567 version_normalized = version or PullRequest.LATEST_VER
568 568 from_version_normalized = from_version or PullRequest.LATEST_VER
569 569
570 570 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
571 571 cache_file_path = diff_cache_exist(
572 572 cache_path, 'pull_request', pull_request_id, version_normalized,
573 573 from_version_normalized, source_ref_id, target_ref_id,
574 574 hide_whitespace_changes, diff_context, c.fulldiff)
575 575
576 576 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
577 577 force_recache = self.get_recache_flag()
578 578
579 579 cached_diff = None
580 580 if caching_enabled:
581 581 cached_diff = load_cached_diff(cache_file_path)
582 582
583 583 has_proper_commit_cache = (
584 584 cached_diff and cached_diff.get('commits')
585 585 and len(cached_diff.get('commits', [])) == 5
586 586 and cached_diff.get('commits')[0]
587 587 and cached_diff.get('commits')[3])
588 588
589 589 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
590 590 diff_commit_cache = \
591 591 (ancestor_commit, commit_cache, missing_requirements,
592 592 source_commit, target_commit) = cached_diff['commits']
593 593 else:
594 594 # NOTE(marcink): we reach potentially unreachable errors when a PR has
595 595 # merge errors resulting in potentially hidden commits in the shadow repo.
596 596 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
597 597 and _merge_check.merge_response
598 598 maybe_unreachable = maybe_unreachable \
599 599 and _merge_check.merge_response.metadata.get('unresolved_files')
600 600 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
601 601 diff_commit_cache = \
602 602 (ancestor_commit, commit_cache, missing_requirements,
603 603 source_commit, target_commit) = self.get_commits(
604 604 commits_source_repo,
605 605 pull_request_at_ver,
606 606 source_commit,
607 607 source_ref_id,
608 608 source_scm,
609 609 target_commit,
610 610 target_ref_id,
611 611 target_scm,
612 612 maybe_unreachable=maybe_unreachable)
613 613
614 614 # register our commit range
615 615 for comm in commit_cache.values():
616 616 c.commit_ranges.append(comm)
617 617
618 618 c.missing_requirements = missing_requirements
619 619 c.ancestor_commit = ancestor_commit
620 620 c.statuses = source_repo.statuses(
621 621 [x.raw_id for x in c.commit_ranges])
622 622
623 623 # auto collapse if we have more than limit
624 624 collapse_limit = diffs.DiffProcessor._collapse_commits_over
625 625 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
626 626 c.compare_mode = compare
627 627
628 628 # diff_limit is the old behavior, will cut off the whole diff
629 629 # if the limit is applied otherwise will just hide the
630 630 # big files from the front-end
631 631 diff_limit = c.visual.cut_off_limit_diff
632 632 file_limit = c.visual.cut_off_limit_file
633 633
634 634 c.missing_commits = False
635 635 if (c.missing_requirements
636 636 or isinstance(source_commit, EmptyCommit)
637 637 or source_commit == target_commit):
638 638
639 639 c.missing_commits = True
640 640 else:
641 641 c.inline_comments = display_inline_comments
642 642
643 643 use_ancestor = True
644 644 if from_version_normalized != version_normalized:
645 645 use_ancestor = False
646 646
647 647 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
648 648 if not force_recache and has_proper_diff_cache:
649 649 c.diffset = cached_diff['diff']
650 650 else:
651 651 try:
652 652 c.diffset = self._get_diffset(
653 653 c.source_repo.repo_name, commits_source_repo,
654 654 c.ancestor_commit,
655 655 source_ref_id, target_ref_id,
656 656 target_commit, source_commit,
657 657 diff_limit, file_limit, c.fulldiff,
658 658 hide_whitespace_changes, diff_context,
659 659 use_ancestor=use_ancestor
660 660 )
661 661
662 662 # save cached diff
663 663 if caching_enabled:
664 664 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
665 665 except CommitDoesNotExistError:
666 666 log.exception('Failed to generate diffset')
667 667 c.missing_commits = True
668 668
669 669 if not c.missing_commits:
670 670
671 671 c.limited_diff = c.diffset.limited_diff
672 672
673 673 # calculate removed files that are bound to comments
674 674 comment_deleted_files = [
675 675 fname for fname in display_inline_comments
676 676 if fname not in c.diffset.file_stats]
677 677
678 678 c.deleted_files_comments = collections.defaultdict(dict)
679 679 for fname, per_line_comments in display_inline_comments.items():
680 680 if fname in comment_deleted_files:
681 681 c.deleted_files_comments[fname]['stats'] = 0
682 682 c.deleted_files_comments[fname]['comments'] = list()
683 683 for lno, comments in per_line_comments.items():
684 684 c.deleted_files_comments[fname]['comments'].extend(comments)
685 685
686 686 # maybe calculate the range diff
687 687 if c.range_diff_on:
688 688 # TODO(marcink): set whitespace/context
689 689 context_lcl = 3
690 690 ign_whitespace_lcl = False
691 691
692 692 for commit in c.commit_ranges:
693 693 commit2 = commit
694 694 commit1 = commit.first_parent
695 695
696 696 range_diff_cache_file_path = diff_cache_exist(
697 697 cache_path, 'diff', commit.raw_id,
698 698 ign_whitespace_lcl, context_lcl, c.fulldiff)
699 699
700 700 cached_diff = None
701 701 if caching_enabled:
702 702 cached_diff = load_cached_diff(range_diff_cache_file_path)
703 703
704 704 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
705 705 if not force_recache and has_proper_diff_cache:
706 706 diffset = cached_diff['diff']
707 707 else:
708 708 diffset = self._get_range_diffset(
709 709 commits_source_repo, source_repo,
710 710 commit1, commit2, diff_limit, file_limit,
711 711 c.fulldiff, ign_whitespace_lcl, context_lcl
712 712 )
713 713
714 714 # save cached diff
715 715 if caching_enabled:
716 716 cache_diff(range_diff_cache_file_path, diffset, None)
717 717
718 718 c.changes[commit.raw_id] = diffset
719 719
720 720 # this is a hack to properly display links, when creating PR, the
721 721 # compare view and others uses different notation, and
722 722 # compare_commits.mako renders links based on the target_repo.
723 723 # We need to swap that here to generate it properly on the html side
724 724 c.target_repo = c.source_repo
725 725
726 726 c.commit_statuses = ChangesetStatus.STATUSES
727 727
728 728 c.show_version_changes = not pr_closed
729 729 if c.show_version_changes:
730 730 cur_obj = pull_request_at_ver
731 731 prev_obj = prev_pull_request_at_ver
732 732
733 733 old_commit_ids = prev_obj.revisions
734 734 new_commit_ids = cur_obj.revisions
735 735 commit_changes = PullRequestModel()._calculate_commit_id_changes(
736 736 old_commit_ids, new_commit_ids)
737 737 c.commit_changes_summary = commit_changes
738 738
739 739 # calculate the diff for commits between versions
740 740 c.commit_changes = []
741 741
742 742 def mark(cs, fw):
743 743 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
744 744
745 745 for c_type, raw_id in mark(commit_changes.added, 'a') \
746 746 + mark(commit_changes.removed, 'r') \
747 747 + mark(commit_changes.common, 'c'):
748 748
749 749 if raw_id in commit_cache:
750 750 commit = commit_cache[raw_id]
751 751 else:
752 752 try:
753 753 commit = commits_source_repo.get_commit(raw_id)
754 754 except CommitDoesNotExistError:
755 755 # in case we fail extracting still use "dummy" commit
756 756 # for display in commit diff
757 757 commit = h.AttributeDict(
758 758 {'raw_id': raw_id,
759 759 'message': 'EMPTY or MISSING COMMIT'})
760 760 c.commit_changes.append([c_type, commit])
761 761
762 762 # current user review statuses for each version
763 763 c.review_versions = {}
764 764 if self._rhodecode_user.user_id in c.allowed_reviewers:
765 765 for co in general_comments:
766 766 if co.author.user_id == self._rhodecode_user.user_id:
767 767 status = co.status_change
768 768 if status:
769 769 _ver_pr = status[0].comment.pull_request_version_id
770 770 c.review_versions[_ver_pr] = status[0]
771 771
772 772 return self._get_template_context(c)
773 773
774 774 def get_commits(
775 775 self, commits_source_repo, pull_request_at_ver, source_commit,
776 776 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
777 777 maybe_unreachable=False):
778 778
779 779 commit_cache = collections.OrderedDict()
780 780 missing_requirements = False
781 781
782 782 try:
783 783 pre_load = ["author", "date", "message", "branch", "parents"]
784 784
785 785 pull_request_commits = pull_request_at_ver.revisions
786 786 log.debug('Loading %s commits from %s',
787 787 len(pull_request_commits), commits_source_repo)
788 788
789 789 for rev in pull_request_commits:
790 790 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
791 791 maybe_unreachable=maybe_unreachable)
792 792 commit_cache[comm.raw_id] = comm
793 793
794 794 # Order here matters, we first need to get target, and then
795 795 # the source
796 796 target_commit = commits_source_repo.get_commit(
797 797 commit_id=safe_str(target_ref_id))
798 798
799 799 source_commit = commits_source_repo.get_commit(
800 800 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
801 801 except CommitDoesNotExistError:
802 802 log.warning('Failed to get commit from `{}` repo'.format(
803 803 commits_source_repo), exc_info=True)
804 804 except RepositoryRequirementError:
805 805 log.warning('Failed to get all required data from repo', exc_info=True)
806 806 missing_requirements = True
807 807
808 808 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
809 809
810 810 try:
811 811 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
812 812 except Exception:
813 813 ancestor_commit = None
814 814
815 815 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
816 816
817 817 def assure_not_empty_repo(self):
818 818 _ = self.request.translate
819 819
820 820 try:
821 821 self.db_repo.scm_instance().get_commit()
822 822 except EmptyRepositoryError:
823 823 h.flash(h.literal(_('There are no commits yet')),
824 824 category='warning')
825 825 raise HTTPFound(
826 826 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
827 827
828 828 @LoginRequired()
829 829 @NotAnonymous()
830 830 @HasRepoPermissionAnyDecorator(
831 831 'repository.read', 'repository.write', 'repository.admin')
832 832 @view_config(
833 833 route_name='pullrequest_new', request_method='GET',
834 834 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
835 835 def pull_request_new(self):
836 836 _ = self.request.translate
837 837 c = self.load_default_context()
838 838
839 839 self.assure_not_empty_repo()
840 840 source_repo = self.db_repo
841 841
842 842 commit_id = self.request.GET.get('commit')
843 843 branch_ref = self.request.GET.get('branch')
844 844 bookmark_ref = self.request.GET.get('bookmark')
845 845
846 846 try:
847 847 source_repo_data = PullRequestModel().generate_repo_data(
848 848 source_repo, commit_id=commit_id,
849 849 branch=branch_ref, bookmark=bookmark_ref,
850 850 translator=self.request.translate)
851 851 except CommitDoesNotExistError as e:
852 852 log.exception(e)
853 853 h.flash(_('Commit does not exist'), 'error')
854 854 raise HTTPFound(
855 855 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
856 856
857 857 default_target_repo = source_repo
858 858
859 859 if source_repo.parent and c.has_origin_repo_read_perm:
860 860 parent_vcs_obj = source_repo.parent.scm_instance()
861 861 if parent_vcs_obj and not parent_vcs_obj.is_empty():
862 862 # change default if we have a parent repo
863 863 default_target_repo = source_repo.parent
864 864
865 865 target_repo_data = PullRequestModel().generate_repo_data(
866 866 default_target_repo, translator=self.request.translate)
867 867
868 868 selected_source_ref = source_repo_data['refs']['selected_ref']
869 869 title_source_ref = ''
870 870 if selected_source_ref:
871 871 title_source_ref = selected_source_ref.split(':', 2)[1]
872 872 c.default_title = PullRequestModel().generate_pullrequest_title(
873 873 source=source_repo.repo_name,
874 874 source_ref=title_source_ref,
875 875 target=default_target_repo.repo_name
876 876 )
877 877
878 878 c.default_repo_data = {
879 879 'source_repo_name': source_repo.repo_name,
880 880 'source_refs_json': json.dumps(source_repo_data),
881 881 'target_repo_name': default_target_repo.repo_name,
882 882 'target_refs_json': json.dumps(target_repo_data),
883 883 }
884 884 c.default_source_ref = selected_source_ref
885 885
886 886 return self._get_template_context(c)
887 887
888 888 @LoginRequired()
889 889 @NotAnonymous()
890 890 @HasRepoPermissionAnyDecorator(
891 891 'repository.read', 'repository.write', 'repository.admin')
892 892 @view_config(
893 893 route_name='pullrequest_repo_refs', request_method='GET',
894 894 renderer='json_ext', xhr=True)
895 895 def pull_request_repo_refs(self):
896 896 self.load_default_context()
897 897 target_repo_name = self.request.matchdict['target_repo_name']
898 898 repo = Repository.get_by_repo_name(target_repo_name)
899 899 if not repo:
900 900 raise HTTPNotFound()
901 901
902 902 target_perm = HasRepoPermissionAny(
903 903 'repository.read', 'repository.write', 'repository.admin')(
904 904 target_repo_name)
905 905 if not target_perm:
906 906 raise HTTPNotFound()
907 907
908 908 return PullRequestModel().generate_repo_data(
909 909 repo, translator=self.request.translate)
910 910
911 911 @LoginRequired()
912 912 @NotAnonymous()
913 913 @HasRepoPermissionAnyDecorator(
914 914 'repository.read', 'repository.write', 'repository.admin')
915 915 @view_config(
916 916 route_name='pullrequest_repo_targets', request_method='GET',
917 917 renderer='json_ext', xhr=True)
918 918 def pullrequest_repo_targets(self):
919 919 _ = self.request.translate
920 920 filter_query = self.request.GET.get('query')
921 921
922 922 # get the parents
923 923 parent_target_repos = []
924 924 if self.db_repo.parent:
925 925 parents_query = Repository.query() \
926 926 .order_by(func.length(Repository.repo_name)) \
927 927 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
928 928
929 929 if filter_query:
930 930 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
931 931 parents_query = parents_query.filter(
932 932 Repository.repo_name.ilike(ilike_expression))
933 933 parents = parents_query.limit(20).all()
934 934
935 935 for parent in parents:
936 936 parent_vcs_obj = parent.scm_instance()
937 937 if parent_vcs_obj and not parent_vcs_obj.is_empty():
938 938 parent_target_repos.append(parent)
939 939
940 940 # get other forks, and repo itself
941 941 query = Repository.query() \
942 942 .order_by(func.length(Repository.repo_name)) \
943 943 .filter(
944 944 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
945 945 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
946 946 ) \
947 947 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
948 948
949 949 if filter_query:
950 950 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
951 951 query = query.filter(Repository.repo_name.ilike(ilike_expression))
952 952
953 953 limit = max(20 - len(parent_target_repos), 5) # not less then 5
954 954 target_repos = query.limit(limit).all()
955 955
956 956 all_target_repos = target_repos + parent_target_repos
957 957
958 958 repos = []
959 959 # This checks permissions to the repositories
960 960 for obj in ScmModel().get_repos(all_target_repos):
961 961 repos.append({
962 962 'id': obj['name'],
963 963 'text': obj['name'],
964 964 'type': 'repo',
965 965 'repo_id': obj['dbrepo']['repo_id'],
966 966 'repo_type': obj['dbrepo']['repo_type'],
967 967 'private': obj['dbrepo']['private'],
968 968
969 969 })
970 970
971 971 data = {
972 972 'more': False,
973 973 'results': [{
974 974 'text': _('Repositories'),
975 975 'children': repos
976 976 }] if repos else []
977 977 }
978 978 return data
979 979
980 def _get_existing_ids(self, post_data):
981 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
982
980 983 @LoginRequired()
981 984 @NotAnonymous()
982 985 @HasRepoPermissionAnyDecorator(
983 986 'repository.read', 'repository.write', 'repository.admin')
984 987 @view_config(
985 988 route_name='pullrequest_comments', request_method='POST',
986 989 renderer='string_html', xhr=True)
987 990 def pullrequest_comments(self):
988 991 self.load_default_context()
989 992
990 993 pull_request = PullRequest.get_or_404(
991 994 self.request.matchdict['pull_request_id'])
992 995 pull_request_id = pull_request.pull_request_id
993 996 version = self.request.GET.get('version')
994 997
995 998 _render = self.request.get_partial_renderer(
996 999 'rhodecode:templates/base/sidebar.mako')
997 1000 c = _render.get_call_context()
998 1001
999 1002 (pull_request_latest,
1000 1003 pull_request_at_ver,
1001 1004 pull_request_display_obj,
1002 1005 at_version) = PullRequestModel().get_pr_version(
1003 1006 pull_request_id, version=version)
1004 1007 versions = pull_request_display_obj.versions()
1005 1008 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1006 1009 c.versions = versions + [latest_ver]
1007 1010
1008 1011 c.at_version = at_version
1009 1012 c.at_version_num = (at_version
1010 1013 if at_version and at_version != PullRequest.LATEST_VER
1011 1014 else None)
1012 1015
1013 1016 self.register_comments_vars(c, pull_request_latest, versions)
1014 1017 all_comments = c.inline_comments_flat + c.comments
1015 1018
1016 existing_ids = filter(
1017 lambda e: e, map(safe_int, aslist(self.request.POST.get('comments'))))
1018
1019 existing_ids = self._get_existing_ids(self.request.POST)
1019 1020 return _render('comments_table', all_comments, len(all_comments),
1020 1021 existing_ids=existing_ids)
1021 1022
1022 1023 @LoginRequired()
1023 1024 @NotAnonymous()
1024 1025 @HasRepoPermissionAnyDecorator(
1025 1026 'repository.read', 'repository.write', 'repository.admin')
1026 1027 @view_config(
1027 1028 route_name='pullrequest_todos', request_method='POST',
1028 1029 renderer='string_html', xhr=True)
1029 1030 def pullrequest_todos(self):
1030 1031 self.load_default_context()
1031 1032
1032 1033 pull_request = PullRequest.get_or_404(
1033 1034 self.request.matchdict['pull_request_id'])
1034 1035 pull_request_id = pull_request.pull_request_id
1035 1036 version = self.request.GET.get('version')
1036 1037
1037 1038 _render = self.request.get_partial_renderer(
1038 1039 'rhodecode:templates/base/sidebar.mako')
1039 1040 c = _render.get_call_context()
1040 1041 (pull_request_latest,
1041 1042 pull_request_at_ver,
1042 1043 pull_request_display_obj,
1043 1044 at_version) = PullRequestModel().get_pr_version(
1044 1045 pull_request_id, version=version)
1045 1046 versions = pull_request_display_obj.versions()
1046 1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 1048 c.versions = versions + [latest_ver]
1048 1049
1049 1050 c.at_version = at_version
1050 1051 c.at_version_num = (at_version
1051 1052 if at_version and at_version != PullRequest.LATEST_VER
1052 1053 else None)
1053 1054
1054 1055 c.unresolved_comments = CommentsModel() \
1055 1056 .get_pull_request_unresolved_todos(pull_request)
1056 1057 c.resolved_comments = CommentsModel() \
1057 1058 .get_pull_request_resolved_todos(pull_request)
1058 1059
1059 1060 all_comments = c.unresolved_comments + c.resolved_comments
1060 existing_ids = filter(
1061 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1061 existing_ids = self._get_existing_ids(self.request.POST)
1062 1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 1063 todo_comments=True, existing_ids=existing_ids)
1064 1064
1065 1065 @LoginRequired()
1066 1066 @NotAnonymous()
1067 1067 @HasRepoPermissionAnyDecorator(
1068 1068 'repository.read', 'repository.write', 'repository.admin')
1069 1069 @CSRFRequired()
1070 1070 @view_config(
1071 1071 route_name='pullrequest_create', request_method='POST',
1072 1072 renderer=None)
1073 1073 def pull_request_create(self):
1074 1074 _ = self.request.translate
1075 1075 self.assure_not_empty_repo()
1076 1076 self.load_default_context()
1077 1077
1078 1078 controls = peppercorn.parse(self.request.POST.items())
1079 1079
1080 1080 try:
1081 1081 form = PullRequestForm(
1082 1082 self.request.translate, self.db_repo.repo_id)()
1083 1083 _form = form.to_python(controls)
1084 1084 except formencode.Invalid as errors:
1085 1085 if errors.error_dict.get('revisions'):
1086 1086 msg = 'Revisions: %s' % errors.error_dict['revisions']
1087 1087 elif errors.error_dict.get('pullrequest_title'):
1088 1088 msg = errors.error_dict.get('pullrequest_title')
1089 1089 else:
1090 1090 msg = _('Error creating pull request: {}').format(errors)
1091 1091 log.exception(msg)
1092 1092 h.flash(msg, 'error')
1093 1093
1094 1094 # would rather just go back to form ...
1095 1095 raise HTTPFound(
1096 1096 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1097 1097
1098 1098 source_repo = _form['source_repo']
1099 1099 source_ref = _form['source_ref']
1100 1100 target_repo = _form['target_repo']
1101 1101 target_ref = _form['target_ref']
1102 1102 commit_ids = _form['revisions'][::-1]
1103 1103 common_ancestor_id = _form['common_ancestor']
1104 1104
1105 1105 # find the ancestor for this pr
1106 1106 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1107 1107 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1108 1108
1109 1109 if not (source_db_repo or target_db_repo):
1110 1110 h.flash(_('source_repo or target repo not found'), category='error')
1111 1111 raise HTTPFound(
1112 1112 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1113 1113
1114 1114 # re-check permissions again here
1115 1115 # source_repo we must have read permissions
1116 1116
1117 1117 source_perm = HasRepoPermissionAny(
1118 1118 'repository.read', 'repository.write', 'repository.admin')(
1119 1119 source_db_repo.repo_name)
1120 1120 if not source_perm:
1121 1121 msg = _('Not Enough permissions to source repo `{}`.'.format(
1122 1122 source_db_repo.repo_name))
1123 1123 h.flash(msg, category='error')
1124 1124 # copy the args back to redirect
1125 1125 org_query = self.request.GET.mixed()
1126 1126 raise HTTPFound(
1127 1127 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1128 1128 _query=org_query))
1129 1129
1130 1130 # target repo we must have read permissions, and also later on
1131 1131 # we want to check branch permissions here
1132 1132 target_perm = HasRepoPermissionAny(
1133 1133 'repository.read', 'repository.write', 'repository.admin')(
1134 1134 target_db_repo.repo_name)
1135 1135 if not target_perm:
1136 1136 msg = _('Not Enough permissions to target repo `{}`.'.format(
1137 1137 target_db_repo.repo_name))
1138 1138 h.flash(msg, category='error')
1139 1139 # copy the args back to redirect
1140 1140 org_query = self.request.GET.mixed()
1141 1141 raise HTTPFound(
1142 1142 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1143 1143 _query=org_query))
1144 1144
1145 1145 source_scm = source_db_repo.scm_instance()
1146 1146 target_scm = target_db_repo.scm_instance()
1147 1147
1148 1148 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1149 1149 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1150 1150
1151 1151 ancestor = source_scm.get_common_ancestor(
1152 1152 source_commit.raw_id, target_commit.raw_id, target_scm)
1153 1153
1154 1154 # recalculate target ref based on ancestor
1155 1155 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1156 1156 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1157 1157
1158 1158 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1159 1159 PullRequestModel().get_reviewer_functions()
1160 1160
1161 1161 # recalculate reviewers logic, to make sure we can validate this
1162 1162 reviewer_rules = get_default_reviewers_data(
1163 1163 self._rhodecode_db_user, source_db_repo,
1164 1164 source_commit, target_db_repo, target_commit)
1165 1165
1166 1166 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1167 1167 observers = validate_observers(_form['observer_members'], reviewer_rules)
1168 1168
1169 1169 pullrequest_title = _form['pullrequest_title']
1170 1170 title_source_ref = source_ref.split(':', 2)[1]
1171 1171 if not pullrequest_title:
1172 1172 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1173 1173 source=source_repo,
1174 1174 source_ref=title_source_ref,
1175 1175 target=target_repo
1176 1176 )
1177 1177
1178 1178 description = _form['pullrequest_desc']
1179 1179 description_renderer = _form['description_renderer']
1180 1180
1181 1181 try:
1182 1182 pull_request = PullRequestModel().create(
1183 1183 created_by=self._rhodecode_user.user_id,
1184 1184 source_repo=source_repo,
1185 1185 source_ref=source_ref,
1186 1186 target_repo=target_repo,
1187 1187 target_ref=target_ref,
1188 1188 revisions=commit_ids,
1189 1189 common_ancestor_id=common_ancestor_id,
1190 1190 reviewers=reviewers,
1191 1191 observers=observers,
1192 1192 title=pullrequest_title,
1193 1193 description=description,
1194 1194 description_renderer=description_renderer,
1195 1195 reviewer_data=reviewer_rules,
1196 1196 auth_user=self._rhodecode_user
1197 1197 )
1198 1198 Session().commit()
1199 1199
1200 1200 h.flash(_('Successfully opened new pull request'),
1201 1201 category='success')
1202 1202 except Exception:
1203 1203 msg = _('Error occurred during creation of this pull request.')
1204 1204 log.exception(msg)
1205 1205 h.flash(msg, category='error')
1206 1206
1207 1207 # copy the args back to redirect
1208 1208 org_query = self.request.GET.mixed()
1209 1209 raise HTTPFound(
1210 1210 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1211 1211 _query=org_query))
1212 1212
1213 1213 raise HTTPFound(
1214 1214 h.route_path('pullrequest_show', repo_name=target_repo,
1215 1215 pull_request_id=pull_request.pull_request_id))
1216 1216
1217 1217 @LoginRequired()
1218 1218 @NotAnonymous()
1219 1219 @HasRepoPermissionAnyDecorator(
1220 1220 'repository.read', 'repository.write', 'repository.admin')
1221 1221 @CSRFRequired()
1222 1222 @view_config(
1223 1223 route_name='pullrequest_update', request_method='POST',
1224 1224 renderer='json_ext')
1225 1225 def pull_request_update(self):
1226 1226 pull_request = PullRequest.get_or_404(
1227 1227 self.request.matchdict['pull_request_id'])
1228 1228 _ = self.request.translate
1229 1229
1230 1230 c = self.load_default_context()
1231 1231 redirect_url = None
1232 1232
1233 1233 if pull_request.is_closed():
1234 1234 log.debug('update: forbidden because pull request is closed')
1235 1235 msg = _(u'Cannot update closed pull requests.')
1236 1236 h.flash(msg, category='error')
1237 1237 return {'response': True,
1238 1238 'redirect_url': redirect_url}
1239 1239
1240 1240 is_state_changing = pull_request.is_state_changing()
1241 1241 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1242 1242 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1243 1243
1244 1244 # only owner or admin can update it
1245 1245 allowed_to_update = PullRequestModel().check_user_update(
1246 1246 pull_request, self._rhodecode_user)
1247 1247
1248 1248 if allowed_to_update:
1249 1249 controls = peppercorn.parse(self.request.POST.items())
1250 1250 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1251 1251
1252 1252 if 'review_members' in controls:
1253 1253 self._update_reviewers(
1254 1254 c,
1255 1255 pull_request, controls['review_members'],
1256 1256 pull_request.reviewer_data,
1257 1257 PullRequestReviewers.ROLE_REVIEWER)
1258 1258 elif 'observer_members' in controls:
1259 1259 self._update_reviewers(
1260 1260 c,
1261 1261 pull_request, controls['observer_members'],
1262 1262 pull_request.reviewer_data,
1263 1263 PullRequestReviewers.ROLE_OBSERVER)
1264 1264 elif str2bool(self.request.POST.get('update_commits', 'false')):
1265 1265 if is_state_changing:
1266 1266 log.debug('commits update: forbidden because pull request is in state %s',
1267 1267 pull_request.pull_request_state)
1268 1268 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1269 1269 u'Current state is: `{}`').format(
1270 1270 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1271 1271 h.flash(msg, category='error')
1272 1272 return {'response': True,
1273 1273 'redirect_url': redirect_url}
1274 1274
1275 1275 self._update_commits(c, pull_request)
1276 1276 if force_refresh:
1277 1277 redirect_url = h.route_path(
1278 1278 'pullrequest_show', repo_name=self.db_repo_name,
1279 1279 pull_request_id=pull_request.pull_request_id,
1280 1280 _query={"force_refresh": 1})
1281 1281 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1282 1282 self._edit_pull_request(pull_request)
1283 1283 else:
1284 1284 log.error('Unhandled update data.')
1285 1285 raise HTTPBadRequest()
1286 1286
1287 1287 return {'response': True,
1288 1288 'redirect_url': redirect_url}
1289 1289 raise HTTPForbidden()
1290 1290
1291 1291 def _edit_pull_request(self, pull_request):
1292 1292 """
1293 1293 Edit title and description
1294 1294 """
1295 1295 _ = self.request.translate
1296 1296
1297 1297 try:
1298 1298 PullRequestModel().edit(
1299 1299 pull_request,
1300 1300 self.request.POST.get('title'),
1301 1301 self.request.POST.get('description'),
1302 1302 self.request.POST.get('description_renderer'),
1303 1303 self._rhodecode_user)
1304 1304 except ValueError:
1305 1305 msg = _(u'Cannot update closed pull requests.')
1306 1306 h.flash(msg, category='error')
1307 1307 return
1308 1308 else:
1309 1309 Session().commit()
1310 1310
1311 1311 msg = _(u'Pull request title & description updated.')
1312 1312 h.flash(msg, category='success')
1313 1313 return
1314 1314
1315 1315 def _update_commits(self, c, pull_request):
1316 1316 _ = self.request.translate
1317 1317
1318 1318 with pull_request.set_state(PullRequest.STATE_UPDATING):
1319 1319 resp = PullRequestModel().update_commits(
1320 1320 pull_request, self._rhodecode_db_user)
1321 1321
1322 1322 if resp.executed:
1323 1323
1324 1324 if resp.target_changed and resp.source_changed:
1325 1325 changed = 'target and source repositories'
1326 1326 elif resp.target_changed and not resp.source_changed:
1327 1327 changed = 'target repository'
1328 1328 elif not resp.target_changed and resp.source_changed:
1329 1329 changed = 'source repository'
1330 1330 else:
1331 1331 changed = 'nothing'
1332 1332
1333 1333 msg = _(u'Pull request updated to "{source_commit_id}" with '
1334 1334 u'{count_added} added, {count_removed} removed commits. '
1335 1335 u'Source of changes: {change_source}.')
1336 1336 msg = msg.format(
1337 1337 source_commit_id=pull_request.source_ref_parts.commit_id,
1338 1338 count_added=len(resp.changes.added),
1339 1339 count_removed=len(resp.changes.removed),
1340 1340 change_source=changed)
1341 1341 h.flash(msg, category='success')
1342 1342 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1343 1343 else:
1344 1344 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1345 1345 warning_reasons = [
1346 1346 UpdateFailureReason.NO_CHANGE,
1347 1347 UpdateFailureReason.WRONG_REF_TYPE,
1348 1348 ]
1349 1349 category = 'warning' if resp.reason in warning_reasons else 'error'
1350 1350 h.flash(msg, category=category)
1351 1351
1352 1352 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1353 1353 _ = self.request.translate
1354 1354
1355 1355 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1356 1356 PullRequestModel().get_reviewer_functions()
1357 1357
1358 1358 if role == PullRequestReviewers.ROLE_REVIEWER:
1359 1359 try:
1360 1360 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1361 1361 except ValueError as e:
1362 1362 log.error('Reviewers Validation: {}'.format(e))
1363 1363 h.flash(e, category='error')
1364 1364 return
1365 1365
1366 1366 old_calculated_status = pull_request.calculated_review_status()
1367 1367 PullRequestModel().update_reviewers(
1368 1368 pull_request, reviewers, self._rhodecode_user)
1369 1369
1370 1370 Session().commit()
1371 1371
1372 1372 msg = _('Pull request reviewers updated.')
1373 1373 h.flash(msg, category='success')
1374 1374 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1375 1375
1376 1376 # trigger status changed if change in reviewers changes the status
1377 1377 calculated_status = pull_request.calculated_review_status()
1378 1378 if old_calculated_status != calculated_status:
1379 1379 PullRequestModel().trigger_pull_request_hook(
1380 1380 pull_request, self._rhodecode_user, 'review_status_change',
1381 1381 data={'status': calculated_status})
1382 1382
1383 1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1384 1384 try:
1385 1385 observers = validate_observers(review_members, reviewer_rules)
1386 1386 except ValueError as e:
1387 1387 log.error('Observers Validation: {}'.format(e))
1388 1388 h.flash(e, category='error')
1389 1389 return
1390 1390
1391 1391 PullRequestModel().update_observers(
1392 1392 pull_request, observers, self._rhodecode_user)
1393 1393
1394 1394 Session().commit()
1395 1395 msg = _('Pull request observers updated.')
1396 1396 h.flash(msg, category='success')
1397 1397 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1398 1398
1399 1399 @LoginRequired()
1400 1400 @NotAnonymous()
1401 1401 @HasRepoPermissionAnyDecorator(
1402 1402 'repository.read', 'repository.write', 'repository.admin')
1403 1403 @CSRFRequired()
1404 1404 @view_config(
1405 1405 route_name='pullrequest_merge', request_method='POST',
1406 1406 renderer='json_ext')
1407 1407 def pull_request_merge(self):
1408 1408 """
1409 1409 Merge will perform a server-side merge of the specified
1410 1410 pull request, if the pull request is approved and mergeable.
1411 1411 After successful merging, the pull request is automatically
1412 1412 closed, with a relevant comment.
1413 1413 """
1414 1414 pull_request = PullRequest.get_or_404(
1415 1415 self.request.matchdict['pull_request_id'])
1416 1416 _ = self.request.translate
1417 1417
1418 1418 if pull_request.is_state_changing():
1419 1419 log.debug('show: forbidden because pull request is in state %s',
1420 1420 pull_request.pull_request_state)
1421 1421 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1422 1422 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1423 1423 pull_request.pull_request_state)
1424 1424 h.flash(msg, category='error')
1425 1425 raise HTTPFound(
1426 1426 h.route_path('pullrequest_show',
1427 1427 repo_name=pull_request.target_repo.repo_name,
1428 1428 pull_request_id=pull_request.pull_request_id))
1429 1429
1430 1430 self.load_default_context()
1431 1431
1432 1432 with pull_request.set_state(PullRequest.STATE_UPDATING):
1433 1433 check = MergeCheck.validate(
1434 1434 pull_request, auth_user=self._rhodecode_user,
1435 1435 translator=self.request.translate)
1436 1436 merge_possible = not check.failed
1437 1437
1438 1438 for err_type, error_msg in check.errors:
1439 1439 h.flash(error_msg, category=err_type)
1440 1440
1441 1441 if merge_possible:
1442 1442 log.debug("Pre-conditions checked, trying to merge.")
1443 1443 extras = vcs_operation_context(
1444 1444 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1445 1445 username=self._rhodecode_db_user.username, action='push',
1446 1446 scm=pull_request.target_repo.repo_type)
1447 1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 1448 self._merge_pull_request(
1449 1449 pull_request, self._rhodecode_db_user, extras)
1450 1450 else:
1451 1451 log.debug("Pre-conditions failed, NOT merging.")
1452 1452
1453 1453 raise HTTPFound(
1454 1454 h.route_path('pullrequest_show',
1455 1455 repo_name=pull_request.target_repo.repo_name,
1456 1456 pull_request_id=pull_request.pull_request_id))
1457 1457
1458 1458 def _merge_pull_request(self, pull_request, user, extras):
1459 1459 _ = self.request.translate
1460 1460 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1461 1461
1462 1462 if merge_resp.executed:
1463 1463 log.debug("The merge was successful, closing the pull request.")
1464 1464 PullRequestModel().close_pull_request(
1465 1465 pull_request.pull_request_id, user)
1466 1466 Session().commit()
1467 1467 msg = _('Pull request was successfully merged and closed.')
1468 1468 h.flash(msg, category='success')
1469 1469 else:
1470 1470 log.debug(
1471 1471 "The merge was not successful. Merge response: %s", merge_resp)
1472 1472 msg = merge_resp.merge_status_message
1473 1473 h.flash(msg, category='error')
1474 1474
1475 1475 @LoginRequired()
1476 1476 @NotAnonymous()
1477 1477 @HasRepoPermissionAnyDecorator(
1478 1478 'repository.read', 'repository.write', 'repository.admin')
1479 1479 @CSRFRequired()
1480 1480 @view_config(
1481 1481 route_name='pullrequest_delete', request_method='POST',
1482 1482 renderer='json_ext')
1483 1483 def pull_request_delete(self):
1484 1484 _ = self.request.translate
1485 1485
1486 1486 pull_request = PullRequest.get_or_404(
1487 1487 self.request.matchdict['pull_request_id'])
1488 1488 self.load_default_context()
1489 1489
1490 1490 pr_closed = pull_request.is_closed()
1491 1491 allowed_to_delete = PullRequestModel().check_user_delete(
1492 1492 pull_request, self._rhodecode_user) and not pr_closed
1493 1493
1494 1494 # only owner can delete it !
1495 1495 if allowed_to_delete:
1496 1496 PullRequestModel().delete(pull_request, self._rhodecode_user)
1497 1497 Session().commit()
1498 1498 h.flash(_('Successfully deleted pull request'),
1499 1499 category='success')
1500 1500 raise HTTPFound(h.route_path('pullrequest_show_all',
1501 1501 repo_name=self.db_repo_name))
1502 1502
1503 1503 log.warning('user %s tried to delete pull request without access',
1504 1504 self._rhodecode_user)
1505 1505 raise HTTPNotFound()
1506 1506
1507 1507 @LoginRequired()
1508 1508 @NotAnonymous()
1509 1509 @HasRepoPermissionAnyDecorator(
1510 1510 'repository.read', 'repository.write', 'repository.admin')
1511 1511 @CSRFRequired()
1512 1512 @view_config(
1513 1513 route_name='pullrequest_comment_create', request_method='POST',
1514 1514 renderer='json_ext')
1515 1515 def pull_request_comment_create(self):
1516 1516 _ = self.request.translate
1517 1517
1518 1518 pull_request = PullRequest.get_or_404(
1519 1519 self.request.matchdict['pull_request_id'])
1520 1520 pull_request_id = pull_request.pull_request_id
1521 1521
1522 1522 if pull_request.is_closed():
1523 1523 log.debug('comment: forbidden because pull request is closed')
1524 1524 raise HTTPForbidden()
1525 1525
1526 1526 allowed_to_comment = PullRequestModel().check_user_comment(
1527 1527 pull_request, self._rhodecode_user)
1528 1528 if not allowed_to_comment:
1529 1529 log.debug('comment: forbidden because pull request is from forbidden repo')
1530 1530 raise HTTPForbidden()
1531 1531
1532 1532 c = self.load_default_context()
1533 1533
1534 1534 status = self.request.POST.get('changeset_status', None)
1535 1535 text = self.request.POST.get('text')
1536 1536 comment_type = self.request.POST.get('comment_type')
1537 1537 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1538 1538 close_pull_request = self.request.POST.get('close_pull_request')
1539 1539
1540 1540 # the logic here should work like following, if we submit close
1541 1541 # pr comment, use `close_pull_request_with_comment` function
1542 1542 # else handle regular comment logic
1543 1543
1544 1544 if close_pull_request:
1545 1545 # only owner or admin or person with write permissions
1546 1546 allowed_to_close = PullRequestModel().check_user_update(
1547 1547 pull_request, self._rhodecode_user)
1548 1548 if not allowed_to_close:
1549 1549 log.debug('comment: forbidden because not allowed to close '
1550 1550 'pull request %s', pull_request_id)
1551 1551 raise HTTPForbidden()
1552 1552
1553 1553 # This also triggers `review_status_change`
1554 1554 comment, status = PullRequestModel().close_pull_request_with_comment(
1555 1555 pull_request, self._rhodecode_user, self.db_repo, message=text,
1556 1556 auth_user=self._rhodecode_user)
1557 1557 Session().flush()
1558 1558
1559 1559 PullRequestModel().trigger_pull_request_hook(
1560 1560 pull_request, self._rhodecode_user, 'comment',
1561 1561 data={'comment': comment})
1562 1562
1563 1563 else:
1564 1564 # regular comment case, could be inline, or one with status.
1565 1565 # for that one we check also permissions
1566 1566
1567 1567 allowed_to_change_status = PullRequestModel().check_user_change_status(
1568 1568 pull_request, self._rhodecode_user)
1569 1569
1570 1570 if status and allowed_to_change_status:
1571 1571 message = (_('Status change %(transition_icon)s %(status)s')
1572 1572 % {'transition_icon': '>',
1573 1573 'status': ChangesetStatus.get_status_lbl(status)})
1574 1574 text = text or message
1575 1575
1576 1576 comment = CommentsModel().create(
1577 1577 text=text,
1578 1578 repo=self.db_repo.repo_id,
1579 1579 user=self._rhodecode_user.user_id,
1580 1580 pull_request=pull_request,
1581 1581 f_path=self.request.POST.get('f_path'),
1582 1582 line_no=self.request.POST.get('line'),
1583 1583 status_change=(ChangesetStatus.get_status_lbl(status)
1584 1584 if status and allowed_to_change_status else None),
1585 1585 status_change_type=(status
1586 1586 if status and allowed_to_change_status else None),
1587 1587 comment_type=comment_type,
1588 1588 resolves_comment_id=resolves_comment_id,
1589 1589 auth_user=self._rhodecode_user
1590 1590 )
1591 1591
1592 1592 if allowed_to_change_status:
1593 1593 # calculate old status before we change it
1594 1594 old_calculated_status = pull_request.calculated_review_status()
1595 1595
1596 1596 # get status if set !
1597 1597 if status:
1598 1598 ChangesetStatusModel().set_status(
1599 1599 self.db_repo.repo_id,
1600 1600 status,
1601 1601 self._rhodecode_user.user_id,
1602 1602 comment,
1603 1603 pull_request=pull_request
1604 1604 )
1605 1605
1606 1606 Session().flush()
1607 1607 # this is somehow required to get access to some relationship
1608 1608 # loaded on comment
1609 1609 Session().refresh(comment)
1610 1610
1611 1611 PullRequestModel().trigger_pull_request_hook(
1612 1612 pull_request, self._rhodecode_user, 'comment',
1613 1613 data={'comment': comment})
1614 1614
1615 1615 # we now calculate the status of pull request, and based on that
1616 1616 # calculation we set the commits status
1617 1617 calculated_status = pull_request.calculated_review_status()
1618 1618 if old_calculated_status != calculated_status:
1619 1619 PullRequestModel().trigger_pull_request_hook(
1620 1620 pull_request, self._rhodecode_user, 'review_status_change',
1621 1621 data={'status': calculated_status})
1622 1622
1623 1623 Session().commit()
1624 1624
1625 1625 data = {
1626 1626 'target_id': h.safeid(h.safe_unicode(
1627 1627 self.request.POST.get('f_path'))),
1628 1628 }
1629 1629 if comment:
1630 1630 c.co = comment
1631 1631 c.at_version_num = None
1632 1632 rendered_comment = render(
1633 1633 'rhodecode:templates/changeset/changeset_comment_block.mako',
1634 1634 self._get_template_context(c), self.request)
1635 1635
1636 1636 data.update(comment.get_dict())
1637 1637 data.update({'rendered_text': rendered_comment})
1638 1638
1639 1639 return data
1640 1640
1641 1641 @LoginRequired()
1642 1642 @NotAnonymous()
1643 1643 @HasRepoPermissionAnyDecorator(
1644 1644 'repository.read', 'repository.write', 'repository.admin')
1645 1645 @CSRFRequired()
1646 1646 @view_config(
1647 1647 route_name='pullrequest_comment_delete', request_method='POST',
1648 1648 renderer='json_ext')
1649 1649 def pull_request_comment_delete(self):
1650 1650 pull_request = PullRequest.get_or_404(
1651 1651 self.request.matchdict['pull_request_id'])
1652 1652
1653 1653 comment = ChangesetComment.get_or_404(
1654 1654 self.request.matchdict['comment_id'])
1655 1655 comment_id = comment.comment_id
1656 1656
1657 1657 if comment.immutable:
1658 1658 # don't allow deleting comments that are immutable
1659 1659 raise HTTPForbidden()
1660 1660
1661 1661 if pull_request.is_closed():
1662 1662 log.debug('comment: forbidden because pull request is closed')
1663 1663 raise HTTPForbidden()
1664 1664
1665 1665 if not comment:
1666 1666 log.debug('Comment with id:%s not found, skipping', comment_id)
1667 1667 # comment already deleted in another call probably
1668 1668 return True
1669 1669
1670 1670 if comment.pull_request.is_closed():
1671 1671 # don't allow deleting comments on closed pull request
1672 1672 raise HTTPForbidden()
1673 1673
1674 1674 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1675 1675 super_admin = h.HasPermissionAny('hg.admin')()
1676 1676 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1677 1677 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1678 1678 comment_repo_admin = is_repo_admin and is_repo_comment
1679 1679
1680 1680 if super_admin or comment_owner or comment_repo_admin:
1681 1681 old_calculated_status = comment.pull_request.calculated_review_status()
1682 1682 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1683 1683 Session().commit()
1684 1684 calculated_status = comment.pull_request.calculated_review_status()
1685 1685 if old_calculated_status != calculated_status:
1686 1686 PullRequestModel().trigger_pull_request_hook(
1687 1687 comment.pull_request, self._rhodecode_user, 'review_status_change',
1688 1688 data={'status': calculated_status})
1689 1689 return True
1690 1690 else:
1691 1691 log.warning('No permissions for user %s to delete comment_id: %s',
1692 1692 self._rhodecode_db_user, comment_id)
1693 1693 raise HTTPNotFound()
1694 1694
1695 1695 @LoginRequired()
1696 1696 @NotAnonymous()
1697 1697 @HasRepoPermissionAnyDecorator(
1698 1698 'repository.read', 'repository.write', 'repository.admin')
1699 1699 @CSRFRequired()
1700 1700 @view_config(
1701 1701 route_name='pullrequest_comment_edit', request_method='POST',
1702 1702 renderer='json_ext')
1703 1703 def pull_request_comment_edit(self):
1704 1704 self.load_default_context()
1705 1705
1706 1706 pull_request = PullRequest.get_or_404(
1707 1707 self.request.matchdict['pull_request_id']
1708 1708 )
1709 1709 comment = ChangesetComment.get_or_404(
1710 1710 self.request.matchdict['comment_id']
1711 1711 )
1712 1712 comment_id = comment.comment_id
1713 1713
1714 1714 if comment.immutable:
1715 1715 # don't allow deleting comments that are immutable
1716 1716 raise HTTPForbidden()
1717 1717
1718 1718 if pull_request.is_closed():
1719 1719 log.debug('comment: forbidden because pull request is closed')
1720 1720 raise HTTPForbidden()
1721 1721
1722 1722 if not comment:
1723 1723 log.debug('Comment with id:%s not found, skipping', comment_id)
1724 1724 # comment already deleted in another call probably
1725 1725 return True
1726 1726
1727 1727 if comment.pull_request.is_closed():
1728 1728 # don't allow deleting comments on closed pull request
1729 1729 raise HTTPForbidden()
1730 1730
1731 1731 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1732 1732 super_admin = h.HasPermissionAny('hg.admin')()
1733 1733 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1734 1734 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1735 1735 comment_repo_admin = is_repo_admin and is_repo_comment
1736 1736
1737 1737 if super_admin or comment_owner or comment_repo_admin:
1738 1738 text = self.request.POST.get('text')
1739 1739 version = self.request.POST.get('version')
1740 1740 if text == comment.text:
1741 1741 log.warning(
1742 1742 'Comment(PR): '
1743 1743 'Trying to create new version '
1744 1744 'with the same comment body {}'.format(
1745 1745 comment_id,
1746 1746 )
1747 1747 )
1748 1748 raise HTTPNotFound()
1749 1749
1750 1750 if version.isdigit():
1751 1751 version = int(version)
1752 1752 else:
1753 1753 log.warning(
1754 1754 'Comment(PR): Wrong version type {} {} '
1755 1755 'for comment {}'.format(
1756 1756 version,
1757 1757 type(version),
1758 1758 comment_id,
1759 1759 )
1760 1760 )
1761 1761 raise HTTPNotFound()
1762 1762
1763 1763 try:
1764 1764 comment_history = CommentsModel().edit(
1765 1765 comment_id=comment_id,
1766 1766 text=text,
1767 1767 auth_user=self._rhodecode_user,
1768 1768 version=version,
1769 1769 )
1770 1770 except CommentVersionMismatch:
1771 1771 raise HTTPConflict()
1772 1772
1773 1773 if not comment_history:
1774 1774 raise HTTPNotFound()
1775 1775
1776 1776 Session().commit()
1777 1777
1778 1778 PullRequestModel().trigger_pull_request_hook(
1779 1779 pull_request, self._rhodecode_user, 'comment_edit',
1780 1780 data={'comment': comment})
1781 1781
1782 1782 return {
1783 1783 'comment_history_id': comment_history.comment_history_id,
1784 1784 'comment_id': comment.comment_id,
1785 1785 'comment_version': comment_history.version,
1786 1786 'comment_author_username': comment_history.author.username,
1787 1787 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1788 1788 'comment_created_on': h.age_component(comment_history.created_on,
1789 1789 time_is_local=True),
1790 1790 }
1791 1791 else:
1792 1792 log.warning('No permissions for user %s to edit comment_id: %s',
1793 1793 self._rhodecode_db_user, comment_id)
1794 1794 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now