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