##// END OF EJS Templates
pull-reqeusts: added option to force-refresh merge workspace in case of problems.
marcink -
r2780:ac1e4aa6 default
parent child Browse files
Show More
@@ -1,1301 +1,1302 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 115 'title': _render(
116 116 'pullrequest_title', pr.title, pr.description),
117 117 'description': h.escape(pr.description),
118 118 'updated_on': _render('pullrequest_updated_on',
119 119 h.datetime_to_time(pr.updated_on)),
120 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 121 'created_on': _render('pullrequest_updated_on',
122 122 h.datetime_to_time(pr.created_on)),
123 123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id)
218 218
219 219 diff_processor = diffs.DiffProcessor(
220 220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 221 file_limit=file_limit, show_full_diff=fulldiff)
222 222
223 223 _parsed = diff_processor.prepare()
224 224
225 225 diffset = codeblocks.DiffSet(
226 226 repo_name=self.db_repo_name,
227 227 source_repo_name=source_repo_name,
228 228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 230 )
231 231 diffset = self.path_filter.render_patchset_filtered(
232 232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 233
234 234 return diffset
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator(
238 238 'repository.read', 'repository.write', 'repository.admin')
239 239 @view_config(
240 240 route_name='pullrequest_show', request_method='GET',
241 241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 242 def pull_request_show(self):
243 243 pull_request_id = self.request.matchdict['pull_request_id']
244 244
245 245 c = self.load_default_context()
246 246
247 247 version = self.request.GET.get('version')
248 248 from_version = self.request.GET.get('from_version') or version
249 249 merge_checks = self.request.GET.get('merge_checks')
250 250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 force_refresh = str2bool(self.request.GET.get('force_refresh'))
251 252
252 253 (pull_request_latest,
253 254 pull_request_at_ver,
254 255 pull_request_display_obj,
255 256 at_version) = PullRequestModel().get_pr_version(
256 257 pull_request_id, version=version)
257 258 pr_closed = pull_request_latest.is_closed()
258 259
259 260 if pr_closed and (version or from_version):
260 261 # not allow to browse versions
261 262 raise HTTPFound(h.route_path(
262 263 'pullrequest_show', repo_name=self.db_repo_name,
263 264 pull_request_id=pull_request_id))
264 265
265 266 versions = pull_request_display_obj.versions()
266 267
267 268 c.at_version = at_version
268 269 c.at_version_num = (at_version
269 270 if at_version and at_version != 'latest'
270 271 else None)
271 272 c.at_version_pos = ChangesetComment.get_index_from_version(
272 273 c.at_version_num, versions)
273 274
274 275 (prev_pull_request_latest,
275 276 prev_pull_request_at_ver,
276 277 prev_pull_request_display_obj,
277 278 prev_at_version) = PullRequestModel().get_pr_version(
278 279 pull_request_id, version=from_version)
279 280
280 281 c.from_version = prev_at_version
281 282 c.from_version_num = (prev_at_version
282 283 if prev_at_version and prev_at_version != 'latest'
283 284 else None)
284 285 c.from_version_pos = ChangesetComment.get_index_from_version(
285 286 c.from_version_num, versions)
286 287
287 288 # define if we're in COMPARE mode or VIEW at version mode
288 289 compare = at_version != prev_at_version
289 290
290 291 # pull_requests repo_name we opened it against
291 292 # ie. target_repo must match
292 293 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
293 294 raise HTTPNotFound()
294 295
295 296 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
296 297 pull_request_at_ver)
297 298
298 299 c.pull_request = pull_request_display_obj
299 300 c.pull_request_latest = pull_request_latest
300 301
301 302 if compare or (at_version and not at_version == 'latest'):
302 303 c.allowed_to_change_status = False
303 304 c.allowed_to_update = False
304 305 c.allowed_to_merge = False
305 306 c.allowed_to_delete = False
306 307 c.allowed_to_comment = False
307 308 c.allowed_to_close = False
308 309 else:
309 310 can_change_status = PullRequestModel().check_user_change_status(
310 311 pull_request_at_ver, self._rhodecode_user)
311 312 c.allowed_to_change_status = can_change_status and not pr_closed
312 313
313 314 c.allowed_to_update = PullRequestModel().check_user_update(
314 315 pull_request_latest, self._rhodecode_user) and not pr_closed
315 316 c.allowed_to_merge = PullRequestModel().check_user_merge(
316 317 pull_request_latest, self._rhodecode_user) and not pr_closed
317 318 c.allowed_to_delete = PullRequestModel().check_user_delete(
318 319 pull_request_latest, self._rhodecode_user) and not pr_closed
319 320 c.allowed_to_comment = not pr_closed
320 321 c.allowed_to_close = c.allowed_to_merge and not pr_closed
321 322
322 323 c.forbid_adding_reviewers = False
323 324 c.forbid_author_to_review = False
324 325 c.forbid_commit_author_to_review = False
325 326
326 327 if pull_request_latest.reviewer_data and \
327 328 'rules' in pull_request_latest.reviewer_data:
328 329 rules = pull_request_latest.reviewer_data['rules'] or {}
329 330 try:
330 331 c.forbid_adding_reviewers = rules.get(
331 332 'forbid_adding_reviewers')
332 333 c.forbid_author_to_review = rules.get(
333 334 'forbid_author_to_review')
334 335 c.forbid_commit_author_to_review = rules.get(
335 336 'forbid_commit_author_to_review')
336 337 except Exception:
337 338 pass
338 339
339 340 # check merge capabilities
340 341 _merge_check = MergeCheck.validate(
341 342 pull_request_latest, user=self._rhodecode_user,
342 translator=self.request.translate)
343 translator=self.request.translate, force_shadow_repo_refresh=force_refresh)
343 344 c.pr_merge_errors = _merge_check.error_details
344 345 c.pr_merge_possible = not _merge_check.failed
345 346 c.pr_merge_message = _merge_check.merge_msg
346 347
347 348 c.pr_merge_info = MergeCheck.get_merge_conditions(
348 349 pull_request_latest, translator=self.request.translate)
349 350
350 351 c.pull_request_review_status = _merge_check.review_status
351 352 if merge_checks:
352 353 self.request.override_renderer = \
353 354 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
354 355 return self._get_template_context(c)
355 356
356 357 comments_model = CommentsModel()
357 358
358 359 # reviewers and statuses
359 360 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
360 361 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
361 362
362 363 # GENERAL COMMENTS with versions #
363 364 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
364 365 q = q.order_by(ChangesetComment.comment_id.asc())
365 366 general_comments = q
366 367
367 368 # pick comments we want to render at current version
368 369 c.comment_versions = comments_model.aggregate_comments(
369 370 general_comments, versions, c.at_version_num)
370 371 c.comments = c.comment_versions[c.at_version_num]['until']
371 372
372 373 # INLINE COMMENTS with versions #
373 374 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
374 375 q = q.order_by(ChangesetComment.comment_id.asc())
375 376 inline_comments = q
376 377
377 378 c.inline_versions = comments_model.aggregate_comments(
378 379 inline_comments, versions, c.at_version_num, inline=True)
379 380
380 381 # inject latest version
381 382 latest_ver = PullRequest.get_pr_display_object(
382 383 pull_request_latest, pull_request_latest)
383 384
384 385 c.versions = versions + [latest_ver]
385 386
386 387 # if we use version, then do not show later comments
387 388 # than current version
388 389 display_inline_comments = collections.defaultdict(
389 390 lambda: collections.defaultdict(list))
390 391 for co in inline_comments:
391 392 if c.at_version_num:
392 393 # pick comments that are at least UPTO given version, so we
393 394 # don't render comments for higher version
394 395 should_render = co.pull_request_version_id and \
395 396 co.pull_request_version_id <= c.at_version_num
396 397 else:
397 398 # showing all, for 'latest'
398 399 should_render = True
399 400
400 401 if should_render:
401 402 display_inline_comments[co.f_path][co.line_no].append(co)
402 403
403 404 # load diff data into template context, if we use compare mode then
404 405 # diff is calculated based on changes between versions of PR
405 406
406 407 source_repo = pull_request_at_ver.source_repo
407 408 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
408 409
409 410 target_repo = pull_request_at_ver.target_repo
410 411 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
411 412
412 413 if compare:
413 414 # in compare switch the diff base to latest commit from prev version
414 415 target_ref_id = prev_pull_request_display_obj.revisions[0]
415 416
416 417 # despite opening commits for bookmarks/branches/tags, we always
417 418 # convert this to rev to prevent changes after bookmark or branch change
418 419 c.source_ref_type = 'rev'
419 420 c.source_ref = source_ref_id
420 421
421 422 c.target_ref_type = 'rev'
422 423 c.target_ref = target_ref_id
423 424
424 425 c.source_repo = source_repo
425 426 c.target_repo = target_repo
426 427
427 428 c.commit_ranges = []
428 429 source_commit = EmptyCommit()
429 430 target_commit = EmptyCommit()
430 431 c.missing_requirements = False
431 432
432 433 source_scm = source_repo.scm_instance()
433 434 target_scm = target_repo.scm_instance()
434 435
435 436 # try first shadow repo, fallback to regular repo
436 437 try:
437 438 commits_source_repo = pull_request_latest.get_shadow_repo()
438 439 except Exception:
439 440 log.debug('Failed to get shadow repo', exc_info=True)
440 441 commits_source_repo = source_scm
441 442
442 443 c.commits_source_repo = commits_source_repo
443 444 c.ancestor = None # set it to None, to hide it from PR view
444 445
445 446 # empty version means latest, so we keep this to prevent
446 447 # double caching
447 448 version_normalized = version or 'latest'
448 449 from_version_normalized = from_version or 'latest'
449 450
450 451 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
451 452 target_repo)
452 453 cache_file_path = diff_cache_exist(
453 454 cache_path, 'pull_request', pull_request_id, version_normalized,
454 455 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
455 456
456 457 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
457 458 force_recache = str2bool(self.request.GET.get('force_recache'))
458 459
459 460 cached_diff = None
460 461 if caching_enabled:
461 462 cached_diff = load_cached_diff(cache_file_path)
462 463
463 464 has_proper_commit_cache = (
464 465 cached_diff and cached_diff.get('commits')
465 466 and len(cached_diff.get('commits', [])) == 5
466 467 and cached_diff.get('commits')[0]
467 468 and cached_diff.get('commits')[3])
468 469 if not force_recache and has_proper_commit_cache:
469 470 diff_commit_cache = \
470 471 (ancestor_commit, commit_cache, missing_requirements,
471 472 source_commit, target_commit) = cached_diff['commits']
472 473 else:
473 474 diff_commit_cache = \
474 475 (ancestor_commit, commit_cache, missing_requirements,
475 476 source_commit, target_commit) = self.get_commits(
476 477 commits_source_repo,
477 478 pull_request_at_ver,
478 479 source_commit,
479 480 source_ref_id,
480 481 source_scm,
481 482 target_commit,
482 483 target_ref_id,
483 484 target_scm)
484 485
485 486 # register our commit range
486 487 for comm in commit_cache.values():
487 488 c.commit_ranges.append(comm)
488 489
489 490 c.missing_requirements = missing_requirements
490 491 c.ancestor_commit = ancestor_commit
491 492 c.statuses = source_repo.statuses(
492 493 [x.raw_id for x in c.commit_ranges])
493 494
494 495 # auto collapse if we have more than limit
495 496 collapse_limit = diffs.DiffProcessor._collapse_commits_over
496 497 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
497 498 c.compare_mode = compare
498 499
499 500 # diff_limit is the old behavior, will cut off the whole diff
500 501 # if the limit is applied otherwise will just hide the
501 502 # big files from the front-end
502 503 diff_limit = c.visual.cut_off_limit_diff
503 504 file_limit = c.visual.cut_off_limit_file
504 505
505 506 c.missing_commits = False
506 507 if (c.missing_requirements
507 508 or isinstance(source_commit, EmptyCommit)
508 509 or source_commit == target_commit):
509 510
510 511 c.missing_commits = True
511 512 else:
512 513 c.inline_comments = display_inline_comments
513 514
514 515 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
515 516 if not force_recache and has_proper_diff_cache:
516 517 c.diffset = cached_diff['diff']
517 518 (ancestor_commit, commit_cache, missing_requirements,
518 519 source_commit, target_commit) = cached_diff['commits']
519 520 else:
520 521 c.diffset = self._get_diffset(
521 522 c.source_repo.repo_name, commits_source_repo,
522 523 source_ref_id, target_ref_id,
523 524 target_commit, source_commit,
524 525 diff_limit, file_limit, c.fulldiff)
525 526
526 527 # save cached diff
527 528 if caching_enabled:
528 529 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
529 530
530 531 c.limited_diff = c.diffset.limited_diff
531 532
532 533 # calculate removed files that are bound to comments
533 534 comment_deleted_files = [
534 535 fname for fname in display_inline_comments
535 536 if fname not in c.diffset.file_stats]
536 537
537 538 c.deleted_files_comments = collections.defaultdict(dict)
538 539 for fname, per_line_comments in display_inline_comments.items():
539 540 if fname in comment_deleted_files:
540 541 c.deleted_files_comments[fname]['stats'] = 0
541 542 c.deleted_files_comments[fname]['comments'] = list()
542 543 for lno, comments in per_line_comments.items():
543 544 c.deleted_files_comments[fname]['comments'].extend(
544 545 comments)
545 546
546 547 # this is a hack to properly display links, when creating PR, the
547 548 # compare view and others uses different notation, and
548 549 # compare_commits.mako renders links based on the target_repo.
549 550 # We need to swap that here to generate it properly on the html side
550 551 c.target_repo = c.source_repo
551 552
552 553 c.commit_statuses = ChangesetStatus.STATUSES
553 554
554 555 c.show_version_changes = not pr_closed
555 556 if c.show_version_changes:
556 557 cur_obj = pull_request_at_ver
557 558 prev_obj = prev_pull_request_at_ver
558 559
559 560 old_commit_ids = prev_obj.revisions
560 561 new_commit_ids = cur_obj.revisions
561 562 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 563 old_commit_ids, new_commit_ids)
563 564 c.commit_changes_summary = commit_changes
564 565
565 566 # calculate the diff for commits between versions
566 567 c.commit_changes = []
567 568 mark = lambda cs, fw: list(
568 569 h.itertools.izip_longest([], cs, fillvalue=fw))
569 570 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 571 + mark(commit_changes.removed, 'r') \
571 572 + mark(commit_changes.common, 'c'):
572 573
573 574 if raw_id in commit_cache:
574 575 commit = commit_cache[raw_id]
575 576 else:
576 577 try:
577 578 commit = commits_source_repo.get_commit(raw_id)
578 579 except CommitDoesNotExistError:
579 580 # in case we fail extracting still use "dummy" commit
580 581 # for display in commit diff
581 582 commit = h.AttributeDict(
582 583 {'raw_id': raw_id,
583 584 'message': 'EMPTY or MISSING COMMIT'})
584 585 c.commit_changes.append([c_type, commit])
585 586
586 587 # current user review statuses for each version
587 588 c.review_versions = {}
588 589 if self._rhodecode_user.user_id in allowed_reviewers:
589 590 for co in general_comments:
590 591 if co.author.user_id == self._rhodecode_user.user_id:
591 592 status = co.status_change
592 593 if status:
593 594 _ver_pr = status[0].comment.pull_request_version_id
594 595 c.review_versions[_ver_pr] = status[0]
595 596
596 597 return self._get_template_context(c)
597 598
598 599 def get_commits(
599 600 self, commits_source_repo, pull_request_at_ver, source_commit,
600 601 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
601 602 commit_cache = collections.OrderedDict()
602 603 missing_requirements = False
603 604 try:
604 605 pre_load = ["author", "branch", "date", "message"]
605 606 show_revs = pull_request_at_ver.revisions
606 607 for rev in show_revs:
607 608 comm = commits_source_repo.get_commit(
608 609 commit_id=rev, pre_load=pre_load)
609 610 commit_cache[comm.raw_id] = comm
610 611
611 612 # Order here matters, we first need to get target, and then
612 613 # the source
613 614 target_commit = commits_source_repo.get_commit(
614 615 commit_id=safe_str(target_ref_id))
615 616
616 617 source_commit = commits_source_repo.get_commit(
617 618 commit_id=safe_str(source_ref_id))
618 619 except CommitDoesNotExistError:
619 620 log.warning(
620 621 'Failed to get commit from `{}` repo'.format(
621 622 commits_source_repo), exc_info=True)
622 623 except RepositoryRequirementError:
623 624 log.warning(
624 625 'Failed to get all required data from repo', exc_info=True)
625 626 missing_requirements = True
626 627 ancestor_commit = None
627 628 try:
628 629 ancestor_id = source_scm.get_common_ancestor(
629 630 source_commit.raw_id, target_commit.raw_id, target_scm)
630 631 ancestor_commit = source_scm.get_commit(ancestor_id)
631 632 except Exception:
632 633 ancestor_commit = None
633 634 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
634 635
635 636 def assure_not_empty_repo(self):
636 637 _ = self.request.translate
637 638
638 639 try:
639 640 self.db_repo.scm_instance().get_commit()
640 641 except EmptyRepositoryError:
641 642 h.flash(h.literal(_('There are no commits yet')),
642 643 category='warning')
643 644 raise HTTPFound(
644 645 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
645 646
646 647 @LoginRequired()
647 648 @NotAnonymous()
648 649 @HasRepoPermissionAnyDecorator(
649 650 'repository.read', 'repository.write', 'repository.admin')
650 651 @view_config(
651 652 route_name='pullrequest_new', request_method='GET',
652 653 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
653 654 def pull_request_new(self):
654 655 _ = self.request.translate
655 656 c = self.load_default_context()
656 657
657 658 self.assure_not_empty_repo()
658 659 source_repo = self.db_repo
659 660
660 661 commit_id = self.request.GET.get('commit')
661 662 branch_ref = self.request.GET.get('branch')
662 663 bookmark_ref = self.request.GET.get('bookmark')
663 664
664 665 try:
665 666 source_repo_data = PullRequestModel().generate_repo_data(
666 667 source_repo, commit_id=commit_id,
667 668 branch=branch_ref, bookmark=bookmark_ref,
668 669 translator=self.request.translate)
669 670 except CommitDoesNotExistError as e:
670 671 log.exception(e)
671 672 h.flash(_('Commit does not exist'), 'error')
672 673 raise HTTPFound(
673 674 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
674 675
675 676 default_target_repo = source_repo
676 677
677 678 if source_repo.parent:
678 679 parent_vcs_obj = source_repo.parent.scm_instance()
679 680 if parent_vcs_obj and not parent_vcs_obj.is_empty():
680 681 # change default if we have a parent repo
681 682 default_target_repo = source_repo.parent
682 683
683 684 target_repo_data = PullRequestModel().generate_repo_data(
684 685 default_target_repo, translator=self.request.translate)
685 686
686 687 selected_source_ref = source_repo_data['refs']['selected_ref']
687 688 title_source_ref = ''
688 689 if selected_source_ref:
689 690 title_source_ref = selected_source_ref.split(':', 2)[1]
690 691 c.default_title = PullRequestModel().generate_pullrequest_title(
691 692 source=source_repo.repo_name,
692 693 source_ref=title_source_ref,
693 694 target=default_target_repo.repo_name
694 695 )
695 696
696 697 c.default_repo_data = {
697 698 'source_repo_name': source_repo.repo_name,
698 699 'source_refs_json': json.dumps(source_repo_data),
699 700 'target_repo_name': default_target_repo.repo_name,
700 701 'target_refs_json': json.dumps(target_repo_data),
701 702 }
702 703 c.default_source_ref = selected_source_ref
703 704
704 705 return self._get_template_context(c)
705 706
706 707 @LoginRequired()
707 708 @NotAnonymous()
708 709 @HasRepoPermissionAnyDecorator(
709 710 'repository.read', 'repository.write', 'repository.admin')
710 711 @view_config(
711 712 route_name='pullrequest_repo_refs', request_method='GET',
712 713 renderer='json_ext', xhr=True)
713 714 def pull_request_repo_refs(self):
714 715 self.load_default_context()
715 716 target_repo_name = self.request.matchdict['target_repo_name']
716 717 repo = Repository.get_by_repo_name(target_repo_name)
717 718 if not repo:
718 719 raise HTTPNotFound()
719 720
720 721 target_perm = HasRepoPermissionAny(
721 722 'repository.read', 'repository.write', 'repository.admin')(
722 723 target_repo_name)
723 724 if not target_perm:
724 725 raise HTTPNotFound()
725 726
726 727 return PullRequestModel().generate_repo_data(
727 728 repo, translator=self.request.translate)
728 729
729 730 @LoginRequired()
730 731 @NotAnonymous()
731 732 @HasRepoPermissionAnyDecorator(
732 733 'repository.read', 'repository.write', 'repository.admin')
733 734 @view_config(
734 735 route_name='pullrequest_repo_destinations', request_method='GET',
735 736 renderer='json_ext', xhr=True)
736 737 def pull_request_repo_destinations(self):
737 738 _ = self.request.translate
738 739 filter_query = self.request.GET.get('query')
739 740
740 741 query = Repository.query() \
741 742 .order_by(func.length(Repository.repo_name)) \
742 743 .filter(
743 744 or_(Repository.repo_name == self.db_repo.repo_name,
744 745 Repository.fork_id == self.db_repo.repo_id))
745 746
746 747 if filter_query:
747 748 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
748 749 query = query.filter(
749 750 Repository.repo_name.ilike(ilike_expression))
750 751
751 752 add_parent = False
752 753 if self.db_repo.parent:
753 754 if filter_query in self.db_repo.parent.repo_name:
754 755 parent_vcs_obj = self.db_repo.parent.scm_instance()
755 756 if parent_vcs_obj and not parent_vcs_obj.is_empty():
756 757 add_parent = True
757 758
758 759 limit = 20 - 1 if add_parent else 20
759 760 all_repos = query.limit(limit).all()
760 761 if add_parent:
761 762 all_repos += [self.db_repo.parent]
762 763
763 764 repos = []
764 765 for obj in ScmModel().get_repos(all_repos):
765 766 repos.append({
766 767 'id': obj['name'],
767 768 'text': obj['name'],
768 769 'type': 'repo',
769 770 'repo_id': obj['dbrepo']['repo_id'],
770 771 'repo_type': obj['dbrepo']['repo_type'],
771 772 'private': obj['dbrepo']['private'],
772 773
773 774 })
774 775
775 776 data = {
776 777 'more': False,
777 778 'results': [{
778 779 'text': _('Repositories'),
779 780 'children': repos
780 781 }] if repos else []
781 782 }
782 783 return data
783 784
784 785 @LoginRequired()
785 786 @NotAnonymous()
786 787 @HasRepoPermissionAnyDecorator(
787 788 'repository.read', 'repository.write', 'repository.admin')
788 789 @CSRFRequired()
789 790 @view_config(
790 791 route_name='pullrequest_create', request_method='POST',
791 792 renderer=None)
792 793 def pull_request_create(self):
793 794 _ = self.request.translate
794 795 self.assure_not_empty_repo()
795 796 self.load_default_context()
796 797
797 798 controls = peppercorn.parse(self.request.POST.items())
798 799
799 800 try:
800 801 form = PullRequestForm(
801 802 self.request.translate, self.db_repo.repo_id)()
802 803 _form = form.to_python(controls)
803 804 except formencode.Invalid as errors:
804 805 if errors.error_dict.get('revisions'):
805 806 msg = 'Revisions: %s' % errors.error_dict['revisions']
806 807 elif errors.error_dict.get('pullrequest_title'):
807 808 msg = errors.error_dict.get('pullrequest_title')
808 809 else:
809 810 msg = _('Error creating pull request: {}').format(errors)
810 811 log.exception(msg)
811 812 h.flash(msg, 'error')
812 813
813 814 # would rather just go back to form ...
814 815 raise HTTPFound(
815 816 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
816 817
817 818 source_repo = _form['source_repo']
818 819 source_ref = _form['source_ref']
819 820 target_repo = _form['target_repo']
820 821 target_ref = _form['target_ref']
821 822 commit_ids = _form['revisions'][::-1]
822 823
823 824 # find the ancestor for this pr
824 825 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
825 826 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
826 827
827 828 # re-check permissions again here
828 829 # source_repo we must have read permissions
829 830
830 831 source_perm = HasRepoPermissionAny(
831 832 'repository.read',
832 833 'repository.write', 'repository.admin')(source_db_repo.repo_name)
833 834 if not source_perm:
834 835 msg = _('Not Enough permissions to source repo `{}`.'.format(
835 836 source_db_repo.repo_name))
836 837 h.flash(msg, category='error')
837 838 # copy the args back to redirect
838 839 org_query = self.request.GET.mixed()
839 840 raise HTTPFound(
840 841 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
841 842 _query=org_query))
842 843
843 844 # target repo we must have read permissions, and also later on
844 845 # we want to check branch permissions here
845 846 target_perm = HasRepoPermissionAny(
846 847 'repository.read',
847 848 'repository.write', 'repository.admin')(target_db_repo.repo_name)
848 849 if not target_perm:
849 850 msg = _('Not Enough permissions to target repo `{}`.'.format(
850 851 target_db_repo.repo_name))
851 852 h.flash(msg, category='error')
852 853 # copy the args back to redirect
853 854 org_query = self.request.GET.mixed()
854 855 raise HTTPFound(
855 856 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
856 857 _query=org_query))
857 858
858 859 source_scm = source_db_repo.scm_instance()
859 860 target_scm = target_db_repo.scm_instance()
860 861
861 862 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
862 863 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
863 864
864 865 ancestor = source_scm.get_common_ancestor(
865 866 source_commit.raw_id, target_commit.raw_id, target_scm)
866 867
867 868 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
868 869 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
869 870
870 871 pullrequest_title = _form['pullrequest_title']
871 872 title_source_ref = source_ref.split(':', 2)[1]
872 873 if not pullrequest_title:
873 874 pullrequest_title = PullRequestModel().generate_pullrequest_title(
874 875 source=source_repo,
875 876 source_ref=title_source_ref,
876 877 target=target_repo
877 878 )
878 879
879 880 description = _form['pullrequest_desc']
880 881
881 882 get_default_reviewers_data, validate_default_reviewers = \
882 883 PullRequestModel().get_reviewer_functions()
883 884
884 885 # recalculate reviewers logic, to make sure we can validate this
885 886 reviewer_rules = get_default_reviewers_data(
886 887 self._rhodecode_db_user, source_db_repo,
887 888 source_commit, target_db_repo, target_commit)
888 889
889 890 given_reviewers = _form['review_members']
890 891 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
891 892
892 893 try:
893 894 pull_request = PullRequestModel().create(
894 895 self._rhodecode_user.user_id, source_repo, source_ref,
895 896 target_repo, target_ref, commit_ids, reviewers,
896 897 pullrequest_title, description, reviewer_rules
897 898 )
898 899 Session().commit()
899 900
900 901 h.flash(_('Successfully opened new pull request'),
901 902 category='success')
902 903 except Exception:
903 904 msg = _('Error occurred during creation of this pull request.')
904 905 log.exception(msg)
905 906 h.flash(msg, category='error')
906 907
907 908 # copy the args back to redirect
908 909 org_query = self.request.GET.mixed()
909 910 raise HTTPFound(
910 911 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
911 912 _query=org_query))
912 913
913 914 raise HTTPFound(
914 915 h.route_path('pullrequest_show', repo_name=target_repo,
915 916 pull_request_id=pull_request.pull_request_id))
916 917
917 918 @LoginRequired()
918 919 @NotAnonymous()
919 920 @HasRepoPermissionAnyDecorator(
920 921 'repository.read', 'repository.write', 'repository.admin')
921 922 @CSRFRequired()
922 923 @view_config(
923 924 route_name='pullrequest_update', request_method='POST',
924 925 renderer='json_ext')
925 926 def pull_request_update(self):
926 927 pull_request = PullRequest.get_or_404(
927 928 self.request.matchdict['pull_request_id'])
928 929 _ = self.request.translate
929 930
930 931 self.load_default_context()
931 932
932 933 if pull_request.is_closed():
933 934 log.debug('update: forbidden because pull request is closed')
934 935 msg = _(u'Cannot update closed pull requests.')
935 936 h.flash(msg, category='error')
936 937 return True
937 938
938 939 # only owner or admin can update it
939 940 allowed_to_update = PullRequestModel().check_user_update(
940 941 pull_request, self._rhodecode_user)
941 942 if allowed_to_update:
942 943 controls = peppercorn.parse(self.request.POST.items())
943 944
944 945 if 'review_members' in controls:
945 946 self._update_reviewers(
946 947 pull_request, controls['review_members'],
947 948 pull_request.reviewer_data)
948 949 elif str2bool(self.request.POST.get('update_commits', 'false')):
949 950 self._update_commits(pull_request)
950 951 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
951 952 self._edit_pull_request(pull_request)
952 953 else:
953 954 raise HTTPBadRequest()
954 955 return True
955 956 raise HTTPForbidden()
956 957
957 958 def _edit_pull_request(self, pull_request):
958 959 _ = self.request.translate
959 960 try:
960 961 PullRequestModel().edit(
961 962 pull_request, self.request.POST.get('title'),
962 963 self.request.POST.get('description'), self._rhodecode_user)
963 964 except ValueError:
964 965 msg = _(u'Cannot update closed pull requests.')
965 966 h.flash(msg, category='error')
966 967 return
967 968 else:
968 969 Session().commit()
969 970
970 971 msg = _(u'Pull request title & description updated.')
971 972 h.flash(msg, category='success')
972 973 return
973 974
974 975 def _update_commits(self, pull_request):
975 976 _ = self.request.translate
976 977 resp = PullRequestModel().update_commits(pull_request)
977 978
978 979 if resp.executed:
979 980
980 981 if resp.target_changed and resp.source_changed:
981 982 changed = 'target and source repositories'
982 983 elif resp.target_changed and not resp.source_changed:
983 984 changed = 'target repository'
984 985 elif not resp.target_changed and resp.source_changed:
985 986 changed = 'source repository'
986 987 else:
987 988 changed = 'nothing'
988 989
989 990 msg = _(
990 991 u'Pull request updated to "{source_commit_id}" with '
991 992 u'{count_added} added, {count_removed} removed commits. '
992 993 u'Source of changes: {change_source}')
993 994 msg = msg.format(
994 995 source_commit_id=pull_request.source_ref_parts.commit_id,
995 996 count_added=len(resp.changes.added),
996 997 count_removed=len(resp.changes.removed),
997 998 change_source=changed)
998 999 h.flash(msg, category='success')
999 1000
1000 1001 channel = '/repo${}$/pr/{}'.format(
1001 1002 pull_request.target_repo.repo_name,
1002 1003 pull_request.pull_request_id)
1003 1004 message = msg + (
1004 1005 ' - <a onclick="window.location.reload()">'
1005 1006 '<strong>{}</strong></a>'.format(_('Reload page')))
1006 1007 channelstream.post_message(
1007 1008 channel, message, self._rhodecode_user.username,
1008 1009 registry=self.request.registry)
1009 1010 else:
1010 1011 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1011 1012 warning_reasons = [
1012 1013 UpdateFailureReason.NO_CHANGE,
1013 1014 UpdateFailureReason.WRONG_REF_TYPE,
1014 1015 ]
1015 1016 category = 'warning' if resp.reason in warning_reasons else 'error'
1016 1017 h.flash(msg, category=category)
1017 1018
1018 1019 @LoginRequired()
1019 1020 @NotAnonymous()
1020 1021 @HasRepoPermissionAnyDecorator(
1021 1022 'repository.read', 'repository.write', 'repository.admin')
1022 1023 @CSRFRequired()
1023 1024 @view_config(
1024 1025 route_name='pullrequest_merge', request_method='POST',
1025 1026 renderer='json_ext')
1026 1027 def pull_request_merge(self):
1027 1028 """
1028 1029 Merge will perform a server-side merge of the specified
1029 1030 pull request, if the pull request is approved and mergeable.
1030 1031 After successful merging, the pull request is automatically
1031 1032 closed, with a relevant comment.
1032 1033 """
1033 1034 pull_request = PullRequest.get_or_404(
1034 1035 self.request.matchdict['pull_request_id'])
1035 1036
1036 1037 self.load_default_context()
1037 1038 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1038 1039 translator=self.request.translate)
1039 1040 merge_possible = not check.failed
1040 1041
1041 1042 for err_type, error_msg in check.errors:
1042 1043 h.flash(error_msg, category=err_type)
1043 1044
1044 1045 if merge_possible:
1045 1046 log.debug("Pre-conditions checked, trying to merge.")
1046 1047 extras = vcs_operation_context(
1047 1048 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1048 1049 username=self._rhodecode_db_user.username, action='push',
1049 1050 scm=pull_request.target_repo.repo_type)
1050 1051 self._merge_pull_request(
1051 1052 pull_request, self._rhodecode_db_user, extras)
1052 1053 else:
1053 1054 log.debug("Pre-conditions failed, NOT merging.")
1054 1055
1055 1056 raise HTTPFound(
1056 1057 h.route_path('pullrequest_show',
1057 1058 repo_name=pull_request.target_repo.repo_name,
1058 1059 pull_request_id=pull_request.pull_request_id))
1059 1060
1060 1061 def _merge_pull_request(self, pull_request, user, extras):
1061 1062 _ = self.request.translate
1062 1063 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1063 1064
1064 1065 if merge_resp.executed:
1065 1066 log.debug("The merge was successful, closing the pull request.")
1066 1067 PullRequestModel().close_pull_request(
1067 1068 pull_request.pull_request_id, user)
1068 1069 Session().commit()
1069 1070 msg = _('Pull request was successfully merged and closed.')
1070 1071 h.flash(msg, category='success')
1071 1072 else:
1072 1073 log.debug(
1073 1074 "The merge was not successful. Merge response: %s",
1074 1075 merge_resp)
1075 1076 msg = PullRequestModel().merge_status_message(
1076 1077 merge_resp.failure_reason)
1077 1078 h.flash(msg, category='error')
1078 1079
1079 1080 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1080 1081 _ = self.request.translate
1081 1082 get_default_reviewers_data, validate_default_reviewers = \
1082 1083 PullRequestModel().get_reviewer_functions()
1083 1084
1084 1085 try:
1085 1086 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1086 1087 except ValueError as e:
1087 1088 log.error('Reviewers Validation: {}'.format(e))
1088 1089 h.flash(e, category='error')
1089 1090 return
1090 1091
1091 1092 PullRequestModel().update_reviewers(
1092 1093 pull_request, reviewers, self._rhodecode_user)
1093 1094 h.flash(_('Pull request reviewers updated.'), category='success')
1094 1095 Session().commit()
1095 1096
1096 1097 @LoginRequired()
1097 1098 @NotAnonymous()
1098 1099 @HasRepoPermissionAnyDecorator(
1099 1100 'repository.read', 'repository.write', 'repository.admin')
1100 1101 @CSRFRequired()
1101 1102 @view_config(
1102 1103 route_name='pullrequest_delete', request_method='POST',
1103 1104 renderer='json_ext')
1104 1105 def pull_request_delete(self):
1105 1106 _ = self.request.translate
1106 1107
1107 1108 pull_request = PullRequest.get_or_404(
1108 1109 self.request.matchdict['pull_request_id'])
1109 1110 self.load_default_context()
1110 1111
1111 1112 pr_closed = pull_request.is_closed()
1112 1113 allowed_to_delete = PullRequestModel().check_user_delete(
1113 1114 pull_request, self._rhodecode_user) and not pr_closed
1114 1115
1115 1116 # only owner can delete it !
1116 1117 if allowed_to_delete:
1117 1118 PullRequestModel().delete(pull_request, self._rhodecode_user)
1118 1119 Session().commit()
1119 1120 h.flash(_('Successfully deleted pull request'),
1120 1121 category='success')
1121 1122 raise HTTPFound(h.route_path('pullrequest_show_all',
1122 1123 repo_name=self.db_repo_name))
1123 1124
1124 1125 log.warning('user %s tried to delete pull request without access',
1125 1126 self._rhodecode_user)
1126 1127 raise HTTPNotFound()
1127 1128
1128 1129 @LoginRequired()
1129 1130 @NotAnonymous()
1130 1131 @HasRepoPermissionAnyDecorator(
1131 1132 'repository.read', 'repository.write', 'repository.admin')
1132 1133 @CSRFRequired()
1133 1134 @view_config(
1134 1135 route_name='pullrequest_comment_create', request_method='POST',
1135 1136 renderer='json_ext')
1136 1137 def pull_request_comment_create(self):
1137 1138 _ = self.request.translate
1138 1139
1139 1140 pull_request = PullRequest.get_or_404(
1140 1141 self.request.matchdict['pull_request_id'])
1141 1142 pull_request_id = pull_request.pull_request_id
1142 1143
1143 1144 if pull_request.is_closed():
1144 1145 log.debug('comment: forbidden because pull request is closed')
1145 1146 raise HTTPForbidden()
1146 1147
1147 1148 allowed_to_comment = PullRequestModel().check_user_comment(
1148 1149 pull_request, self._rhodecode_user)
1149 1150 if not allowed_to_comment:
1150 1151 log.debug(
1151 1152 'comment: forbidden because pull request is from forbidden repo')
1152 1153 raise HTTPForbidden()
1153 1154
1154 1155 c = self.load_default_context()
1155 1156
1156 1157 status = self.request.POST.get('changeset_status', None)
1157 1158 text = self.request.POST.get('text')
1158 1159 comment_type = self.request.POST.get('comment_type')
1159 1160 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1160 1161 close_pull_request = self.request.POST.get('close_pull_request')
1161 1162
1162 1163 # the logic here should work like following, if we submit close
1163 1164 # pr comment, use `close_pull_request_with_comment` function
1164 1165 # else handle regular comment logic
1165 1166
1166 1167 if close_pull_request:
1167 1168 # only owner or admin or person with write permissions
1168 1169 allowed_to_close = PullRequestModel().check_user_update(
1169 1170 pull_request, self._rhodecode_user)
1170 1171 if not allowed_to_close:
1171 1172 log.debug('comment: forbidden because not allowed to close '
1172 1173 'pull request %s', pull_request_id)
1173 1174 raise HTTPForbidden()
1174 1175 comment, status = PullRequestModel().close_pull_request_with_comment(
1175 1176 pull_request, self._rhodecode_user, self.db_repo, message=text)
1176 1177 Session().flush()
1177 1178 events.trigger(
1178 1179 events.PullRequestCommentEvent(pull_request, comment))
1179 1180
1180 1181 else:
1181 1182 # regular comment case, could be inline, or one with status.
1182 1183 # for that one we check also permissions
1183 1184
1184 1185 allowed_to_change_status = PullRequestModel().check_user_change_status(
1185 1186 pull_request, self._rhodecode_user)
1186 1187
1187 1188 if status and allowed_to_change_status:
1188 1189 message = (_('Status change %(transition_icon)s %(status)s')
1189 1190 % {'transition_icon': '>',
1190 1191 'status': ChangesetStatus.get_status_lbl(status)})
1191 1192 text = text or message
1192 1193
1193 1194 comment = CommentsModel().create(
1194 1195 text=text,
1195 1196 repo=self.db_repo.repo_id,
1196 1197 user=self._rhodecode_user.user_id,
1197 1198 pull_request=pull_request,
1198 1199 f_path=self.request.POST.get('f_path'),
1199 1200 line_no=self.request.POST.get('line'),
1200 1201 status_change=(ChangesetStatus.get_status_lbl(status)
1201 1202 if status and allowed_to_change_status else None),
1202 1203 status_change_type=(status
1203 1204 if status and allowed_to_change_status else None),
1204 1205 comment_type=comment_type,
1205 1206 resolves_comment_id=resolves_comment_id
1206 1207 )
1207 1208
1208 1209 if allowed_to_change_status:
1209 1210 # calculate old status before we change it
1210 1211 old_calculated_status = pull_request.calculated_review_status()
1211 1212
1212 1213 # get status if set !
1213 1214 if status:
1214 1215 ChangesetStatusModel().set_status(
1215 1216 self.db_repo.repo_id,
1216 1217 status,
1217 1218 self._rhodecode_user.user_id,
1218 1219 comment,
1219 1220 pull_request=pull_request
1220 1221 )
1221 1222
1222 1223 Session().flush()
1223 1224 # this is somehow required to get access to some relationship
1224 1225 # loaded on comment
1225 1226 Session().refresh(comment)
1226 1227
1227 1228 events.trigger(
1228 1229 events.PullRequestCommentEvent(pull_request, comment))
1229 1230
1230 1231 # we now calculate the status of pull request, and based on that
1231 1232 # calculation we set the commits status
1232 1233 calculated_status = pull_request.calculated_review_status()
1233 1234 if old_calculated_status != calculated_status:
1234 1235 PullRequestModel()._trigger_pull_request_hook(
1235 1236 pull_request, self._rhodecode_user, 'review_status_change')
1236 1237
1237 1238 Session().commit()
1238 1239
1239 1240 data = {
1240 1241 'target_id': h.safeid(h.safe_unicode(
1241 1242 self.request.POST.get('f_path'))),
1242 1243 }
1243 1244 if comment:
1244 1245 c.co = comment
1245 1246 rendered_comment = render(
1246 1247 'rhodecode:templates/changeset/changeset_comment_block.mako',
1247 1248 self._get_template_context(c), self.request)
1248 1249
1249 1250 data.update(comment.get_dict())
1250 1251 data.update({'rendered_text': rendered_comment})
1251 1252
1252 1253 return data
1253 1254
1254 1255 @LoginRequired()
1255 1256 @NotAnonymous()
1256 1257 @HasRepoPermissionAnyDecorator(
1257 1258 'repository.read', 'repository.write', 'repository.admin')
1258 1259 @CSRFRequired()
1259 1260 @view_config(
1260 1261 route_name='pullrequest_comment_delete', request_method='POST',
1261 1262 renderer='json_ext')
1262 1263 def pull_request_comment_delete(self):
1263 1264 pull_request = PullRequest.get_or_404(
1264 1265 self.request.matchdict['pull_request_id'])
1265 1266
1266 1267 comment = ChangesetComment.get_or_404(
1267 1268 self.request.matchdict['comment_id'])
1268 1269 comment_id = comment.comment_id
1269 1270
1270 1271 if pull_request.is_closed():
1271 1272 log.debug('comment: forbidden because pull request is closed')
1272 1273 raise HTTPForbidden()
1273 1274
1274 1275 if not comment:
1275 1276 log.debug('Comment with id:%s not found, skipping', comment_id)
1276 1277 # comment already deleted in another call probably
1277 1278 return True
1278 1279
1279 1280 if comment.pull_request.is_closed():
1280 1281 # don't allow deleting comments on closed pull request
1281 1282 raise HTTPForbidden()
1282 1283
1283 1284 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1284 1285 super_admin = h.HasPermissionAny('hg.admin')()
1285 1286 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1286 1287 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1287 1288 comment_repo_admin = is_repo_admin and is_repo_comment
1288 1289
1289 1290 if super_admin or comment_owner or comment_repo_admin:
1290 1291 old_calculated_status = comment.pull_request.calculated_review_status()
1291 1292 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1292 1293 Session().commit()
1293 1294 calculated_status = comment.pull_request.calculated_review_status()
1294 1295 if old_calculated_status != calculated_status:
1295 1296 PullRequestModel()._trigger_pull_request_hook(
1296 1297 comment.pull_request, self._rhodecode_user, 'review_status_change')
1297 1298 return True
1298 1299 else:
1299 1300 log.warning('No permissions for user %s to delete comment_id: %s',
1300 1301 self._rhodecode_db_user, comment_id)
1301 1302 raise HTTPNotFound()
@@ -1,1681 +1,1687 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31 import collections
32 32
33 33 from pyramid.threadlocal import get_current_request
34 34
35 35 from rhodecode import events
36 36 from rhodecode.translation import lazy_ugettext#, _
37 37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 41 from rhodecode.lib.markup_renderer import (
42 42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 44 from rhodecode.lib.vcs.backends.base import (
45 45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 47 from rhodecode.lib.vcs.exceptions import (
48 48 CommitDoesNotExistError, EmptyRepositoryError)
49 49 from rhodecode.model import BaseModel
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.db import (
53 53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.notification import NotificationModel, \
57 57 EmailNotificationModel
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.settings import VcsSettingsModel
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 # Data structure to hold the response data when updating commits during a pull
66 66 # request update.
67 67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 68 'executed', 'reason', 'new', 'old', 'changes',
69 69 'source_changed', 'target_changed'])
70 70
71 71
72 72 class PullRequestModel(BaseModel):
73 73
74 74 cls = PullRequest
75 75
76 76 DIFF_CONTEXT = 3
77 77
78 78 MERGE_STATUS_MESSAGES = {
79 79 MergeFailureReason.NONE: lazy_ugettext(
80 80 'This pull request can be automatically merged.'),
81 81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 82 'This pull request cannot be merged because of an unhandled'
83 83 ' exception.'),
84 84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 85 'This pull request cannot be merged because of merge conflicts.'),
86 86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 87 'This pull request could not be merged because push to target'
88 88 ' failed.'),
89 89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 90 'This pull request cannot be merged because the target is not a'
91 91 ' head.'),
92 92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 93 'This pull request cannot be merged because the source contains'
94 94 ' more branches than the target.'),
95 95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 96 'This pull request cannot be merged because the target has'
97 97 ' multiple heads.'),
98 98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 99 'This pull request cannot be merged because the target repository'
100 100 ' is locked.'),
101 101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 102 'This pull request cannot be merged because the target or the '
103 103 'source reference is missing.'),
104 104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 105 'This pull request cannot be merged because the target '
106 106 'reference is missing.'),
107 107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 108 'This pull request cannot be merged because the source '
109 109 'reference is missing.'),
110 110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 111 'This pull request cannot be merged because of conflicts related '
112 112 'to sub repositories.'),
113 113 }
114 114
115 115 UPDATE_STATUS_MESSAGES = {
116 116 UpdateFailureReason.NONE: lazy_ugettext(
117 117 'Pull request update successful.'),
118 118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 119 'Pull request update failed because of an unknown error.'),
120 120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 121 'No update needed because the source and target have not changed.'),
122 122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 123 'Pull request cannot be updated because the reference type is '
124 124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 126 'This pull request cannot be updated because the target '
127 127 'reference is missing.'),
128 128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 129 'This pull request cannot be updated because the source '
130 130 'reference is missing.'),
131 131 }
132 132
133 133 def __get_pull_request(self, pull_request):
134 134 return self._get_instance((
135 135 PullRequest, PullRequestVersion), pull_request)
136 136
137 137 def _check_perms(self, perms, pull_request, user, api=False):
138 138 if not api:
139 139 return h.HasRepoPermissionAny(*perms)(
140 140 user=user, repo_name=pull_request.target_repo.repo_name)
141 141 else:
142 142 return h.HasRepoPermissionAnyApi(*perms)(
143 143 user=user, repo_name=pull_request.target_repo.repo_name)
144 144
145 145 def check_user_read(self, pull_request, user, api=False):
146 146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 147 return self._check_perms(_perms, pull_request, user, api)
148 148
149 149 def check_user_merge(self, pull_request, user, api=False):
150 150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 151 return self._check_perms(_perms, pull_request, user, api)
152 152
153 153 def check_user_update(self, pull_request, user, api=False):
154 154 owner = user.user_id == pull_request.user_id
155 155 return self.check_user_merge(pull_request, user, api) or owner
156 156
157 157 def check_user_delete(self, pull_request, user):
158 158 owner = user.user_id == pull_request.user_id
159 159 _perms = ('repository.admin',)
160 160 return self._check_perms(_perms, pull_request, user) or owner
161 161
162 162 def check_user_change_status(self, pull_request, user, api=False):
163 163 reviewer = user.user_id in [x.user_id for x in
164 164 pull_request.reviewers]
165 165 return self.check_user_update(pull_request, user, api) or reviewer
166 166
167 167 def check_user_comment(self, pull_request, user):
168 168 owner = user.user_id == pull_request.user_id
169 169 return self.check_user_read(pull_request, user) or owner
170 170
171 171 def get(self, pull_request):
172 172 return self.__get_pull_request(pull_request)
173 173
174 174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 175 opened_by=None, order_by=None,
176 176 order_dir='desc'):
177 177 repo = None
178 178 if repo_name:
179 179 repo = self._get_repo(repo_name)
180 180
181 181 q = PullRequest.query()
182 182
183 183 # source or target
184 184 if repo and source:
185 185 q = q.filter(PullRequest.source_repo == repo)
186 186 elif repo:
187 187 q = q.filter(PullRequest.target_repo == repo)
188 188
189 189 # closed,opened
190 190 if statuses:
191 191 q = q.filter(PullRequest.status.in_(statuses))
192 192
193 193 # opened by filter
194 194 if opened_by:
195 195 q = q.filter(PullRequest.user_id.in_(opened_by))
196 196
197 197 if order_by:
198 198 order_map = {
199 199 'name_raw': PullRequest.pull_request_id,
200 200 'title': PullRequest.title,
201 201 'updated_on_raw': PullRequest.updated_on,
202 202 'target_repo': PullRequest.target_repo_id
203 203 }
204 204 if order_dir == 'asc':
205 205 q = q.order_by(order_map[order_by].asc())
206 206 else:
207 207 q = q.order_by(order_map[order_by].desc())
208 208
209 209 return q
210 210
211 211 def count_all(self, repo_name, source=False, statuses=None,
212 212 opened_by=None):
213 213 """
214 214 Count the number of pull requests for a specific repository.
215 215
216 216 :param repo_name: target or source repo
217 217 :param source: boolean flag to specify if repo_name refers to source
218 218 :param statuses: list of pull request statuses
219 219 :param opened_by: author user of the pull request
220 220 :returns: int number of pull requests
221 221 """
222 222 q = self._prepare_get_all_query(
223 223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224 224
225 225 return q.count()
226 226
227 227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 228 offset=0, length=None, order_by=None, order_dir='desc'):
229 229 """
230 230 Get all pull requests for a specific repository.
231 231
232 232 :param repo_name: target or source repo
233 233 :param source: boolean flag to specify if repo_name refers to source
234 234 :param statuses: list of pull request statuses
235 235 :param opened_by: author user of the pull request
236 236 :param offset: pagination offset
237 237 :param length: length of returned list
238 238 :param order_by: order of the returned list
239 239 :param order_dir: 'asc' or 'desc' ordering direction
240 240 :returns: list of pull requests
241 241 """
242 242 q = self._prepare_get_all_query(
243 243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 244 order_by=order_by, order_dir=order_dir)
245 245
246 246 if length:
247 247 pull_requests = q.limit(length).offset(offset).all()
248 248 else:
249 249 pull_requests = q.all()
250 250
251 251 return pull_requests
252 252
253 253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 254 opened_by=None):
255 255 """
256 256 Count the number of pull requests for a specific repository that are
257 257 awaiting review.
258 258
259 259 :param repo_name: target or source repo
260 260 :param source: boolean flag to specify if repo_name refers to source
261 261 :param statuses: list of pull request statuses
262 262 :param opened_by: author user of the pull request
263 263 :returns: int number of pull requests
264 264 """
265 265 pull_requests = self.get_awaiting_review(
266 266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267 267
268 268 return len(pull_requests)
269 269
270 270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 271 opened_by=None, offset=0, length=None,
272 272 order_by=None, order_dir='desc'):
273 273 """
274 274 Get all pull requests for a specific repository that are awaiting
275 275 review.
276 276
277 277 :param repo_name: target or source repo
278 278 :param source: boolean flag to specify if repo_name refers to source
279 279 :param statuses: list of pull request statuses
280 280 :param opened_by: author user of the pull request
281 281 :param offset: pagination offset
282 282 :param length: length of returned list
283 283 :param order_by: order of the returned list
284 284 :param order_dir: 'asc' or 'desc' ordering direction
285 285 :returns: list of pull requests
286 286 """
287 287 pull_requests = self.get_all(
288 288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 289 order_by=order_by, order_dir=order_dir)
290 290
291 291 _filtered_pull_requests = []
292 292 for pr in pull_requests:
293 293 status = pr.calculated_review_status()
294 294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 296 _filtered_pull_requests.append(pr)
297 297 if length:
298 298 return _filtered_pull_requests[offset:offset+length]
299 299 else:
300 300 return _filtered_pull_requests
301 301
302 302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 303 opened_by=None, user_id=None):
304 304 """
305 305 Count the number of pull requests for a specific repository that are
306 306 awaiting review from a specific user.
307 307
308 308 :param repo_name: target or source repo
309 309 :param source: boolean flag to specify if repo_name refers to source
310 310 :param statuses: list of pull request statuses
311 311 :param opened_by: author user of the pull request
312 312 :param user_id: reviewer user of the pull request
313 313 :returns: int number of pull requests
314 314 """
315 315 pull_requests = self.get_awaiting_my_review(
316 316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 317 user_id=user_id)
318 318
319 319 return len(pull_requests)
320 320
321 321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 322 opened_by=None, user_id=None, offset=0,
323 323 length=None, order_by=None, order_dir='desc'):
324 324 """
325 325 Get all pull requests for a specific repository that are awaiting
326 326 review from a specific user.
327 327
328 328 :param repo_name: target or source repo
329 329 :param source: boolean flag to specify if repo_name refers to source
330 330 :param statuses: list of pull request statuses
331 331 :param opened_by: author user of the pull request
332 332 :param user_id: reviewer user of the pull request
333 333 :param offset: pagination offset
334 334 :param length: length of returned list
335 335 :param order_by: order of the returned list
336 336 :param order_dir: 'asc' or 'desc' ordering direction
337 337 :returns: list of pull requests
338 338 """
339 339 pull_requests = self.get_all(
340 340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 341 order_by=order_by, order_dir=order_dir)
342 342
343 343 _my = PullRequestModel().get_not_reviewed(user_id)
344 344 my_participation = []
345 345 for pr in pull_requests:
346 346 if pr in _my:
347 347 my_participation.append(pr)
348 348 _filtered_pull_requests = my_participation
349 349 if length:
350 350 return _filtered_pull_requests[offset:offset+length]
351 351 else:
352 352 return _filtered_pull_requests
353 353
354 354 def get_not_reviewed(self, user_id):
355 355 return [
356 356 x.pull_request for x in PullRequestReviewers.query().filter(
357 357 PullRequestReviewers.user_id == user_id).all()
358 358 ]
359 359
360 360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 361 order_by=None, order_dir='desc'):
362 362 q = PullRequest.query()
363 363 if user_id:
364 364 reviewers_subquery = Session().query(
365 365 PullRequestReviewers.pull_request_id).filter(
366 366 PullRequestReviewers.user_id == user_id).subquery()
367 367 user_filter = or_(
368 368 PullRequest.user_id == user_id,
369 369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 370 )
371 371 q = PullRequest.query().filter(user_filter)
372 372
373 373 # closed,opened
374 374 if statuses:
375 375 q = q.filter(PullRequest.status.in_(statuses))
376 376
377 377 if order_by:
378 378 order_map = {
379 379 'name_raw': PullRequest.pull_request_id,
380 380 'title': PullRequest.title,
381 381 'updated_on_raw': PullRequest.updated_on,
382 382 'target_repo': PullRequest.target_repo_id
383 383 }
384 384 if order_dir == 'asc':
385 385 q = q.order_by(order_map[order_by].asc())
386 386 else:
387 387 q = q.order_by(order_map[order_by].desc())
388 388
389 389 return q
390 390
391 391 def count_im_participating_in(self, user_id=None, statuses=None):
392 392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 393 return q.count()
394 394
395 395 def get_im_participating_in(
396 396 self, user_id=None, statuses=None, offset=0,
397 397 length=None, order_by=None, order_dir='desc'):
398 398 """
399 399 Get all Pull requests that i'm participating in, or i have opened
400 400 """
401 401
402 402 q = self._prepare_participating_query(
403 403 user_id, statuses=statuses, order_by=order_by,
404 404 order_dir=order_dir)
405 405
406 406 if length:
407 407 pull_requests = q.limit(length).offset(offset).all()
408 408 else:
409 409 pull_requests = q.all()
410 410
411 411 return pull_requests
412 412
413 413 def get_versions(self, pull_request):
414 414 """
415 415 returns version of pull request sorted by ID descending
416 416 """
417 417 return PullRequestVersion.query()\
418 418 .filter(PullRequestVersion.pull_request == pull_request)\
419 419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 420 .all()
421 421
422 422 def get_pr_version(self, pull_request_id, version=None):
423 423 at_version = None
424 424
425 425 if version and version == 'latest':
426 426 pull_request_ver = PullRequest.get(pull_request_id)
427 427 pull_request_obj = pull_request_ver
428 428 _org_pull_request_obj = pull_request_obj
429 429 at_version = 'latest'
430 430 elif version:
431 431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 432 pull_request_obj = pull_request_ver
433 433 _org_pull_request_obj = pull_request_ver.pull_request
434 434 at_version = pull_request_ver.pull_request_version_id
435 435 else:
436 436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 437 pull_request_id)
438 438
439 439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 440 pull_request_obj, _org_pull_request_obj)
441 441
442 442 return _org_pull_request_obj, pull_request_obj, \
443 443 pull_request_display_obj, at_version
444 444
445 445 def create(self, created_by, source_repo, source_ref, target_repo,
446 446 target_ref, revisions, reviewers, title, description=None,
447 447 reviewer_data=None, translator=None):
448 448 translator = translator or get_current_request().translate
449 449
450 450 created_by_user = self._get_user(created_by)
451 451 source_repo = self._get_repo(source_repo)
452 452 target_repo = self._get_repo(target_repo)
453 453
454 454 pull_request = PullRequest()
455 455 pull_request.source_repo = source_repo
456 456 pull_request.source_ref = source_ref
457 457 pull_request.target_repo = target_repo
458 458 pull_request.target_ref = target_ref
459 459 pull_request.revisions = revisions
460 460 pull_request.title = title
461 461 pull_request.description = description
462 462 pull_request.author = created_by_user
463 463 pull_request.reviewer_data = reviewer_data
464 464
465 465 Session().add(pull_request)
466 466 Session().flush()
467 467
468 468 reviewer_ids = set()
469 469 # members / reviewers
470 470 for reviewer_object in reviewers:
471 471 user_id, reasons, mandatory, rules = reviewer_object
472 472 user = self._get_user(user_id)
473 473
474 474 # skip duplicates
475 475 if user.user_id in reviewer_ids:
476 476 continue
477 477
478 478 reviewer_ids.add(user.user_id)
479 479
480 480 reviewer = PullRequestReviewers()
481 481 reviewer.user = user
482 482 reviewer.pull_request = pull_request
483 483 reviewer.reasons = reasons
484 484 reviewer.mandatory = mandatory
485 485
486 486 # NOTE(marcink): pick only first rule for now
487 487 rule_id = rules[0] if rules else None
488 488 rule = RepoReviewRule.get(rule_id) if rule_id else None
489 489 if rule:
490 490 review_group = rule.user_group_vote_rule()
491 491 if review_group:
492 492 # NOTE(marcink):
493 493 # again, can be that user is member of more,
494 494 # but we pick the first same, as default reviewers algo
495 495 review_group = review_group[0]
496 496
497 497 rule_data = {
498 498 'rule_name':
499 499 rule.review_rule_name,
500 500 'rule_user_group_entry_id':
501 501 review_group.repo_review_rule_users_group_id,
502 502 'rule_user_group_name':
503 503 review_group.users_group.users_group_name,
504 504 'rule_user_group_members':
505 505 [x.user.username for x in review_group.users_group.members],
506 506 }
507 507 # e.g {'vote_rule': -1, 'mandatory': True}
508 508 rule_data.update(review_group.rule_data())
509 509
510 510 reviewer.rule_data = rule_data
511 511
512 512 Session().add(reviewer)
513 513
514 514 # Set approval status to "Under Review" for all commits which are
515 515 # part of this pull request.
516 516 ChangesetStatusModel().set_status(
517 517 repo=target_repo,
518 518 status=ChangesetStatus.STATUS_UNDER_REVIEW,
519 519 user=created_by_user,
520 520 pull_request=pull_request
521 521 )
522 522
523 523 MergeCheck.validate(
524 524 pull_request, user=created_by_user, translator=translator)
525 525
526 526 self.notify_reviewers(pull_request, reviewer_ids)
527 527 self._trigger_pull_request_hook(
528 528 pull_request, created_by_user, 'create')
529 529
530 530 creation_data = pull_request.get_api_data(with_merge_state=False)
531 531 self._log_audit_action(
532 532 'repo.pull_request.create', {'data': creation_data},
533 533 created_by_user, pull_request)
534 534
535 535 return pull_request
536 536
537 537 def _trigger_pull_request_hook(self, pull_request, user, action):
538 538 pull_request = self.__get_pull_request(pull_request)
539 539 target_scm = pull_request.target_repo.scm_instance()
540 540 if action == 'create':
541 541 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
542 542 elif action == 'merge':
543 543 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
544 544 elif action == 'close':
545 545 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
546 546 elif action == 'review_status_change':
547 547 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
548 548 elif action == 'update':
549 549 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
550 550 else:
551 551 return
552 552
553 553 trigger_hook(
554 554 username=user.username,
555 555 repo_name=pull_request.target_repo.repo_name,
556 556 repo_alias=target_scm.alias,
557 557 pull_request=pull_request)
558 558
559 559 def _get_commit_ids(self, pull_request):
560 560 """
561 561 Return the commit ids of the merged pull request.
562 562
563 563 This method is not dealing correctly yet with the lack of autoupdates
564 564 nor with the implicit target updates.
565 565 For example: if a commit in the source repo is already in the target it
566 566 will be reported anyways.
567 567 """
568 568 merge_rev = pull_request.merge_rev
569 569 if merge_rev is None:
570 570 raise ValueError('This pull request was not merged yet')
571 571
572 572 commit_ids = list(pull_request.revisions)
573 573 if merge_rev not in commit_ids:
574 574 commit_ids.append(merge_rev)
575 575
576 576 return commit_ids
577 577
578 578 def merge(self, pull_request, user, extras):
579 579 log.debug("Merging pull request %s", pull_request.pull_request_id)
580 580 merge_state = self._merge_pull_request(pull_request, user, extras)
581 581 if merge_state.executed:
582 582 log.debug(
583 583 "Merge was successful, updating the pull request comments.")
584 584 self._comment_and_close_pr(pull_request, user, merge_state)
585 585
586 586 self._log_audit_action(
587 587 'repo.pull_request.merge',
588 588 {'merge_state': merge_state.__dict__},
589 589 user, pull_request)
590 590
591 591 else:
592 592 log.warn("Merge failed, not updating the pull request.")
593 593 return merge_state
594 594
595 595 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
596 596 target_vcs = pull_request.target_repo.scm_instance()
597 597 source_vcs = pull_request.source_repo.scm_instance()
598 598 target_ref = self._refresh_reference(
599 599 pull_request.target_ref_parts, target_vcs)
600 600
601 601 message = merge_msg or (
602 602 'Merge pull request #%(pr_id)s from '
603 603 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
604 604 'pr_id': pull_request.pull_request_id,
605 605 'source_repo': source_vcs.name,
606 606 'source_ref_name': pull_request.source_ref_parts.name,
607 607 'pr_title': pull_request.title
608 608 }
609 609
610 610 workspace_id = self._workspace_id(pull_request)
611 611 use_rebase = self._use_rebase_for_merging(pull_request)
612 612 close_branch = self._close_branch_before_merging(pull_request)
613 613
614 614 callback_daemon, extras = prepare_callback_daemon(
615 615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617 617
618 618 with callback_daemon:
619 619 # TODO: johbo: Implement a clean way to run a config_override
620 620 # for a single call.
621 621 target_vcs.config.set(
622 622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 623 merge_state = target_vcs.merge(
624 624 target_ref, source_vcs, pull_request.source_ref_parts,
625 625 workspace_id, user_name=user.username,
626 626 user_email=user.email, message=message, use_rebase=use_rebase,
627 627 close_branch=close_branch)
628 628 return merge_state
629 629
630 630 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
631 631 pull_request.merge_rev = merge_state.merge_ref.commit_id
632 632 pull_request.updated_on = datetime.datetime.now()
633 633 close_msg = close_msg or 'Pull request merged and closed'
634 634
635 635 CommentsModel().create(
636 636 text=safe_unicode(close_msg),
637 637 repo=pull_request.target_repo.repo_id,
638 638 user=user.user_id,
639 639 pull_request=pull_request.pull_request_id,
640 640 f_path=None,
641 641 line_no=None,
642 642 closing_pr=True
643 643 )
644 644
645 645 Session().add(pull_request)
646 646 Session().flush()
647 647 # TODO: paris: replace invalidation with less radical solution
648 648 ScmModel().mark_for_invalidation(
649 649 pull_request.target_repo.repo_name)
650 650 self._trigger_pull_request_hook(pull_request, user, 'merge')
651 651
652 652 def has_valid_update_type(self, pull_request):
653 653 source_ref_type = pull_request.source_ref_parts.type
654 654 return source_ref_type in ['book', 'branch', 'tag']
655 655
656 656 def update_commits(self, pull_request):
657 657 """
658 658 Get the updated list of commits for the pull request
659 659 and return the new pull request version and the list
660 660 of commits processed by this update action
661 661 """
662 662 pull_request = self.__get_pull_request(pull_request)
663 663 source_ref_type = pull_request.source_ref_parts.type
664 664 source_ref_name = pull_request.source_ref_parts.name
665 665 source_ref_id = pull_request.source_ref_parts.commit_id
666 666
667 667 target_ref_type = pull_request.target_ref_parts.type
668 668 target_ref_name = pull_request.target_ref_parts.name
669 669 target_ref_id = pull_request.target_ref_parts.commit_id
670 670
671 671 if not self.has_valid_update_type(pull_request):
672 672 log.debug(
673 673 "Skipping update of pull request %s due to ref type: %s",
674 674 pull_request, source_ref_type)
675 675 return UpdateResponse(
676 676 executed=False,
677 677 reason=UpdateFailureReason.WRONG_REF_TYPE,
678 678 old=pull_request, new=None, changes=None,
679 679 source_changed=False, target_changed=False)
680 680
681 681 # source repo
682 682 source_repo = pull_request.source_repo.scm_instance()
683 683 try:
684 684 source_commit = source_repo.get_commit(commit_id=source_ref_name)
685 685 except CommitDoesNotExistError:
686 686 return UpdateResponse(
687 687 executed=False,
688 688 reason=UpdateFailureReason.MISSING_SOURCE_REF,
689 689 old=pull_request, new=None, changes=None,
690 690 source_changed=False, target_changed=False)
691 691
692 692 source_changed = source_ref_id != source_commit.raw_id
693 693
694 694 # target repo
695 695 target_repo = pull_request.target_repo.scm_instance()
696 696 try:
697 697 target_commit = target_repo.get_commit(commit_id=target_ref_name)
698 698 except CommitDoesNotExistError:
699 699 return UpdateResponse(
700 700 executed=False,
701 701 reason=UpdateFailureReason.MISSING_TARGET_REF,
702 702 old=pull_request, new=None, changes=None,
703 703 source_changed=False, target_changed=False)
704 704 target_changed = target_ref_id != target_commit.raw_id
705 705
706 706 if not (source_changed or target_changed):
707 707 log.debug("Nothing changed in pull request %s", pull_request)
708 708 return UpdateResponse(
709 709 executed=False,
710 710 reason=UpdateFailureReason.NO_CHANGE,
711 711 old=pull_request, new=None, changes=None,
712 712 source_changed=target_changed, target_changed=source_changed)
713 713
714 714 change_in_found = 'target repo' if target_changed else 'source repo'
715 715 log.debug('Updating pull request because of change in %s detected',
716 716 change_in_found)
717 717
718 718 # Finally there is a need for an update, in case of source change
719 719 # we create a new version, else just an update
720 720 if source_changed:
721 721 pull_request_version = self._create_version_from_snapshot(pull_request)
722 722 self._link_comments_to_version(pull_request_version)
723 723 else:
724 724 try:
725 725 ver = pull_request.versions[-1]
726 726 except IndexError:
727 727 ver = None
728 728
729 729 pull_request.pull_request_version_id = \
730 730 ver.pull_request_version_id if ver else None
731 731 pull_request_version = pull_request
732 732
733 733 try:
734 734 if target_ref_type in ('tag', 'branch', 'book'):
735 735 target_commit = target_repo.get_commit(target_ref_name)
736 736 else:
737 737 target_commit = target_repo.get_commit(target_ref_id)
738 738 except CommitDoesNotExistError:
739 739 return UpdateResponse(
740 740 executed=False,
741 741 reason=UpdateFailureReason.MISSING_TARGET_REF,
742 742 old=pull_request, new=None, changes=None,
743 743 source_changed=source_changed, target_changed=target_changed)
744 744
745 745 # re-compute commit ids
746 746 old_commit_ids = pull_request.revisions
747 747 pre_load = ["author", "branch", "date", "message"]
748 748 commit_ranges = target_repo.compare(
749 749 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
750 750 pre_load=pre_load)
751 751
752 752 ancestor = target_repo.get_common_ancestor(
753 753 target_commit.raw_id, source_commit.raw_id, source_repo)
754 754
755 755 pull_request.source_ref = '%s:%s:%s' % (
756 756 source_ref_type, source_ref_name, source_commit.raw_id)
757 757 pull_request.target_ref = '%s:%s:%s' % (
758 758 target_ref_type, target_ref_name, ancestor)
759 759
760 760 pull_request.revisions = [
761 761 commit.raw_id for commit in reversed(commit_ranges)]
762 762 pull_request.updated_on = datetime.datetime.now()
763 763 Session().add(pull_request)
764 764 new_commit_ids = pull_request.revisions
765 765
766 766 old_diff_data, new_diff_data = self._generate_update_diffs(
767 767 pull_request, pull_request_version)
768 768
769 769 # calculate commit and file changes
770 770 changes = self._calculate_commit_id_changes(
771 771 old_commit_ids, new_commit_ids)
772 772 file_changes = self._calculate_file_changes(
773 773 old_diff_data, new_diff_data)
774 774
775 775 # set comments as outdated if DIFFS changed
776 776 CommentsModel().outdate_comments(
777 777 pull_request, old_diff_data=old_diff_data,
778 778 new_diff_data=new_diff_data)
779 779
780 780 commit_changes = (changes.added or changes.removed)
781 781 file_node_changes = (
782 782 file_changes.added or file_changes.modified or file_changes.removed)
783 783 pr_has_changes = commit_changes or file_node_changes
784 784
785 785 # Add an automatic comment to the pull request, in case
786 786 # anything has changed
787 787 if pr_has_changes:
788 788 update_comment = CommentsModel().create(
789 789 text=self._render_update_message(changes, file_changes),
790 790 repo=pull_request.target_repo,
791 791 user=pull_request.author,
792 792 pull_request=pull_request,
793 793 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
794 794
795 795 # Update status to "Under Review" for added commits
796 796 for commit_id in changes.added:
797 797 ChangesetStatusModel().set_status(
798 798 repo=pull_request.source_repo,
799 799 status=ChangesetStatus.STATUS_UNDER_REVIEW,
800 800 comment=update_comment,
801 801 user=pull_request.author,
802 802 pull_request=pull_request,
803 803 revision=commit_id)
804 804
805 805 log.debug(
806 806 'Updated pull request %s, added_ids: %s, common_ids: %s, '
807 807 'removed_ids: %s', pull_request.pull_request_id,
808 808 changes.added, changes.common, changes.removed)
809 809 log.debug(
810 810 'Updated pull request with the following file changes: %s',
811 811 file_changes)
812 812
813 813 log.info(
814 814 "Updated pull request %s from commit %s to commit %s, "
815 815 "stored new version %s of this pull request.",
816 816 pull_request.pull_request_id, source_ref_id,
817 817 pull_request.source_ref_parts.commit_id,
818 818 pull_request_version.pull_request_version_id)
819 819 Session().commit()
820 820 self._trigger_pull_request_hook(
821 821 pull_request, pull_request.author, 'update')
822 822
823 823 return UpdateResponse(
824 824 executed=True, reason=UpdateFailureReason.NONE,
825 825 old=pull_request, new=pull_request_version, changes=changes,
826 826 source_changed=source_changed, target_changed=target_changed)
827 827
828 828 def _create_version_from_snapshot(self, pull_request):
829 829 version = PullRequestVersion()
830 830 version.title = pull_request.title
831 831 version.description = pull_request.description
832 832 version.status = pull_request.status
833 833 version.created_on = datetime.datetime.now()
834 834 version.updated_on = pull_request.updated_on
835 835 version.user_id = pull_request.user_id
836 836 version.source_repo = pull_request.source_repo
837 837 version.source_ref = pull_request.source_ref
838 838 version.target_repo = pull_request.target_repo
839 839 version.target_ref = pull_request.target_ref
840 840
841 841 version._last_merge_source_rev = pull_request._last_merge_source_rev
842 842 version._last_merge_target_rev = pull_request._last_merge_target_rev
843 843 version.last_merge_status = pull_request.last_merge_status
844 844 version.shadow_merge_ref = pull_request.shadow_merge_ref
845 845 version.merge_rev = pull_request.merge_rev
846 846 version.reviewer_data = pull_request.reviewer_data
847 847
848 848 version.revisions = pull_request.revisions
849 849 version.pull_request = pull_request
850 850 Session().add(version)
851 851 Session().flush()
852 852
853 853 return version
854 854
855 855 def _generate_update_diffs(self, pull_request, pull_request_version):
856 856
857 857 diff_context = (
858 858 self.DIFF_CONTEXT +
859 859 CommentsModel.needed_extra_diff_context())
860 860
861 861 source_repo = pull_request_version.source_repo
862 862 source_ref_id = pull_request_version.source_ref_parts.commit_id
863 863 target_ref_id = pull_request_version.target_ref_parts.commit_id
864 864 old_diff = self._get_diff_from_pr_or_version(
865 865 source_repo, source_ref_id, target_ref_id, context=diff_context)
866 866
867 867 source_repo = pull_request.source_repo
868 868 source_ref_id = pull_request.source_ref_parts.commit_id
869 869 target_ref_id = pull_request.target_ref_parts.commit_id
870 870
871 871 new_diff = self._get_diff_from_pr_or_version(
872 872 source_repo, source_ref_id, target_ref_id, context=diff_context)
873 873
874 874 old_diff_data = diffs.DiffProcessor(old_diff)
875 875 old_diff_data.prepare()
876 876 new_diff_data = diffs.DiffProcessor(new_diff)
877 877 new_diff_data.prepare()
878 878
879 879 return old_diff_data, new_diff_data
880 880
881 881 def _link_comments_to_version(self, pull_request_version):
882 882 """
883 883 Link all unlinked comments of this pull request to the given version.
884 884
885 885 :param pull_request_version: The `PullRequestVersion` to which
886 886 the comments shall be linked.
887 887
888 888 """
889 889 pull_request = pull_request_version.pull_request
890 890 comments = ChangesetComment.query()\
891 891 .filter(
892 892 # TODO: johbo: Should we query for the repo at all here?
893 893 # Pending decision on how comments of PRs are to be related
894 894 # to either the source repo, the target repo or no repo at all.
895 895 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
896 896 ChangesetComment.pull_request == pull_request,
897 897 ChangesetComment.pull_request_version == None)\
898 898 .order_by(ChangesetComment.comment_id.asc())
899 899
900 900 # TODO: johbo: Find out why this breaks if it is done in a bulk
901 901 # operation.
902 902 for comment in comments:
903 903 comment.pull_request_version_id = (
904 904 pull_request_version.pull_request_version_id)
905 905 Session().add(comment)
906 906
907 907 def _calculate_commit_id_changes(self, old_ids, new_ids):
908 908 added = [x for x in new_ids if x not in old_ids]
909 909 common = [x for x in new_ids if x in old_ids]
910 910 removed = [x for x in old_ids if x not in new_ids]
911 911 total = new_ids
912 912 return ChangeTuple(added, common, removed, total)
913 913
914 914 def _calculate_file_changes(self, old_diff_data, new_diff_data):
915 915
916 916 old_files = OrderedDict()
917 917 for diff_data in old_diff_data.parsed_diff:
918 918 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
919 919
920 920 added_files = []
921 921 modified_files = []
922 922 removed_files = []
923 923 for diff_data in new_diff_data.parsed_diff:
924 924 new_filename = diff_data['filename']
925 925 new_hash = md5_safe(diff_data['raw_diff'])
926 926
927 927 old_hash = old_files.get(new_filename)
928 928 if not old_hash:
929 929 # file is not present in old diff, means it's added
930 930 added_files.append(new_filename)
931 931 else:
932 932 if new_hash != old_hash:
933 933 modified_files.append(new_filename)
934 934 # now remove a file from old, since we have seen it already
935 935 del old_files[new_filename]
936 936
937 937 # removed files is when there are present in old, but not in NEW,
938 938 # since we remove old files that are present in new diff, left-overs
939 939 # if any should be the removed files
940 940 removed_files.extend(old_files.keys())
941 941
942 942 return FileChangeTuple(added_files, modified_files, removed_files)
943 943
944 944 def _render_update_message(self, changes, file_changes):
945 945 """
946 946 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
947 947 so it's always looking the same disregarding on which default
948 948 renderer system is using.
949 949
950 950 :param changes: changes named tuple
951 951 :param file_changes: file changes named tuple
952 952
953 953 """
954 954 new_status = ChangesetStatus.get_status_lbl(
955 955 ChangesetStatus.STATUS_UNDER_REVIEW)
956 956
957 957 changed_files = (
958 958 file_changes.added + file_changes.modified + file_changes.removed)
959 959
960 960 params = {
961 961 'under_review_label': new_status,
962 962 'added_commits': changes.added,
963 963 'removed_commits': changes.removed,
964 964 'changed_files': changed_files,
965 965 'added_files': file_changes.added,
966 966 'modified_files': file_changes.modified,
967 967 'removed_files': file_changes.removed,
968 968 }
969 969 renderer = RstTemplateRenderer()
970 970 return renderer.render('pull_request_update.mako', **params)
971 971
972 972 def edit(self, pull_request, title, description, user):
973 973 pull_request = self.__get_pull_request(pull_request)
974 974 old_data = pull_request.get_api_data(with_merge_state=False)
975 975 if pull_request.is_closed():
976 976 raise ValueError('This pull request is closed')
977 977 if title:
978 978 pull_request.title = title
979 979 pull_request.description = description
980 980 pull_request.updated_on = datetime.datetime.now()
981 981 Session().add(pull_request)
982 982 self._log_audit_action(
983 983 'repo.pull_request.edit', {'old_data': old_data},
984 984 user, pull_request)
985 985
986 986 def update_reviewers(self, pull_request, reviewer_data, user):
987 987 """
988 988 Update the reviewers in the pull request
989 989
990 990 :param pull_request: the pr to update
991 991 :param reviewer_data: list of tuples
992 992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
993 993 """
994 994 pull_request = self.__get_pull_request(pull_request)
995 995 if pull_request.is_closed():
996 996 raise ValueError('This pull request is closed')
997 997
998 998 reviewers = {}
999 999 for user_id, reasons, mandatory, rules in reviewer_data:
1000 1000 if isinstance(user_id, (int, basestring)):
1001 1001 user_id = self._get_user(user_id).user_id
1002 1002 reviewers[user_id] = {
1003 1003 'reasons': reasons, 'mandatory': mandatory}
1004 1004
1005 1005 reviewers_ids = set(reviewers.keys())
1006 1006 current_reviewers = PullRequestReviewers.query()\
1007 1007 .filter(PullRequestReviewers.pull_request ==
1008 1008 pull_request).all()
1009 1009 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1010 1010
1011 1011 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1012 1012 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1013 1013
1014 1014 log.debug("Adding %s reviewers", ids_to_add)
1015 1015 log.debug("Removing %s reviewers", ids_to_remove)
1016 1016 changed = False
1017 1017 for uid in ids_to_add:
1018 1018 changed = True
1019 1019 _usr = self._get_user(uid)
1020 1020 reviewer = PullRequestReviewers()
1021 1021 reviewer.user = _usr
1022 1022 reviewer.pull_request = pull_request
1023 1023 reviewer.reasons = reviewers[uid]['reasons']
1024 1024 # NOTE(marcink): mandatory shouldn't be changed now
1025 1025 # reviewer.mandatory = reviewers[uid]['reasons']
1026 1026 Session().add(reviewer)
1027 1027 self._log_audit_action(
1028 1028 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1029 1029 user, pull_request)
1030 1030
1031 1031 for uid in ids_to_remove:
1032 1032 changed = True
1033 1033 reviewers = PullRequestReviewers.query()\
1034 1034 .filter(PullRequestReviewers.user_id == uid,
1035 1035 PullRequestReviewers.pull_request == pull_request)\
1036 1036 .all()
1037 1037 # use .all() in case we accidentally added the same person twice
1038 1038 # this CAN happen due to the lack of DB checks
1039 1039 for obj in reviewers:
1040 1040 old_data = obj.get_dict()
1041 1041 Session().delete(obj)
1042 1042 self._log_audit_action(
1043 1043 'repo.pull_request.reviewer.delete',
1044 1044 {'old_data': old_data}, user, pull_request)
1045 1045
1046 1046 if changed:
1047 1047 pull_request.updated_on = datetime.datetime.now()
1048 1048 Session().add(pull_request)
1049 1049
1050 1050 self.notify_reviewers(pull_request, ids_to_add)
1051 1051 return ids_to_add, ids_to_remove
1052 1052
1053 1053 def get_url(self, pull_request, request=None, permalink=False):
1054 1054 if not request:
1055 1055 request = get_current_request()
1056 1056
1057 1057 if permalink:
1058 1058 return request.route_url(
1059 1059 'pull_requests_global',
1060 1060 pull_request_id=pull_request.pull_request_id,)
1061 1061 else:
1062 1062 return request.route_url('pullrequest_show',
1063 1063 repo_name=safe_str(pull_request.target_repo.repo_name),
1064 1064 pull_request_id=pull_request.pull_request_id,)
1065 1065
1066 1066 def get_shadow_clone_url(self, pull_request, request=None):
1067 1067 """
1068 1068 Returns qualified url pointing to the shadow repository. If this pull
1069 1069 request is closed there is no shadow repository and ``None`` will be
1070 1070 returned.
1071 1071 """
1072 1072 if pull_request.is_closed():
1073 1073 return None
1074 1074 else:
1075 1075 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1076 1076 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1077 1077
1078 1078 def notify_reviewers(self, pull_request, reviewers_ids):
1079 1079 # notification to reviewers
1080 1080 if not reviewers_ids:
1081 1081 return
1082 1082
1083 1083 pull_request_obj = pull_request
1084 1084 # get the current participants of this pull request
1085 1085 recipients = reviewers_ids
1086 1086 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1087 1087
1088 1088 pr_source_repo = pull_request_obj.source_repo
1089 1089 pr_target_repo = pull_request_obj.target_repo
1090 1090
1091 1091 pr_url = h.route_url('pullrequest_show',
1092 1092 repo_name=pr_target_repo.repo_name,
1093 1093 pull_request_id=pull_request_obj.pull_request_id,)
1094 1094
1095 1095 # set some variables for email notification
1096 1096 pr_target_repo_url = h.route_url(
1097 1097 'repo_summary', repo_name=pr_target_repo.repo_name)
1098 1098
1099 1099 pr_source_repo_url = h.route_url(
1100 1100 'repo_summary', repo_name=pr_source_repo.repo_name)
1101 1101
1102 1102 # pull request specifics
1103 1103 pull_request_commits = [
1104 1104 (x.raw_id, x.message)
1105 1105 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1106 1106
1107 1107 kwargs = {
1108 1108 'user': pull_request.author,
1109 1109 'pull_request': pull_request_obj,
1110 1110 'pull_request_commits': pull_request_commits,
1111 1111
1112 1112 'pull_request_target_repo': pr_target_repo,
1113 1113 'pull_request_target_repo_url': pr_target_repo_url,
1114 1114
1115 1115 'pull_request_source_repo': pr_source_repo,
1116 1116 'pull_request_source_repo_url': pr_source_repo_url,
1117 1117
1118 1118 'pull_request_url': pr_url,
1119 1119 }
1120 1120
1121 1121 # pre-generate the subject for notification itself
1122 1122 (subject,
1123 1123 _h, _e, # we don't care about those
1124 1124 body_plaintext) = EmailNotificationModel().render_email(
1125 1125 notification_type, **kwargs)
1126 1126
1127 1127 # create notification objects, and emails
1128 1128 NotificationModel().create(
1129 1129 created_by=pull_request.author,
1130 1130 notification_subject=subject,
1131 1131 notification_body=body_plaintext,
1132 1132 notification_type=notification_type,
1133 1133 recipients=recipients,
1134 1134 email_kwargs=kwargs,
1135 1135 )
1136 1136
1137 1137 def delete(self, pull_request, user):
1138 1138 pull_request = self.__get_pull_request(pull_request)
1139 1139 old_data = pull_request.get_api_data(with_merge_state=False)
1140 1140 self._cleanup_merge_workspace(pull_request)
1141 1141 self._log_audit_action(
1142 1142 'repo.pull_request.delete', {'old_data': old_data},
1143 1143 user, pull_request)
1144 1144 Session().delete(pull_request)
1145 1145
1146 1146 def close_pull_request(self, pull_request, user):
1147 1147 pull_request = self.__get_pull_request(pull_request)
1148 1148 self._cleanup_merge_workspace(pull_request)
1149 1149 pull_request.status = PullRequest.STATUS_CLOSED
1150 1150 pull_request.updated_on = datetime.datetime.now()
1151 1151 Session().add(pull_request)
1152 1152 self._trigger_pull_request_hook(
1153 1153 pull_request, pull_request.author, 'close')
1154 1154
1155 1155 pr_data = pull_request.get_api_data(with_merge_state=False)
1156 1156 self._log_audit_action(
1157 1157 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1158 1158
1159 1159 def close_pull_request_with_comment(
1160 1160 self, pull_request, user, repo, message=None):
1161 1161
1162 1162 pull_request_review_status = pull_request.calculated_review_status()
1163 1163
1164 1164 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1165 1165 # approved only if we have voting consent
1166 1166 status = ChangesetStatus.STATUS_APPROVED
1167 1167 else:
1168 1168 status = ChangesetStatus.STATUS_REJECTED
1169 1169 status_lbl = ChangesetStatus.get_status_lbl(status)
1170 1170
1171 1171 default_message = (
1172 1172 'Closing with status change {transition_icon} {status}.'
1173 1173 ).format(transition_icon='>', status=status_lbl)
1174 1174 text = message or default_message
1175 1175
1176 1176 # create a comment, and link it to new status
1177 1177 comment = CommentsModel().create(
1178 1178 text=text,
1179 1179 repo=repo.repo_id,
1180 1180 user=user.user_id,
1181 1181 pull_request=pull_request.pull_request_id,
1182 1182 status_change=status_lbl,
1183 1183 status_change_type=status,
1184 1184 closing_pr=True
1185 1185 )
1186 1186
1187 1187 # calculate old status before we change it
1188 1188 old_calculated_status = pull_request.calculated_review_status()
1189 1189 ChangesetStatusModel().set_status(
1190 1190 repo.repo_id,
1191 1191 status,
1192 1192 user.user_id,
1193 1193 comment=comment,
1194 1194 pull_request=pull_request.pull_request_id
1195 1195 )
1196 1196
1197 1197 Session().flush()
1198 1198 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1199 1199 # we now calculate the status of pull request again, and based on that
1200 1200 # calculation trigger status change. This might happen in cases
1201 1201 # that non-reviewer admin closes a pr, which means his vote doesn't
1202 1202 # change the status, while if he's a reviewer this might change it.
1203 1203 calculated_status = pull_request.calculated_review_status()
1204 1204 if old_calculated_status != calculated_status:
1205 1205 self._trigger_pull_request_hook(
1206 1206 pull_request, user, 'review_status_change')
1207 1207
1208 1208 # finally close the PR
1209 1209 PullRequestModel().close_pull_request(
1210 1210 pull_request.pull_request_id, user)
1211 1211
1212 1212 return comment, status
1213 1213
1214 def merge_status(self, pull_request, translator=None):
1214 def merge_status(self, pull_request, translator=None,
1215 force_shadow_repo_refresh=False):
1215 1216 _ = translator or get_current_request().translate
1216 1217
1217 1218 if not self._is_merge_enabled(pull_request):
1218 1219 return False, _('Server-side pull request merging is disabled.')
1219 1220 if pull_request.is_closed():
1220 1221 return False, _('This pull request is closed.')
1221 1222 merge_possible, msg = self._check_repo_requirements(
1222 1223 target=pull_request.target_repo, source=pull_request.source_repo,
1223 1224 translator=_)
1224 1225 if not merge_possible:
1225 1226 return merge_possible, msg
1226 1227
1227 1228 try:
1228 resp = self._try_merge(pull_request)
1229 resp = self._try_merge(
1230 pull_request,
1231 force_shadow_repo_refresh=force_shadow_repo_refresh)
1229 1232 log.debug("Merge response: %s", resp)
1230 1233 status = resp.possible, self.merge_status_message(
1231 1234 resp.failure_reason)
1232 1235 except NotImplementedError:
1233 1236 status = False, _('Pull request merging is not supported.')
1234 1237
1235 1238 return status
1236 1239
1237 1240 def _check_repo_requirements(self, target, source, translator):
1238 1241 """
1239 1242 Check if `target` and `source` have compatible requirements.
1240 1243
1241 1244 Currently this is just checking for largefiles.
1242 1245 """
1243 1246 _ = translator
1244 1247 target_has_largefiles = self._has_largefiles(target)
1245 1248 source_has_largefiles = self._has_largefiles(source)
1246 1249 merge_possible = True
1247 1250 message = u''
1248 1251
1249 1252 if target_has_largefiles != source_has_largefiles:
1250 1253 merge_possible = False
1251 1254 if source_has_largefiles:
1252 1255 message = _(
1253 1256 'Target repository large files support is disabled.')
1254 1257 else:
1255 1258 message = _(
1256 1259 'Source repository large files support is disabled.')
1257 1260
1258 1261 return merge_possible, message
1259 1262
1260 1263 def _has_largefiles(self, repo):
1261 1264 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1262 1265 'extensions', 'largefiles')
1263 1266 return largefiles_ui and largefiles_ui[0].active
1264 1267
1265 def _try_merge(self, pull_request):
1268 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1266 1269 """
1267 1270 Try to merge the pull request and return the merge status.
1268 1271 """
1269 1272 log.debug(
1270 "Trying out if the pull request %s can be merged.",
1271 pull_request.pull_request_id)
1273 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1274 pull_request.pull_request_id, force_shadow_repo_refresh)
1272 1275 target_vcs = pull_request.target_repo.scm_instance()
1273 1276
1274 1277 # Refresh the target reference.
1275 1278 try:
1276 1279 target_ref = self._refresh_reference(
1277 1280 pull_request.target_ref_parts, target_vcs)
1278 1281 except CommitDoesNotExistError:
1279 1282 merge_state = MergeResponse(
1280 1283 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1281 1284 return merge_state
1282 1285
1283 1286 target_locked = pull_request.target_repo.locked
1284 1287 if target_locked and target_locked[0]:
1285 1288 log.debug("The target repository is locked.")
1286 1289 merge_state = MergeResponse(
1287 1290 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1288 elif self._needs_merge_state_refresh(pull_request, target_ref):
1291 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1292 pull_request, target_ref):
1289 1293 log.debug("Refreshing the merge status of the repository.")
1290 1294 merge_state = self._refresh_merge_state(
1291 1295 pull_request, target_vcs, target_ref)
1292 1296 else:
1293 1297 possible = pull_request.\
1294 1298 last_merge_status == MergeFailureReason.NONE
1295 1299 merge_state = MergeResponse(
1296 1300 possible, False, None, pull_request.last_merge_status)
1297 1301
1298 1302 return merge_state
1299 1303
1300 1304 def _refresh_reference(self, reference, vcs_repository):
1301 1305 if reference.type in ('branch', 'book'):
1302 1306 name_or_id = reference.name
1303 1307 else:
1304 1308 name_or_id = reference.commit_id
1305 1309 refreshed_commit = vcs_repository.get_commit(name_or_id)
1306 1310 refreshed_reference = Reference(
1307 1311 reference.type, reference.name, refreshed_commit.raw_id)
1308 1312 return refreshed_reference
1309 1313
1310 1314 def _needs_merge_state_refresh(self, pull_request, target_reference):
1311 1315 return not(
1312 1316 pull_request.revisions and
1313 1317 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1314 1318 target_reference.commit_id == pull_request._last_merge_target_rev)
1315 1319
1316 1320 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1317 1321 workspace_id = self._workspace_id(pull_request)
1318 1322 source_vcs = pull_request.source_repo.scm_instance()
1319 1323 use_rebase = self._use_rebase_for_merging(pull_request)
1320 1324 close_branch = self._close_branch_before_merging(pull_request)
1321 1325 merge_state = target_vcs.merge(
1322 1326 target_reference, source_vcs, pull_request.source_ref_parts,
1323 1327 workspace_id, dry_run=True, use_rebase=use_rebase,
1324 1328 close_branch=close_branch)
1325 1329
1326 1330 # Do not store the response if there was an unknown error.
1327 1331 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1328 1332 pull_request._last_merge_source_rev = \
1329 1333 pull_request.source_ref_parts.commit_id
1330 1334 pull_request._last_merge_target_rev = target_reference.commit_id
1331 1335 pull_request.last_merge_status = merge_state.failure_reason
1332 1336 pull_request.shadow_merge_ref = merge_state.merge_ref
1333 1337 Session().add(pull_request)
1334 1338 Session().commit()
1335 1339
1336 1340 return merge_state
1337 1341
1338 1342 def _workspace_id(self, pull_request):
1339 1343 workspace_id = 'pr-%s' % pull_request.pull_request_id
1340 1344 return workspace_id
1341 1345
1342 1346 def merge_status_message(self, status_code):
1343 1347 """
1344 1348 Return a human friendly error message for the given merge status code.
1345 1349 """
1346 1350 return self.MERGE_STATUS_MESSAGES[status_code]
1347 1351
1348 1352 def generate_repo_data(self, repo, commit_id=None, branch=None,
1349 1353 bookmark=None, translator=None):
1350 1354 from rhodecode.model.repo import RepoModel
1351 1355
1352 1356 all_refs, selected_ref = \
1353 1357 self._get_repo_pullrequest_sources(
1354 1358 repo.scm_instance(), commit_id=commit_id,
1355 1359 branch=branch, bookmark=bookmark, translator=translator)
1356 1360
1357 1361 refs_select2 = []
1358 1362 for element in all_refs:
1359 1363 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1360 1364 refs_select2.append({'text': element[1], 'children': children})
1361 1365
1362 1366 return {
1363 1367 'user': {
1364 1368 'user_id': repo.user.user_id,
1365 1369 'username': repo.user.username,
1366 1370 'firstname': repo.user.first_name,
1367 1371 'lastname': repo.user.last_name,
1368 1372 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1369 1373 },
1370 1374 'name': repo.repo_name,
1371 1375 'link': RepoModel().get_url(repo),
1372 1376 'description': h.chop_at_smart(repo.description_safe, '\n'),
1373 1377 'refs': {
1374 1378 'all_refs': all_refs,
1375 1379 'selected_ref': selected_ref,
1376 1380 'select2_refs': refs_select2
1377 1381 }
1378 1382 }
1379 1383
1380 1384 def generate_pullrequest_title(self, source, source_ref, target):
1381 1385 return u'{source}#{at_ref} to {target}'.format(
1382 1386 source=source,
1383 1387 at_ref=source_ref,
1384 1388 target=target,
1385 1389 )
1386 1390
1387 1391 def _cleanup_merge_workspace(self, pull_request):
1388 1392 # Merging related cleanup
1389 1393 target_scm = pull_request.target_repo.scm_instance()
1390 1394 workspace_id = 'pr-%s' % pull_request.pull_request_id
1391 1395
1392 1396 try:
1393 1397 target_scm.cleanup_merge_workspace(workspace_id)
1394 1398 except NotImplementedError:
1395 1399 pass
1396 1400
1397 1401 def _get_repo_pullrequest_sources(
1398 1402 self, repo, commit_id=None, branch=None, bookmark=None,
1399 1403 translator=None):
1400 1404 """
1401 1405 Return a structure with repo's interesting commits, suitable for
1402 1406 the selectors in pullrequest controller
1403 1407
1404 1408 :param commit_id: a commit that must be in the list somehow
1405 1409 and selected by default
1406 1410 :param branch: a branch that must be in the list and selected
1407 1411 by default - even if closed
1408 1412 :param bookmark: a bookmark that must be in the list and selected
1409 1413 """
1410 1414 _ = translator or get_current_request().translate
1411 1415
1412 1416 commit_id = safe_str(commit_id) if commit_id else None
1413 1417 branch = safe_str(branch) if branch else None
1414 1418 bookmark = safe_str(bookmark) if bookmark else None
1415 1419
1416 1420 selected = None
1417 1421
1418 1422 # order matters: first source that has commit_id in it will be selected
1419 1423 sources = []
1420 1424 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1421 1425 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1422 1426
1423 1427 if commit_id:
1424 1428 ref_commit = (h.short_id(commit_id), commit_id)
1425 1429 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1426 1430
1427 1431 sources.append(
1428 1432 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1429 1433 )
1430 1434
1431 1435 groups = []
1432 1436 for group_key, ref_list, group_name, match in sources:
1433 1437 group_refs = []
1434 1438 for ref_name, ref_id in ref_list:
1435 1439 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1436 1440 group_refs.append((ref_key, ref_name))
1437 1441
1438 1442 if not selected:
1439 1443 if set([commit_id, match]) & set([ref_id, ref_name]):
1440 1444 selected = ref_key
1441 1445
1442 1446 if group_refs:
1443 1447 groups.append((group_refs, group_name))
1444 1448
1445 1449 if not selected:
1446 1450 ref = commit_id or branch or bookmark
1447 1451 if ref:
1448 1452 raise CommitDoesNotExistError(
1449 1453 'No commit refs could be found matching: %s' % ref)
1450 1454 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1451 1455 selected = 'branch:%s:%s' % (
1452 1456 repo.DEFAULT_BRANCH_NAME,
1453 1457 repo.branches[repo.DEFAULT_BRANCH_NAME]
1454 1458 )
1455 1459 elif repo.commit_ids:
1456 1460 # make the user select in this case
1457 1461 selected = None
1458 1462 else:
1459 1463 raise EmptyRepositoryError()
1460 1464 return groups, selected
1461 1465
1462 1466 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1463 1467 return self._get_diff_from_pr_or_version(
1464 1468 source_repo, source_ref_id, target_ref_id, context=context)
1465 1469
1466 1470 def _get_diff_from_pr_or_version(
1467 1471 self, source_repo, source_ref_id, target_ref_id, context):
1468 1472 target_commit = source_repo.get_commit(
1469 1473 commit_id=safe_str(target_ref_id))
1470 1474 source_commit = source_repo.get_commit(
1471 1475 commit_id=safe_str(source_ref_id))
1472 1476 if isinstance(source_repo, Repository):
1473 1477 vcs_repo = source_repo.scm_instance()
1474 1478 else:
1475 1479 vcs_repo = source_repo
1476 1480
1477 1481 # TODO: johbo: In the context of an update, we cannot reach
1478 1482 # the old commit anymore with our normal mechanisms. It needs
1479 1483 # some sort of special support in the vcs layer to avoid this
1480 1484 # workaround.
1481 1485 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1482 1486 vcs_repo.alias == 'git'):
1483 1487 source_commit.raw_id = safe_str(source_ref_id)
1484 1488
1485 1489 log.debug('calculating diff between '
1486 1490 'source_ref:%s and target_ref:%s for repo `%s`',
1487 1491 target_ref_id, source_ref_id,
1488 1492 safe_unicode(vcs_repo.path))
1489 1493
1490 1494 vcs_diff = vcs_repo.get_diff(
1491 1495 commit1=target_commit, commit2=source_commit, context=context)
1492 1496 return vcs_diff
1493 1497
1494 1498 def _is_merge_enabled(self, pull_request):
1495 1499 return self._get_general_setting(
1496 1500 pull_request, 'rhodecode_pr_merge_enabled')
1497 1501
1498 1502 def _use_rebase_for_merging(self, pull_request):
1499 1503 repo_type = pull_request.target_repo.repo_type
1500 1504 if repo_type == 'hg':
1501 1505 return self._get_general_setting(
1502 1506 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1503 1507 elif repo_type == 'git':
1504 1508 return self._get_general_setting(
1505 1509 pull_request, 'rhodecode_git_use_rebase_for_merging')
1506 1510
1507 1511 return False
1508 1512
1509 1513 def _close_branch_before_merging(self, pull_request):
1510 1514 repo_type = pull_request.target_repo.repo_type
1511 1515 if repo_type == 'hg':
1512 1516 return self._get_general_setting(
1513 1517 pull_request, 'rhodecode_hg_close_branch_before_merging')
1514 1518 elif repo_type == 'git':
1515 1519 return self._get_general_setting(
1516 1520 pull_request, 'rhodecode_git_close_branch_before_merging')
1517 1521
1518 1522 return False
1519 1523
1520 1524 def _get_general_setting(self, pull_request, settings_key, default=False):
1521 1525 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1522 1526 settings = settings_model.get_general_settings()
1523 1527 return settings.get(settings_key, default)
1524 1528
1525 1529 def _log_audit_action(self, action, action_data, user, pull_request):
1526 1530 audit_logger.store(
1527 1531 action=action,
1528 1532 action_data=action_data,
1529 1533 user=user,
1530 1534 repo=pull_request.target_repo)
1531 1535
1532 1536 def get_reviewer_functions(self):
1533 1537 """
1534 1538 Fetches functions for validation and fetching default reviewers.
1535 1539 If available we use the EE package, else we fallback to CE
1536 1540 package functions
1537 1541 """
1538 1542 try:
1539 1543 from rc_reviewers.utils import get_default_reviewers_data
1540 1544 from rc_reviewers.utils import validate_default_reviewers
1541 1545 except ImportError:
1542 1546 from rhodecode.apps.repository.utils import \
1543 1547 get_default_reviewers_data
1544 1548 from rhodecode.apps.repository.utils import \
1545 1549 validate_default_reviewers
1546 1550
1547 1551 return get_default_reviewers_data, validate_default_reviewers
1548 1552
1549 1553
1550 1554 class MergeCheck(object):
1551 1555 """
1552 1556 Perform Merge Checks and returns a check object which stores information
1553 1557 about merge errors, and merge conditions
1554 1558 """
1555 1559 TODO_CHECK = 'todo'
1556 1560 PERM_CHECK = 'perm'
1557 1561 REVIEW_CHECK = 'review'
1558 1562 MERGE_CHECK = 'merge'
1559 1563
1560 1564 def __init__(self):
1561 1565 self.review_status = None
1562 1566 self.merge_possible = None
1563 1567 self.merge_msg = ''
1564 1568 self.failed = None
1565 1569 self.errors = []
1566 1570 self.error_details = OrderedDict()
1567 1571
1568 1572 def push_error(self, error_type, message, error_key, details):
1569 1573 self.failed = True
1570 1574 self.errors.append([error_type, message])
1571 1575 self.error_details[error_key] = dict(
1572 1576 details=details,
1573 1577 error_type=error_type,
1574 1578 message=message
1575 1579 )
1576 1580
1577 1581 @classmethod
1578 def validate(cls, pull_request, user, translator, fail_early=False):
1582 def validate(cls, pull_request, user, translator, fail_early=False,
1583 force_shadow_repo_refresh=False):
1579 1584 _ = translator
1580 1585 merge_check = cls()
1581 1586
1582 1587 # permissions to merge
1583 1588 user_allowed_to_merge = PullRequestModel().check_user_merge(
1584 1589 pull_request, user)
1585 1590 if not user_allowed_to_merge:
1586 1591 log.debug("MergeCheck: cannot merge, approval is pending.")
1587 1592
1588 1593 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1589 1594 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1590 1595 if fail_early:
1591 1596 return merge_check
1592 1597
1593 1598 # review status, must be always present
1594 1599 review_status = pull_request.calculated_review_status()
1595 1600 merge_check.review_status = review_status
1596 1601
1597 1602 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1598 1603 if not status_approved:
1599 1604 log.debug("MergeCheck: cannot merge, approval is pending.")
1600 1605
1601 1606 msg = _('Pull request reviewer approval is pending.')
1602 1607
1603 1608 merge_check.push_error(
1604 1609 'warning', msg, cls.REVIEW_CHECK, review_status)
1605 1610
1606 1611 if fail_early:
1607 1612 return merge_check
1608 1613
1609 1614 # left over TODOs
1610 1615 todos = CommentsModel().get_unresolved_todos(pull_request)
1611 1616 if todos:
1612 1617 log.debug("MergeCheck: cannot merge, {} "
1613 1618 "unresolved todos left.".format(len(todos)))
1614 1619
1615 1620 if len(todos) == 1:
1616 1621 msg = _('Cannot merge, {} TODO still not resolved.').format(
1617 1622 len(todos))
1618 1623 else:
1619 1624 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1620 1625 len(todos))
1621 1626
1622 1627 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1623 1628
1624 1629 if fail_early:
1625 1630 return merge_check
1626 1631
1627 1632 # merge possible
1628 1633 merge_status, msg = PullRequestModel().merge_status(
1629 pull_request, translator=translator)
1634 pull_request, translator=translator,
1635 force_shadow_repo_refresh=force_shadow_repo_refresh)
1630 1636 merge_check.merge_possible = merge_status
1631 1637 merge_check.merge_msg = msg
1632 1638 if not merge_status:
1633 1639 log.debug(
1634 1640 "MergeCheck: cannot merge, pull request merge not possible.")
1635 1641 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1636 1642
1637 1643 if fail_early:
1638 1644 return merge_check
1639 1645
1640 1646 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1641 1647 return merge_check
1642 1648
1643 1649 @classmethod
1644 1650 def get_merge_conditions(cls, pull_request, translator):
1645 1651 _ = translator
1646 1652 merge_details = {}
1647 1653
1648 1654 model = PullRequestModel()
1649 1655 use_rebase = model._use_rebase_for_merging(pull_request)
1650 1656
1651 1657 if use_rebase:
1652 1658 merge_details['merge_strategy'] = dict(
1653 1659 details={},
1654 1660 message=_('Merge strategy: rebase')
1655 1661 )
1656 1662 else:
1657 1663 merge_details['merge_strategy'] = dict(
1658 1664 details={},
1659 1665 message=_('Merge strategy: explicit merge commit')
1660 1666 )
1661 1667
1662 1668 close_branch = model._close_branch_before_merging(pull_request)
1663 1669 if close_branch:
1664 1670 repo_type = pull_request.target_repo.repo_type
1665 1671 if repo_type == 'hg':
1666 1672 close_msg = _('Source branch will be closed after merge.')
1667 1673 elif repo_type == 'git':
1668 1674 close_msg = _('Source branch will be deleted after merge.')
1669 1675
1670 1676 merge_details['close_branch'] = dict(
1671 1677 details={},
1672 1678 message=close_msg
1673 1679 )
1674 1680
1675 1681 return merge_details
1676 1682
1677 1683 ChangeTuple = collections.namedtuple(
1678 1684 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1679 1685
1680 1686 FileChangeTuple = collections.namedtuple(
1681 1687 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now