##// END OF EJS Templates
pr: Return update response objects instead of tuples.
Martin Bornhold -
r1074:175746dd default
parent child Browse files
Show More
@@ -1,1230 +1,1237 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
515 514 pull_request = self.__get_pull_request(pull_request)
516 515 source_ref_type = pull_request.source_ref_parts.type
517 516 source_ref_name = pull_request.source_ref_parts.name
518 517 source_ref_id = pull_request.source_ref_parts.commit_id
519 518
520 519 if not self.has_valid_update_type(pull_request):
521 520 log.debug(
522 521 "Skipping update of pull request %s due to ref type: %s",
523 522 pull_request, source_ref_type)
524 return (None, None)
523 return UpdateResponse(
524 success=False,
525 reason=UpdateFailureReason.WRONG_REF_TPYE,
526 old=pull_request, new=None, changes=None)
525 527
526 528 source_repo = pull_request.source_repo.scm_instance()
527 529 source_commit = source_repo.get_commit(commit_id=source_ref_name)
528 530 if source_ref_id == source_commit.raw_id:
529 531 log.debug("Nothing changed in pull request %s", pull_request)
530 return (None, None)
532 return UpdateResponse(
533 success=True,
534 reason=UpdateFailureReason.NO_CHANGE,
535 old=pull_request, new=None, changes=None)
531 536
532 537 # Finally there is a need for an update
533 538 pull_request_version = self._create_version_from_snapshot(pull_request)
534 539 self._link_comments_to_version(pull_request_version)
535 540
536 541 target_ref_type = pull_request.target_ref_parts.type
537 542 target_ref_name = pull_request.target_ref_parts.name
538 543 target_ref_id = pull_request.target_ref_parts.commit_id
539 544 target_repo = pull_request.target_repo.scm_instance()
540 545
541 546 if target_ref_type in ('tag', 'branch', 'book'):
542 547 target_commit = target_repo.get_commit(target_ref_name)
543 548 else:
544 549 target_commit = target_repo.get_commit(target_ref_id)
545 550
546 551 # re-compute commit ids
547 552 old_commit_ids = set(pull_request.revisions)
548 553 pre_load = ["author", "branch", "date", "message"]
549 554 commit_ranges = target_repo.compare(
550 555 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
551 556 pre_load=pre_load)
552 557
553 558 ancestor = target_repo.get_common_ancestor(
554 559 target_commit.raw_id, source_commit.raw_id, source_repo)
555 560
556 561 pull_request.source_ref = '%s:%s:%s' % (
557 562 source_ref_type, source_ref_name, source_commit.raw_id)
558 563 pull_request.target_ref = '%s:%s:%s' % (
559 564 target_ref_type, target_ref_name, ancestor)
560 565 pull_request.revisions = [
561 566 commit.raw_id for commit in reversed(commit_ranges)]
562 567 pull_request.updated_on = datetime.datetime.now()
563 568 Session().add(pull_request)
564 569 new_commit_ids = set(pull_request.revisions)
565 570
566 571 changes = self._calculate_commit_id_changes(
567 572 old_commit_ids, new_commit_ids)
568 573
569 574 old_diff_data, new_diff_data = self._generate_update_diffs(
570 575 pull_request, pull_request_version)
571 576
572 577 ChangesetCommentsModel().outdate_comments(
573 578 pull_request, old_diff_data=old_diff_data,
574 579 new_diff_data=new_diff_data)
575 580
576 581 file_changes = self._calculate_file_changes(
577 582 old_diff_data, new_diff_data)
578 583
579 584 # Add an automatic comment to the pull request
580 585 update_comment = ChangesetCommentsModel().create(
581 586 text=self._render_update_message(changes, file_changes),
582 587 repo=pull_request.target_repo,
583 588 user=pull_request.author,
584 589 pull_request=pull_request,
585 590 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
586 591
587 592 # Update status to "Under Review" for added commits
588 593 for commit_id in changes.added:
589 594 ChangesetStatusModel().set_status(
590 595 repo=pull_request.source_repo,
591 596 status=ChangesetStatus.STATUS_UNDER_REVIEW,
592 597 comment=update_comment,
593 598 user=pull_request.author,
594 599 pull_request=pull_request,
595 600 revision=commit_id)
596 601
597 602 log.debug(
598 603 'Updated pull request %s, added_ids: %s, common_ids: %s, '
599 604 'removed_ids: %s', pull_request.pull_request_id,
600 605 changes.added, changes.common, changes.removed)
601 606 log.debug('Updated pull request with the following file changes: %s',
602 607 file_changes)
603 608
604 609 log.info(
605 610 "Updated pull request %s from commit %s to commit %s, "
606 611 "stored new version %s of this pull request.",
607 612 pull_request.pull_request_id, source_ref_id,
608 613 pull_request.source_ref_parts.commit_id,
609 614 pull_request_version.pull_request_version_id)
610 615 Session().commit()
611 616 self._trigger_pull_request_hook(pull_request, pull_request.author,
612 617 'update')
613 618
614 return (pull_request_version, changes)
619 return UpdateResponse(
620 success=True, reason=UpdateFailureReason.NONE,
621 old=pull_request, new=pull_request_version, changes=changes)
615 622
616 623 def _create_version_from_snapshot(self, pull_request):
617 624 version = PullRequestVersion()
618 625 version.title = pull_request.title
619 626 version.description = pull_request.description
620 627 version.status = pull_request.status
621 628 version.created_on = pull_request.created_on
622 629 version.updated_on = pull_request.updated_on
623 630 version.user_id = pull_request.user_id
624 631 version.source_repo = pull_request.source_repo
625 632 version.source_ref = pull_request.source_ref
626 633 version.target_repo = pull_request.target_repo
627 634 version.target_ref = pull_request.target_ref
628 635
629 636 version._last_merge_source_rev = pull_request._last_merge_source_rev
630 637 version._last_merge_target_rev = pull_request._last_merge_target_rev
631 638 version._last_merge_status = pull_request._last_merge_status
632 639 version.shadow_merge_ref = pull_request.shadow_merge_ref
633 640 version.merge_rev = pull_request.merge_rev
634 641
635 642 version.revisions = pull_request.revisions
636 643 version.pull_request = pull_request
637 644 Session().add(version)
638 645 Session().flush()
639 646
640 647 return version
641 648
642 649 def _generate_update_diffs(self, pull_request, pull_request_version):
643 650 diff_context = (
644 651 self.DIFF_CONTEXT +
645 652 ChangesetCommentsModel.needed_extra_diff_context())
646 653 old_diff = self._get_diff_from_pr_or_version(
647 654 pull_request_version, context=diff_context)
648 655 new_diff = self._get_diff_from_pr_or_version(
649 656 pull_request, context=diff_context)
650 657
651 658 old_diff_data = diffs.DiffProcessor(old_diff)
652 659 old_diff_data.prepare()
653 660 new_diff_data = diffs.DiffProcessor(new_diff)
654 661 new_diff_data.prepare()
655 662
656 663 return old_diff_data, new_diff_data
657 664
658 665 def _link_comments_to_version(self, pull_request_version):
659 666 """
660 667 Link all unlinked comments of this pull request to the given version.
661 668
662 669 :param pull_request_version: The `PullRequestVersion` to which
663 670 the comments shall be linked.
664 671
665 672 """
666 673 pull_request = pull_request_version.pull_request
667 674 comments = ChangesetComment.query().filter(
668 675 # TODO: johbo: Should we query for the repo at all here?
669 676 # Pending decision on how comments of PRs are to be related
670 677 # to either the source repo, the target repo or no repo at all.
671 678 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
672 679 ChangesetComment.pull_request == pull_request,
673 680 ChangesetComment.pull_request_version == None)
674 681
675 682 # TODO: johbo: Find out why this breaks if it is done in a bulk
676 683 # operation.
677 684 for comment in comments:
678 685 comment.pull_request_version_id = (
679 686 pull_request_version.pull_request_version_id)
680 687 Session().add(comment)
681 688
682 689 def _calculate_commit_id_changes(self, old_ids, new_ids):
683 690 added = new_ids.difference(old_ids)
684 691 common = old_ids.intersection(new_ids)
685 692 removed = old_ids.difference(new_ids)
686 693 return ChangeTuple(added, common, removed)
687 694
688 695 def _calculate_file_changes(self, old_diff_data, new_diff_data):
689 696
690 697 old_files = OrderedDict()
691 698 for diff_data in old_diff_data.parsed_diff:
692 699 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
693 700
694 701 added_files = []
695 702 modified_files = []
696 703 removed_files = []
697 704 for diff_data in new_diff_data.parsed_diff:
698 705 new_filename = diff_data['filename']
699 706 new_hash = md5_safe(diff_data['raw_diff'])
700 707
701 708 old_hash = old_files.get(new_filename)
702 709 if not old_hash:
703 710 # file is not present in old diff, means it's added
704 711 added_files.append(new_filename)
705 712 else:
706 713 if new_hash != old_hash:
707 714 modified_files.append(new_filename)
708 715 # now remove a file from old, since we have seen it already
709 716 del old_files[new_filename]
710 717
711 718 # removed files is when there are present in old, but not in NEW,
712 719 # since we remove old files that are present in new diff, left-overs
713 720 # if any should be the removed files
714 721 removed_files.extend(old_files.keys())
715 722
716 723 return FileChangeTuple(added_files, modified_files, removed_files)
717 724
718 725 def _render_update_message(self, changes, file_changes):
719 726 """
720 727 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
721 728 so it's always looking the same disregarding on which default
722 729 renderer system is using.
723 730
724 731 :param changes: changes named tuple
725 732 :param file_changes: file changes named tuple
726 733
727 734 """
728 735 new_status = ChangesetStatus.get_status_lbl(
729 736 ChangesetStatus.STATUS_UNDER_REVIEW)
730 737
731 738 changed_files = (
732 739 file_changes.added + file_changes.modified + file_changes.removed)
733 740
734 741 params = {
735 742 'under_review_label': new_status,
736 743 'added_commits': changes.added,
737 744 'removed_commits': changes.removed,
738 745 'changed_files': changed_files,
739 746 'added_files': file_changes.added,
740 747 'modified_files': file_changes.modified,
741 748 'removed_files': file_changes.removed,
742 749 }
743 750 renderer = RstTemplateRenderer()
744 751 return renderer.render('pull_request_update.mako', **params)
745 752
746 753 def edit(self, pull_request, title, description):
747 754 pull_request = self.__get_pull_request(pull_request)
748 755 if pull_request.is_closed():
749 756 raise ValueError('This pull request is closed')
750 757 if title:
751 758 pull_request.title = title
752 759 pull_request.description = description
753 760 pull_request.updated_on = datetime.datetime.now()
754 761 Session().add(pull_request)
755 762
756 763 def update_reviewers(self, pull_request, reviewer_data):
757 764 """
758 765 Update the reviewers in the pull request
759 766
760 767 :param pull_request: the pr to update
761 768 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
762 769 """
763 770
764 771 reviewers_reasons = {}
765 772 for user_id, reasons in reviewer_data:
766 773 if isinstance(user_id, (int, basestring)):
767 774 user_id = self._get_user(user_id).user_id
768 775 reviewers_reasons[user_id] = reasons
769 776
770 777 reviewers_ids = set(reviewers_reasons.keys())
771 778 pull_request = self.__get_pull_request(pull_request)
772 779 current_reviewers = PullRequestReviewers.query()\
773 780 .filter(PullRequestReviewers.pull_request ==
774 781 pull_request).all()
775 782 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
776 783
777 784 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
778 785 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
779 786
780 787 log.debug("Adding %s reviewers", ids_to_add)
781 788 log.debug("Removing %s reviewers", ids_to_remove)
782 789 changed = False
783 790 for uid in ids_to_add:
784 791 changed = True
785 792 _usr = self._get_user(uid)
786 793 reasons = reviewers_reasons[uid]
787 794 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
788 795 Session().add(reviewer)
789 796
790 797 self.notify_reviewers(pull_request, ids_to_add)
791 798
792 799 for uid in ids_to_remove:
793 800 changed = True
794 801 reviewer = PullRequestReviewers.query()\
795 802 .filter(PullRequestReviewers.user_id == uid,
796 803 PullRequestReviewers.pull_request == pull_request)\
797 804 .scalar()
798 805 if reviewer:
799 806 Session().delete(reviewer)
800 807 if changed:
801 808 pull_request.updated_on = datetime.datetime.now()
802 809 Session().add(pull_request)
803 810
804 811 return ids_to_add, ids_to_remove
805 812
806 813 def get_url(self, pull_request):
807 814 return h.url('pullrequest_show',
808 815 repo_name=safe_str(pull_request.target_repo.repo_name),
809 816 pull_request_id=pull_request.pull_request_id,
810 817 qualified=True)
811 818
812 819 def get_shadow_clone_url(self, pull_request):
813 820 """
814 821 Returns qualified url pointing to the shadow repository. If this pull
815 822 request is closed there is no shadow repository and ``None`` will be
816 823 returned.
817 824 """
818 825 if pull_request.is_closed():
819 826 return None
820 827 else:
821 828 pr_url = urllib.unquote(self.get_url(pull_request))
822 829 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
823 830
824 831 def notify_reviewers(self, pull_request, reviewers_ids):
825 832 # notification to reviewers
826 833 if not reviewers_ids:
827 834 return
828 835
829 836 pull_request_obj = pull_request
830 837 # get the current participants of this pull request
831 838 recipients = reviewers_ids
832 839 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
833 840
834 841 pr_source_repo = pull_request_obj.source_repo
835 842 pr_target_repo = pull_request_obj.target_repo
836 843
837 844 pr_url = h.url(
838 845 'pullrequest_show',
839 846 repo_name=pr_target_repo.repo_name,
840 847 pull_request_id=pull_request_obj.pull_request_id,
841 848 qualified=True,)
842 849
843 850 # set some variables for email notification
844 851 pr_target_repo_url = h.url(
845 852 'summary_home',
846 853 repo_name=pr_target_repo.repo_name,
847 854 qualified=True)
848 855
849 856 pr_source_repo_url = h.url(
850 857 'summary_home',
851 858 repo_name=pr_source_repo.repo_name,
852 859 qualified=True)
853 860
854 861 # pull request specifics
855 862 pull_request_commits = [
856 863 (x.raw_id, x.message)
857 864 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
858 865
859 866 kwargs = {
860 867 'user': pull_request.author,
861 868 'pull_request': pull_request_obj,
862 869 'pull_request_commits': pull_request_commits,
863 870
864 871 'pull_request_target_repo': pr_target_repo,
865 872 'pull_request_target_repo_url': pr_target_repo_url,
866 873
867 874 'pull_request_source_repo': pr_source_repo,
868 875 'pull_request_source_repo_url': pr_source_repo_url,
869 876
870 877 'pull_request_url': pr_url,
871 878 }
872 879
873 880 # pre-generate the subject for notification itself
874 881 (subject,
875 882 _h, _e, # we don't care about those
876 883 body_plaintext) = EmailNotificationModel().render_email(
877 884 notification_type, **kwargs)
878 885
879 886 # create notification objects, and emails
880 887 NotificationModel().create(
881 888 created_by=pull_request.author,
882 889 notification_subject=subject,
883 890 notification_body=body_plaintext,
884 891 notification_type=notification_type,
885 892 recipients=recipients,
886 893 email_kwargs=kwargs,
887 894 )
888 895
889 896 def delete(self, pull_request):
890 897 pull_request = self.__get_pull_request(pull_request)
891 898 self._cleanup_merge_workspace(pull_request)
892 899 Session().delete(pull_request)
893 900
894 901 def close_pull_request(self, pull_request, user):
895 902 pull_request = self.__get_pull_request(pull_request)
896 903 self._cleanup_merge_workspace(pull_request)
897 904 pull_request.status = PullRequest.STATUS_CLOSED
898 905 pull_request.updated_on = datetime.datetime.now()
899 906 Session().add(pull_request)
900 907 self._trigger_pull_request_hook(
901 908 pull_request, pull_request.author, 'close')
902 909 self._log_action('user_closed_pull_request', user, pull_request)
903 910
904 911 def close_pull_request_with_comment(self, pull_request, user, repo,
905 912 message=None):
906 913 status = ChangesetStatus.STATUS_REJECTED
907 914
908 915 if not message:
909 916 message = (
910 917 _('Status change %(transition_icon)s %(status)s') % {
911 918 'transition_icon': '>',
912 919 'status': ChangesetStatus.get_status_lbl(status)})
913 920
914 921 internal_message = _('Closing with') + ' ' + message
915 922
916 923 comm = ChangesetCommentsModel().create(
917 924 text=internal_message,
918 925 repo=repo.repo_id,
919 926 user=user.user_id,
920 927 pull_request=pull_request.pull_request_id,
921 928 f_path=None,
922 929 line_no=None,
923 930 status_change=ChangesetStatus.get_status_lbl(status),
924 931 status_change_type=status,
925 932 closing_pr=True
926 933 )
927 934
928 935 ChangesetStatusModel().set_status(
929 936 repo.repo_id,
930 937 status,
931 938 user.user_id,
932 939 comm,
933 940 pull_request=pull_request.pull_request_id
934 941 )
935 942 Session().flush()
936 943
937 944 PullRequestModel().close_pull_request(
938 945 pull_request.pull_request_id, user)
939 946
940 947 def merge_status(self, pull_request):
941 948 if not self._is_merge_enabled(pull_request):
942 949 return False, _('Server-side pull request merging is disabled.')
943 950 if pull_request.is_closed():
944 951 return False, _('This pull request is closed.')
945 952 merge_possible, msg = self._check_repo_requirements(
946 953 target=pull_request.target_repo, source=pull_request.source_repo)
947 954 if not merge_possible:
948 955 return merge_possible, msg
949 956
950 957 try:
951 958 resp = self._try_merge(pull_request)
952 959 log.debug("Merge response: %s", resp)
953 960 status = resp.possible, self.merge_status_message(
954 961 resp.failure_reason)
955 962 except NotImplementedError:
956 963 status = False, _('Pull request merging is not supported.')
957 964
958 965 return status
959 966
960 967 def _check_repo_requirements(self, target, source):
961 968 """
962 969 Check if `target` and `source` have compatible requirements.
963 970
964 971 Currently this is just checking for largefiles.
965 972 """
966 973 target_has_largefiles = self._has_largefiles(target)
967 974 source_has_largefiles = self._has_largefiles(source)
968 975 merge_possible = True
969 976 message = u''
970 977
971 978 if target_has_largefiles != source_has_largefiles:
972 979 merge_possible = False
973 980 if source_has_largefiles:
974 981 message = _(
975 982 'Target repository large files support is disabled.')
976 983 else:
977 984 message = _(
978 985 'Source repository large files support is disabled.')
979 986
980 987 return merge_possible, message
981 988
982 989 def _has_largefiles(self, repo):
983 990 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
984 991 'extensions', 'largefiles')
985 992 return largefiles_ui and largefiles_ui[0].active
986 993
987 994 def _try_merge(self, pull_request):
988 995 """
989 996 Try to merge the pull request and return the merge status.
990 997 """
991 998 log.debug(
992 999 "Trying out if the pull request %s can be merged.",
993 1000 pull_request.pull_request_id)
994 1001 target_vcs = pull_request.target_repo.scm_instance()
995 1002
996 1003 # Refresh the target reference.
997 1004 try:
998 1005 target_ref = self._refresh_reference(
999 1006 pull_request.target_ref_parts, target_vcs)
1000 1007 except CommitDoesNotExistError:
1001 1008 merge_state = MergeResponse(
1002 1009 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1003 1010 return merge_state
1004 1011
1005 1012 target_locked = pull_request.target_repo.locked
1006 1013 if target_locked and target_locked[0]:
1007 1014 log.debug("The target repository is locked.")
1008 1015 merge_state = MergeResponse(
1009 1016 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1010 1017 elif self._needs_merge_state_refresh(pull_request, target_ref):
1011 1018 log.debug("Refreshing the merge status of the repository.")
1012 1019 merge_state = self._refresh_merge_state(
1013 1020 pull_request, target_vcs, target_ref)
1014 1021 else:
1015 1022 possible = pull_request.\
1016 1023 _last_merge_status == MergeFailureReason.NONE
1017 1024 merge_state = MergeResponse(
1018 1025 possible, False, None, pull_request._last_merge_status)
1019 1026
1020 1027 return merge_state
1021 1028
1022 1029 def _refresh_reference(self, reference, vcs_repository):
1023 1030 if reference.type in ('branch', 'book'):
1024 1031 name_or_id = reference.name
1025 1032 else:
1026 1033 name_or_id = reference.commit_id
1027 1034 refreshed_commit = vcs_repository.get_commit(name_or_id)
1028 1035 refreshed_reference = Reference(
1029 1036 reference.type, reference.name, refreshed_commit.raw_id)
1030 1037 return refreshed_reference
1031 1038
1032 1039 def _needs_merge_state_refresh(self, pull_request, target_reference):
1033 1040 return not(
1034 1041 pull_request.revisions and
1035 1042 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1036 1043 target_reference.commit_id == pull_request._last_merge_target_rev)
1037 1044
1038 1045 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1039 1046 workspace_id = self._workspace_id(pull_request)
1040 1047 source_vcs = pull_request.source_repo.scm_instance()
1041 1048 use_rebase = self._use_rebase_for_merging(pull_request)
1042 1049 merge_state = target_vcs.merge(
1043 1050 target_reference, source_vcs, pull_request.source_ref_parts,
1044 1051 workspace_id, dry_run=True, use_rebase=use_rebase)
1045 1052
1046 1053 # Do not store the response if there was an unknown error.
1047 1054 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1048 1055 pull_request._last_merge_source_rev = \
1049 1056 pull_request.source_ref_parts.commit_id
1050 1057 pull_request._last_merge_target_rev = target_reference.commit_id
1051 1058 pull_request._last_merge_status = merge_state.failure_reason
1052 1059 pull_request.shadow_merge_ref = merge_state.merge_ref
1053 1060 Session().add(pull_request)
1054 1061 Session().commit()
1055 1062
1056 1063 return merge_state
1057 1064
1058 1065 def _workspace_id(self, pull_request):
1059 1066 workspace_id = 'pr-%s' % pull_request.pull_request_id
1060 1067 return workspace_id
1061 1068
1062 1069 def merge_status_message(self, status_code):
1063 1070 """
1064 1071 Return a human friendly error message for the given merge status code.
1065 1072 """
1066 1073 return self.MERGE_STATUS_MESSAGES[status_code]
1067 1074
1068 1075 def generate_repo_data(self, repo, commit_id=None, branch=None,
1069 1076 bookmark=None):
1070 1077 all_refs, selected_ref = \
1071 1078 self._get_repo_pullrequest_sources(
1072 1079 repo.scm_instance(), commit_id=commit_id,
1073 1080 branch=branch, bookmark=bookmark)
1074 1081
1075 1082 refs_select2 = []
1076 1083 for element in all_refs:
1077 1084 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1078 1085 refs_select2.append({'text': element[1], 'children': children})
1079 1086
1080 1087 return {
1081 1088 'user': {
1082 1089 'user_id': repo.user.user_id,
1083 1090 'username': repo.user.username,
1084 1091 'firstname': repo.user.firstname,
1085 1092 'lastname': repo.user.lastname,
1086 1093 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1087 1094 },
1088 1095 'description': h.chop_at_smart(repo.description, '\n'),
1089 1096 'refs': {
1090 1097 'all_refs': all_refs,
1091 1098 'selected_ref': selected_ref,
1092 1099 'select2_refs': refs_select2
1093 1100 }
1094 1101 }
1095 1102
1096 1103 def generate_pullrequest_title(self, source, source_ref, target):
1097 1104 return u'{source}#{at_ref} to {target}'.format(
1098 1105 source=source,
1099 1106 at_ref=source_ref,
1100 1107 target=target,
1101 1108 )
1102 1109
1103 1110 def _cleanup_merge_workspace(self, pull_request):
1104 1111 # Merging related cleanup
1105 1112 target_scm = pull_request.target_repo.scm_instance()
1106 1113 workspace_id = 'pr-%s' % pull_request.pull_request_id
1107 1114
1108 1115 try:
1109 1116 target_scm.cleanup_merge_workspace(workspace_id)
1110 1117 except NotImplementedError:
1111 1118 pass
1112 1119
1113 1120 def _get_repo_pullrequest_sources(
1114 1121 self, repo, commit_id=None, branch=None, bookmark=None):
1115 1122 """
1116 1123 Return a structure with repo's interesting commits, suitable for
1117 1124 the selectors in pullrequest controller
1118 1125
1119 1126 :param commit_id: a commit that must be in the list somehow
1120 1127 and selected by default
1121 1128 :param branch: a branch that must be in the list and selected
1122 1129 by default - even if closed
1123 1130 :param bookmark: a bookmark that must be in the list and selected
1124 1131 """
1125 1132
1126 1133 commit_id = safe_str(commit_id) if commit_id else None
1127 1134 branch = safe_str(branch) if branch else None
1128 1135 bookmark = safe_str(bookmark) if bookmark else None
1129 1136
1130 1137 selected = None
1131 1138
1132 1139 # order matters: first source that has commit_id in it will be selected
1133 1140 sources = []
1134 1141 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1135 1142 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1136 1143
1137 1144 if commit_id:
1138 1145 ref_commit = (h.short_id(commit_id), commit_id)
1139 1146 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1140 1147
1141 1148 sources.append(
1142 1149 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1143 1150 )
1144 1151
1145 1152 groups = []
1146 1153 for group_key, ref_list, group_name, match in sources:
1147 1154 group_refs = []
1148 1155 for ref_name, ref_id in ref_list:
1149 1156 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1150 1157 group_refs.append((ref_key, ref_name))
1151 1158
1152 1159 if not selected:
1153 1160 if set([commit_id, match]) & set([ref_id, ref_name]):
1154 1161 selected = ref_key
1155 1162
1156 1163 if group_refs:
1157 1164 groups.append((group_refs, group_name))
1158 1165
1159 1166 if not selected:
1160 1167 ref = commit_id or branch or bookmark
1161 1168 if ref:
1162 1169 raise CommitDoesNotExistError(
1163 1170 'No commit refs could be found matching: %s' % ref)
1164 1171 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1165 1172 selected = 'branch:%s:%s' % (
1166 1173 repo.DEFAULT_BRANCH_NAME,
1167 1174 repo.branches[repo.DEFAULT_BRANCH_NAME]
1168 1175 )
1169 1176 elif repo.commit_ids:
1170 1177 rev = repo.commit_ids[0]
1171 1178 selected = 'rev:%s:%s' % (rev, rev)
1172 1179 else:
1173 1180 raise EmptyRepositoryError()
1174 1181 return groups, selected
1175 1182
1176 1183 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1177 1184 pull_request = self.__get_pull_request(pull_request)
1178 1185 return self._get_diff_from_pr_or_version(pull_request, context=context)
1179 1186
1180 1187 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1181 1188 source_repo = pr_or_version.source_repo
1182 1189
1183 1190 # we swap org/other ref since we run a simple diff on one repo
1184 1191 target_ref_id = pr_or_version.target_ref_parts.commit_id
1185 1192 source_ref_id = pr_or_version.source_ref_parts.commit_id
1186 1193 target_commit = source_repo.get_commit(
1187 1194 commit_id=safe_str(target_ref_id))
1188 1195 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1189 1196 vcs_repo = source_repo.scm_instance()
1190 1197
1191 1198 # TODO: johbo: In the context of an update, we cannot reach
1192 1199 # the old commit anymore with our normal mechanisms. It needs
1193 1200 # some sort of special support in the vcs layer to avoid this
1194 1201 # workaround.
1195 1202 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1196 1203 vcs_repo.alias == 'git'):
1197 1204 source_commit.raw_id = safe_str(source_ref_id)
1198 1205
1199 1206 log.debug('calculating diff between '
1200 1207 'source_ref:%s and target_ref:%s for repo `%s`',
1201 1208 target_ref_id, source_ref_id,
1202 1209 safe_unicode(vcs_repo.path))
1203 1210
1204 1211 vcs_diff = vcs_repo.get_diff(
1205 1212 commit1=target_commit, commit2=source_commit, context=context)
1206 1213 return vcs_diff
1207 1214
1208 1215 def _is_merge_enabled(self, pull_request):
1209 1216 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1210 1217 settings = settings_model.get_general_settings()
1211 1218 return settings.get('rhodecode_pr_merge_enabled', False)
1212 1219
1213 1220 def _use_rebase_for_merging(self, pull_request):
1214 1221 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1215 1222 settings = settings_model.get_general_settings()
1216 1223 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1217 1224
1218 1225 def _log_action(self, action, user, pull_request):
1219 1226 action_logger(
1220 1227 user,
1221 1228 '{action}:{pr_id}'.format(
1222 1229 action=action, pr_id=pull_request.pull_request_id),
1223 1230 pull_request.target_repo)
1224 1231
1225 1232
1226 1233 ChangeTuple = namedtuple('ChangeTuple',
1227 1234 ['added', 'common', 'removed'])
1228 1235
1229 1236 FileChangeTuple = namedtuple('FileChangeTuple',
1230 1237 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now