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