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