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