##// END OF EJS Templates
pull-requests: updates on pull requests that don't have versions shouldn't...
marcink -
r1596:892a8297 default
parent child Browse files
Show More
@@ -1,1450 +1,1454 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 34 from sqlalchemy import or_
35 35
36 36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 37 from rhodecode.lib.compat import OrderedDict
38 38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 39 from rhodecode.lib.markup_renderer import (
40 40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 41 from rhodecode.lib.utils import action_logger
42 42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 CommitDoesNotExistError, EmptyRepositoryError)
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 50 from rhodecode.model.comment import CommentsModel
51 51 from rhodecode.model.db import (
52 52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 53 PullRequestVersion, ChangesetComment, Repository)
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.notification import NotificationModel, \
56 56 EmailNotificationModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.settings import VcsSettingsModel
59 59
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 # Data structure to hold the response data when updating commits during a pull
65 65 # request update.
66 66 UpdateResponse = namedtuple(
67 67 'UpdateResponse', 'executed, reason, new, old, changes')
68 68
69 69
70 70 class PullRequestModel(BaseModel):
71 71
72 72 cls = PullRequest
73 73
74 74 DIFF_CONTEXT = 3
75 75
76 76 MERGE_STATUS_MESSAGES = {
77 77 MergeFailureReason.NONE: lazy_ugettext(
78 78 'This pull request can be automatically merged.'),
79 79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 80 'This pull request cannot be merged because of an unhandled'
81 81 ' exception.'),
82 82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 83 'This pull request cannot be merged because of merge conflicts.'),
84 84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 85 'This pull request could not be merged because push to target'
86 86 ' failed.'),
87 87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 88 'This pull request cannot be merged because the target is not a'
89 89 ' head.'),
90 90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 91 'This pull request cannot be merged because the source contains'
92 92 ' more branches than the target.'),
93 93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 94 'This pull request cannot be merged because the target has'
95 95 ' multiple heads.'),
96 96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 97 'This pull request cannot be merged because the target repository'
98 98 ' is locked.'),
99 99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 100 'This pull request cannot be merged because the target or the '
101 101 'source reference is missing.'),
102 102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 103 'This pull request cannot be merged because the target '
104 104 'reference is missing.'),
105 105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 106 'This pull request cannot be merged because the source '
107 107 'reference is missing.'),
108 108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 109 'This pull request cannot be merged because of conflicts related '
110 110 'to sub repositories.'),
111 111 }
112 112
113 113 UPDATE_STATUS_MESSAGES = {
114 114 UpdateFailureReason.NONE: lazy_ugettext(
115 115 'Pull request update successful.'),
116 116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 117 'Pull request update failed because of an unknown error.'),
118 118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 119 'No update needed because the source reference is already '
120 120 'up to date.'),
121 121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 122 'Pull request cannot be updated because the reference type is '
123 123 'not supported for an update.'),
124 124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 125 'This pull request cannot be updated because the target '
126 126 'reference is missing.'),
127 127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 128 'This pull request cannot be updated because the source '
129 129 'reference is missing.'),
130 130 }
131 131
132 132 def __get_pull_request(self, pull_request):
133 133 return self._get_instance((
134 134 PullRequest, PullRequestVersion), pull_request)
135 135
136 136 def _check_perms(self, perms, pull_request, user, api=False):
137 137 if not api:
138 138 return h.HasRepoPermissionAny(*perms)(
139 139 user=user, repo_name=pull_request.target_repo.repo_name)
140 140 else:
141 141 return h.HasRepoPermissionAnyApi(*perms)(
142 142 user=user, repo_name=pull_request.target_repo.repo_name)
143 143
144 144 def check_user_read(self, pull_request, user, api=False):
145 145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 146 return self._check_perms(_perms, pull_request, user, api)
147 147
148 148 def check_user_merge(self, pull_request, user, api=False):
149 149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 150 return self._check_perms(_perms, pull_request, user, api)
151 151
152 152 def check_user_update(self, pull_request, user, api=False):
153 153 owner = user.user_id == pull_request.user_id
154 154 return self.check_user_merge(pull_request, user, api) or owner
155 155
156 156 def check_user_delete(self, pull_request, user):
157 157 owner = user.user_id == pull_request.user_id
158 158 _perms = ('repository.admin',)
159 159 return self._check_perms(_perms, pull_request, user) or owner
160 160
161 161 def check_user_change_status(self, pull_request, user, api=False):
162 162 reviewer = user.user_id in [x.user_id for x in
163 163 pull_request.reviewers]
164 164 return self.check_user_update(pull_request, user, api) or reviewer
165 165
166 166 def get(self, pull_request):
167 167 return self.__get_pull_request(pull_request)
168 168
169 169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 170 opened_by=None, order_by=None,
171 171 order_dir='desc'):
172 172 repo = None
173 173 if repo_name:
174 174 repo = self._get_repo(repo_name)
175 175
176 176 q = PullRequest.query()
177 177
178 178 # source or target
179 179 if repo and source:
180 180 q = q.filter(PullRequest.source_repo == repo)
181 181 elif repo:
182 182 q = q.filter(PullRequest.target_repo == repo)
183 183
184 184 # closed,opened
185 185 if statuses:
186 186 q = q.filter(PullRequest.status.in_(statuses))
187 187
188 188 # opened by filter
189 189 if opened_by:
190 190 q = q.filter(PullRequest.user_id.in_(opened_by))
191 191
192 192 if order_by:
193 193 order_map = {
194 194 'name_raw': PullRequest.pull_request_id,
195 195 'title': PullRequest.title,
196 196 'updated_on_raw': PullRequest.updated_on,
197 197 'target_repo': PullRequest.target_repo_id
198 198 }
199 199 if order_dir == 'asc':
200 200 q = q.order_by(order_map[order_by].asc())
201 201 else:
202 202 q = q.order_by(order_map[order_by].desc())
203 203
204 204 return q
205 205
206 206 def count_all(self, repo_name, source=False, statuses=None,
207 207 opened_by=None):
208 208 """
209 209 Count the number of pull requests for a specific repository.
210 210
211 211 :param repo_name: target or source repo
212 212 :param source: boolean flag to specify if repo_name refers to source
213 213 :param statuses: list of pull request statuses
214 214 :param opened_by: author user of the pull request
215 215 :returns: int number of pull requests
216 216 """
217 217 q = self._prepare_get_all_query(
218 218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219 219
220 220 return q.count()
221 221
222 222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 223 offset=0, length=None, order_by=None, order_dir='desc'):
224 224 """
225 225 Get all pull requests for a specific repository.
226 226
227 227 :param repo_name: target or source repo
228 228 :param source: boolean flag to specify if repo_name refers to source
229 229 :param statuses: list of pull request statuses
230 230 :param opened_by: author user of the pull request
231 231 :param offset: pagination offset
232 232 :param length: length of returned list
233 233 :param order_by: order of the returned list
234 234 :param order_dir: 'asc' or 'desc' ordering direction
235 235 :returns: list of pull requests
236 236 """
237 237 q = self._prepare_get_all_query(
238 238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 239 order_by=order_by, order_dir=order_dir)
240 240
241 241 if length:
242 242 pull_requests = q.limit(length).offset(offset).all()
243 243 else:
244 244 pull_requests = q.all()
245 245
246 246 return pull_requests
247 247
248 248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 249 opened_by=None):
250 250 """
251 251 Count the number of pull requests for a specific repository that are
252 252 awaiting review.
253 253
254 254 :param repo_name: target or source repo
255 255 :param source: boolean flag to specify if repo_name refers to source
256 256 :param statuses: list of pull request statuses
257 257 :param opened_by: author user of the pull request
258 258 :returns: int number of pull requests
259 259 """
260 260 pull_requests = self.get_awaiting_review(
261 261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262 262
263 263 return len(pull_requests)
264 264
265 265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 266 opened_by=None, offset=0, length=None,
267 267 order_by=None, order_dir='desc'):
268 268 """
269 269 Get all pull requests for a specific repository that are awaiting
270 270 review.
271 271
272 272 :param repo_name: target or source repo
273 273 :param source: boolean flag to specify if repo_name refers to source
274 274 :param statuses: list of pull request statuses
275 275 :param opened_by: author user of the pull request
276 276 :param offset: pagination offset
277 277 :param length: length of returned list
278 278 :param order_by: order of the returned list
279 279 :param order_dir: 'asc' or 'desc' ordering direction
280 280 :returns: list of pull requests
281 281 """
282 282 pull_requests = self.get_all(
283 283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 284 order_by=order_by, order_dir=order_dir)
285 285
286 286 _filtered_pull_requests = []
287 287 for pr in pull_requests:
288 288 status = pr.calculated_review_status()
289 289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 291 _filtered_pull_requests.append(pr)
292 292 if length:
293 293 return _filtered_pull_requests[offset:offset+length]
294 294 else:
295 295 return _filtered_pull_requests
296 296
297 297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 298 opened_by=None, user_id=None):
299 299 """
300 300 Count the number of pull requests for a specific repository that are
301 301 awaiting review from a specific user.
302 302
303 303 :param repo_name: target or source repo
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, source=source, statuses=statuses, opened_by=opened_by,
312 312 user_id=user_id)
313 313
314 314 return len(pull_requests)
315 315
316 316 def get_awaiting_my_review(self, repo_name, 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 source: boolean flag to specify if repo_name refers to source
325 325 :param statuses: list of pull request statuses
326 326 :param opened_by: author user of the pull request
327 327 :param user_id: reviewer user of the pull request
328 328 :param offset: pagination offset
329 329 :param length: length of returned list
330 330 :param order_by: order of the returned list
331 331 :param order_dir: 'asc' or 'desc' ordering direction
332 332 :returns: list of pull requests
333 333 """
334 334 pull_requests = self.get_all(
335 335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 336 order_by=order_by, order_dir=order_dir)
337 337
338 338 _my = PullRequestModel().get_not_reviewed(user_id)
339 339 my_participation = []
340 340 for pr in pull_requests:
341 341 if pr in _my:
342 342 my_participation.append(pr)
343 343 _filtered_pull_requests = my_participation
344 344 if length:
345 345 return _filtered_pull_requests[offset:offset+length]
346 346 else:
347 347 return _filtered_pull_requests
348 348
349 349 def get_not_reviewed(self, user_id):
350 350 return [
351 351 x.pull_request for x in PullRequestReviewers.query().filter(
352 352 PullRequestReviewers.user_id == user_id).all()
353 353 ]
354 354
355 355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 356 order_by=None, order_dir='desc'):
357 357 q = PullRequest.query()
358 358 if user_id:
359 359 reviewers_subquery = Session().query(
360 360 PullRequestReviewers.pull_request_id).filter(
361 361 PullRequestReviewers.user_id == user_id).subquery()
362 362 user_filter= or_(
363 363 PullRequest.user_id == user_id,
364 364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 365 )
366 366 q = PullRequest.query().filter(user_filter)
367 367
368 368 # closed,opened
369 369 if statuses:
370 370 q = q.filter(PullRequest.status.in_(statuses))
371 371
372 372 if order_by:
373 373 order_map = {
374 374 'name_raw': PullRequest.pull_request_id,
375 375 'title': PullRequest.title,
376 376 'updated_on_raw': PullRequest.updated_on,
377 377 'target_repo': PullRequest.target_repo_id
378 378 }
379 379 if order_dir == 'asc':
380 380 q = q.order_by(order_map[order_by].asc())
381 381 else:
382 382 q = q.order_by(order_map[order_by].desc())
383 383
384 384 return q
385 385
386 386 def count_im_participating_in(self, user_id=None, statuses=None):
387 387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 388 return q.count()
389 389
390 390 def get_im_participating_in(
391 391 self, user_id=None, statuses=None, offset=0,
392 392 length=None, order_by=None, order_dir='desc'):
393 393 """
394 394 Get all Pull requests that i'm participating in, or i have opened
395 395 """
396 396
397 397 q = self._prepare_participating_query(
398 398 user_id, statuses=statuses, order_by=order_by,
399 399 order_dir=order_dir)
400 400
401 401 if length:
402 402 pull_requests = q.limit(length).offset(offset).all()
403 403 else:
404 404 pull_requests = q.all()
405 405
406 406 return pull_requests
407 407
408 408 def get_versions(self, pull_request):
409 409 """
410 410 returns version of pull request sorted by ID descending
411 411 """
412 412 return PullRequestVersion.query()\
413 413 .filter(PullRequestVersion.pull_request == pull_request)\
414 414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 415 .all()
416 416
417 417 def create(self, created_by, source_repo, source_ref, target_repo,
418 418 target_ref, revisions, reviewers, title, description=None):
419 419 created_by_user = self._get_user(created_by)
420 420 source_repo = self._get_repo(source_repo)
421 421 target_repo = self._get_repo(target_repo)
422 422
423 423 pull_request = PullRequest()
424 424 pull_request.source_repo = source_repo
425 425 pull_request.source_ref = source_ref
426 426 pull_request.target_repo = target_repo
427 427 pull_request.target_ref = target_ref
428 428 pull_request.revisions = revisions
429 429 pull_request.title = title
430 430 pull_request.description = description
431 431 pull_request.author = created_by_user
432 432
433 433 Session().add(pull_request)
434 434 Session().flush()
435 435
436 436 reviewer_ids = set()
437 437 # members / reviewers
438 438 for reviewer_object in reviewers:
439 439 if isinstance(reviewer_object, tuple):
440 440 user_id, reasons = reviewer_object
441 441 else:
442 442 user_id, reasons = reviewer_object, []
443 443
444 444 user = self._get_user(user_id)
445 445 reviewer_ids.add(user.user_id)
446 446
447 447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 448 Session().add(reviewer)
449 449
450 450 # Set approval status to "Under Review" for all commits which are
451 451 # part of this pull request.
452 452 ChangesetStatusModel().set_status(
453 453 repo=target_repo,
454 454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 455 user=created_by_user,
456 456 pull_request=pull_request
457 457 )
458 458
459 459 self.notify_reviewers(pull_request, reviewer_ids)
460 460 self._trigger_pull_request_hook(
461 461 pull_request, created_by_user, 'create')
462 462
463 463 return pull_request
464 464
465 465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 466 pull_request = self.__get_pull_request(pull_request)
467 467 target_scm = pull_request.target_repo.scm_instance()
468 468 if action == 'create':
469 469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 470 elif action == 'merge':
471 471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 472 elif action == 'close':
473 473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 474 elif action == 'review_status_change':
475 475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 476 elif action == 'update':
477 477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 478 else:
479 479 return
480 480
481 481 trigger_hook(
482 482 username=user.username,
483 483 repo_name=pull_request.target_repo.repo_name,
484 484 repo_alias=target_scm.alias,
485 485 pull_request=pull_request)
486 486
487 487 def _get_commit_ids(self, pull_request):
488 488 """
489 489 Return the commit ids of the merged pull request.
490 490
491 491 This method is not dealing correctly yet with the lack of autoupdates
492 492 nor with the implicit target updates.
493 493 For example: if a commit in the source repo is already in the target it
494 494 will be reported anyways.
495 495 """
496 496 merge_rev = pull_request.merge_rev
497 497 if merge_rev is None:
498 498 raise ValueError('This pull request was not merged yet')
499 499
500 500 commit_ids = list(pull_request.revisions)
501 501 if merge_rev not in commit_ids:
502 502 commit_ids.append(merge_rev)
503 503
504 504 return commit_ids
505 505
506 506 def merge(self, pull_request, user, extras):
507 507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 509 if merge_state.executed:
510 510 log.debug(
511 511 "Merge was successful, updating the pull request comments.")
512 512 self._comment_and_close_pr(pull_request, user, merge_state)
513 513 self._log_action('user_merged_pull_request', user, pull_request)
514 514 else:
515 515 log.warn("Merge failed, not updating the pull request.")
516 516 return merge_state
517 517
518 518 def _merge_pull_request(self, pull_request, user, extras):
519 519 target_vcs = pull_request.target_repo.scm_instance()
520 520 source_vcs = pull_request.source_repo.scm_instance()
521 521 target_ref = self._refresh_reference(
522 522 pull_request.target_ref_parts, target_vcs)
523 523
524 524 message = _(
525 525 'Merge pull request #%(pr_id)s from '
526 526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 527 'pr_id': pull_request.pull_request_id,
528 528 'source_repo': source_vcs.name,
529 529 'source_ref_name': pull_request.source_ref_parts.name,
530 530 'pr_title': pull_request.title
531 531 }
532 532
533 533 workspace_id = self._workspace_id(pull_request)
534 534 use_rebase = self._use_rebase_for_merging(pull_request)
535 535
536 536 callback_daemon, extras = prepare_callback_daemon(
537 537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539 539
540 540 with callback_daemon:
541 541 # TODO: johbo: Implement a clean way to run a config_override
542 542 # for a single call.
543 543 target_vcs.config.set(
544 544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 545 merge_state = target_vcs.merge(
546 546 target_ref, source_vcs, pull_request.source_ref_parts,
547 547 workspace_id, user_name=user.username,
548 548 user_email=user.email, message=message, use_rebase=use_rebase)
549 549 return merge_state
550 550
551 551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 553 pull_request.updated_on = datetime.datetime.now()
554 554
555 555 CommentsModel().create(
556 556 text=unicode(_('Pull request merged and closed')),
557 557 repo=pull_request.target_repo.repo_id,
558 558 user=user.user_id,
559 559 pull_request=pull_request.pull_request_id,
560 560 f_path=None,
561 561 line_no=None,
562 562 closing_pr=True
563 563 )
564 564
565 565 Session().add(pull_request)
566 566 Session().flush()
567 567 # TODO: paris: replace invalidation with less radical solution
568 568 ScmModel().mark_for_invalidation(
569 569 pull_request.target_repo.repo_name)
570 570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571 571
572 572 def has_valid_update_type(self, pull_request):
573 573 source_ref_type = pull_request.source_ref_parts.type
574 574 return source_ref_type in ['book', 'branch', 'tag']
575 575
576 576 def update_commits(self, pull_request):
577 577 """
578 578 Get the updated list of commits for the pull request
579 579 and return the new pull request version and the list
580 580 of commits processed by this update action
581 581 """
582 582 pull_request = self.__get_pull_request(pull_request)
583 583 source_ref_type = pull_request.source_ref_parts.type
584 584 source_ref_name = pull_request.source_ref_parts.name
585 585 source_ref_id = pull_request.source_ref_parts.commit_id
586 586
587 587 target_ref_type = pull_request.target_ref_parts.type
588 588 target_ref_name = pull_request.target_ref_parts.name
589 589 target_ref_id = pull_request.target_ref_parts.commit_id
590 590
591 591 if not self.has_valid_update_type(pull_request):
592 592 log.debug(
593 593 "Skipping update of pull request %s due to ref type: %s",
594 594 pull_request, source_ref_type)
595 595 return UpdateResponse(
596 596 executed=False,
597 597 reason=UpdateFailureReason.WRONG_REF_TPYE,
598 598 old=pull_request, new=None, changes=None)
599 599
600 600 # source repo
601 601 source_repo = pull_request.source_repo.scm_instance()
602 602 try:
603 603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
604 604 except CommitDoesNotExistError:
605 605 return UpdateResponse(
606 606 executed=False,
607 607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
608 608 old=pull_request, new=None, changes=None)
609 609
610 610 source_changed = source_ref_id != source_commit.raw_id
611 611
612 612 # target repo
613 613 target_repo = pull_request.target_repo.scm_instance()
614 614 try:
615 615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
616 616 except CommitDoesNotExistError:
617 617 return UpdateResponse(
618 618 executed=False,
619 619 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 620 old=pull_request, new=None, changes=None)
621 621 target_changed = target_ref_id != target_commit.raw_id
622 622
623 623 if not (source_changed or target_changed):
624 624 log.debug("Nothing changed in pull request %s", pull_request)
625 625 return UpdateResponse(
626 626 executed=False,
627 627 reason=UpdateFailureReason.NO_CHANGE,
628 628 old=pull_request, new=None, changes=None)
629 629
630 630 change_in_found = 'target repo' if target_changed else 'source repo'
631 631 log.debug('Updating pull request because of change in %s detected',
632 632 change_in_found)
633 633
634 634 # Finally there is a need for an update, in case of source change
635 635 # we create a new version, else just an update
636 636 if source_changed:
637 637 pull_request_version = self._create_version_from_snapshot(pull_request)
638 638 self._link_comments_to_version(pull_request_version)
639 639 else:
640 ver = pull_request.versions[-1]
640 try:
641 ver = pull_request.versions[-1]
642 except IndexError:
643 ver = None
644
641 645 pull_request.pull_request_version_id = \
642 646 ver.pull_request_version_id if ver else None
643 647 pull_request_version = pull_request
644 648
645 649 try:
646 650 if target_ref_type in ('tag', 'branch', 'book'):
647 651 target_commit = target_repo.get_commit(target_ref_name)
648 652 else:
649 653 target_commit = target_repo.get_commit(target_ref_id)
650 654 except CommitDoesNotExistError:
651 655 return UpdateResponse(
652 656 executed=False,
653 657 reason=UpdateFailureReason.MISSING_TARGET_REF,
654 658 old=pull_request, new=None, changes=None)
655 659
656 660 # re-compute commit ids
657 661 old_commit_ids = pull_request.revisions
658 662 pre_load = ["author", "branch", "date", "message"]
659 663 commit_ranges = target_repo.compare(
660 664 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
661 665 pre_load=pre_load)
662 666
663 667 ancestor = target_repo.get_common_ancestor(
664 668 target_commit.raw_id, source_commit.raw_id, source_repo)
665 669
666 670 pull_request.source_ref = '%s:%s:%s' % (
667 671 source_ref_type, source_ref_name, source_commit.raw_id)
668 672 pull_request.target_ref = '%s:%s:%s' % (
669 673 target_ref_type, target_ref_name, ancestor)
670 674
671 675 pull_request.revisions = [
672 676 commit.raw_id for commit in reversed(commit_ranges)]
673 677 pull_request.updated_on = datetime.datetime.now()
674 678 Session().add(pull_request)
675 679 new_commit_ids = pull_request.revisions
676 680
677 681 changes = self._calculate_commit_id_changes(
678 682 old_commit_ids, new_commit_ids)
679 683
680 684 old_diff_data, new_diff_data = self._generate_update_diffs(
681 685 pull_request, pull_request_version)
682 686
683 687 CommentsModel().outdate_comments(
684 688 pull_request, old_diff_data=old_diff_data,
685 689 new_diff_data=new_diff_data)
686 690
687 691 file_changes = self._calculate_file_changes(
688 692 old_diff_data, new_diff_data)
689 693
690 694 # Add an automatic comment to the pull request
691 695 update_comment = CommentsModel().create(
692 696 text=self._render_update_message(changes, file_changes),
693 697 repo=pull_request.target_repo,
694 698 user=pull_request.author,
695 699 pull_request=pull_request,
696 700 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
697 701
698 702 # Update status to "Under Review" for added commits
699 703 for commit_id in changes.added:
700 704 ChangesetStatusModel().set_status(
701 705 repo=pull_request.source_repo,
702 706 status=ChangesetStatus.STATUS_UNDER_REVIEW,
703 707 comment=update_comment,
704 708 user=pull_request.author,
705 709 pull_request=pull_request,
706 710 revision=commit_id)
707 711
708 712 log.debug(
709 713 'Updated pull request %s, added_ids: %s, common_ids: %s, '
710 714 'removed_ids: %s', pull_request.pull_request_id,
711 715 changes.added, changes.common, changes.removed)
712 716 log.debug('Updated pull request with the following file changes: %s',
713 717 file_changes)
714 718
715 719 log.info(
716 720 "Updated pull request %s from commit %s to commit %s, "
717 721 "stored new version %s of this pull request.",
718 722 pull_request.pull_request_id, source_ref_id,
719 723 pull_request.source_ref_parts.commit_id,
720 724 pull_request_version.pull_request_version_id)
721 725 Session().commit()
722 726 self._trigger_pull_request_hook(pull_request, pull_request.author,
723 727 'update')
724 728
725 729 return UpdateResponse(
726 730 executed=True, reason=UpdateFailureReason.NONE,
727 731 old=pull_request, new=pull_request_version, changes=changes)
728 732
729 733 def _create_version_from_snapshot(self, pull_request):
730 734 version = PullRequestVersion()
731 735 version.title = pull_request.title
732 736 version.description = pull_request.description
733 737 version.status = pull_request.status
734 738 version.created_on = datetime.datetime.now()
735 739 version.updated_on = pull_request.updated_on
736 740 version.user_id = pull_request.user_id
737 741 version.source_repo = pull_request.source_repo
738 742 version.source_ref = pull_request.source_ref
739 743 version.target_repo = pull_request.target_repo
740 744 version.target_ref = pull_request.target_ref
741 745
742 746 version._last_merge_source_rev = pull_request._last_merge_source_rev
743 747 version._last_merge_target_rev = pull_request._last_merge_target_rev
744 748 version._last_merge_status = pull_request._last_merge_status
745 749 version.shadow_merge_ref = pull_request.shadow_merge_ref
746 750 version.merge_rev = pull_request.merge_rev
747 751
748 752 version.revisions = pull_request.revisions
749 753 version.pull_request = pull_request
750 754 Session().add(version)
751 755 Session().flush()
752 756
753 757 return version
754 758
755 759 def _generate_update_diffs(self, pull_request, pull_request_version):
756 760
757 761 diff_context = (
758 762 self.DIFF_CONTEXT +
759 763 CommentsModel.needed_extra_diff_context())
760 764
761 765 source_repo = pull_request_version.source_repo
762 766 source_ref_id = pull_request_version.source_ref_parts.commit_id
763 767 target_ref_id = pull_request_version.target_ref_parts.commit_id
764 768 old_diff = self._get_diff_from_pr_or_version(
765 769 source_repo, source_ref_id, target_ref_id, context=diff_context)
766 770
767 771 source_repo = pull_request.source_repo
768 772 source_ref_id = pull_request.source_ref_parts.commit_id
769 773 target_ref_id = pull_request.target_ref_parts.commit_id
770 774
771 775 new_diff = self._get_diff_from_pr_or_version(
772 776 source_repo, source_ref_id, target_ref_id, context=diff_context)
773 777
774 778 old_diff_data = diffs.DiffProcessor(old_diff)
775 779 old_diff_data.prepare()
776 780 new_diff_data = diffs.DiffProcessor(new_diff)
777 781 new_diff_data.prepare()
778 782
779 783 return old_diff_data, new_diff_data
780 784
781 785 def _link_comments_to_version(self, pull_request_version):
782 786 """
783 787 Link all unlinked comments of this pull request to the given version.
784 788
785 789 :param pull_request_version: The `PullRequestVersion` to which
786 790 the comments shall be linked.
787 791
788 792 """
789 793 pull_request = pull_request_version.pull_request
790 794 comments = ChangesetComment.query().filter(
791 795 # TODO: johbo: Should we query for the repo at all here?
792 796 # Pending decision on how comments of PRs are to be related
793 797 # to either the source repo, the target repo or no repo at all.
794 798 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
795 799 ChangesetComment.pull_request == pull_request,
796 800 ChangesetComment.pull_request_version == None)
797 801
798 802 # TODO: johbo: Find out why this breaks if it is done in a bulk
799 803 # operation.
800 804 for comment in comments:
801 805 comment.pull_request_version_id = (
802 806 pull_request_version.pull_request_version_id)
803 807 Session().add(comment)
804 808
805 809 def _calculate_commit_id_changes(self, old_ids, new_ids):
806 810 added = [x for x in new_ids if x not in old_ids]
807 811 common = [x for x in new_ids if x in old_ids]
808 812 removed = [x for x in old_ids if x not in new_ids]
809 813 total = new_ids
810 814 return ChangeTuple(added, common, removed, total)
811 815
812 816 def _calculate_file_changes(self, old_diff_data, new_diff_data):
813 817
814 818 old_files = OrderedDict()
815 819 for diff_data in old_diff_data.parsed_diff:
816 820 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
817 821
818 822 added_files = []
819 823 modified_files = []
820 824 removed_files = []
821 825 for diff_data in new_diff_data.parsed_diff:
822 826 new_filename = diff_data['filename']
823 827 new_hash = md5_safe(diff_data['raw_diff'])
824 828
825 829 old_hash = old_files.get(new_filename)
826 830 if not old_hash:
827 831 # file is not present in old diff, means it's added
828 832 added_files.append(new_filename)
829 833 else:
830 834 if new_hash != old_hash:
831 835 modified_files.append(new_filename)
832 836 # now remove a file from old, since we have seen it already
833 837 del old_files[new_filename]
834 838
835 839 # removed files is when there are present in old, but not in NEW,
836 840 # since we remove old files that are present in new diff, left-overs
837 841 # if any should be the removed files
838 842 removed_files.extend(old_files.keys())
839 843
840 844 return FileChangeTuple(added_files, modified_files, removed_files)
841 845
842 846 def _render_update_message(self, changes, file_changes):
843 847 """
844 848 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
845 849 so it's always looking the same disregarding on which default
846 850 renderer system is using.
847 851
848 852 :param changes: changes named tuple
849 853 :param file_changes: file changes named tuple
850 854
851 855 """
852 856 new_status = ChangesetStatus.get_status_lbl(
853 857 ChangesetStatus.STATUS_UNDER_REVIEW)
854 858
855 859 changed_files = (
856 860 file_changes.added + file_changes.modified + file_changes.removed)
857 861
858 862 params = {
859 863 'under_review_label': new_status,
860 864 'added_commits': changes.added,
861 865 'removed_commits': changes.removed,
862 866 'changed_files': changed_files,
863 867 'added_files': file_changes.added,
864 868 'modified_files': file_changes.modified,
865 869 'removed_files': file_changes.removed,
866 870 }
867 871 renderer = RstTemplateRenderer()
868 872 return renderer.render('pull_request_update.mako', **params)
869 873
870 874 def edit(self, pull_request, title, description):
871 875 pull_request = self.__get_pull_request(pull_request)
872 876 if pull_request.is_closed():
873 877 raise ValueError('This pull request is closed')
874 878 if title:
875 879 pull_request.title = title
876 880 pull_request.description = description
877 881 pull_request.updated_on = datetime.datetime.now()
878 882 Session().add(pull_request)
879 883
880 884 def update_reviewers(self, pull_request, reviewer_data):
881 885 """
882 886 Update the reviewers in the pull request
883 887
884 888 :param pull_request: the pr to update
885 889 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
886 890 """
887 891
888 892 reviewers_reasons = {}
889 893 for user_id, reasons in reviewer_data:
890 894 if isinstance(user_id, (int, basestring)):
891 895 user_id = self._get_user(user_id).user_id
892 896 reviewers_reasons[user_id] = reasons
893 897
894 898 reviewers_ids = set(reviewers_reasons.keys())
895 899 pull_request = self.__get_pull_request(pull_request)
896 900 current_reviewers = PullRequestReviewers.query()\
897 901 .filter(PullRequestReviewers.pull_request ==
898 902 pull_request).all()
899 903 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
900 904
901 905 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
902 906 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
903 907
904 908 log.debug("Adding %s reviewers", ids_to_add)
905 909 log.debug("Removing %s reviewers", ids_to_remove)
906 910 changed = False
907 911 for uid in ids_to_add:
908 912 changed = True
909 913 _usr = self._get_user(uid)
910 914 reasons = reviewers_reasons[uid]
911 915 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
912 916 Session().add(reviewer)
913 917
914 918 for uid in ids_to_remove:
915 919 changed = True
916 920 reviewers = PullRequestReviewers.query()\
917 921 .filter(PullRequestReviewers.user_id == uid,
918 922 PullRequestReviewers.pull_request == pull_request)\
919 923 .all()
920 924 # use .all() in case we accidentally added the same person twice
921 925 # this CAN happen due to the lack of DB checks
922 926 for obj in reviewers:
923 927 Session().delete(obj)
924 928
925 929 if changed:
926 930 pull_request.updated_on = datetime.datetime.now()
927 931 Session().add(pull_request)
928 932
929 933 self.notify_reviewers(pull_request, ids_to_add)
930 934 return ids_to_add, ids_to_remove
931 935
932 936 def get_url(self, pull_request):
933 937 return h.url('pullrequest_show',
934 938 repo_name=safe_str(pull_request.target_repo.repo_name),
935 939 pull_request_id=pull_request.pull_request_id,
936 940 qualified=True)
937 941
938 942 def get_shadow_clone_url(self, pull_request):
939 943 """
940 944 Returns qualified url pointing to the shadow repository. If this pull
941 945 request is closed there is no shadow repository and ``None`` will be
942 946 returned.
943 947 """
944 948 if pull_request.is_closed():
945 949 return None
946 950 else:
947 951 pr_url = urllib.unquote(self.get_url(pull_request))
948 952 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
949 953
950 954 def notify_reviewers(self, pull_request, reviewers_ids):
951 955 # notification to reviewers
952 956 if not reviewers_ids:
953 957 return
954 958
955 959 pull_request_obj = pull_request
956 960 # get the current participants of this pull request
957 961 recipients = reviewers_ids
958 962 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
959 963
960 964 pr_source_repo = pull_request_obj.source_repo
961 965 pr_target_repo = pull_request_obj.target_repo
962 966
963 967 pr_url = h.url(
964 968 'pullrequest_show',
965 969 repo_name=pr_target_repo.repo_name,
966 970 pull_request_id=pull_request_obj.pull_request_id,
967 971 qualified=True,)
968 972
969 973 # set some variables for email notification
970 974 pr_target_repo_url = h.url(
971 975 'summary_home',
972 976 repo_name=pr_target_repo.repo_name,
973 977 qualified=True)
974 978
975 979 pr_source_repo_url = h.url(
976 980 'summary_home',
977 981 repo_name=pr_source_repo.repo_name,
978 982 qualified=True)
979 983
980 984 # pull request specifics
981 985 pull_request_commits = [
982 986 (x.raw_id, x.message)
983 987 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
984 988
985 989 kwargs = {
986 990 'user': pull_request.author,
987 991 'pull_request': pull_request_obj,
988 992 'pull_request_commits': pull_request_commits,
989 993
990 994 'pull_request_target_repo': pr_target_repo,
991 995 'pull_request_target_repo_url': pr_target_repo_url,
992 996
993 997 'pull_request_source_repo': pr_source_repo,
994 998 'pull_request_source_repo_url': pr_source_repo_url,
995 999
996 1000 'pull_request_url': pr_url,
997 1001 }
998 1002
999 1003 # pre-generate the subject for notification itself
1000 1004 (subject,
1001 1005 _h, _e, # we don't care about those
1002 1006 body_plaintext) = EmailNotificationModel().render_email(
1003 1007 notification_type, **kwargs)
1004 1008
1005 1009 # create notification objects, and emails
1006 1010 NotificationModel().create(
1007 1011 created_by=pull_request.author,
1008 1012 notification_subject=subject,
1009 1013 notification_body=body_plaintext,
1010 1014 notification_type=notification_type,
1011 1015 recipients=recipients,
1012 1016 email_kwargs=kwargs,
1013 1017 )
1014 1018
1015 1019 def delete(self, pull_request):
1016 1020 pull_request = self.__get_pull_request(pull_request)
1017 1021 self._cleanup_merge_workspace(pull_request)
1018 1022 Session().delete(pull_request)
1019 1023
1020 1024 def close_pull_request(self, pull_request, user):
1021 1025 pull_request = self.__get_pull_request(pull_request)
1022 1026 self._cleanup_merge_workspace(pull_request)
1023 1027 pull_request.status = PullRequest.STATUS_CLOSED
1024 1028 pull_request.updated_on = datetime.datetime.now()
1025 1029 Session().add(pull_request)
1026 1030 self._trigger_pull_request_hook(
1027 1031 pull_request, pull_request.author, 'close')
1028 1032 self._log_action('user_closed_pull_request', user, pull_request)
1029 1033
1030 1034 def close_pull_request_with_comment(self, pull_request, user, repo,
1031 1035 message=None):
1032 1036 status = ChangesetStatus.STATUS_REJECTED
1033 1037
1034 1038 if not message:
1035 1039 message = (
1036 1040 _('Status change %(transition_icon)s %(status)s') % {
1037 1041 'transition_icon': '>',
1038 1042 'status': ChangesetStatus.get_status_lbl(status)})
1039 1043
1040 1044 internal_message = _('Closing with') + ' ' + message
1041 1045
1042 1046 comm = CommentsModel().create(
1043 1047 text=internal_message,
1044 1048 repo=repo.repo_id,
1045 1049 user=user.user_id,
1046 1050 pull_request=pull_request.pull_request_id,
1047 1051 f_path=None,
1048 1052 line_no=None,
1049 1053 status_change=ChangesetStatus.get_status_lbl(status),
1050 1054 status_change_type=status,
1051 1055 closing_pr=True
1052 1056 )
1053 1057
1054 1058 ChangesetStatusModel().set_status(
1055 1059 repo.repo_id,
1056 1060 status,
1057 1061 user.user_id,
1058 1062 comm,
1059 1063 pull_request=pull_request.pull_request_id
1060 1064 )
1061 1065 Session().flush()
1062 1066
1063 1067 PullRequestModel().close_pull_request(
1064 1068 pull_request.pull_request_id, user)
1065 1069
1066 1070 def merge_status(self, pull_request):
1067 1071 if not self._is_merge_enabled(pull_request):
1068 1072 return False, _('Server-side pull request merging is disabled.')
1069 1073 if pull_request.is_closed():
1070 1074 return False, _('This pull request is closed.')
1071 1075 merge_possible, msg = self._check_repo_requirements(
1072 1076 target=pull_request.target_repo, source=pull_request.source_repo)
1073 1077 if not merge_possible:
1074 1078 return merge_possible, msg
1075 1079
1076 1080 try:
1077 1081 resp = self._try_merge(pull_request)
1078 1082 log.debug("Merge response: %s", resp)
1079 1083 status = resp.possible, self.merge_status_message(
1080 1084 resp.failure_reason)
1081 1085 except NotImplementedError:
1082 1086 status = False, _('Pull request merging is not supported.')
1083 1087
1084 1088 return status
1085 1089
1086 1090 def _check_repo_requirements(self, target, source):
1087 1091 """
1088 1092 Check if `target` and `source` have compatible requirements.
1089 1093
1090 1094 Currently this is just checking for largefiles.
1091 1095 """
1092 1096 target_has_largefiles = self._has_largefiles(target)
1093 1097 source_has_largefiles = self._has_largefiles(source)
1094 1098 merge_possible = True
1095 1099 message = u''
1096 1100
1097 1101 if target_has_largefiles != source_has_largefiles:
1098 1102 merge_possible = False
1099 1103 if source_has_largefiles:
1100 1104 message = _(
1101 1105 'Target repository large files support is disabled.')
1102 1106 else:
1103 1107 message = _(
1104 1108 'Source repository large files support is disabled.')
1105 1109
1106 1110 return merge_possible, message
1107 1111
1108 1112 def _has_largefiles(self, repo):
1109 1113 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1110 1114 'extensions', 'largefiles')
1111 1115 return largefiles_ui and largefiles_ui[0].active
1112 1116
1113 1117 def _try_merge(self, pull_request):
1114 1118 """
1115 1119 Try to merge the pull request and return the merge status.
1116 1120 """
1117 1121 log.debug(
1118 1122 "Trying out if the pull request %s can be merged.",
1119 1123 pull_request.pull_request_id)
1120 1124 target_vcs = pull_request.target_repo.scm_instance()
1121 1125
1122 1126 # Refresh the target reference.
1123 1127 try:
1124 1128 target_ref = self._refresh_reference(
1125 1129 pull_request.target_ref_parts, target_vcs)
1126 1130 except CommitDoesNotExistError:
1127 1131 merge_state = MergeResponse(
1128 1132 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1129 1133 return merge_state
1130 1134
1131 1135 target_locked = pull_request.target_repo.locked
1132 1136 if target_locked and target_locked[0]:
1133 1137 log.debug("The target repository is locked.")
1134 1138 merge_state = MergeResponse(
1135 1139 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1136 1140 elif self._needs_merge_state_refresh(pull_request, target_ref):
1137 1141 log.debug("Refreshing the merge status of the repository.")
1138 1142 merge_state = self._refresh_merge_state(
1139 1143 pull_request, target_vcs, target_ref)
1140 1144 else:
1141 1145 possible = pull_request.\
1142 1146 _last_merge_status == MergeFailureReason.NONE
1143 1147 merge_state = MergeResponse(
1144 1148 possible, False, None, pull_request._last_merge_status)
1145 1149
1146 1150 return merge_state
1147 1151
1148 1152 def _refresh_reference(self, reference, vcs_repository):
1149 1153 if reference.type in ('branch', 'book'):
1150 1154 name_or_id = reference.name
1151 1155 else:
1152 1156 name_or_id = reference.commit_id
1153 1157 refreshed_commit = vcs_repository.get_commit(name_or_id)
1154 1158 refreshed_reference = Reference(
1155 1159 reference.type, reference.name, refreshed_commit.raw_id)
1156 1160 return refreshed_reference
1157 1161
1158 1162 def _needs_merge_state_refresh(self, pull_request, target_reference):
1159 1163 return not(
1160 1164 pull_request.revisions and
1161 1165 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1162 1166 target_reference.commit_id == pull_request._last_merge_target_rev)
1163 1167
1164 1168 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1165 1169 workspace_id = self._workspace_id(pull_request)
1166 1170 source_vcs = pull_request.source_repo.scm_instance()
1167 1171 use_rebase = self._use_rebase_for_merging(pull_request)
1168 1172 merge_state = target_vcs.merge(
1169 1173 target_reference, source_vcs, pull_request.source_ref_parts,
1170 1174 workspace_id, dry_run=True, use_rebase=use_rebase)
1171 1175
1172 1176 # Do not store the response if there was an unknown error.
1173 1177 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1174 1178 pull_request._last_merge_source_rev = \
1175 1179 pull_request.source_ref_parts.commit_id
1176 1180 pull_request._last_merge_target_rev = target_reference.commit_id
1177 1181 pull_request._last_merge_status = merge_state.failure_reason
1178 1182 pull_request.shadow_merge_ref = merge_state.merge_ref
1179 1183 Session().add(pull_request)
1180 1184 Session().commit()
1181 1185
1182 1186 return merge_state
1183 1187
1184 1188 def _workspace_id(self, pull_request):
1185 1189 workspace_id = 'pr-%s' % pull_request.pull_request_id
1186 1190 return workspace_id
1187 1191
1188 1192 def merge_status_message(self, status_code):
1189 1193 """
1190 1194 Return a human friendly error message for the given merge status code.
1191 1195 """
1192 1196 return self.MERGE_STATUS_MESSAGES[status_code]
1193 1197
1194 1198 def generate_repo_data(self, repo, commit_id=None, branch=None,
1195 1199 bookmark=None):
1196 1200 all_refs, selected_ref = \
1197 1201 self._get_repo_pullrequest_sources(
1198 1202 repo.scm_instance(), commit_id=commit_id,
1199 1203 branch=branch, bookmark=bookmark)
1200 1204
1201 1205 refs_select2 = []
1202 1206 for element in all_refs:
1203 1207 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1204 1208 refs_select2.append({'text': element[1], 'children': children})
1205 1209
1206 1210 return {
1207 1211 'user': {
1208 1212 'user_id': repo.user.user_id,
1209 1213 'username': repo.user.username,
1210 1214 'firstname': repo.user.firstname,
1211 1215 'lastname': repo.user.lastname,
1212 1216 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1213 1217 },
1214 1218 'description': h.chop_at_smart(repo.description, '\n'),
1215 1219 'refs': {
1216 1220 'all_refs': all_refs,
1217 1221 'selected_ref': selected_ref,
1218 1222 'select2_refs': refs_select2
1219 1223 }
1220 1224 }
1221 1225
1222 1226 def generate_pullrequest_title(self, source, source_ref, target):
1223 1227 return u'{source}#{at_ref} to {target}'.format(
1224 1228 source=source,
1225 1229 at_ref=source_ref,
1226 1230 target=target,
1227 1231 )
1228 1232
1229 1233 def _cleanup_merge_workspace(self, pull_request):
1230 1234 # Merging related cleanup
1231 1235 target_scm = pull_request.target_repo.scm_instance()
1232 1236 workspace_id = 'pr-%s' % pull_request.pull_request_id
1233 1237
1234 1238 try:
1235 1239 target_scm.cleanup_merge_workspace(workspace_id)
1236 1240 except NotImplementedError:
1237 1241 pass
1238 1242
1239 1243 def _get_repo_pullrequest_sources(
1240 1244 self, repo, commit_id=None, branch=None, bookmark=None):
1241 1245 """
1242 1246 Return a structure with repo's interesting commits, suitable for
1243 1247 the selectors in pullrequest controller
1244 1248
1245 1249 :param commit_id: a commit that must be in the list somehow
1246 1250 and selected by default
1247 1251 :param branch: a branch that must be in the list and selected
1248 1252 by default - even if closed
1249 1253 :param bookmark: a bookmark that must be in the list and selected
1250 1254 """
1251 1255
1252 1256 commit_id = safe_str(commit_id) if commit_id else None
1253 1257 branch = safe_str(branch) if branch else None
1254 1258 bookmark = safe_str(bookmark) if bookmark else None
1255 1259
1256 1260 selected = None
1257 1261
1258 1262 # order matters: first source that has commit_id in it will be selected
1259 1263 sources = []
1260 1264 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1261 1265 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1262 1266
1263 1267 if commit_id:
1264 1268 ref_commit = (h.short_id(commit_id), commit_id)
1265 1269 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1266 1270
1267 1271 sources.append(
1268 1272 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1269 1273 )
1270 1274
1271 1275 groups = []
1272 1276 for group_key, ref_list, group_name, match in sources:
1273 1277 group_refs = []
1274 1278 for ref_name, ref_id in ref_list:
1275 1279 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1276 1280 group_refs.append((ref_key, ref_name))
1277 1281
1278 1282 if not selected:
1279 1283 if set([commit_id, match]) & set([ref_id, ref_name]):
1280 1284 selected = ref_key
1281 1285
1282 1286 if group_refs:
1283 1287 groups.append((group_refs, group_name))
1284 1288
1285 1289 if not selected:
1286 1290 ref = commit_id or branch or bookmark
1287 1291 if ref:
1288 1292 raise CommitDoesNotExistError(
1289 1293 'No commit refs could be found matching: %s' % ref)
1290 1294 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1291 1295 selected = 'branch:%s:%s' % (
1292 1296 repo.DEFAULT_BRANCH_NAME,
1293 1297 repo.branches[repo.DEFAULT_BRANCH_NAME]
1294 1298 )
1295 1299 elif repo.commit_ids:
1296 1300 rev = repo.commit_ids[0]
1297 1301 selected = 'rev:%s:%s' % (rev, rev)
1298 1302 else:
1299 1303 raise EmptyRepositoryError()
1300 1304 return groups, selected
1301 1305
1302 1306 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1303 1307 return self._get_diff_from_pr_or_version(
1304 1308 source_repo, source_ref_id, target_ref_id, context=context)
1305 1309
1306 1310 def _get_diff_from_pr_or_version(
1307 1311 self, source_repo, source_ref_id, target_ref_id, context):
1308 1312 target_commit = source_repo.get_commit(
1309 1313 commit_id=safe_str(target_ref_id))
1310 1314 source_commit = source_repo.get_commit(
1311 1315 commit_id=safe_str(source_ref_id))
1312 1316 if isinstance(source_repo, Repository):
1313 1317 vcs_repo = source_repo.scm_instance()
1314 1318 else:
1315 1319 vcs_repo = source_repo
1316 1320
1317 1321 # TODO: johbo: In the context of an update, we cannot reach
1318 1322 # the old commit anymore with our normal mechanisms. It needs
1319 1323 # some sort of special support in the vcs layer to avoid this
1320 1324 # workaround.
1321 1325 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1322 1326 vcs_repo.alias == 'git'):
1323 1327 source_commit.raw_id = safe_str(source_ref_id)
1324 1328
1325 1329 log.debug('calculating diff between '
1326 1330 'source_ref:%s and target_ref:%s for repo `%s`',
1327 1331 target_ref_id, source_ref_id,
1328 1332 safe_unicode(vcs_repo.path))
1329 1333
1330 1334 vcs_diff = vcs_repo.get_diff(
1331 1335 commit1=target_commit, commit2=source_commit, context=context)
1332 1336 return vcs_diff
1333 1337
1334 1338 def _is_merge_enabled(self, pull_request):
1335 1339 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1336 1340 settings = settings_model.get_general_settings()
1337 1341 return settings.get('rhodecode_pr_merge_enabled', False)
1338 1342
1339 1343 def _use_rebase_for_merging(self, pull_request):
1340 1344 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1341 1345 settings = settings_model.get_general_settings()
1342 1346 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1343 1347
1344 1348 def _log_action(self, action, user, pull_request):
1345 1349 action_logger(
1346 1350 user,
1347 1351 '{action}:{pr_id}'.format(
1348 1352 action=action, pr_id=pull_request.pull_request_id),
1349 1353 pull_request.target_repo)
1350 1354
1351 1355
1352 1356 class MergeCheck(object):
1353 1357 """
1354 1358 Perform Merge Checks and returns a check object which stores information
1355 1359 about merge errors, and merge conditions
1356 1360 """
1357 1361 TODO_CHECK = 'todo'
1358 1362 PERM_CHECK = 'perm'
1359 1363 REVIEW_CHECK = 'review'
1360 1364 MERGE_CHECK = 'merge'
1361 1365
1362 1366 def __init__(self):
1363 1367 self.review_status = None
1364 1368 self.merge_possible = None
1365 1369 self.merge_msg = ''
1366 1370 self.failed = None
1367 1371 self.errors = []
1368 1372 self.error_details = OrderedDict()
1369 1373
1370 1374 def push_error(self, error_type, message, error_key, details):
1371 1375 self.failed = True
1372 1376 self.errors.append([error_type, message])
1373 1377 self.error_details[error_key] = dict(
1374 1378 details=details,
1375 1379 error_type=error_type,
1376 1380 message=message
1377 1381 )
1378 1382
1379 1383 @classmethod
1380 1384 def validate(cls, pull_request, user, fail_early=False, translator=None):
1381 1385 # if migrated to pyramid...
1382 1386 # _ = lambda: translator or _ # use passed in translator if any
1383 1387
1384 1388 merge_check = cls()
1385 1389
1386 1390 # permissions to merge
1387 1391 user_allowed_to_merge = PullRequestModel().check_user_merge(
1388 1392 pull_request, user)
1389 1393 if not user_allowed_to_merge:
1390 1394 log.debug("MergeCheck: cannot merge, approval is pending.")
1391 1395
1392 1396 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1393 1397 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1394 1398 if fail_early:
1395 1399 return merge_check
1396 1400
1397 1401 # review status, must be always present
1398 1402 review_status = pull_request.calculated_review_status()
1399 1403 merge_check.review_status = review_status
1400 1404
1401 1405 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1402 1406 if not status_approved:
1403 1407 log.debug("MergeCheck: cannot merge, approval is pending.")
1404 1408
1405 1409 msg = _('Pull request reviewer approval is pending.')
1406 1410
1407 1411 merge_check.push_error(
1408 1412 'warning', msg, cls.REVIEW_CHECK, review_status)
1409 1413
1410 1414 if fail_early:
1411 1415 return merge_check
1412 1416
1413 1417 # left over TODOs
1414 1418 todos = CommentsModel().get_unresolved_todos(pull_request)
1415 1419 if todos:
1416 1420 log.debug("MergeCheck: cannot merge, {} "
1417 1421 "unresolved todos left.".format(len(todos)))
1418 1422
1419 1423 if len(todos) == 1:
1420 1424 msg = _('Cannot merge, {} TODO still not resolved.').format(
1421 1425 len(todos))
1422 1426 else:
1423 1427 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1424 1428 len(todos))
1425 1429
1426 1430 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1427 1431
1428 1432 if fail_early:
1429 1433 return merge_check
1430 1434
1431 1435 # merge possible
1432 1436 merge_status, msg = PullRequestModel().merge_status(pull_request)
1433 1437 merge_check.merge_possible = merge_status
1434 1438 merge_check.merge_msg = msg
1435 1439 if not merge_status:
1436 1440 log.debug(
1437 1441 "MergeCheck: cannot merge, pull request merge not possible.")
1438 1442 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1439 1443
1440 1444 if fail_early:
1441 1445 return merge_check
1442 1446
1443 1447 return merge_check
1444 1448
1445 1449
1446 1450 ChangeTuple = namedtuple('ChangeTuple',
1447 1451 ['added', 'common', 'removed', 'total'])
1448 1452
1449 1453 FileChangeTuple = namedtuple('FileChangeTuple',
1450 1454 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now