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