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