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