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