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