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