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