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