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