##// END OF EJS Templates
pull-requests: small code cleanup to define other type of merge username...
marcink -
r4191:10d818a8 stable
parent child Browse files
Show More
@@ -1,1868 +1,1880 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 import os
30
29 31 import datetime
30 32 import urllib
31 33 import collections
32 34
33 35 from pyramid import compat
34 36 from pyramid.threadlocal import get_current_request
35 37
36 38 from rhodecode import events
37 39 from rhodecode.translation import lazy_ugettext
38 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 41 from rhodecode.lib import audit_logger
40 42 from rhodecode.lib.compat import OrderedDict
41 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 44 from rhodecode.lib.markup_renderer import (
43 45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 46 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 47 from rhodecode.lib.vcs.backends.base import (
46 48 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 49 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 50 from rhodecode.lib.vcs.exceptions import (
49 51 CommitDoesNotExistError, EmptyRepositoryError)
50 52 from rhodecode.model import BaseModel
51 53 from rhodecode.model.changeset_status import ChangesetStatusModel
52 54 from rhodecode.model.comment import CommentsModel
53 55 from rhodecode.model.db import (
54 56 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
55 57 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 58 from rhodecode.model.meta import Session
57 59 from rhodecode.model.notification import NotificationModel, \
58 60 EmailNotificationModel
59 61 from rhodecode.model.scm import ScmModel
60 62 from rhodecode.model.settings import VcsSettingsModel
61 63
62 64
63 65 log = logging.getLogger(__name__)
64 66
65 67
66 68 # Data structure to hold the response data when updating commits during a pull
67 69 # request update.
68 70 class UpdateResponse(object):
69 71
70 72 def __init__(self, executed, reason, new, old, common_ancestor_id,
71 73 commit_changes, source_changed, target_changed):
72 74
73 75 self.executed = executed
74 76 self.reason = reason
75 77 self.new = new
76 78 self.old = old
77 79 self.common_ancestor_id = common_ancestor_id
78 80 self.changes = commit_changes
79 81 self.source_changed = source_changed
80 82 self.target_changed = target_changed
81 83
82 84
83 85 class PullRequestModel(BaseModel):
84 86
85 87 cls = PullRequest
86 88
87 89 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
88 90
89 91 UPDATE_STATUS_MESSAGES = {
90 92 UpdateFailureReason.NONE: lazy_ugettext(
91 93 'Pull request update successful.'),
92 94 UpdateFailureReason.UNKNOWN: lazy_ugettext(
93 95 'Pull request update failed because of an unknown error.'),
94 96 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
95 97 'No update needed because the source and target have not changed.'),
96 98 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
97 99 'Pull request cannot be updated because the reference type is '
98 100 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
99 101 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
100 102 'This pull request cannot be updated because the target '
101 103 'reference is missing.'),
102 104 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
103 105 'This pull request cannot be updated because the source '
104 106 'reference is missing.'),
105 107 }
106 108 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
107 109 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
108 110
109 111 def __get_pull_request(self, pull_request):
110 112 return self._get_instance((
111 113 PullRequest, PullRequestVersion), pull_request)
112 114
113 115 def _check_perms(self, perms, pull_request, user, api=False):
114 116 if not api:
115 117 return h.HasRepoPermissionAny(*perms)(
116 118 user=user, repo_name=pull_request.target_repo.repo_name)
117 119 else:
118 120 return h.HasRepoPermissionAnyApi(*perms)(
119 121 user=user, repo_name=pull_request.target_repo.repo_name)
120 122
121 123 def check_user_read(self, pull_request, user, api=False):
122 124 _perms = ('repository.admin', 'repository.write', 'repository.read',)
123 125 return self._check_perms(_perms, pull_request, user, api)
124 126
125 127 def check_user_merge(self, pull_request, user, api=False):
126 128 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
127 129 return self._check_perms(_perms, pull_request, user, api)
128 130
129 131 def check_user_update(self, pull_request, user, api=False):
130 132 owner = user.user_id == pull_request.user_id
131 133 return self.check_user_merge(pull_request, user, api) or owner
132 134
133 135 def check_user_delete(self, pull_request, user):
134 136 owner = user.user_id == pull_request.user_id
135 137 _perms = ('repository.admin',)
136 138 return self._check_perms(_perms, pull_request, user) or owner
137 139
138 140 def check_user_change_status(self, pull_request, user, api=False):
139 141 reviewer = user.user_id in [x.user_id for x in
140 142 pull_request.reviewers]
141 143 return self.check_user_update(pull_request, user, api) or reviewer
142 144
143 145 def check_user_comment(self, pull_request, user):
144 146 owner = user.user_id == pull_request.user_id
145 147 return self.check_user_read(pull_request, user) or owner
146 148
147 149 def get(self, pull_request):
148 150 return self.__get_pull_request(pull_request)
149 151
150 152 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
151 153 statuses=None, opened_by=None, order_by=None,
152 154 order_dir='desc', only_created=False):
153 155 repo = None
154 156 if repo_name:
155 157 repo = self._get_repo(repo_name)
156 158
157 159 q = PullRequest.query()
158 160
159 161 if search_q:
160 162 like_expression = u'%{}%'.format(safe_unicode(search_q))
161 163 q = q.filter(or_(
162 164 cast(PullRequest.pull_request_id, String).ilike(like_expression),
163 165 PullRequest.title.ilike(like_expression),
164 166 PullRequest.description.ilike(like_expression),
165 167 ))
166 168
167 169 # source or target
168 170 if repo and source:
169 171 q = q.filter(PullRequest.source_repo == repo)
170 172 elif repo:
171 173 q = q.filter(PullRequest.target_repo == repo)
172 174
173 175 # closed,opened
174 176 if statuses:
175 177 q = q.filter(PullRequest.status.in_(statuses))
176 178
177 179 # opened by filter
178 180 if opened_by:
179 181 q = q.filter(PullRequest.user_id.in_(opened_by))
180 182
181 183 # only get those that are in "created" state
182 184 if only_created:
183 185 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
184 186
185 187 if order_by:
186 188 order_map = {
187 189 'name_raw': PullRequest.pull_request_id,
188 190 'id': PullRequest.pull_request_id,
189 191 'title': PullRequest.title,
190 192 'updated_on_raw': PullRequest.updated_on,
191 193 'target_repo': PullRequest.target_repo_id
192 194 }
193 195 if order_dir == 'asc':
194 196 q = q.order_by(order_map[order_by].asc())
195 197 else:
196 198 q = q.order_by(order_map[order_by].desc())
197 199
198 200 return q
199 201
200 202 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
201 203 opened_by=None):
202 204 """
203 205 Count the number of pull requests for a specific repository.
204 206
205 207 :param repo_name: target or source repo
206 208 :param search_q: filter by text
207 209 :param source: boolean flag to specify if repo_name refers to source
208 210 :param statuses: list of pull request statuses
209 211 :param opened_by: author user of the pull request
210 212 :returns: int number of pull requests
211 213 """
212 214 q = self._prepare_get_all_query(
213 215 repo_name, search_q=search_q, source=source, statuses=statuses,
214 216 opened_by=opened_by)
215 217
216 218 return q.count()
217 219
218 220 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
219 221 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
220 222 """
221 223 Get all pull requests for a specific repository.
222 224
223 225 :param repo_name: target or source repo
224 226 :param search_q: filter by text
225 227 :param source: boolean flag to specify if repo_name refers to source
226 228 :param statuses: list of pull request statuses
227 229 :param opened_by: author user of the pull request
228 230 :param offset: pagination offset
229 231 :param length: length of returned list
230 232 :param order_by: order of the returned list
231 233 :param order_dir: 'asc' or 'desc' ordering direction
232 234 :returns: list of pull requests
233 235 """
234 236 q = self._prepare_get_all_query(
235 237 repo_name, search_q=search_q, source=source, statuses=statuses,
236 238 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
237 239
238 240 if length:
239 241 pull_requests = q.limit(length).offset(offset).all()
240 242 else:
241 243 pull_requests = q.all()
242 244
243 245 return pull_requests
244 246
245 247 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
246 248 opened_by=None):
247 249 """
248 250 Count the number of pull requests for a specific repository that are
249 251 awaiting review.
250 252
251 253 :param repo_name: target or source repo
252 254 :param search_q: filter by text
253 255 :param source: boolean flag to specify if repo_name refers to source
254 256 :param statuses: list of pull request statuses
255 257 :param opened_by: author user of the pull request
256 258 :returns: int number of pull requests
257 259 """
258 260 pull_requests = self.get_awaiting_review(
259 261 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
260 262
261 263 return len(pull_requests)
262 264
263 265 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
264 266 opened_by=None, offset=0, length=None,
265 267 order_by=None, order_dir='desc'):
266 268 """
267 269 Get all pull requests for a specific repository that are awaiting
268 270 review.
269 271
270 272 :param repo_name: target or source repo
271 273 :param search_q: filter by text
272 274 :param source: boolean flag to specify if repo_name refers to source
273 275 :param statuses: list of pull request statuses
274 276 :param opened_by: author user of the pull request
275 277 :param offset: pagination offset
276 278 :param length: length of returned list
277 279 :param order_by: order of the returned list
278 280 :param order_dir: 'asc' or 'desc' ordering direction
279 281 :returns: list of pull requests
280 282 """
281 283 pull_requests = self.get_all(
282 284 repo_name, search_q=search_q, source=source, statuses=statuses,
283 285 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
284 286
285 287 _filtered_pull_requests = []
286 288 for pr in pull_requests:
287 289 status = pr.calculated_review_status()
288 290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 291 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 292 _filtered_pull_requests.append(pr)
291 293 if length:
292 294 return _filtered_pull_requests[offset:offset+length]
293 295 else:
294 296 return _filtered_pull_requests
295 297
296 298 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
297 299 opened_by=None, user_id=None):
298 300 """
299 301 Count the number of pull requests for a specific repository that are
300 302 awaiting review from a specific user.
301 303
302 304 :param repo_name: target or source repo
303 305 :param search_q: filter by text
304 306 :param source: boolean flag to specify if repo_name refers to source
305 307 :param statuses: list of pull request statuses
306 308 :param opened_by: author user of the pull request
307 309 :param user_id: reviewer user of the pull request
308 310 :returns: int number of pull requests
309 311 """
310 312 pull_requests = self.get_awaiting_my_review(
311 313 repo_name, search_q=search_q, source=source, statuses=statuses,
312 314 opened_by=opened_by, user_id=user_id)
313 315
314 316 return len(pull_requests)
315 317
316 318 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
317 319 opened_by=None, user_id=None, offset=0,
318 320 length=None, order_by=None, order_dir='desc'):
319 321 """
320 322 Get all pull requests for a specific repository that are awaiting
321 323 review from a specific user.
322 324
323 325 :param repo_name: target or source repo
324 326 :param search_q: filter by text
325 327 :param source: boolean flag to specify if repo_name refers to source
326 328 :param statuses: list of pull request statuses
327 329 :param opened_by: author user of the pull request
328 330 :param user_id: reviewer user of the pull request
329 331 :param offset: pagination offset
330 332 :param length: length of returned list
331 333 :param order_by: order of the returned list
332 334 :param order_dir: 'asc' or 'desc' ordering direction
333 335 :returns: list of pull requests
334 336 """
335 337 pull_requests = self.get_all(
336 338 repo_name, search_q=search_q, source=source, statuses=statuses,
337 339 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
338 340
339 341 _my = PullRequestModel().get_not_reviewed(user_id)
340 342 my_participation = []
341 343 for pr in pull_requests:
342 344 if pr in _my:
343 345 my_participation.append(pr)
344 346 _filtered_pull_requests = my_participation
345 347 if length:
346 348 return _filtered_pull_requests[offset:offset+length]
347 349 else:
348 350 return _filtered_pull_requests
349 351
350 352 def get_not_reviewed(self, user_id):
351 353 return [
352 354 x.pull_request for x in PullRequestReviewers.query().filter(
353 355 PullRequestReviewers.user_id == user_id).all()
354 356 ]
355 357
356 358 def _prepare_participating_query(self, user_id=None, statuses=None,
357 359 order_by=None, order_dir='desc'):
358 360 q = PullRequest.query()
359 361 if user_id:
360 362 reviewers_subquery = Session().query(
361 363 PullRequestReviewers.pull_request_id).filter(
362 364 PullRequestReviewers.user_id == user_id).subquery()
363 365 user_filter = or_(
364 366 PullRequest.user_id == user_id,
365 367 PullRequest.pull_request_id.in_(reviewers_subquery)
366 368 )
367 369 q = PullRequest.query().filter(user_filter)
368 370
369 371 # closed,opened
370 372 if statuses:
371 373 q = q.filter(PullRequest.status.in_(statuses))
372 374
373 375 if order_by:
374 376 order_map = {
375 377 'name_raw': PullRequest.pull_request_id,
376 378 'title': PullRequest.title,
377 379 'updated_on_raw': PullRequest.updated_on,
378 380 'target_repo': PullRequest.target_repo_id
379 381 }
380 382 if order_dir == 'asc':
381 383 q = q.order_by(order_map[order_by].asc())
382 384 else:
383 385 q = q.order_by(order_map[order_by].desc())
384 386
385 387 return q
386 388
387 389 def count_im_participating_in(self, user_id=None, statuses=None):
388 390 q = self._prepare_participating_query(user_id, statuses=statuses)
389 391 return q.count()
390 392
391 393 def get_im_participating_in(
392 394 self, user_id=None, statuses=None, offset=0,
393 395 length=None, order_by=None, order_dir='desc'):
394 396 """
395 397 Get all Pull requests that i'm participating in, or i have opened
396 398 """
397 399
398 400 q = self._prepare_participating_query(
399 401 user_id, statuses=statuses, order_by=order_by,
400 402 order_dir=order_dir)
401 403
402 404 if length:
403 405 pull_requests = q.limit(length).offset(offset).all()
404 406 else:
405 407 pull_requests = q.all()
406 408
407 409 return pull_requests
408 410
409 411 def get_versions(self, pull_request):
410 412 """
411 413 returns version of pull request sorted by ID descending
412 414 """
413 415 return PullRequestVersion.query()\
414 416 .filter(PullRequestVersion.pull_request == pull_request)\
415 417 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 418 .all()
417 419
418 420 def get_pr_version(self, pull_request_id, version=None):
419 421 at_version = None
420 422
421 423 if version and version == 'latest':
422 424 pull_request_ver = PullRequest.get(pull_request_id)
423 425 pull_request_obj = pull_request_ver
424 426 _org_pull_request_obj = pull_request_obj
425 427 at_version = 'latest'
426 428 elif version:
427 429 pull_request_ver = PullRequestVersion.get_or_404(version)
428 430 pull_request_obj = pull_request_ver
429 431 _org_pull_request_obj = pull_request_ver.pull_request
430 432 at_version = pull_request_ver.pull_request_version_id
431 433 else:
432 434 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
433 435 pull_request_id)
434 436
435 437 pull_request_display_obj = PullRequest.get_pr_display_object(
436 438 pull_request_obj, _org_pull_request_obj)
437 439
438 440 return _org_pull_request_obj, pull_request_obj, \
439 441 pull_request_display_obj, at_version
440 442
441 443 def create(self, created_by, source_repo, source_ref, target_repo,
442 444 target_ref, revisions, reviewers, title, description=None,
443 445 description_renderer=None,
444 446 reviewer_data=None, translator=None, auth_user=None):
445 447 translator = translator or get_current_request().translate
446 448
447 449 created_by_user = self._get_user(created_by)
448 450 auth_user = auth_user or created_by_user.AuthUser()
449 451 source_repo = self._get_repo(source_repo)
450 452 target_repo = self._get_repo(target_repo)
451 453
452 454 pull_request = PullRequest()
453 455 pull_request.source_repo = source_repo
454 456 pull_request.source_ref = source_ref
455 457 pull_request.target_repo = target_repo
456 458 pull_request.target_ref = target_ref
457 459 pull_request.revisions = revisions
458 460 pull_request.title = title
459 461 pull_request.description = description
460 462 pull_request.description_renderer = description_renderer
461 463 pull_request.author = created_by_user
462 464 pull_request.reviewer_data = reviewer_data
463 465 pull_request.pull_request_state = pull_request.STATE_CREATING
464 466 Session().add(pull_request)
465 467 Session().flush()
466 468
467 469 reviewer_ids = set()
468 470 # members / reviewers
469 471 for reviewer_object in reviewers:
470 472 user_id, reasons, mandatory, rules = reviewer_object
471 473 user = self._get_user(user_id)
472 474
473 475 # skip duplicates
474 476 if user.user_id in reviewer_ids:
475 477 continue
476 478
477 479 reviewer_ids.add(user.user_id)
478 480
479 481 reviewer = PullRequestReviewers()
480 482 reviewer.user = user
481 483 reviewer.pull_request = pull_request
482 484 reviewer.reasons = reasons
483 485 reviewer.mandatory = mandatory
484 486
485 487 # NOTE(marcink): pick only first rule for now
486 488 rule_id = list(rules)[0] if rules else None
487 489 rule = RepoReviewRule.get(rule_id) if rule_id else None
488 490 if rule:
489 491 review_group = rule.user_group_vote_rule(user_id)
490 492 # we check if this particular reviewer is member of a voting group
491 493 if review_group:
492 494 # NOTE(marcink):
493 495 # can be that user is member of more but we pick the first same,
494 496 # same as default reviewers algo
495 497 review_group = review_group[0]
496 498
497 499 rule_data = {
498 500 'rule_name':
499 501 rule.review_rule_name,
500 502 'rule_user_group_entry_id':
501 503 review_group.repo_review_rule_users_group_id,
502 504 'rule_user_group_name':
503 505 review_group.users_group.users_group_name,
504 506 'rule_user_group_members':
505 507 [x.user.username for x in review_group.users_group.members],
506 508 'rule_user_group_members_id':
507 509 [x.user.user_id for x in review_group.users_group.members],
508 510 }
509 511 # e.g {'vote_rule': -1, 'mandatory': True}
510 512 rule_data.update(review_group.rule_data())
511 513
512 514 reviewer.rule_data = rule_data
513 515
514 516 Session().add(reviewer)
515 517 Session().flush()
516 518
517 519 # Set approval status to "Under Review" for all commits which are
518 520 # part of this pull request.
519 521 ChangesetStatusModel().set_status(
520 522 repo=target_repo,
521 523 status=ChangesetStatus.STATUS_UNDER_REVIEW,
522 524 user=created_by_user,
523 525 pull_request=pull_request
524 526 )
525 527 # we commit early at this point. This has to do with a fact
526 528 # that before queries do some row-locking. And because of that
527 529 # we need to commit and finish transaction before below validate call
528 530 # that for large repos could be long resulting in long row locks
529 531 Session().commit()
530 532
531 533 # prepare workspace, and run initial merge simulation. Set state during that
532 534 # operation
533 535 pull_request = PullRequest.get(pull_request.pull_request_id)
534 536
535 537 # set as merging, for merge simulation, and if finished to created so we mark
536 538 # simulation is working fine
537 539 with pull_request.set_state(PullRequest.STATE_MERGING,
538 540 final_state=PullRequest.STATE_CREATED) as state_obj:
539 541 MergeCheck.validate(
540 542 pull_request, auth_user=auth_user, translator=translator)
541 543
542 544 self.notify_reviewers(pull_request, reviewer_ids)
543 545 self.trigger_pull_request_hook(
544 546 pull_request, created_by_user, 'create')
545 547
546 548 creation_data = pull_request.get_api_data(with_merge_state=False)
547 549 self._log_audit_action(
548 550 'repo.pull_request.create', {'data': creation_data},
549 551 auth_user, pull_request)
550 552
551 553 return pull_request
552 554
553 555 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
554 556 pull_request = self.__get_pull_request(pull_request)
555 557 target_scm = pull_request.target_repo.scm_instance()
556 558 if action == 'create':
557 559 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
558 560 elif action == 'merge':
559 561 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
560 562 elif action == 'close':
561 563 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
562 564 elif action == 'review_status_change':
563 565 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
564 566 elif action == 'update':
565 567 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
566 568 elif action == 'comment':
567 569 # dummy hook ! for comment. We want this function to handle all cases
568 570 def trigger_hook(*args, **kwargs):
569 571 pass
570 572 comment = data['comment']
571 573 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
572 574 else:
573 575 return
574 576
575 577 trigger_hook(
576 578 username=user.username,
577 579 repo_name=pull_request.target_repo.repo_name,
578 580 repo_alias=target_scm.alias,
579 581 pull_request=pull_request,
580 582 data=data)
581 583
582 584 def _get_commit_ids(self, pull_request):
583 585 """
584 586 Return the commit ids of the merged pull request.
585 587
586 588 This method is not dealing correctly yet with the lack of autoupdates
587 589 nor with the implicit target updates.
588 590 For example: if a commit in the source repo is already in the target it
589 591 will be reported anyways.
590 592 """
591 593 merge_rev = pull_request.merge_rev
592 594 if merge_rev is None:
593 595 raise ValueError('This pull request was not merged yet')
594 596
595 597 commit_ids = list(pull_request.revisions)
596 598 if merge_rev not in commit_ids:
597 599 commit_ids.append(merge_rev)
598 600
599 601 return commit_ids
600 602
601 603 def merge_repo(self, pull_request, user, extras):
602 604 log.debug("Merging pull request %s", pull_request.pull_request_id)
603 605 extras['user_agent'] = 'internal-merge'
604 606 merge_state = self._merge_pull_request(pull_request, user, extras)
605 607 if merge_state.executed:
606 608 log.debug("Merge was successful, updating the pull request comments.")
607 609 self._comment_and_close_pr(pull_request, user, merge_state)
608 610
609 611 self._log_audit_action(
610 612 'repo.pull_request.merge',
611 613 {'merge_state': merge_state.__dict__},
612 614 user, pull_request)
613 615
614 616 else:
615 617 log.warn("Merge failed, not updating the pull request.")
616 618 return merge_state
617 619
618 620 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
619 621 target_vcs = pull_request.target_repo.scm_instance()
620 622 source_vcs = pull_request.source_repo.scm_instance()
621 623
622 624 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
623 625 pr_id=pull_request.pull_request_id,
624 626 pr_title=pull_request.title,
625 627 source_repo=source_vcs.name,
626 628 source_ref_name=pull_request.source_ref_parts.name,
627 629 target_repo=target_vcs.name,
628 630 target_ref_name=pull_request.target_ref_parts.name,
629 631 )
630 632
631 633 workspace_id = self._workspace_id(pull_request)
632 634 repo_id = pull_request.target_repo.repo_id
633 635 use_rebase = self._use_rebase_for_merging(pull_request)
634 636 close_branch = self._close_branch_before_merging(pull_request)
637 user_name = self._user_name_for_merging(pull_request, user)
635 638
636 639 target_ref = self._refresh_reference(
637 640 pull_request.target_ref_parts, target_vcs)
638 641
639 642 callback_daemon, extras = prepare_callback_daemon(
640 643 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
641 644 host=vcs_settings.HOOKS_HOST,
642 645 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
643 646
644 647 with callback_daemon:
645 648 # TODO: johbo: Implement a clean way to run a config_override
646 649 # for a single call.
647 650 target_vcs.config.set(
648 651 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
649 652
650 user_name = user.short_contact
651 653 merge_state = target_vcs.merge(
652 654 repo_id, workspace_id, target_ref, source_vcs,
653 655 pull_request.source_ref_parts,
654 656 user_name=user_name, user_email=user.email,
655 657 message=message, use_rebase=use_rebase,
656 658 close_branch=close_branch)
657 659 return merge_state
658 660
659 661 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
660 662 pull_request.merge_rev = merge_state.merge_ref.commit_id
661 663 pull_request.updated_on = datetime.datetime.now()
662 664 close_msg = close_msg or 'Pull request merged and closed'
663 665
664 666 CommentsModel().create(
665 667 text=safe_unicode(close_msg),
666 668 repo=pull_request.target_repo.repo_id,
667 669 user=user.user_id,
668 670 pull_request=pull_request.pull_request_id,
669 671 f_path=None,
670 672 line_no=None,
671 673 closing_pr=True
672 674 )
673 675
674 676 Session().add(pull_request)
675 677 Session().flush()
676 678 # TODO: paris: replace invalidation with less radical solution
677 679 ScmModel().mark_for_invalidation(
678 680 pull_request.target_repo.repo_name)
679 681 self.trigger_pull_request_hook(pull_request, user, 'merge')
680 682
681 683 def has_valid_update_type(self, pull_request):
682 684 source_ref_type = pull_request.source_ref_parts.type
683 685 return source_ref_type in self.REF_TYPES
684 686
685 687 def update_commits(self, pull_request, updating_user):
686 688 """
687 689 Get the updated list of commits for the pull request
688 690 and return the new pull request version and the list
689 691 of commits processed by this update action
690 692
691 693 updating_user is the user_object who triggered the update
692 694 """
693 695 pull_request = self.__get_pull_request(pull_request)
694 696 source_ref_type = pull_request.source_ref_parts.type
695 697 source_ref_name = pull_request.source_ref_parts.name
696 698 source_ref_id = pull_request.source_ref_parts.commit_id
697 699
698 700 target_ref_type = pull_request.target_ref_parts.type
699 701 target_ref_name = pull_request.target_ref_parts.name
700 702 target_ref_id = pull_request.target_ref_parts.commit_id
701 703
702 704 if not self.has_valid_update_type(pull_request):
703 705 log.debug("Skipping update of pull request %s due to ref type: %s",
704 706 pull_request, source_ref_type)
705 707 return UpdateResponse(
706 708 executed=False,
707 709 reason=UpdateFailureReason.WRONG_REF_TYPE,
708 710 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
709 711 source_changed=False, target_changed=False)
710 712
711 713 # source repo
712 714 source_repo = pull_request.source_repo.scm_instance()
713 715
714 716 try:
715 717 source_commit = source_repo.get_commit(commit_id=source_ref_name)
716 718 except CommitDoesNotExistError:
717 719 return UpdateResponse(
718 720 executed=False,
719 721 reason=UpdateFailureReason.MISSING_SOURCE_REF,
720 722 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
721 723 source_changed=False, target_changed=False)
722 724
723 725 source_changed = source_ref_id != source_commit.raw_id
724 726
725 727 # target repo
726 728 target_repo = pull_request.target_repo.scm_instance()
727 729
728 730 try:
729 731 target_commit = target_repo.get_commit(commit_id=target_ref_name)
730 732 except CommitDoesNotExistError:
731 733 return UpdateResponse(
732 734 executed=False,
733 735 reason=UpdateFailureReason.MISSING_TARGET_REF,
734 736 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
735 737 source_changed=False, target_changed=False)
736 738 target_changed = target_ref_id != target_commit.raw_id
737 739
738 740 if not (source_changed or target_changed):
739 741 log.debug("Nothing changed in pull request %s", pull_request)
740 742 return UpdateResponse(
741 743 executed=False,
742 744 reason=UpdateFailureReason.NO_CHANGE,
743 745 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
744 746 source_changed=target_changed, target_changed=source_changed)
745 747
746 748 change_in_found = 'target repo' if target_changed else 'source repo'
747 749 log.debug('Updating pull request because of change in %s detected',
748 750 change_in_found)
749 751
750 752 # Finally there is a need for an update, in case of source change
751 753 # we create a new version, else just an update
752 754 if source_changed:
753 755 pull_request_version = self._create_version_from_snapshot(pull_request)
754 756 self._link_comments_to_version(pull_request_version)
755 757 else:
756 758 try:
757 759 ver = pull_request.versions[-1]
758 760 except IndexError:
759 761 ver = None
760 762
761 763 pull_request.pull_request_version_id = \
762 764 ver.pull_request_version_id if ver else None
763 765 pull_request_version = pull_request
764 766
765 767 try:
766 768 if target_ref_type in self.REF_TYPES:
767 769 target_commit = target_repo.get_commit(target_ref_name)
768 770 else:
769 771 target_commit = target_repo.get_commit(target_ref_id)
770 772 except CommitDoesNotExistError:
771 773 return UpdateResponse(
772 774 executed=False,
773 775 reason=UpdateFailureReason.MISSING_TARGET_REF,
774 776 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
775 777 source_changed=source_changed, target_changed=target_changed)
776 778
777 779 # re-compute commit ids
778 780 old_commit_ids = pull_request.revisions
779 781 pre_load = ["author", "date", "message", "branch"]
780 782 commit_ranges = target_repo.compare(
781 783 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
782 784 pre_load=pre_load)
783 785
784 786 ancestor_commit_id = source_repo.get_common_ancestor(
785 787 source_commit.raw_id, target_commit.raw_id, target_repo)
786 788
787 789 pull_request.source_ref = '%s:%s:%s' % (
788 790 source_ref_type, source_ref_name, source_commit.raw_id)
789 791 pull_request.target_ref = '%s:%s:%s' % (
790 792 target_ref_type, target_ref_name, ancestor_commit_id)
791 793
792 794 pull_request.revisions = [
793 795 commit.raw_id for commit in reversed(commit_ranges)]
794 796 pull_request.updated_on = datetime.datetime.now()
795 797 Session().add(pull_request)
796 798 new_commit_ids = pull_request.revisions
797 799
798 800 old_diff_data, new_diff_data = self._generate_update_diffs(
799 801 pull_request, pull_request_version)
800 802
801 803 # calculate commit and file changes
802 804 commit_changes = self._calculate_commit_id_changes(
803 805 old_commit_ids, new_commit_ids)
804 806 file_changes = self._calculate_file_changes(
805 807 old_diff_data, new_diff_data)
806 808
807 809 # set comments as outdated if DIFFS changed
808 810 CommentsModel().outdate_comments(
809 811 pull_request, old_diff_data=old_diff_data,
810 812 new_diff_data=new_diff_data)
811 813
812 814 valid_commit_changes = (commit_changes.added or commit_changes.removed)
813 815 file_node_changes = (
814 816 file_changes.added or file_changes.modified or file_changes.removed)
815 817 pr_has_changes = valid_commit_changes or file_node_changes
816 818
817 819 # Add an automatic comment to the pull request, in case
818 820 # anything has changed
819 821 if pr_has_changes:
820 822 update_comment = CommentsModel().create(
821 823 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
822 824 repo=pull_request.target_repo,
823 825 user=pull_request.author,
824 826 pull_request=pull_request,
825 827 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
826 828
827 829 # Update status to "Under Review" for added commits
828 830 for commit_id in commit_changes.added:
829 831 ChangesetStatusModel().set_status(
830 832 repo=pull_request.source_repo,
831 833 status=ChangesetStatus.STATUS_UNDER_REVIEW,
832 834 comment=update_comment,
833 835 user=pull_request.author,
834 836 pull_request=pull_request,
835 837 revision=commit_id)
836 838
837 839 # send update email to users
838 840 try:
839 841 self.notify_users(pull_request=pull_request, updating_user=updating_user,
840 842 ancestor_commit_id=ancestor_commit_id,
841 843 commit_changes=commit_changes,
842 844 file_changes=file_changes)
843 845 except Exception:
844 846 log.exception('Failed to send email notification to users')
845 847
846 848 log.debug(
847 849 'Updated pull request %s, added_ids: %s, common_ids: %s, '
848 850 'removed_ids: %s', pull_request.pull_request_id,
849 851 commit_changes.added, commit_changes.common, commit_changes.removed)
850 852 log.debug(
851 853 'Updated pull request with the following file changes: %s',
852 854 file_changes)
853 855
854 856 log.info(
855 857 "Updated pull request %s from commit %s to commit %s, "
856 858 "stored new version %s of this pull request.",
857 859 pull_request.pull_request_id, source_ref_id,
858 860 pull_request.source_ref_parts.commit_id,
859 861 pull_request_version.pull_request_version_id)
860 862 Session().commit()
861 863 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
862 864
863 865 return UpdateResponse(
864 866 executed=True, reason=UpdateFailureReason.NONE,
865 867 old=pull_request, new=pull_request_version,
866 868 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
867 869 source_changed=source_changed, target_changed=target_changed)
868 870
869 871 def _create_version_from_snapshot(self, pull_request):
870 872 version = PullRequestVersion()
871 873 version.title = pull_request.title
872 874 version.description = pull_request.description
873 875 version.status = pull_request.status
874 876 version.pull_request_state = pull_request.pull_request_state
875 877 version.created_on = datetime.datetime.now()
876 878 version.updated_on = pull_request.updated_on
877 879 version.user_id = pull_request.user_id
878 880 version.source_repo = pull_request.source_repo
879 881 version.source_ref = pull_request.source_ref
880 882 version.target_repo = pull_request.target_repo
881 883 version.target_ref = pull_request.target_ref
882 884
883 885 version._last_merge_source_rev = pull_request._last_merge_source_rev
884 886 version._last_merge_target_rev = pull_request._last_merge_target_rev
885 887 version.last_merge_status = pull_request.last_merge_status
886 888 version.shadow_merge_ref = pull_request.shadow_merge_ref
887 889 version.merge_rev = pull_request.merge_rev
888 890 version.reviewer_data = pull_request.reviewer_data
889 891
890 892 version.revisions = pull_request.revisions
891 893 version.pull_request = pull_request
892 894 Session().add(version)
893 895 Session().flush()
894 896
895 897 return version
896 898
897 899 def _generate_update_diffs(self, pull_request, pull_request_version):
898 900
899 901 diff_context = (
900 902 self.DIFF_CONTEXT +
901 903 CommentsModel.needed_extra_diff_context())
902 904 hide_whitespace_changes = False
903 905 source_repo = pull_request_version.source_repo
904 906 source_ref_id = pull_request_version.source_ref_parts.commit_id
905 907 target_ref_id = pull_request_version.target_ref_parts.commit_id
906 908 old_diff = self._get_diff_from_pr_or_version(
907 909 source_repo, source_ref_id, target_ref_id,
908 910 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
909 911
910 912 source_repo = pull_request.source_repo
911 913 source_ref_id = pull_request.source_ref_parts.commit_id
912 914 target_ref_id = pull_request.target_ref_parts.commit_id
913 915
914 916 new_diff = self._get_diff_from_pr_or_version(
915 917 source_repo, source_ref_id, target_ref_id,
916 918 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
917 919
918 920 old_diff_data = diffs.DiffProcessor(old_diff)
919 921 old_diff_data.prepare()
920 922 new_diff_data = diffs.DiffProcessor(new_diff)
921 923 new_diff_data.prepare()
922 924
923 925 return old_diff_data, new_diff_data
924 926
925 927 def _link_comments_to_version(self, pull_request_version):
926 928 """
927 929 Link all unlinked comments of this pull request to the given version.
928 930
929 931 :param pull_request_version: The `PullRequestVersion` to which
930 932 the comments shall be linked.
931 933
932 934 """
933 935 pull_request = pull_request_version.pull_request
934 936 comments = ChangesetComment.query()\
935 937 .filter(
936 938 # TODO: johbo: Should we query for the repo at all here?
937 939 # Pending decision on how comments of PRs are to be related
938 940 # to either the source repo, the target repo or no repo at all.
939 941 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
940 942 ChangesetComment.pull_request == pull_request,
941 943 ChangesetComment.pull_request_version == None)\
942 944 .order_by(ChangesetComment.comment_id.asc())
943 945
944 946 # TODO: johbo: Find out why this breaks if it is done in a bulk
945 947 # operation.
946 948 for comment in comments:
947 949 comment.pull_request_version_id = (
948 950 pull_request_version.pull_request_version_id)
949 951 Session().add(comment)
950 952
951 953 def _calculate_commit_id_changes(self, old_ids, new_ids):
952 954 added = [x for x in new_ids if x not in old_ids]
953 955 common = [x for x in new_ids if x in old_ids]
954 956 removed = [x for x in old_ids if x not in new_ids]
955 957 total = new_ids
956 958 return ChangeTuple(added, common, removed, total)
957 959
958 960 def _calculate_file_changes(self, old_diff_data, new_diff_data):
959 961
960 962 old_files = OrderedDict()
961 963 for diff_data in old_diff_data.parsed_diff:
962 964 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
963 965
964 966 added_files = []
965 967 modified_files = []
966 968 removed_files = []
967 969 for diff_data in new_diff_data.parsed_diff:
968 970 new_filename = diff_data['filename']
969 971 new_hash = md5_safe(diff_data['raw_diff'])
970 972
971 973 old_hash = old_files.get(new_filename)
972 974 if not old_hash:
973 975 # file is not present in old diff, we have to figure out from parsed diff
974 976 # operation ADD/REMOVE
975 977 operations_dict = diff_data['stats']['ops']
976 978 if diffs.DEL_FILENODE in operations_dict:
977 979 removed_files.append(new_filename)
978 980 else:
979 981 added_files.append(new_filename)
980 982 else:
981 983 if new_hash != old_hash:
982 984 modified_files.append(new_filename)
983 985 # now remove a file from old, since we have seen it already
984 986 del old_files[new_filename]
985 987
986 988 # removed files is when there are present in old, but not in NEW,
987 989 # since we remove old files that are present in new diff, left-overs
988 990 # if any should be the removed files
989 991 removed_files.extend(old_files.keys())
990 992
991 993 return FileChangeTuple(added_files, modified_files, removed_files)
992 994
993 995 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
994 996 """
995 997 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
996 998 so it's always looking the same disregarding on which default
997 999 renderer system is using.
998 1000
999 1001 :param ancestor_commit_id: ancestor raw_id
1000 1002 :param changes: changes named tuple
1001 1003 :param file_changes: file changes named tuple
1002 1004
1003 1005 """
1004 1006 new_status = ChangesetStatus.get_status_lbl(
1005 1007 ChangesetStatus.STATUS_UNDER_REVIEW)
1006 1008
1007 1009 changed_files = (
1008 1010 file_changes.added + file_changes.modified + file_changes.removed)
1009 1011
1010 1012 params = {
1011 1013 'under_review_label': new_status,
1012 1014 'added_commits': changes.added,
1013 1015 'removed_commits': changes.removed,
1014 1016 'changed_files': changed_files,
1015 1017 'added_files': file_changes.added,
1016 1018 'modified_files': file_changes.modified,
1017 1019 'removed_files': file_changes.removed,
1018 1020 'ancestor_commit_id': ancestor_commit_id
1019 1021 }
1020 1022 renderer = RstTemplateRenderer()
1021 1023 return renderer.render('pull_request_update.mako', **params)
1022 1024
1023 1025 def edit(self, pull_request, title, description, description_renderer, user):
1024 1026 pull_request = self.__get_pull_request(pull_request)
1025 1027 old_data = pull_request.get_api_data(with_merge_state=False)
1026 1028 if pull_request.is_closed():
1027 1029 raise ValueError('This pull request is closed')
1028 1030 if title:
1029 1031 pull_request.title = title
1030 1032 pull_request.description = description
1031 1033 pull_request.updated_on = datetime.datetime.now()
1032 1034 pull_request.description_renderer = description_renderer
1033 1035 Session().add(pull_request)
1034 1036 self._log_audit_action(
1035 1037 'repo.pull_request.edit', {'old_data': old_data},
1036 1038 user, pull_request)
1037 1039
1038 1040 def update_reviewers(self, pull_request, reviewer_data, user):
1039 1041 """
1040 1042 Update the reviewers in the pull request
1041 1043
1042 1044 :param pull_request: the pr to update
1043 1045 :param reviewer_data: list of tuples
1044 1046 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1045 1047 """
1046 1048 pull_request = self.__get_pull_request(pull_request)
1047 1049 if pull_request.is_closed():
1048 1050 raise ValueError('This pull request is closed')
1049 1051
1050 1052 reviewers = {}
1051 1053 for user_id, reasons, mandatory, rules in reviewer_data:
1052 1054 if isinstance(user_id, (int, compat.string_types)):
1053 1055 user_id = self._get_user(user_id).user_id
1054 1056 reviewers[user_id] = {
1055 1057 'reasons': reasons, 'mandatory': mandatory}
1056 1058
1057 1059 reviewers_ids = set(reviewers.keys())
1058 1060 current_reviewers = PullRequestReviewers.query()\
1059 1061 .filter(PullRequestReviewers.pull_request ==
1060 1062 pull_request).all()
1061 1063 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1062 1064
1063 1065 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1064 1066 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1065 1067
1066 1068 log.debug("Adding %s reviewers", ids_to_add)
1067 1069 log.debug("Removing %s reviewers", ids_to_remove)
1068 1070 changed = False
1069 1071 added_audit_reviewers = []
1070 1072 removed_audit_reviewers = []
1071 1073
1072 1074 for uid in ids_to_add:
1073 1075 changed = True
1074 1076 _usr = self._get_user(uid)
1075 1077 reviewer = PullRequestReviewers()
1076 1078 reviewer.user = _usr
1077 1079 reviewer.pull_request = pull_request
1078 1080 reviewer.reasons = reviewers[uid]['reasons']
1079 1081 # NOTE(marcink): mandatory shouldn't be changed now
1080 1082 # reviewer.mandatory = reviewers[uid]['reasons']
1081 1083 Session().add(reviewer)
1082 1084 added_audit_reviewers.append(reviewer.get_dict())
1083 1085
1084 1086 for uid in ids_to_remove:
1085 1087 changed = True
1086 1088 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1087 1089 # that prevents and fixes cases that we added the same reviewer twice.
1088 1090 # this CAN happen due to the lack of DB checks
1089 1091 reviewers = PullRequestReviewers.query()\
1090 1092 .filter(PullRequestReviewers.user_id == uid,
1091 1093 PullRequestReviewers.pull_request == pull_request)\
1092 1094 .all()
1093 1095
1094 1096 for obj in reviewers:
1095 1097 added_audit_reviewers.append(obj.get_dict())
1096 1098 Session().delete(obj)
1097 1099
1098 1100 if changed:
1099 1101 Session().expire_all()
1100 1102 pull_request.updated_on = datetime.datetime.now()
1101 1103 Session().add(pull_request)
1102 1104
1103 1105 # finally store audit logs
1104 1106 for user_data in added_audit_reviewers:
1105 1107 self._log_audit_action(
1106 1108 'repo.pull_request.reviewer.add', {'data': user_data},
1107 1109 user, pull_request)
1108 1110 for user_data in removed_audit_reviewers:
1109 1111 self._log_audit_action(
1110 1112 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1111 1113 user, pull_request)
1112 1114
1113 1115 self.notify_reviewers(pull_request, ids_to_add)
1114 1116 return ids_to_add, ids_to_remove
1115 1117
1116 1118 def get_url(self, pull_request, request=None, permalink=False):
1117 1119 if not request:
1118 1120 request = get_current_request()
1119 1121
1120 1122 if permalink:
1121 1123 return request.route_url(
1122 1124 'pull_requests_global',
1123 1125 pull_request_id=pull_request.pull_request_id,)
1124 1126 else:
1125 1127 return request.route_url('pullrequest_show',
1126 1128 repo_name=safe_str(pull_request.target_repo.repo_name),
1127 1129 pull_request_id=pull_request.pull_request_id,)
1128 1130
1129 1131 def get_shadow_clone_url(self, pull_request, request=None):
1130 1132 """
1131 1133 Returns qualified url pointing to the shadow repository. If this pull
1132 1134 request is closed there is no shadow repository and ``None`` will be
1133 1135 returned.
1134 1136 """
1135 1137 if pull_request.is_closed():
1136 1138 return None
1137 1139 else:
1138 1140 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1139 1141 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1140 1142
1141 1143 def notify_reviewers(self, pull_request, reviewers_ids):
1142 1144 # notification to reviewers
1143 1145 if not reviewers_ids:
1144 1146 return
1145 1147
1146 1148 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1147 1149
1148 1150 pull_request_obj = pull_request
1149 1151 # get the current participants of this pull request
1150 1152 recipients = reviewers_ids
1151 1153 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1152 1154
1153 1155 pr_source_repo = pull_request_obj.source_repo
1154 1156 pr_target_repo = pull_request_obj.target_repo
1155 1157
1156 1158 pr_url = h.route_url('pullrequest_show',
1157 1159 repo_name=pr_target_repo.repo_name,
1158 1160 pull_request_id=pull_request_obj.pull_request_id,)
1159 1161
1160 1162 # set some variables for email notification
1161 1163 pr_target_repo_url = h.route_url(
1162 1164 'repo_summary', repo_name=pr_target_repo.repo_name)
1163 1165
1164 1166 pr_source_repo_url = h.route_url(
1165 1167 'repo_summary', repo_name=pr_source_repo.repo_name)
1166 1168
1167 1169 # pull request specifics
1168 1170 pull_request_commits = [
1169 1171 (x.raw_id, x.message)
1170 1172 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1171 1173
1172 1174 kwargs = {
1173 1175 'user': pull_request.author,
1174 1176 'pull_request': pull_request_obj,
1175 1177 'pull_request_commits': pull_request_commits,
1176 1178
1177 1179 'pull_request_target_repo': pr_target_repo,
1178 1180 'pull_request_target_repo_url': pr_target_repo_url,
1179 1181
1180 1182 'pull_request_source_repo': pr_source_repo,
1181 1183 'pull_request_source_repo_url': pr_source_repo_url,
1182 1184
1183 1185 'pull_request_url': pr_url,
1184 1186 }
1185 1187
1186 1188 # pre-generate the subject for notification itself
1187 1189 (subject,
1188 1190 _h, _e, # we don't care about those
1189 1191 body_plaintext) = EmailNotificationModel().render_email(
1190 1192 notification_type, **kwargs)
1191 1193
1192 1194 # create notification objects, and emails
1193 1195 NotificationModel().create(
1194 1196 created_by=pull_request.author,
1195 1197 notification_subject=subject,
1196 1198 notification_body=body_plaintext,
1197 1199 notification_type=notification_type,
1198 1200 recipients=recipients,
1199 1201 email_kwargs=kwargs,
1200 1202 )
1201 1203
1202 1204 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1203 1205 commit_changes, file_changes):
1204 1206
1205 1207 updating_user_id = updating_user.user_id
1206 1208 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1207 1209 # NOTE(marcink): send notification to all other users except to
1208 1210 # person who updated the PR
1209 1211 recipients = reviewers.difference(set([updating_user_id]))
1210 1212
1211 1213 log.debug('Notify following recipients about pull-request update %s', recipients)
1212 1214
1213 1215 pull_request_obj = pull_request
1214 1216
1215 1217 # send email about the update
1216 1218 changed_files = (
1217 1219 file_changes.added + file_changes.modified + file_changes.removed)
1218 1220
1219 1221 pr_source_repo = pull_request_obj.source_repo
1220 1222 pr_target_repo = pull_request_obj.target_repo
1221 1223
1222 1224 pr_url = h.route_url('pullrequest_show',
1223 1225 repo_name=pr_target_repo.repo_name,
1224 1226 pull_request_id=pull_request_obj.pull_request_id,)
1225 1227
1226 1228 # set some variables for email notification
1227 1229 pr_target_repo_url = h.route_url(
1228 1230 'repo_summary', repo_name=pr_target_repo.repo_name)
1229 1231
1230 1232 pr_source_repo_url = h.route_url(
1231 1233 'repo_summary', repo_name=pr_source_repo.repo_name)
1232 1234
1233 1235 email_kwargs = {
1234 1236 'date': datetime.datetime.now(),
1235 1237 'updating_user': updating_user,
1236 1238
1237 1239 'pull_request': pull_request_obj,
1238 1240
1239 1241 'pull_request_target_repo': pr_target_repo,
1240 1242 'pull_request_target_repo_url': pr_target_repo_url,
1241 1243
1242 1244 'pull_request_source_repo': pr_source_repo,
1243 1245 'pull_request_source_repo_url': pr_source_repo_url,
1244 1246
1245 1247 'pull_request_url': pr_url,
1246 1248
1247 1249 'ancestor_commit_id': ancestor_commit_id,
1248 1250 'added_commits': commit_changes.added,
1249 1251 'removed_commits': commit_changes.removed,
1250 1252 'changed_files': changed_files,
1251 1253 'added_files': file_changes.added,
1252 1254 'modified_files': file_changes.modified,
1253 1255 'removed_files': file_changes.removed,
1254 1256 }
1255 1257
1256 1258 (subject,
1257 1259 _h, _e, # we don't care about those
1258 1260 body_plaintext) = EmailNotificationModel().render_email(
1259 1261 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1260 1262
1261 1263 # create notification objects, and emails
1262 1264 NotificationModel().create(
1263 1265 created_by=updating_user,
1264 1266 notification_subject=subject,
1265 1267 notification_body=body_plaintext,
1266 1268 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1267 1269 recipients=recipients,
1268 1270 email_kwargs=email_kwargs,
1269 1271 )
1270 1272
1271 1273 def delete(self, pull_request, user):
1272 1274 pull_request = self.__get_pull_request(pull_request)
1273 1275 old_data = pull_request.get_api_data(with_merge_state=False)
1274 1276 self._cleanup_merge_workspace(pull_request)
1275 1277 self._log_audit_action(
1276 1278 'repo.pull_request.delete', {'old_data': old_data},
1277 1279 user, pull_request)
1278 1280 Session().delete(pull_request)
1279 1281
1280 1282 def close_pull_request(self, pull_request, user):
1281 1283 pull_request = self.__get_pull_request(pull_request)
1282 1284 self._cleanup_merge_workspace(pull_request)
1283 1285 pull_request.status = PullRequest.STATUS_CLOSED
1284 1286 pull_request.updated_on = datetime.datetime.now()
1285 1287 Session().add(pull_request)
1286 1288 self.trigger_pull_request_hook(
1287 1289 pull_request, pull_request.author, 'close')
1288 1290
1289 1291 pr_data = pull_request.get_api_data(with_merge_state=False)
1290 1292 self._log_audit_action(
1291 1293 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1292 1294
1293 1295 def close_pull_request_with_comment(
1294 1296 self, pull_request, user, repo, message=None, auth_user=None):
1295 1297
1296 1298 pull_request_review_status = pull_request.calculated_review_status()
1297 1299
1298 1300 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1299 1301 # approved only if we have voting consent
1300 1302 status = ChangesetStatus.STATUS_APPROVED
1301 1303 else:
1302 1304 status = ChangesetStatus.STATUS_REJECTED
1303 1305 status_lbl = ChangesetStatus.get_status_lbl(status)
1304 1306
1305 1307 default_message = (
1306 1308 'Closing with status change {transition_icon} {status}.'
1307 1309 ).format(transition_icon='>', status=status_lbl)
1308 1310 text = message or default_message
1309 1311
1310 1312 # create a comment, and link it to new status
1311 1313 comment = CommentsModel().create(
1312 1314 text=text,
1313 1315 repo=repo.repo_id,
1314 1316 user=user.user_id,
1315 1317 pull_request=pull_request.pull_request_id,
1316 1318 status_change=status_lbl,
1317 1319 status_change_type=status,
1318 1320 closing_pr=True,
1319 1321 auth_user=auth_user,
1320 1322 )
1321 1323
1322 1324 # calculate old status before we change it
1323 1325 old_calculated_status = pull_request.calculated_review_status()
1324 1326 ChangesetStatusModel().set_status(
1325 1327 repo.repo_id,
1326 1328 status,
1327 1329 user.user_id,
1328 1330 comment=comment,
1329 1331 pull_request=pull_request.pull_request_id
1330 1332 )
1331 1333
1332 1334 Session().flush()
1333 1335 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1334 1336 # we now calculate the status of pull request again, and based on that
1335 1337 # calculation trigger status change. This might happen in cases
1336 1338 # that non-reviewer admin closes a pr, which means his vote doesn't
1337 1339 # change the status, while if he's a reviewer this might change it.
1338 1340 calculated_status = pull_request.calculated_review_status()
1339 1341 if old_calculated_status != calculated_status:
1340 1342 self.trigger_pull_request_hook(
1341 1343 pull_request, user, 'review_status_change',
1342 1344 data={'status': calculated_status})
1343 1345
1344 1346 # finally close the PR
1345 1347 PullRequestModel().close_pull_request(
1346 1348 pull_request.pull_request_id, user)
1347 1349
1348 1350 return comment, status
1349 1351
1350 1352 def merge_status(self, pull_request, translator=None,
1351 1353 force_shadow_repo_refresh=False):
1352 1354 _ = translator or get_current_request().translate
1353 1355
1354 1356 if not self._is_merge_enabled(pull_request):
1355 1357 return False, _('Server-side pull request merging is disabled.')
1356 1358 if pull_request.is_closed():
1357 1359 return False, _('This pull request is closed.')
1358 1360 merge_possible, msg = self._check_repo_requirements(
1359 1361 target=pull_request.target_repo, source=pull_request.source_repo,
1360 1362 translator=_)
1361 1363 if not merge_possible:
1362 1364 return merge_possible, msg
1363 1365
1364 1366 try:
1365 1367 resp = self._try_merge(
1366 1368 pull_request,
1367 1369 force_shadow_repo_refresh=force_shadow_repo_refresh)
1368 1370 log.debug("Merge response: %s", resp)
1369 1371 status = resp.possible, resp.merge_status_message
1370 1372 except NotImplementedError:
1371 1373 status = False, _('Pull request merging is not supported.')
1372 1374
1373 1375 return status
1374 1376
1375 1377 def _check_repo_requirements(self, target, source, translator):
1376 1378 """
1377 1379 Check if `target` and `source` have compatible requirements.
1378 1380
1379 1381 Currently this is just checking for largefiles.
1380 1382 """
1381 1383 _ = translator
1382 1384 target_has_largefiles = self._has_largefiles(target)
1383 1385 source_has_largefiles = self._has_largefiles(source)
1384 1386 merge_possible = True
1385 1387 message = u''
1386 1388
1387 1389 if target_has_largefiles != source_has_largefiles:
1388 1390 merge_possible = False
1389 1391 if source_has_largefiles:
1390 1392 message = _(
1391 1393 'Target repository large files support is disabled.')
1392 1394 else:
1393 1395 message = _(
1394 1396 'Source repository large files support is disabled.')
1395 1397
1396 1398 return merge_possible, message
1397 1399
1398 1400 def _has_largefiles(self, repo):
1399 1401 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1400 1402 'extensions', 'largefiles')
1401 1403 return largefiles_ui and largefiles_ui[0].active
1402 1404
1403 1405 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1404 1406 """
1405 1407 Try to merge the pull request and return the merge status.
1406 1408 """
1407 1409 log.debug(
1408 1410 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1409 1411 pull_request.pull_request_id, force_shadow_repo_refresh)
1410 1412 target_vcs = pull_request.target_repo.scm_instance()
1411 1413 # Refresh the target reference.
1412 1414 try:
1413 1415 target_ref = self._refresh_reference(
1414 1416 pull_request.target_ref_parts, target_vcs)
1415 1417 except CommitDoesNotExistError:
1416 1418 merge_state = MergeResponse(
1417 1419 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1418 1420 metadata={'target_ref': pull_request.target_ref_parts})
1419 1421 return merge_state
1420 1422
1421 1423 target_locked = pull_request.target_repo.locked
1422 1424 if target_locked and target_locked[0]:
1423 1425 locked_by = 'user:{}'.format(target_locked[0])
1424 1426 log.debug("The target repository is locked by %s.", locked_by)
1425 1427 merge_state = MergeResponse(
1426 1428 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1427 1429 metadata={'locked_by': locked_by})
1428 1430 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1429 1431 pull_request, target_ref):
1430 1432 log.debug("Refreshing the merge status of the repository.")
1431 1433 merge_state = self._refresh_merge_state(
1432 1434 pull_request, target_vcs, target_ref)
1433 1435 else:
1434 1436 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1435 1437 metadata = {
1436 1438 'unresolved_files': '',
1437 1439 'target_ref': pull_request.target_ref_parts,
1438 1440 'source_ref': pull_request.source_ref_parts,
1439 1441 }
1440 1442 if not possible and target_ref.type == 'branch':
1441 1443 # NOTE(marcink): case for mercurial multiple heads on branch
1442 1444 heads = target_vcs._heads(target_ref.name)
1443 1445 if len(heads) != 1:
1444 1446 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1445 1447 metadata.update({
1446 1448 'heads': heads
1447 1449 })
1448 1450 merge_state = MergeResponse(
1449 1451 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1450 1452
1451 1453 return merge_state
1452 1454
1453 1455 def _refresh_reference(self, reference, vcs_repository):
1454 1456 if reference.type in self.UPDATABLE_REF_TYPES:
1455 1457 name_or_id = reference.name
1456 1458 else:
1457 1459 name_or_id = reference.commit_id
1458 1460
1459 1461 refreshed_commit = vcs_repository.get_commit(name_or_id)
1460 1462 refreshed_reference = Reference(
1461 1463 reference.type, reference.name, refreshed_commit.raw_id)
1462 1464 return refreshed_reference
1463 1465
1464 1466 def _needs_merge_state_refresh(self, pull_request, target_reference):
1465 1467 return not(
1466 1468 pull_request.revisions and
1467 1469 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1468 1470 target_reference.commit_id == pull_request._last_merge_target_rev)
1469 1471
1470 1472 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1471 1473 workspace_id = self._workspace_id(pull_request)
1472 1474 source_vcs = pull_request.source_repo.scm_instance()
1473 1475 repo_id = pull_request.target_repo.repo_id
1474 1476 use_rebase = self._use_rebase_for_merging(pull_request)
1475 1477 close_branch = self._close_branch_before_merging(pull_request)
1476 1478 merge_state = target_vcs.merge(
1477 1479 repo_id, workspace_id,
1478 1480 target_reference, source_vcs, pull_request.source_ref_parts,
1479 1481 dry_run=True, use_rebase=use_rebase,
1480 1482 close_branch=close_branch)
1481 1483
1482 1484 # Do not store the response if there was an unknown error.
1483 1485 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1484 1486 pull_request._last_merge_source_rev = \
1485 1487 pull_request.source_ref_parts.commit_id
1486 1488 pull_request._last_merge_target_rev = target_reference.commit_id
1487 1489 pull_request.last_merge_status = merge_state.failure_reason
1488 1490 pull_request.shadow_merge_ref = merge_state.merge_ref
1489 1491 Session().add(pull_request)
1490 1492 Session().commit()
1491 1493
1492 1494 return merge_state
1493 1495
1494 1496 def _workspace_id(self, pull_request):
1495 1497 workspace_id = 'pr-%s' % pull_request.pull_request_id
1496 1498 return workspace_id
1497 1499
1498 1500 def generate_repo_data(self, repo, commit_id=None, branch=None,
1499 1501 bookmark=None, translator=None):
1500 1502 from rhodecode.model.repo import RepoModel
1501 1503
1502 1504 all_refs, selected_ref = \
1503 1505 self._get_repo_pullrequest_sources(
1504 1506 repo.scm_instance(), commit_id=commit_id,
1505 1507 branch=branch, bookmark=bookmark, translator=translator)
1506 1508
1507 1509 refs_select2 = []
1508 1510 for element in all_refs:
1509 1511 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1510 1512 refs_select2.append({'text': element[1], 'children': children})
1511 1513
1512 1514 return {
1513 1515 'user': {
1514 1516 'user_id': repo.user.user_id,
1515 1517 'username': repo.user.username,
1516 1518 'firstname': repo.user.first_name,
1517 1519 'lastname': repo.user.last_name,
1518 1520 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1519 1521 },
1520 1522 'name': repo.repo_name,
1521 1523 'link': RepoModel().get_url(repo),
1522 1524 'description': h.chop_at_smart(repo.description_safe, '\n'),
1523 1525 'refs': {
1524 1526 'all_refs': all_refs,
1525 1527 'selected_ref': selected_ref,
1526 1528 'select2_refs': refs_select2
1527 1529 }
1528 1530 }
1529 1531
1530 1532 def generate_pullrequest_title(self, source, source_ref, target):
1531 1533 return u'{source}#{at_ref} to {target}'.format(
1532 1534 source=source,
1533 1535 at_ref=source_ref,
1534 1536 target=target,
1535 1537 )
1536 1538
1537 1539 def _cleanup_merge_workspace(self, pull_request):
1538 1540 # Merging related cleanup
1539 1541 repo_id = pull_request.target_repo.repo_id
1540 1542 target_scm = pull_request.target_repo.scm_instance()
1541 1543 workspace_id = self._workspace_id(pull_request)
1542 1544
1543 1545 try:
1544 1546 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1545 1547 except NotImplementedError:
1546 1548 pass
1547 1549
1548 1550 def _get_repo_pullrequest_sources(
1549 1551 self, repo, commit_id=None, branch=None, bookmark=None,
1550 1552 translator=None):
1551 1553 """
1552 1554 Return a structure with repo's interesting commits, suitable for
1553 1555 the selectors in pullrequest controller
1554 1556
1555 1557 :param commit_id: a commit that must be in the list somehow
1556 1558 and selected by default
1557 1559 :param branch: a branch that must be in the list and selected
1558 1560 by default - even if closed
1559 1561 :param bookmark: a bookmark that must be in the list and selected
1560 1562 """
1561 1563 _ = translator or get_current_request().translate
1562 1564
1563 1565 commit_id = safe_str(commit_id) if commit_id else None
1564 1566 branch = safe_unicode(branch) if branch else None
1565 1567 bookmark = safe_unicode(bookmark) if bookmark else None
1566 1568
1567 1569 selected = None
1568 1570
1569 1571 # order matters: first source that has commit_id in it will be selected
1570 1572 sources = []
1571 1573 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1572 1574 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1573 1575
1574 1576 if commit_id:
1575 1577 ref_commit = (h.short_id(commit_id), commit_id)
1576 1578 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1577 1579
1578 1580 sources.append(
1579 1581 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1580 1582 )
1581 1583
1582 1584 groups = []
1583 1585
1584 1586 for group_key, ref_list, group_name, match in sources:
1585 1587 group_refs = []
1586 1588 for ref_name, ref_id in ref_list:
1587 1589 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1588 1590 group_refs.append((ref_key, ref_name))
1589 1591
1590 1592 if not selected:
1591 1593 if set([commit_id, match]) & set([ref_id, ref_name]):
1592 1594 selected = ref_key
1593 1595
1594 1596 if group_refs:
1595 1597 groups.append((group_refs, group_name))
1596 1598
1597 1599 if not selected:
1598 1600 ref = commit_id or branch or bookmark
1599 1601 if ref:
1600 1602 raise CommitDoesNotExistError(
1601 1603 u'No commit refs could be found matching: {}'.format(ref))
1602 1604 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1603 1605 selected = u'branch:{}:{}'.format(
1604 1606 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1605 1607 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1606 1608 )
1607 1609 elif repo.commit_ids:
1608 1610 # make the user select in this case
1609 1611 selected = None
1610 1612 else:
1611 1613 raise EmptyRepositoryError()
1612 1614 return groups, selected
1613 1615
1614 1616 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1615 1617 hide_whitespace_changes, diff_context):
1616 1618
1617 1619 return self._get_diff_from_pr_or_version(
1618 1620 source_repo, source_ref_id, target_ref_id,
1619 1621 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1620 1622
1621 1623 def _get_diff_from_pr_or_version(
1622 1624 self, source_repo, source_ref_id, target_ref_id,
1623 1625 hide_whitespace_changes, diff_context):
1624 1626
1625 1627 target_commit = source_repo.get_commit(
1626 1628 commit_id=safe_str(target_ref_id))
1627 1629 source_commit = source_repo.get_commit(
1628 1630 commit_id=safe_str(source_ref_id))
1629 1631 if isinstance(source_repo, Repository):
1630 1632 vcs_repo = source_repo.scm_instance()
1631 1633 else:
1632 1634 vcs_repo = source_repo
1633 1635
1634 1636 # TODO: johbo: In the context of an update, we cannot reach
1635 1637 # the old commit anymore with our normal mechanisms. It needs
1636 1638 # some sort of special support in the vcs layer to avoid this
1637 1639 # workaround.
1638 1640 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1639 1641 vcs_repo.alias == 'git'):
1640 1642 source_commit.raw_id = safe_str(source_ref_id)
1641 1643
1642 1644 log.debug('calculating diff between '
1643 1645 'source_ref:%s and target_ref:%s for repo `%s`',
1644 1646 target_ref_id, source_ref_id,
1645 1647 safe_unicode(vcs_repo.path))
1646 1648
1647 1649 vcs_diff = vcs_repo.get_diff(
1648 1650 commit1=target_commit, commit2=source_commit,
1649 1651 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1650 1652 return vcs_diff
1651 1653
1652 1654 def _is_merge_enabled(self, pull_request):
1653 1655 return self._get_general_setting(
1654 1656 pull_request, 'rhodecode_pr_merge_enabled')
1655 1657
1656 1658 def _use_rebase_for_merging(self, pull_request):
1657 1659 repo_type = pull_request.target_repo.repo_type
1658 1660 if repo_type == 'hg':
1659 1661 return self._get_general_setting(
1660 1662 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1661 1663 elif repo_type == 'git':
1662 1664 return self._get_general_setting(
1663 1665 pull_request, 'rhodecode_git_use_rebase_for_merging')
1664 1666
1665 1667 return False
1666 1668
1669 def _user_name_for_merging(self, pull_request, user):
1670 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1671 if env_user_name_attr and hasattr(user, env_user_name_attr):
1672 user_name_attr = env_user_name_attr
1673 else:
1674 user_name_attr = 'short_contact'
1675
1676 user_name = getattr(user, user_name_attr)
1677 return user_name
1678
1667 1679 def _close_branch_before_merging(self, pull_request):
1668 1680 repo_type = pull_request.target_repo.repo_type
1669 1681 if repo_type == 'hg':
1670 1682 return self._get_general_setting(
1671 1683 pull_request, 'rhodecode_hg_close_branch_before_merging')
1672 1684 elif repo_type == 'git':
1673 1685 return self._get_general_setting(
1674 1686 pull_request, 'rhodecode_git_close_branch_before_merging')
1675 1687
1676 1688 return False
1677 1689
1678 1690 def _get_general_setting(self, pull_request, settings_key, default=False):
1679 1691 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1680 1692 settings = settings_model.get_general_settings()
1681 1693 return settings.get(settings_key, default)
1682 1694
1683 1695 def _log_audit_action(self, action, action_data, user, pull_request):
1684 1696 audit_logger.store(
1685 1697 action=action,
1686 1698 action_data=action_data,
1687 1699 user=user,
1688 1700 repo=pull_request.target_repo)
1689 1701
1690 1702 def get_reviewer_functions(self):
1691 1703 """
1692 1704 Fetches functions for validation and fetching default reviewers.
1693 1705 If available we use the EE package, else we fallback to CE
1694 1706 package functions
1695 1707 """
1696 1708 try:
1697 1709 from rc_reviewers.utils import get_default_reviewers_data
1698 1710 from rc_reviewers.utils import validate_default_reviewers
1699 1711 except ImportError:
1700 1712 from rhodecode.apps.repository.utils import get_default_reviewers_data
1701 1713 from rhodecode.apps.repository.utils import validate_default_reviewers
1702 1714
1703 1715 return get_default_reviewers_data, validate_default_reviewers
1704 1716
1705 1717
1706 1718 class MergeCheck(object):
1707 1719 """
1708 1720 Perform Merge Checks and returns a check object which stores information
1709 1721 about merge errors, and merge conditions
1710 1722 """
1711 1723 TODO_CHECK = 'todo'
1712 1724 PERM_CHECK = 'perm'
1713 1725 REVIEW_CHECK = 'review'
1714 1726 MERGE_CHECK = 'merge'
1715 1727 WIP_CHECK = 'wip'
1716 1728
1717 1729 def __init__(self):
1718 1730 self.review_status = None
1719 1731 self.merge_possible = None
1720 1732 self.merge_msg = ''
1721 1733 self.failed = None
1722 1734 self.errors = []
1723 1735 self.error_details = OrderedDict()
1724 1736
1725 1737 def push_error(self, error_type, message, error_key, details):
1726 1738 self.failed = True
1727 1739 self.errors.append([error_type, message])
1728 1740 self.error_details[error_key] = dict(
1729 1741 details=details,
1730 1742 error_type=error_type,
1731 1743 message=message
1732 1744 )
1733 1745
1734 1746 @classmethod
1735 1747 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1736 1748 force_shadow_repo_refresh=False):
1737 1749 _ = translator
1738 1750 merge_check = cls()
1739 1751
1740 1752 # title has WIP:
1741 1753 if pull_request.work_in_progress:
1742 1754 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1743 1755
1744 1756 msg = _('WIP marker in title prevents from accidental merge.')
1745 1757 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1746 1758 if fail_early:
1747 1759 return merge_check
1748 1760
1749 1761 # permissions to merge
1750 1762 user_allowed_to_merge = PullRequestModel().check_user_merge(
1751 1763 pull_request, auth_user)
1752 1764 if not user_allowed_to_merge:
1753 1765 log.debug("MergeCheck: cannot merge, approval is pending.")
1754 1766
1755 1767 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1756 1768 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1757 1769 if fail_early:
1758 1770 return merge_check
1759 1771
1760 1772 # permission to merge into the target branch
1761 1773 target_commit_id = pull_request.target_ref_parts.commit_id
1762 1774 if pull_request.target_ref_parts.type == 'branch':
1763 1775 branch_name = pull_request.target_ref_parts.name
1764 1776 else:
1765 1777 # for mercurial we can always figure out the branch from the commit
1766 1778 # in case of bookmark
1767 1779 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1768 1780 branch_name = target_commit.branch
1769 1781
1770 1782 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1771 1783 pull_request.target_repo.repo_name, branch_name)
1772 1784 if branch_perm and branch_perm == 'branch.none':
1773 1785 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1774 1786 branch_name, rule)
1775 1787 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1776 1788 if fail_early:
1777 1789 return merge_check
1778 1790
1779 1791 # review status, must be always present
1780 1792 review_status = pull_request.calculated_review_status()
1781 1793 merge_check.review_status = review_status
1782 1794
1783 1795 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1784 1796 if not status_approved:
1785 1797 log.debug("MergeCheck: cannot merge, approval is pending.")
1786 1798
1787 1799 msg = _('Pull request reviewer approval is pending.')
1788 1800
1789 1801 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1790 1802
1791 1803 if fail_early:
1792 1804 return merge_check
1793 1805
1794 1806 # left over TODOs
1795 1807 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1796 1808 if todos:
1797 1809 log.debug("MergeCheck: cannot merge, {} "
1798 1810 "unresolved TODOs left.".format(len(todos)))
1799 1811
1800 1812 if len(todos) == 1:
1801 1813 msg = _('Cannot merge, {} TODO still not resolved.').format(
1802 1814 len(todos))
1803 1815 else:
1804 1816 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1805 1817 len(todos))
1806 1818
1807 1819 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1808 1820
1809 1821 if fail_early:
1810 1822 return merge_check
1811 1823
1812 1824 # merge possible, here is the filesystem simulation + shadow repo
1813 1825 merge_status, msg = PullRequestModel().merge_status(
1814 1826 pull_request, translator=translator,
1815 1827 force_shadow_repo_refresh=force_shadow_repo_refresh)
1816 1828 merge_check.merge_possible = merge_status
1817 1829 merge_check.merge_msg = msg
1818 1830 if not merge_status:
1819 1831 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1820 1832 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1821 1833
1822 1834 if fail_early:
1823 1835 return merge_check
1824 1836
1825 1837 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1826 1838 return merge_check
1827 1839
1828 1840 @classmethod
1829 1841 def get_merge_conditions(cls, pull_request, translator):
1830 1842 _ = translator
1831 1843 merge_details = {}
1832 1844
1833 1845 model = PullRequestModel()
1834 1846 use_rebase = model._use_rebase_for_merging(pull_request)
1835 1847
1836 1848 if use_rebase:
1837 1849 merge_details['merge_strategy'] = dict(
1838 1850 details={},
1839 1851 message=_('Merge strategy: rebase')
1840 1852 )
1841 1853 else:
1842 1854 merge_details['merge_strategy'] = dict(
1843 1855 details={},
1844 1856 message=_('Merge strategy: explicit merge commit')
1845 1857 )
1846 1858
1847 1859 close_branch = model._close_branch_before_merging(pull_request)
1848 1860 if close_branch:
1849 1861 repo_type = pull_request.target_repo.repo_type
1850 1862 close_msg = ''
1851 1863 if repo_type == 'hg':
1852 1864 close_msg = _('Source branch will be closed after merge.')
1853 1865 elif repo_type == 'git':
1854 1866 close_msg = _('Source branch will be deleted after merge.')
1855 1867
1856 1868 merge_details['close_branch'] = dict(
1857 1869 details={},
1858 1870 message=close_msg
1859 1871 )
1860 1872
1861 1873 return merge_details
1862 1874
1863 1875
1864 1876 ChangeTuple = collections.namedtuple(
1865 1877 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1866 1878
1867 1879 FileChangeTuple = collections.namedtuple(
1868 1880 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now