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