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