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