##// END OF EJS Templates
pull-requests: add link to switch base based on the target.
marcink -
r2469:da49a829 default
parent child Browse files
Show More
@@ -1,1651 +1,1654 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31 import collections
32 32
33 33 from pyramid.threadlocal import get_current_request
34 34
35 35 from rhodecode import events
36 36 from rhodecode.translation import lazy_ugettext#, _
37 37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 41 from rhodecode.lib.markup_renderer import (
42 42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 44 from rhodecode.lib.vcs.backends.base import (
45 45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 47 from rhodecode.lib.vcs.exceptions import (
48 48 CommitDoesNotExistError, EmptyRepositoryError)
49 49 from rhodecode.model import BaseModel
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.db import (
53 53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 54 PullRequestVersion, ChangesetComment, Repository)
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.notification import NotificationModel, \
57 57 EmailNotificationModel
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.settings import VcsSettingsModel
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 # Data structure to hold the response data when updating commits during a pull
66 66 # request update.
67 67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 68 'executed', 'reason', 'new', 'old', 'changes',
69 69 'source_changed', 'target_changed'])
70 70
71 71
72 72 class PullRequestModel(BaseModel):
73 73
74 74 cls = PullRequest
75 75
76 76 DIFF_CONTEXT = 3
77 77
78 78 MERGE_STATUS_MESSAGES = {
79 79 MergeFailureReason.NONE: lazy_ugettext(
80 80 'This pull request can be automatically merged.'),
81 81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 82 'This pull request cannot be merged because of an unhandled'
83 83 ' exception.'),
84 84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 85 'This pull request cannot be merged because of merge conflicts.'),
86 86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 87 'This pull request could not be merged because push to target'
88 88 ' failed.'),
89 89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 90 'This pull request cannot be merged because the target is not a'
91 91 ' head.'),
92 92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 93 'This pull request cannot be merged because the source contains'
94 94 ' more branches than the target.'),
95 95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 96 'This pull request cannot be merged because the target has'
97 97 ' multiple heads.'),
98 98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 99 'This pull request cannot be merged because the target repository'
100 100 ' is locked.'),
101 101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 102 'This pull request cannot be merged because the target or the '
103 103 'source reference is missing.'),
104 104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 105 'This pull request cannot be merged because the target '
106 106 'reference is missing.'),
107 107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 108 'This pull request cannot be merged because the source '
109 109 'reference is missing.'),
110 110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 111 'This pull request cannot be merged because of conflicts related '
112 112 'to sub repositories.'),
113 113 }
114 114
115 115 UPDATE_STATUS_MESSAGES = {
116 116 UpdateFailureReason.NONE: lazy_ugettext(
117 117 'Pull request update successful.'),
118 118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 119 'Pull request update failed because of an unknown error.'),
120 120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 121 'No update needed because the source and target have not changed.'),
122 122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 123 'Pull request cannot be updated because the reference type is '
124 124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 126 'This pull request cannot be updated because the target '
127 127 'reference is missing.'),
128 128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 129 'This pull request cannot be updated because the source '
130 130 'reference is missing.'),
131 131 }
132 132
133 133 def __get_pull_request(self, pull_request):
134 134 return self._get_instance((
135 135 PullRequest, PullRequestVersion), pull_request)
136 136
137 137 def _check_perms(self, perms, pull_request, user, api=False):
138 138 if not api:
139 139 return h.HasRepoPermissionAny(*perms)(
140 140 user=user, repo_name=pull_request.target_repo.repo_name)
141 141 else:
142 142 return h.HasRepoPermissionAnyApi(*perms)(
143 143 user=user, repo_name=pull_request.target_repo.repo_name)
144 144
145 145 def check_user_read(self, pull_request, user, api=False):
146 146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 147 return self._check_perms(_perms, pull_request, user, api)
148 148
149 149 def check_user_merge(self, pull_request, user, api=False):
150 150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 151 return self._check_perms(_perms, pull_request, user, api)
152 152
153 153 def check_user_update(self, pull_request, user, api=False):
154 154 owner = user.user_id == pull_request.user_id
155 155 return self.check_user_merge(pull_request, user, api) or owner
156 156
157 157 def check_user_delete(self, pull_request, user):
158 158 owner = user.user_id == pull_request.user_id
159 159 _perms = ('repository.admin',)
160 160 return self._check_perms(_perms, pull_request, user) or owner
161 161
162 162 def check_user_change_status(self, pull_request, user, api=False):
163 163 reviewer = user.user_id in [x.user_id for x in
164 164 pull_request.reviewers]
165 165 return self.check_user_update(pull_request, user, api) or reviewer
166 166
167 167 def check_user_comment(self, pull_request, user):
168 168 owner = user.user_id == pull_request.user_id
169 169 return self.check_user_read(pull_request, user) or owner
170 170
171 171 def get(self, pull_request):
172 172 return self.__get_pull_request(pull_request)
173 173
174 174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 175 opened_by=None, order_by=None,
176 176 order_dir='desc'):
177 177 repo = None
178 178 if repo_name:
179 179 repo = self._get_repo(repo_name)
180 180
181 181 q = PullRequest.query()
182 182
183 183 # source or target
184 184 if repo and source:
185 185 q = q.filter(PullRequest.source_repo == repo)
186 186 elif repo:
187 187 q = q.filter(PullRequest.target_repo == repo)
188 188
189 189 # closed,opened
190 190 if statuses:
191 191 q = q.filter(PullRequest.status.in_(statuses))
192 192
193 193 # opened by filter
194 194 if opened_by:
195 195 q = q.filter(PullRequest.user_id.in_(opened_by))
196 196
197 197 if order_by:
198 198 order_map = {
199 199 'name_raw': PullRequest.pull_request_id,
200 200 'title': PullRequest.title,
201 201 'updated_on_raw': PullRequest.updated_on,
202 202 'target_repo': PullRequest.target_repo_id
203 203 }
204 204 if order_dir == 'asc':
205 205 q = q.order_by(order_map[order_by].asc())
206 206 else:
207 207 q = q.order_by(order_map[order_by].desc())
208 208
209 209 return q
210 210
211 211 def count_all(self, repo_name, source=False, statuses=None,
212 212 opened_by=None):
213 213 """
214 214 Count the number of pull requests for a specific repository.
215 215
216 216 :param repo_name: target or source repo
217 217 :param source: boolean flag to specify if repo_name refers to source
218 218 :param statuses: list of pull request statuses
219 219 :param opened_by: author user of the pull request
220 220 :returns: int number of pull requests
221 221 """
222 222 q = self._prepare_get_all_query(
223 223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224 224
225 225 return q.count()
226 226
227 227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 228 offset=0, length=None, order_by=None, order_dir='desc'):
229 229 """
230 230 Get all pull requests for a specific repository.
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 q = self._prepare_get_all_query(
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 if length:
247 247 pull_requests = q.limit(length).offset(offset).all()
248 248 else:
249 249 pull_requests = q.all()
250 250
251 251 return pull_requests
252 252
253 253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 254 opened_by=None):
255 255 """
256 256 Count the number of pull requests for a specific repository that are
257 257 awaiting review.
258 258
259 259 :param repo_name: target or source repo
260 260 :param source: boolean flag to specify if repo_name refers to source
261 261 :param statuses: list of pull request statuses
262 262 :param opened_by: author user of the pull request
263 263 :returns: int number of pull requests
264 264 """
265 265 pull_requests = self.get_awaiting_review(
266 266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267 267
268 268 return len(pull_requests)
269 269
270 270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 271 opened_by=None, offset=0, length=None,
272 272 order_by=None, order_dir='desc'):
273 273 """
274 274 Get all pull requests for a specific repository that are awaiting
275 275 review.
276 276
277 277 :param repo_name: target or source repo
278 278 :param source: boolean flag to specify if repo_name refers to source
279 279 :param statuses: list of pull request statuses
280 280 :param opened_by: author user of the pull request
281 281 :param offset: pagination offset
282 282 :param length: length of returned list
283 283 :param order_by: order of the returned list
284 284 :param order_dir: 'asc' or 'desc' ordering direction
285 285 :returns: list of pull requests
286 286 """
287 287 pull_requests = self.get_all(
288 288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 289 order_by=order_by, order_dir=order_dir)
290 290
291 291 _filtered_pull_requests = []
292 292 for pr in pull_requests:
293 293 status = pr.calculated_review_status()
294 294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 296 _filtered_pull_requests.append(pr)
297 297 if length:
298 298 return _filtered_pull_requests[offset:offset+length]
299 299 else:
300 300 return _filtered_pull_requests
301 301
302 302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 303 opened_by=None, user_id=None):
304 304 """
305 305 Count the number of pull requests for a specific repository that are
306 306 awaiting review from a specific user.
307 307
308 308 :param repo_name: target or source repo
309 309 :param source: boolean flag to specify if repo_name refers to source
310 310 :param statuses: list of pull request statuses
311 311 :param opened_by: author user of the pull request
312 312 :param user_id: reviewer user of the pull request
313 313 :returns: int number of pull requests
314 314 """
315 315 pull_requests = self.get_awaiting_my_review(
316 316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 317 user_id=user_id)
318 318
319 319 return len(pull_requests)
320 320
321 321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 322 opened_by=None, user_id=None, offset=0,
323 323 length=None, order_by=None, order_dir='desc'):
324 324 """
325 325 Get all pull requests for a specific repository that are awaiting
326 326 review from a specific user.
327 327
328 328 :param repo_name: target or source repo
329 329 :param source: boolean flag to specify if repo_name refers to source
330 330 :param statuses: list of pull request statuses
331 331 :param opened_by: author user of the pull request
332 332 :param user_id: reviewer user of the pull request
333 333 :param offset: pagination offset
334 334 :param length: length of returned list
335 335 :param order_by: order of the returned list
336 336 :param order_dir: 'asc' or 'desc' ordering direction
337 337 :returns: list of pull requests
338 338 """
339 339 pull_requests = self.get_all(
340 340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 341 order_by=order_by, order_dir=order_dir)
342 342
343 343 _my = PullRequestModel().get_not_reviewed(user_id)
344 344 my_participation = []
345 345 for pr in pull_requests:
346 346 if pr in _my:
347 347 my_participation.append(pr)
348 348 _filtered_pull_requests = my_participation
349 349 if length:
350 350 return _filtered_pull_requests[offset:offset+length]
351 351 else:
352 352 return _filtered_pull_requests
353 353
354 354 def get_not_reviewed(self, user_id):
355 355 return [
356 356 x.pull_request for x in PullRequestReviewers.query().filter(
357 357 PullRequestReviewers.user_id == user_id).all()
358 358 ]
359 359
360 360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 361 order_by=None, order_dir='desc'):
362 362 q = PullRequest.query()
363 363 if user_id:
364 364 reviewers_subquery = Session().query(
365 365 PullRequestReviewers.pull_request_id).filter(
366 366 PullRequestReviewers.user_id == user_id).subquery()
367 367 user_filter = or_(
368 368 PullRequest.user_id == user_id,
369 369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 370 )
371 371 q = PullRequest.query().filter(user_filter)
372 372
373 373 # closed,opened
374 374 if statuses:
375 375 q = q.filter(PullRequest.status.in_(statuses))
376 376
377 377 if order_by:
378 378 order_map = {
379 379 'name_raw': PullRequest.pull_request_id,
380 380 'title': PullRequest.title,
381 381 'updated_on_raw': PullRequest.updated_on,
382 382 'target_repo': PullRequest.target_repo_id
383 383 }
384 384 if order_dir == 'asc':
385 385 q = q.order_by(order_map[order_by].asc())
386 386 else:
387 387 q = q.order_by(order_map[order_by].desc())
388 388
389 389 return q
390 390
391 391 def count_im_participating_in(self, user_id=None, statuses=None):
392 392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 393 return q.count()
394 394
395 395 def get_im_participating_in(
396 396 self, user_id=None, statuses=None, offset=0,
397 397 length=None, order_by=None, order_dir='desc'):
398 398 """
399 399 Get all Pull requests that i'm participating in, or i have opened
400 400 """
401 401
402 402 q = self._prepare_participating_query(
403 403 user_id, statuses=statuses, order_by=order_by,
404 404 order_dir=order_dir)
405 405
406 406 if length:
407 407 pull_requests = q.limit(length).offset(offset).all()
408 408 else:
409 409 pull_requests = q.all()
410 410
411 411 return pull_requests
412 412
413 413 def get_versions(self, pull_request):
414 414 """
415 415 returns version of pull request sorted by ID descending
416 416 """
417 417 return PullRequestVersion.query()\
418 418 .filter(PullRequestVersion.pull_request == pull_request)\
419 419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 420 .all()
421 421
422 422 def get_pr_version(self, pull_request_id, version=None):
423 423 at_version = None
424 424
425 425 if version and version == 'latest':
426 426 pull_request_ver = PullRequest.get(pull_request_id)
427 427 pull_request_obj = pull_request_ver
428 428 _org_pull_request_obj = pull_request_obj
429 429 at_version = 'latest'
430 430 elif version:
431 431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 432 pull_request_obj = pull_request_ver
433 433 _org_pull_request_obj = pull_request_ver.pull_request
434 434 at_version = pull_request_ver.pull_request_version_id
435 435 else:
436 436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 437 pull_request_id)
438 438
439 439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 440 pull_request_obj, _org_pull_request_obj)
441 441
442 442 return _org_pull_request_obj, pull_request_obj, \
443 443 pull_request_display_obj, at_version
444 444
445 445 def create(self, created_by, source_repo, source_ref, target_repo,
446 446 target_ref, revisions, reviewers, title, description=None,
447 447 reviewer_data=None, translator=None):
448 448 translator = translator or get_current_request().translate
449 449
450 450 created_by_user = self._get_user(created_by)
451 451 source_repo = self._get_repo(source_repo)
452 452 target_repo = self._get_repo(target_repo)
453 453
454 454 pull_request = PullRequest()
455 455 pull_request.source_repo = source_repo
456 456 pull_request.source_ref = source_ref
457 457 pull_request.target_repo = target_repo
458 458 pull_request.target_ref = target_ref
459 459 pull_request.revisions = revisions
460 460 pull_request.title = title
461 461 pull_request.description = description
462 462 pull_request.author = created_by_user
463 463 pull_request.reviewer_data = reviewer_data
464 464
465 465 Session().add(pull_request)
466 466 Session().flush()
467 467
468 468 reviewer_ids = set()
469 469 # members / reviewers
470 470 for reviewer_object in reviewers:
471 471 user_id, reasons, mandatory = reviewer_object
472 472 user = self._get_user(user_id)
473 473
474 474 # skip duplicates
475 475 if user.user_id in reviewer_ids:
476 476 continue
477 477
478 478 reviewer_ids.add(user.user_id)
479 479
480 480 reviewer = PullRequestReviewers()
481 481 reviewer.user = user
482 482 reviewer.pull_request = pull_request
483 483 reviewer.reasons = reasons
484 484 reviewer.mandatory = mandatory
485 485 Session().add(reviewer)
486 486
487 487 # Set approval status to "Under Review" for all commits which are
488 488 # part of this pull request.
489 489 ChangesetStatusModel().set_status(
490 490 repo=target_repo,
491 491 status=ChangesetStatus.STATUS_UNDER_REVIEW,
492 492 user=created_by_user,
493 493 pull_request=pull_request
494 494 )
495 495
496 496 MergeCheck.validate(
497 497 pull_request, user=created_by_user, translator=translator)
498 498
499 499 self.notify_reviewers(pull_request, reviewer_ids)
500 500 self._trigger_pull_request_hook(
501 501 pull_request, created_by_user, 'create')
502 502
503 503 creation_data = pull_request.get_api_data(with_merge_state=False)
504 504 self._log_audit_action(
505 505 'repo.pull_request.create', {'data': creation_data},
506 506 created_by_user, pull_request)
507 507
508 508 return pull_request
509 509
510 510 def _trigger_pull_request_hook(self, pull_request, user, action):
511 511 pull_request = self.__get_pull_request(pull_request)
512 512 target_scm = pull_request.target_repo.scm_instance()
513 513 if action == 'create':
514 514 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
515 515 elif action == 'merge':
516 516 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
517 517 elif action == 'close':
518 518 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
519 519 elif action == 'review_status_change':
520 520 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
521 521 elif action == 'update':
522 522 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
523 523 else:
524 524 return
525 525
526 526 trigger_hook(
527 527 username=user.username,
528 528 repo_name=pull_request.target_repo.repo_name,
529 529 repo_alias=target_scm.alias,
530 530 pull_request=pull_request)
531 531
532 532 def _get_commit_ids(self, pull_request):
533 533 """
534 534 Return the commit ids of the merged pull request.
535 535
536 536 This method is not dealing correctly yet with the lack of autoupdates
537 537 nor with the implicit target updates.
538 538 For example: if a commit in the source repo is already in the target it
539 539 will be reported anyways.
540 540 """
541 541 merge_rev = pull_request.merge_rev
542 542 if merge_rev is None:
543 543 raise ValueError('This pull request was not merged yet')
544 544
545 545 commit_ids = list(pull_request.revisions)
546 546 if merge_rev not in commit_ids:
547 547 commit_ids.append(merge_rev)
548 548
549 549 return commit_ids
550 550
551 551 def merge(self, pull_request, user, extras):
552 552 log.debug("Merging pull request %s", pull_request.pull_request_id)
553 553 merge_state = self._merge_pull_request(pull_request, user, extras)
554 554 if merge_state.executed:
555 555 log.debug(
556 556 "Merge was successful, updating the pull request comments.")
557 557 self._comment_and_close_pr(pull_request, user, merge_state)
558 558
559 559 self._log_audit_action(
560 560 'repo.pull_request.merge',
561 561 {'merge_state': merge_state.__dict__},
562 562 user, pull_request)
563 563
564 564 else:
565 565 log.warn("Merge failed, not updating the pull request.")
566 566 return merge_state
567 567
568 568 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
569 569 target_vcs = pull_request.target_repo.scm_instance()
570 570 source_vcs = pull_request.source_repo.scm_instance()
571 571 target_ref = self._refresh_reference(
572 572 pull_request.target_ref_parts, target_vcs)
573 573
574 574 message = merge_msg or (
575 575 'Merge pull request #%(pr_id)s from '
576 576 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
577 577 'pr_id': pull_request.pull_request_id,
578 578 'source_repo': source_vcs.name,
579 579 'source_ref_name': pull_request.source_ref_parts.name,
580 580 'pr_title': pull_request.title
581 581 }
582 582
583 583 workspace_id = self._workspace_id(pull_request)
584 584 use_rebase = self._use_rebase_for_merging(pull_request)
585 585 close_branch = self._close_branch_before_merging(pull_request)
586 586
587 587 callback_daemon, extras = prepare_callback_daemon(
588 588 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
589 589 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
590 590
591 591 with callback_daemon:
592 592 # TODO: johbo: Implement a clean way to run a config_override
593 593 # for a single call.
594 594 target_vcs.config.set(
595 595 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
596 596 merge_state = target_vcs.merge(
597 597 target_ref, source_vcs, pull_request.source_ref_parts,
598 598 workspace_id, user_name=user.username,
599 599 user_email=user.email, message=message, use_rebase=use_rebase,
600 600 close_branch=close_branch)
601 601 return merge_state
602 602
603 603 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
604 604 pull_request.merge_rev = merge_state.merge_ref.commit_id
605 605 pull_request.updated_on = datetime.datetime.now()
606 606 close_msg = close_msg or 'Pull request merged and closed'
607 607
608 608 CommentsModel().create(
609 609 text=safe_unicode(close_msg),
610 610 repo=pull_request.target_repo.repo_id,
611 611 user=user.user_id,
612 612 pull_request=pull_request.pull_request_id,
613 613 f_path=None,
614 614 line_no=None,
615 615 closing_pr=True
616 616 )
617 617
618 618 Session().add(pull_request)
619 619 Session().flush()
620 620 # TODO: paris: replace invalidation with less radical solution
621 621 ScmModel().mark_for_invalidation(
622 622 pull_request.target_repo.repo_name)
623 623 self._trigger_pull_request_hook(pull_request, user, 'merge')
624 624
625 625 def has_valid_update_type(self, pull_request):
626 626 source_ref_type = pull_request.source_ref_parts.type
627 627 return source_ref_type in ['book', 'branch', 'tag']
628 628
629 629 def update_commits(self, pull_request):
630 630 """
631 631 Get the updated list of commits for the pull request
632 632 and return the new pull request version and the list
633 633 of commits processed by this update action
634 634 """
635 635 pull_request = self.__get_pull_request(pull_request)
636 636 source_ref_type = pull_request.source_ref_parts.type
637 637 source_ref_name = pull_request.source_ref_parts.name
638 638 source_ref_id = pull_request.source_ref_parts.commit_id
639 639
640 640 target_ref_type = pull_request.target_ref_parts.type
641 641 target_ref_name = pull_request.target_ref_parts.name
642 642 target_ref_id = pull_request.target_ref_parts.commit_id
643 643
644 644 if not self.has_valid_update_type(pull_request):
645 645 log.debug(
646 646 "Skipping update of pull request %s due to ref type: %s",
647 647 pull_request, source_ref_type)
648 648 return UpdateResponse(
649 649 executed=False,
650 650 reason=UpdateFailureReason.WRONG_REF_TYPE,
651 651 old=pull_request, new=None, changes=None,
652 652 source_changed=False, target_changed=False)
653 653
654 654 # source repo
655 655 source_repo = pull_request.source_repo.scm_instance()
656 656 try:
657 657 source_commit = source_repo.get_commit(commit_id=source_ref_name)
658 658 except CommitDoesNotExistError:
659 659 return UpdateResponse(
660 660 executed=False,
661 661 reason=UpdateFailureReason.MISSING_SOURCE_REF,
662 662 old=pull_request, new=None, changes=None,
663 663 source_changed=False, target_changed=False)
664 664
665 665 source_changed = source_ref_id != source_commit.raw_id
666 666
667 667 # target repo
668 668 target_repo = pull_request.target_repo.scm_instance()
669 669 try:
670 670 target_commit = target_repo.get_commit(commit_id=target_ref_name)
671 671 except CommitDoesNotExistError:
672 672 return UpdateResponse(
673 673 executed=False,
674 674 reason=UpdateFailureReason.MISSING_TARGET_REF,
675 675 old=pull_request, new=None, changes=None,
676 676 source_changed=False, target_changed=False)
677 677 target_changed = target_ref_id != target_commit.raw_id
678 678
679 679 if not (source_changed or target_changed):
680 680 log.debug("Nothing changed in pull request %s", pull_request)
681 681 return UpdateResponse(
682 682 executed=False,
683 683 reason=UpdateFailureReason.NO_CHANGE,
684 684 old=pull_request, new=None, changes=None,
685 685 source_changed=target_changed, target_changed=source_changed)
686 686
687 687 change_in_found = 'target repo' if target_changed else 'source repo'
688 688 log.debug('Updating pull request because of change in %s detected',
689 689 change_in_found)
690 690
691 691 # Finally there is a need for an update, in case of source change
692 692 # we create a new version, else just an update
693 693 if source_changed:
694 694 pull_request_version = self._create_version_from_snapshot(pull_request)
695 695 self._link_comments_to_version(pull_request_version)
696 696 else:
697 697 try:
698 698 ver = pull_request.versions[-1]
699 699 except IndexError:
700 700 ver = None
701 701
702 702 pull_request.pull_request_version_id = \
703 703 ver.pull_request_version_id if ver else None
704 704 pull_request_version = pull_request
705 705
706 706 try:
707 707 if target_ref_type in ('tag', 'branch', 'book'):
708 708 target_commit = target_repo.get_commit(target_ref_name)
709 709 else:
710 710 target_commit = target_repo.get_commit(target_ref_id)
711 711 except CommitDoesNotExistError:
712 712 return UpdateResponse(
713 713 executed=False,
714 714 reason=UpdateFailureReason.MISSING_TARGET_REF,
715 715 old=pull_request, new=None, changes=None,
716 716 source_changed=source_changed, target_changed=target_changed)
717 717
718 718 # re-compute commit ids
719 719 old_commit_ids = pull_request.revisions
720 720 pre_load = ["author", "branch", "date", "message"]
721 721 commit_ranges = target_repo.compare(
722 722 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
723 723 pre_load=pre_load)
724 724
725 725 ancestor = target_repo.get_common_ancestor(
726 726 target_commit.raw_id, source_commit.raw_id, source_repo)
727 727
728 728 pull_request.source_ref = '%s:%s:%s' % (
729 729 source_ref_type, source_ref_name, source_commit.raw_id)
730 730 pull_request.target_ref = '%s:%s:%s' % (
731 731 target_ref_type, target_ref_name, ancestor)
732 732
733 733 pull_request.revisions = [
734 734 commit.raw_id for commit in reversed(commit_ranges)]
735 735 pull_request.updated_on = datetime.datetime.now()
736 736 Session().add(pull_request)
737 737 new_commit_ids = pull_request.revisions
738 738
739 739 old_diff_data, new_diff_data = self._generate_update_diffs(
740 740 pull_request, pull_request_version)
741 741
742 742 # calculate commit and file changes
743 743 changes = self._calculate_commit_id_changes(
744 744 old_commit_ids, new_commit_ids)
745 745 file_changes = self._calculate_file_changes(
746 746 old_diff_data, new_diff_data)
747 747
748 748 # set comments as outdated if DIFFS changed
749 749 CommentsModel().outdate_comments(
750 750 pull_request, old_diff_data=old_diff_data,
751 751 new_diff_data=new_diff_data)
752 752
753 753 commit_changes = (changes.added or changes.removed)
754 754 file_node_changes = (
755 755 file_changes.added or file_changes.modified or file_changes.removed)
756 756 pr_has_changes = commit_changes or file_node_changes
757 757
758 758 # Add an automatic comment to the pull request, in case
759 759 # anything has changed
760 760 if pr_has_changes:
761 761 update_comment = CommentsModel().create(
762 762 text=self._render_update_message(changes, file_changes),
763 763 repo=pull_request.target_repo,
764 764 user=pull_request.author,
765 765 pull_request=pull_request,
766 766 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
767 767
768 768 # Update status to "Under Review" for added commits
769 769 for commit_id in changes.added:
770 770 ChangesetStatusModel().set_status(
771 771 repo=pull_request.source_repo,
772 772 status=ChangesetStatus.STATUS_UNDER_REVIEW,
773 773 comment=update_comment,
774 774 user=pull_request.author,
775 775 pull_request=pull_request,
776 776 revision=commit_id)
777 777
778 778 log.debug(
779 779 'Updated pull request %s, added_ids: %s, common_ids: %s, '
780 780 'removed_ids: %s', pull_request.pull_request_id,
781 781 changes.added, changes.common, changes.removed)
782 782 log.debug(
783 783 'Updated pull request with the following file changes: %s',
784 784 file_changes)
785 785
786 786 log.info(
787 787 "Updated pull request %s from commit %s to commit %s, "
788 788 "stored new version %s of this pull request.",
789 789 pull_request.pull_request_id, source_ref_id,
790 790 pull_request.source_ref_parts.commit_id,
791 791 pull_request_version.pull_request_version_id)
792 792 Session().commit()
793 793 self._trigger_pull_request_hook(
794 794 pull_request, pull_request.author, 'update')
795 795
796 796 return UpdateResponse(
797 797 executed=True, reason=UpdateFailureReason.NONE,
798 798 old=pull_request, new=pull_request_version, changes=changes,
799 799 source_changed=source_changed, target_changed=target_changed)
800 800
801 801 def _create_version_from_snapshot(self, pull_request):
802 802 version = PullRequestVersion()
803 803 version.title = pull_request.title
804 804 version.description = pull_request.description
805 805 version.status = pull_request.status
806 806 version.created_on = datetime.datetime.now()
807 807 version.updated_on = pull_request.updated_on
808 808 version.user_id = pull_request.user_id
809 809 version.source_repo = pull_request.source_repo
810 810 version.source_ref = pull_request.source_ref
811 811 version.target_repo = pull_request.target_repo
812 812 version.target_ref = pull_request.target_ref
813 813
814 814 version._last_merge_source_rev = pull_request._last_merge_source_rev
815 815 version._last_merge_target_rev = pull_request._last_merge_target_rev
816 816 version.last_merge_status = pull_request.last_merge_status
817 817 version.shadow_merge_ref = pull_request.shadow_merge_ref
818 818 version.merge_rev = pull_request.merge_rev
819 819 version.reviewer_data = pull_request.reviewer_data
820 820
821 821 version.revisions = pull_request.revisions
822 822 version.pull_request = pull_request
823 823 Session().add(version)
824 824 Session().flush()
825 825
826 826 return version
827 827
828 828 def _generate_update_diffs(self, pull_request, pull_request_version):
829 829
830 830 diff_context = (
831 831 self.DIFF_CONTEXT +
832 832 CommentsModel.needed_extra_diff_context())
833 833
834 834 source_repo = pull_request_version.source_repo
835 835 source_ref_id = pull_request_version.source_ref_parts.commit_id
836 836 target_ref_id = pull_request_version.target_ref_parts.commit_id
837 837 old_diff = self._get_diff_from_pr_or_version(
838 838 source_repo, source_ref_id, target_ref_id, context=diff_context)
839 839
840 840 source_repo = pull_request.source_repo
841 841 source_ref_id = pull_request.source_ref_parts.commit_id
842 842 target_ref_id = pull_request.target_ref_parts.commit_id
843 843
844 844 new_diff = self._get_diff_from_pr_or_version(
845 845 source_repo, source_ref_id, target_ref_id, context=diff_context)
846 846
847 847 old_diff_data = diffs.DiffProcessor(old_diff)
848 848 old_diff_data.prepare()
849 849 new_diff_data = diffs.DiffProcessor(new_diff)
850 850 new_diff_data.prepare()
851 851
852 852 return old_diff_data, new_diff_data
853 853
854 854 def _link_comments_to_version(self, pull_request_version):
855 855 """
856 856 Link all unlinked comments of this pull request to the given version.
857 857
858 858 :param pull_request_version: The `PullRequestVersion` to which
859 859 the comments shall be linked.
860 860
861 861 """
862 862 pull_request = pull_request_version.pull_request
863 863 comments = ChangesetComment.query()\
864 864 .filter(
865 865 # TODO: johbo: Should we query for the repo at all here?
866 866 # Pending decision on how comments of PRs are to be related
867 867 # to either the source repo, the target repo or no repo at all.
868 868 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
869 869 ChangesetComment.pull_request == pull_request,
870 870 ChangesetComment.pull_request_version == None)\
871 871 .order_by(ChangesetComment.comment_id.asc())
872 872
873 873 # TODO: johbo: Find out why this breaks if it is done in a bulk
874 874 # operation.
875 875 for comment in comments:
876 876 comment.pull_request_version_id = (
877 877 pull_request_version.pull_request_version_id)
878 878 Session().add(comment)
879 879
880 880 def _calculate_commit_id_changes(self, old_ids, new_ids):
881 881 added = [x for x in new_ids if x not in old_ids]
882 882 common = [x for x in new_ids if x in old_ids]
883 883 removed = [x for x in old_ids if x not in new_ids]
884 884 total = new_ids
885 885 return ChangeTuple(added, common, removed, total)
886 886
887 887 def _calculate_file_changes(self, old_diff_data, new_diff_data):
888 888
889 889 old_files = OrderedDict()
890 890 for diff_data in old_diff_data.parsed_diff:
891 891 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
892 892
893 893 added_files = []
894 894 modified_files = []
895 895 removed_files = []
896 896 for diff_data in new_diff_data.parsed_diff:
897 897 new_filename = diff_data['filename']
898 898 new_hash = md5_safe(diff_data['raw_diff'])
899 899
900 900 old_hash = old_files.get(new_filename)
901 901 if not old_hash:
902 902 # file is not present in old diff, means it's added
903 903 added_files.append(new_filename)
904 904 else:
905 905 if new_hash != old_hash:
906 906 modified_files.append(new_filename)
907 907 # now remove a file from old, since we have seen it already
908 908 del old_files[new_filename]
909 909
910 910 # removed files is when there are present in old, but not in NEW,
911 911 # since we remove old files that are present in new diff, left-overs
912 912 # if any should be the removed files
913 913 removed_files.extend(old_files.keys())
914 914
915 915 return FileChangeTuple(added_files, modified_files, removed_files)
916 916
917 917 def _render_update_message(self, changes, file_changes):
918 918 """
919 919 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
920 920 so it's always looking the same disregarding on which default
921 921 renderer system is using.
922 922
923 923 :param changes: changes named tuple
924 924 :param file_changes: file changes named tuple
925 925
926 926 """
927 927 new_status = ChangesetStatus.get_status_lbl(
928 928 ChangesetStatus.STATUS_UNDER_REVIEW)
929 929
930 930 changed_files = (
931 931 file_changes.added + file_changes.modified + file_changes.removed)
932 932
933 933 params = {
934 934 'under_review_label': new_status,
935 935 'added_commits': changes.added,
936 936 'removed_commits': changes.removed,
937 937 'changed_files': changed_files,
938 938 'added_files': file_changes.added,
939 939 'modified_files': file_changes.modified,
940 940 'removed_files': file_changes.removed,
941 941 }
942 942 renderer = RstTemplateRenderer()
943 943 return renderer.render('pull_request_update.mako', **params)
944 944
945 945 def edit(self, pull_request, title, description, user):
946 946 pull_request = self.__get_pull_request(pull_request)
947 947 old_data = pull_request.get_api_data(with_merge_state=False)
948 948 if pull_request.is_closed():
949 949 raise ValueError('This pull request is closed')
950 950 if title:
951 951 pull_request.title = title
952 952 pull_request.description = description
953 953 pull_request.updated_on = datetime.datetime.now()
954 954 Session().add(pull_request)
955 955 self._log_audit_action(
956 956 'repo.pull_request.edit', {'old_data': old_data},
957 957 user, pull_request)
958 958
959 959 def update_reviewers(self, pull_request, reviewer_data, user):
960 960 """
961 961 Update the reviewers in the pull request
962 962
963 963 :param pull_request: the pr to update
964 964 :param reviewer_data: list of tuples
965 965 [(user, ['reason1', 'reason2'], mandatory_flag)]
966 966 """
967 967 pull_request = self.__get_pull_request(pull_request)
968 968 if pull_request.is_closed():
969 969 raise ValueError('This pull request is closed')
970 970
971 971 reviewers = {}
972 972 for user_id, reasons, mandatory in reviewer_data:
973 973 if isinstance(user_id, (int, basestring)):
974 974 user_id = self._get_user(user_id).user_id
975 975 reviewers[user_id] = {
976 976 'reasons': reasons, 'mandatory': mandatory}
977 977
978 978 reviewers_ids = set(reviewers.keys())
979 979 current_reviewers = PullRequestReviewers.query()\
980 980 .filter(PullRequestReviewers.pull_request ==
981 981 pull_request).all()
982 982 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
983 983
984 984 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
985 985 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
986 986
987 987 log.debug("Adding %s reviewers", ids_to_add)
988 988 log.debug("Removing %s reviewers", ids_to_remove)
989 989 changed = False
990 990 for uid in ids_to_add:
991 991 changed = True
992 992 _usr = self._get_user(uid)
993 993 reviewer = PullRequestReviewers()
994 994 reviewer.user = _usr
995 995 reviewer.pull_request = pull_request
996 996 reviewer.reasons = reviewers[uid]['reasons']
997 997 # NOTE(marcink): mandatory shouldn't be changed now
998 998 # reviewer.mandatory = reviewers[uid]['reasons']
999 999 Session().add(reviewer)
1000 1000 self._log_audit_action(
1001 1001 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1002 1002 user, pull_request)
1003 1003
1004 1004 for uid in ids_to_remove:
1005 1005 changed = True
1006 1006 reviewers = PullRequestReviewers.query()\
1007 1007 .filter(PullRequestReviewers.user_id == uid,
1008 1008 PullRequestReviewers.pull_request == pull_request)\
1009 1009 .all()
1010 1010 # use .all() in case we accidentally added the same person twice
1011 1011 # this CAN happen due to the lack of DB checks
1012 1012 for obj in reviewers:
1013 1013 old_data = obj.get_dict()
1014 1014 Session().delete(obj)
1015 1015 self._log_audit_action(
1016 1016 'repo.pull_request.reviewer.delete',
1017 1017 {'old_data': old_data}, user, pull_request)
1018 1018
1019 1019 if changed:
1020 1020 pull_request.updated_on = datetime.datetime.now()
1021 1021 Session().add(pull_request)
1022 1022
1023 1023 self.notify_reviewers(pull_request, ids_to_add)
1024 1024 return ids_to_add, ids_to_remove
1025 1025
1026 1026 def get_url(self, pull_request, request=None, permalink=False):
1027 1027 if not request:
1028 1028 request = get_current_request()
1029 1029
1030 1030 if permalink:
1031 1031 return request.route_url(
1032 1032 'pull_requests_global',
1033 1033 pull_request_id=pull_request.pull_request_id,)
1034 1034 else:
1035 1035 return request.route_url('pullrequest_show',
1036 1036 repo_name=safe_str(pull_request.target_repo.repo_name),
1037 1037 pull_request_id=pull_request.pull_request_id,)
1038 1038
1039 1039 def get_shadow_clone_url(self, pull_request):
1040 1040 """
1041 1041 Returns qualified url pointing to the shadow repository. If this pull
1042 1042 request is closed there is no shadow repository and ``None`` will be
1043 1043 returned.
1044 1044 """
1045 1045 if pull_request.is_closed():
1046 1046 return None
1047 1047 else:
1048 1048 pr_url = urllib.unquote(self.get_url(pull_request))
1049 1049 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1050 1050
1051 1051 def notify_reviewers(self, pull_request, reviewers_ids):
1052 1052 # notification to reviewers
1053 1053 if not reviewers_ids:
1054 1054 return
1055 1055
1056 1056 pull_request_obj = pull_request
1057 1057 # get the current participants of this pull request
1058 1058 recipients = reviewers_ids
1059 1059 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1060 1060
1061 1061 pr_source_repo = pull_request_obj.source_repo
1062 1062 pr_target_repo = pull_request_obj.target_repo
1063 1063
1064 1064 pr_url = h.route_url('pullrequest_show',
1065 1065 repo_name=pr_target_repo.repo_name,
1066 1066 pull_request_id=pull_request_obj.pull_request_id,)
1067 1067
1068 1068 # set some variables for email notification
1069 1069 pr_target_repo_url = h.route_url(
1070 1070 'repo_summary', repo_name=pr_target_repo.repo_name)
1071 1071
1072 1072 pr_source_repo_url = h.route_url(
1073 1073 'repo_summary', repo_name=pr_source_repo.repo_name)
1074 1074
1075 1075 # pull request specifics
1076 1076 pull_request_commits = [
1077 1077 (x.raw_id, x.message)
1078 1078 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1079 1079
1080 1080 kwargs = {
1081 1081 'user': pull_request.author,
1082 1082 'pull_request': pull_request_obj,
1083 1083 'pull_request_commits': pull_request_commits,
1084 1084
1085 1085 'pull_request_target_repo': pr_target_repo,
1086 1086 'pull_request_target_repo_url': pr_target_repo_url,
1087 1087
1088 1088 'pull_request_source_repo': pr_source_repo,
1089 1089 'pull_request_source_repo_url': pr_source_repo_url,
1090 1090
1091 1091 'pull_request_url': pr_url,
1092 1092 }
1093 1093
1094 1094 # pre-generate the subject for notification itself
1095 1095 (subject,
1096 1096 _h, _e, # we don't care about those
1097 1097 body_plaintext) = EmailNotificationModel().render_email(
1098 1098 notification_type, **kwargs)
1099 1099
1100 1100 # create notification objects, and emails
1101 1101 NotificationModel().create(
1102 1102 created_by=pull_request.author,
1103 1103 notification_subject=subject,
1104 1104 notification_body=body_plaintext,
1105 1105 notification_type=notification_type,
1106 1106 recipients=recipients,
1107 1107 email_kwargs=kwargs,
1108 1108 )
1109 1109
1110 1110 def delete(self, pull_request, user):
1111 1111 pull_request = self.__get_pull_request(pull_request)
1112 1112 old_data = pull_request.get_api_data(with_merge_state=False)
1113 1113 self._cleanup_merge_workspace(pull_request)
1114 1114 self._log_audit_action(
1115 1115 'repo.pull_request.delete', {'old_data': old_data},
1116 1116 user, pull_request)
1117 1117 Session().delete(pull_request)
1118 1118
1119 1119 def close_pull_request(self, pull_request, user):
1120 1120 pull_request = self.__get_pull_request(pull_request)
1121 1121 self._cleanup_merge_workspace(pull_request)
1122 1122 pull_request.status = PullRequest.STATUS_CLOSED
1123 1123 pull_request.updated_on = datetime.datetime.now()
1124 1124 Session().add(pull_request)
1125 1125 self._trigger_pull_request_hook(
1126 1126 pull_request, pull_request.author, 'close')
1127 1127
1128 1128 pr_data = pull_request.get_api_data(with_merge_state=False)
1129 1129 self._log_audit_action(
1130 1130 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1131 1131
1132 1132 def close_pull_request_with_comment(
1133 1133 self, pull_request, user, repo, message=None):
1134 1134
1135 1135 pull_request_review_status = pull_request.calculated_review_status()
1136 1136
1137 1137 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1138 1138 # approved only if we have voting consent
1139 1139 status = ChangesetStatus.STATUS_APPROVED
1140 1140 else:
1141 1141 status = ChangesetStatus.STATUS_REJECTED
1142 1142 status_lbl = ChangesetStatus.get_status_lbl(status)
1143 1143
1144 1144 default_message = (
1145 1145 'Closing with status change {transition_icon} {status}.'
1146 1146 ).format(transition_icon='>', status=status_lbl)
1147 1147 text = message or default_message
1148 1148
1149 1149 # create a comment, and link it to new status
1150 1150 comment = CommentsModel().create(
1151 1151 text=text,
1152 1152 repo=repo.repo_id,
1153 1153 user=user.user_id,
1154 1154 pull_request=pull_request.pull_request_id,
1155 1155 status_change=status_lbl,
1156 1156 status_change_type=status,
1157 1157 closing_pr=True
1158 1158 )
1159 1159
1160 1160 # calculate old status before we change it
1161 1161 old_calculated_status = pull_request.calculated_review_status()
1162 1162 ChangesetStatusModel().set_status(
1163 1163 repo.repo_id,
1164 1164 status,
1165 1165 user.user_id,
1166 1166 comment=comment,
1167 1167 pull_request=pull_request.pull_request_id
1168 1168 )
1169 1169
1170 1170 Session().flush()
1171 1171 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1172 1172 # we now calculate the status of pull request again, and based on that
1173 1173 # calculation trigger status change. This might happen in cases
1174 1174 # that non-reviewer admin closes a pr, which means his vote doesn't
1175 1175 # change the status, while if he's a reviewer this might change it.
1176 1176 calculated_status = pull_request.calculated_review_status()
1177 1177 if old_calculated_status != calculated_status:
1178 1178 self._trigger_pull_request_hook(
1179 1179 pull_request, user, 'review_status_change')
1180 1180
1181 1181 # finally close the PR
1182 1182 PullRequestModel().close_pull_request(
1183 1183 pull_request.pull_request_id, user)
1184 1184
1185 1185 return comment, status
1186 1186
1187 1187 def merge_status(self, pull_request, translator=None):
1188 1188 _ = translator or get_current_request().translate
1189 1189
1190 1190 if not self._is_merge_enabled(pull_request):
1191 1191 return False, _('Server-side pull request merging is disabled.')
1192 1192 if pull_request.is_closed():
1193 1193 return False, _('This pull request is closed.')
1194 1194 merge_possible, msg = self._check_repo_requirements(
1195 1195 target=pull_request.target_repo, source=pull_request.source_repo,
1196 1196 translator=_)
1197 1197 if not merge_possible:
1198 1198 return merge_possible, msg
1199 1199
1200 1200 try:
1201 1201 resp = self._try_merge(pull_request)
1202 1202 log.debug("Merge response: %s", resp)
1203 1203 status = resp.possible, self.merge_status_message(
1204 1204 resp.failure_reason)
1205 1205 except NotImplementedError:
1206 1206 status = False, _('Pull request merging is not supported.')
1207 1207
1208 1208 return status
1209 1209
1210 1210 def _check_repo_requirements(self, target, source, translator):
1211 1211 """
1212 1212 Check if `target` and `source` have compatible requirements.
1213 1213
1214 1214 Currently this is just checking for largefiles.
1215 1215 """
1216 1216 _ = translator
1217 1217 target_has_largefiles = self._has_largefiles(target)
1218 1218 source_has_largefiles = self._has_largefiles(source)
1219 1219 merge_possible = True
1220 1220 message = u''
1221 1221
1222 1222 if target_has_largefiles != source_has_largefiles:
1223 1223 merge_possible = False
1224 1224 if source_has_largefiles:
1225 1225 message = _(
1226 1226 'Target repository large files support is disabled.')
1227 1227 else:
1228 1228 message = _(
1229 1229 'Source repository large files support is disabled.')
1230 1230
1231 1231 return merge_possible, message
1232 1232
1233 1233 def _has_largefiles(self, repo):
1234 1234 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1235 1235 'extensions', 'largefiles')
1236 1236 return largefiles_ui and largefiles_ui[0].active
1237 1237
1238 1238 def _try_merge(self, pull_request):
1239 1239 """
1240 1240 Try to merge the pull request and return the merge status.
1241 1241 """
1242 1242 log.debug(
1243 1243 "Trying out if the pull request %s can be merged.",
1244 1244 pull_request.pull_request_id)
1245 1245 target_vcs = pull_request.target_repo.scm_instance()
1246 1246
1247 1247 # Refresh the target reference.
1248 1248 try:
1249 1249 target_ref = self._refresh_reference(
1250 1250 pull_request.target_ref_parts, target_vcs)
1251 1251 except CommitDoesNotExistError:
1252 1252 merge_state = MergeResponse(
1253 1253 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1254 1254 return merge_state
1255 1255
1256 1256 target_locked = pull_request.target_repo.locked
1257 1257 if target_locked and target_locked[0]:
1258 1258 log.debug("The target repository is locked.")
1259 1259 merge_state = MergeResponse(
1260 1260 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1261 1261 elif self._needs_merge_state_refresh(pull_request, target_ref):
1262 1262 log.debug("Refreshing the merge status of the repository.")
1263 1263 merge_state = self._refresh_merge_state(
1264 1264 pull_request, target_vcs, target_ref)
1265 1265 else:
1266 1266 possible = pull_request.\
1267 1267 last_merge_status == MergeFailureReason.NONE
1268 1268 merge_state = MergeResponse(
1269 1269 possible, False, None, pull_request.last_merge_status)
1270 1270
1271 1271 return merge_state
1272 1272
1273 1273 def _refresh_reference(self, reference, vcs_repository):
1274 1274 if reference.type in ('branch', 'book'):
1275 1275 name_or_id = reference.name
1276 1276 else:
1277 1277 name_or_id = reference.commit_id
1278 1278 refreshed_commit = vcs_repository.get_commit(name_or_id)
1279 1279 refreshed_reference = Reference(
1280 1280 reference.type, reference.name, refreshed_commit.raw_id)
1281 1281 return refreshed_reference
1282 1282
1283 1283 def _needs_merge_state_refresh(self, pull_request, target_reference):
1284 1284 return not(
1285 1285 pull_request.revisions and
1286 1286 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1287 1287 target_reference.commit_id == pull_request._last_merge_target_rev)
1288 1288
1289 1289 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1290 1290 workspace_id = self._workspace_id(pull_request)
1291 1291 source_vcs = pull_request.source_repo.scm_instance()
1292 1292 use_rebase = self._use_rebase_for_merging(pull_request)
1293 1293 close_branch = self._close_branch_before_merging(pull_request)
1294 1294 merge_state = target_vcs.merge(
1295 1295 target_reference, source_vcs, pull_request.source_ref_parts,
1296 1296 workspace_id, dry_run=True, use_rebase=use_rebase,
1297 1297 close_branch=close_branch)
1298 1298
1299 1299 # Do not store the response if there was an unknown error.
1300 1300 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1301 1301 pull_request._last_merge_source_rev = \
1302 1302 pull_request.source_ref_parts.commit_id
1303 1303 pull_request._last_merge_target_rev = target_reference.commit_id
1304 1304 pull_request.last_merge_status = merge_state.failure_reason
1305 1305 pull_request.shadow_merge_ref = merge_state.merge_ref
1306 1306 Session().add(pull_request)
1307 1307 Session().commit()
1308 1308
1309 1309 return merge_state
1310 1310
1311 1311 def _workspace_id(self, pull_request):
1312 1312 workspace_id = 'pr-%s' % pull_request.pull_request_id
1313 1313 return workspace_id
1314 1314
1315 1315 def merge_status_message(self, status_code):
1316 1316 """
1317 1317 Return a human friendly error message for the given merge status code.
1318 1318 """
1319 1319 return self.MERGE_STATUS_MESSAGES[status_code]
1320 1320
1321 1321 def generate_repo_data(self, repo, commit_id=None, branch=None,
1322 1322 bookmark=None, translator=None):
1323 from rhodecode.model.repo import RepoModel
1323 1324
1324 1325 all_refs, selected_ref = \
1325 1326 self._get_repo_pullrequest_sources(
1326 1327 repo.scm_instance(), commit_id=commit_id,
1327 1328 branch=branch, bookmark=bookmark, translator=translator)
1328 1329
1329 1330 refs_select2 = []
1330 1331 for element in all_refs:
1331 1332 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1332 1333 refs_select2.append({'text': element[1], 'children': children})
1333 1334
1334 1335 return {
1335 1336 'user': {
1336 1337 'user_id': repo.user.user_id,
1337 1338 'username': repo.user.username,
1338 1339 'firstname': repo.user.first_name,
1339 1340 'lastname': repo.user.last_name,
1340 1341 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1341 1342 },
1343 'name': repo.repo_name,
1344 'link': RepoModel().get_url(repo),
1342 1345 'description': h.chop_at_smart(repo.description_safe, '\n'),
1343 1346 'refs': {
1344 1347 'all_refs': all_refs,
1345 1348 'selected_ref': selected_ref,
1346 1349 'select2_refs': refs_select2
1347 1350 }
1348 1351 }
1349 1352
1350 1353 def generate_pullrequest_title(self, source, source_ref, target):
1351 1354 return u'{source}#{at_ref} to {target}'.format(
1352 1355 source=source,
1353 1356 at_ref=source_ref,
1354 1357 target=target,
1355 1358 )
1356 1359
1357 1360 def _cleanup_merge_workspace(self, pull_request):
1358 1361 # Merging related cleanup
1359 1362 target_scm = pull_request.target_repo.scm_instance()
1360 1363 workspace_id = 'pr-%s' % pull_request.pull_request_id
1361 1364
1362 1365 try:
1363 1366 target_scm.cleanup_merge_workspace(workspace_id)
1364 1367 except NotImplementedError:
1365 1368 pass
1366 1369
1367 1370 def _get_repo_pullrequest_sources(
1368 1371 self, repo, commit_id=None, branch=None, bookmark=None,
1369 1372 translator=None):
1370 1373 """
1371 1374 Return a structure with repo's interesting commits, suitable for
1372 1375 the selectors in pullrequest controller
1373 1376
1374 1377 :param commit_id: a commit that must be in the list somehow
1375 1378 and selected by default
1376 1379 :param branch: a branch that must be in the list and selected
1377 1380 by default - even if closed
1378 1381 :param bookmark: a bookmark that must be in the list and selected
1379 1382 """
1380 1383 _ = translator or get_current_request().translate
1381 1384
1382 1385 commit_id = safe_str(commit_id) if commit_id else None
1383 1386 branch = safe_str(branch) if branch else None
1384 1387 bookmark = safe_str(bookmark) if bookmark else None
1385 1388
1386 1389 selected = None
1387 1390
1388 1391 # order matters: first source that has commit_id in it will be selected
1389 1392 sources = []
1390 1393 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1391 1394 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1392 1395
1393 1396 if commit_id:
1394 1397 ref_commit = (h.short_id(commit_id), commit_id)
1395 1398 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1396 1399
1397 1400 sources.append(
1398 1401 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1399 1402 )
1400 1403
1401 1404 groups = []
1402 1405 for group_key, ref_list, group_name, match in sources:
1403 1406 group_refs = []
1404 1407 for ref_name, ref_id in ref_list:
1405 1408 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1406 1409 group_refs.append((ref_key, ref_name))
1407 1410
1408 1411 if not selected:
1409 1412 if set([commit_id, match]) & set([ref_id, ref_name]):
1410 1413 selected = ref_key
1411 1414
1412 1415 if group_refs:
1413 1416 groups.append((group_refs, group_name))
1414 1417
1415 1418 if not selected:
1416 1419 ref = commit_id or branch or bookmark
1417 1420 if ref:
1418 1421 raise CommitDoesNotExistError(
1419 1422 'No commit refs could be found matching: %s' % ref)
1420 1423 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1421 1424 selected = 'branch:%s:%s' % (
1422 1425 repo.DEFAULT_BRANCH_NAME,
1423 1426 repo.branches[repo.DEFAULT_BRANCH_NAME]
1424 1427 )
1425 1428 elif repo.commit_ids:
1426 1429 rev = repo.commit_ids[0]
1427 1430 selected = 'rev:%s:%s' % (rev, rev)
1428 1431 else:
1429 1432 raise EmptyRepositoryError()
1430 1433 return groups, selected
1431 1434
1432 1435 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1433 1436 return self._get_diff_from_pr_or_version(
1434 1437 source_repo, source_ref_id, target_ref_id, context=context)
1435 1438
1436 1439 def _get_diff_from_pr_or_version(
1437 1440 self, source_repo, source_ref_id, target_ref_id, context):
1438 1441 target_commit = source_repo.get_commit(
1439 1442 commit_id=safe_str(target_ref_id))
1440 1443 source_commit = source_repo.get_commit(
1441 1444 commit_id=safe_str(source_ref_id))
1442 1445 if isinstance(source_repo, Repository):
1443 1446 vcs_repo = source_repo.scm_instance()
1444 1447 else:
1445 1448 vcs_repo = source_repo
1446 1449
1447 1450 # TODO: johbo: In the context of an update, we cannot reach
1448 1451 # the old commit anymore with our normal mechanisms. It needs
1449 1452 # some sort of special support in the vcs layer to avoid this
1450 1453 # workaround.
1451 1454 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1452 1455 vcs_repo.alias == 'git'):
1453 1456 source_commit.raw_id = safe_str(source_ref_id)
1454 1457
1455 1458 log.debug('calculating diff between '
1456 1459 'source_ref:%s and target_ref:%s for repo `%s`',
1457 1460 target_ref_id, source_ref_id,
1458 1461 safe_unicode(vcs_repo.path))
1459 1462
1460 1463 vcs_diff = vcs_repo.get_diff(
1461 1464 commit1=target_commit, commit2=source_commit, context=context)
1462 1465 return vcs_diff
1463 1466
1464 1467 def _is_merge_enabled(self, pull_request):
1465 1468 return self._get_general_setting(
1466 1469 pull_request, 'rhodecode_pr_merge_enabled')
1467 1470
1468 1471 def _use_rebase_for_merging(self, pull_request):
1469 1472 repo_type = pull_request.target_repo.repo_type
1470 1473 if repo_type == 'hg':
1471 1474 return self._get_general_setting(
1472 1475 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1473 1476 elif repo_type == 'git':
1474 1477 return self._get_general_setting(
1475 1478 pull_request, 'rhodecode_git_use_rebase_for_merging')
1476 1479
1477 1480 return False
1478 1481
1479 1482 def _close_branch_before_merging(self, pull_request):
1480 1483 repo_type = pull_request.target_repo.repo_type
1481 1484 if repo_type == 'hg':
1482 1485 return self._get_general_setting(
1483 1486 pull_request, 'rhodecode_hg_close_branch_before_merging')
1484 1487 elif repo_type == 'git':
1485 1488 return self._get_general_setting(
1486 1489 pull_request, 'rhodecode_git_close_branch_before_merging')
1487 1490
1488 1491 return False
1489 1492
1490 1493 def _get_general_setting(self, pull_request, settings_key, default=False):
1491 1494 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1492 1495 settings = settings_model.get_general_settings()
1493 1496 return settings.get(settings_key, default)
1494 1497
1495 1498 def _log_audit_action(self, action, action_data, user, pull_request):
1496 1499 audit_logger.store(
1497 1500 action=action,
1498 1501 action_data=action_data,
1499 1502 user=user,
1500 1503 repo=pull_request.target_repo)
1501 1504
1502 1505 def get_reviewer_functions(self):
1503 1506 """
1504 1507 Fetches functions for validation and fetching default reviewers.
1505 1508 If available we use the EE package, else we fallback to CE
1506 1509 package functions
1507 1510 """
1508 1511 try:
1509 1512 from rc_reviewers.utils import get_default_reviewers_data
1510 1513 from rc_reviewers.utils import validate_default_reviewers
1511 1514 except ImportError:
1512 1515 from rhodecode.apps.repository.utils import \
1513 1516 get_default_reviewers_data
1514 1517 from rhodecode.apps.repository.utils import \
1515 1518 validate_default_reviewers
1516 1519
1517 1520 return get_default_reviewers_data, validate_default_reviewers
1518 1521
1519 1522
1520 1523 class MergeCheck(object):
1521 1524 """
1522 1525 Perform Merge Checks and returns a check object which stores information
1523 1526 about merge errors, and merge conditions
1524 1527 """
1525 1528 TODO_CHECK = 'todo'
1526 1529 PERM_CHECK = 'perm'
1527 1530 REVIEW_CHECK = 'review'
1528 1531 MERGE_CHECK = 'merge'
1529 1532
1530 1533 def __init__(self):
1531 1534 self.review_status = None
1532 1535 self.merge_possible = None
1533 1536 self.merge_msg = ''
1534 1537 self.failed = None
1535 1538 self.errors = []
1536 1539 self.error_details = OrderedDict()
1537 1540
1538 1541 def push_error(self, error_type, message, error_key, details):
1539 1542 self.failed = True
1540 1543 self.errors.append([error_type, message])
1541 1544 self.error_details[error_key] = dict(
1542 1545 details=details,
1543 1546 error_type=error_type,
1544 1547 message=message
1545 1548 )
1546 1549
1547 1550 @classmethod
1548 1551 def validate(cls, pull_request, user, translator, fail_early=False):
1549 1552 _ = translator
1550 1553 merge_check = cls()
1551 1554
1552 1555 # permissions to merge
1553 1556 user_allowed_to_merge = PullRequestModel().check_user_merge(
1554 1557 pull_request, user)
1555 1558 if not user_allowed_to_merge:
1556 1559 log.debug("MergeCheck: cannot merge, approval is pending.")
1557 1560
1558 1561 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1559 1562 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1560 1563 if fail_early:
1561 1564 return merge_check
1562 1565
1563 1566 # review status, must be always present
1564 1567 review_status = pull_request.calculated_review_status()
1565 1568 merge_check.review_status = review_status
1566 1569
1567 1570 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1568 1571 if not status_approved:
1569 1572 log.debug("MergeCheck: cannot merge, approval is pending.")
1570 1573
1571 1574 msg = _('Pull request reviewer approval is pending.')
1572 1575
1573 1576 merge_check.push_error(
1574 1577 'warning', msg, cls.REVIEW_CHECK, review_status)
1575 1578
1576 1579 if fail_early:
1577 1580 return merge_check
1578 1581
1579 1582 # left over TODOs
1580 1583 todos = CommentsModel().get_unresolved_todos(pull_request)
1581 1584 if todos:
1582 1585 log.debug("MergeCheck: cannot merge, {} "
1583 1586 "unresolved todos left.".format(len(todos)))
1584 1587
1585 1588 if len(todos) == 1:
1586 1589 msg = _('Cannot merge, {} TODO still not resolved.').format(
1587 1590 len(todos))
1588 1591 else:
1589 1592 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1590 1593 len(todos))
1591 1594
1592 1595 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1593 1596
1594 1597 if fail_early:
1595 1598 return merge_check
1596 1599
1597 1600 # merge possible
1598 1601 merge_status, msg = PullRequestModel().merge_status(
1599 1602 pull_request, translator=translator)
1600 1603 merge_check.merge_possible = merge_status
1601 1604 merge_check.merge_msg = msg
1602 1605 if not merge_status:
1603 1606 log.debug(
1604 1607 "MergeCheck: cannot merge, pull request merge not possible.")
1605 1608 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1606 1609
1607 1610 if fail_early:
1608 1611 return merge_check
1609 1612
1610 1613 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1611 1614 return merge_check
1612 1615
1613 1616 @classmethod
1614 1617 def get_merge_conditions(cls, pull_request, translator):
1615 1618 _ = translator
1616 1619 merge_details = {}
1617 1620
1618 1621 model = PullRequestModel()
1619 1622 use_rebase = model._use_rebase_for_merging(pull_request)
1620 1623
1621 1624 if use_rebase:
1622 1625 merge_details['merge_strategy'] = dict(
1623 1626 details={},
1624 1627 message=_('Merge strategy: rebase')
1625 1628 )
1626 1629 else:
1627 1630 merge_details['merge_strategy'] = dict(
1628 1631 details={},
1629 1632 message=_('Merge strategy: explicit merge commit')
1630 1633 )
1631 1634
1632 1635 close_branch = model._close_branch_before_merging(pull_request)
1633 1636 if close_branch:
1634 1637 repo_type = pull_request.target_repo.repo_type
1635 1638 if repo_type == 'hg':
1636 1639 close_msg = _('Source branch will be closed after merge.')
1637 1640 elif repo_type == 'git':
1638 1641 close_msg = _('Source branch will be deleted after merge.')
1639 1642
1640 1643 merge_details['close_branch'] = dict(
1641 1644 details={},
1642 1645 message=close_msg
1643 1646 )
1644 1647
1645 1648 return merge_details
1646 1649
1647 1650 ChangeTuple = collections.namedtuple(
1648 1651 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1649 1652
1650 1653 FileChangeTuple = collections.namedtuple(
1651 1654 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,526 +1,527 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208 } else {
209 209 id = selectedRef;
210 210 refData = selectedRef.split(':');
211 211 }
212 212
213 213 var text = refData[1];
214 214 if (refData[0] === 'rev') {
215 215 text = text.substring(0, 12);
216 216 }
217 217
218 218 var data = {id: id, text: text};
219 219
220 220 callback(data);
221 221 };
222 222 };
223 223
224 224 var formatRefSelection = function(item) {
225 225 var prefix = '';
226 226 var refData = item.id.split(':');
227 227 if (refData[0] === 'branch') {
228 228 prefix = '<i class="icon-branch"></i>';
229 229 }
230 230 else if (refData[0] === 'book') {
231 231 prefix = '<i class="icon-bookmark"></i>';
232 232 }
233 233 else if (refData[0] === 'tag') {
234 234 prefix = '<i class="icon-tag"></i>';
235 235 }
236 236
237 237 var originalOption = item.element;
238 238 return prefix + item.text;
239 239 };
240 240
241 241 // custom code mirror
242 242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243 243
244 244 reviewersController = new ReviewersController();
245 245
246 246 var queryTargetRepo = function(self, query) {
247 247 // cache ALL results if query is empty
248 248 var cacheKey = query.term || '__';
249 249 var cachedData = self.cachedDataSource[cacheKey];
250 250
251 251 if (cachedData) {
252 252 query.callback({results: cachedData.results});
253 253 } else {
254 254 $.ajax({
255 255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 256 data: {query: query.term},
257 257 dataType: 'json',
258 258 type: 'GET',
259 259 success: function(data) {
260 260 self.cachedDataSource[cacheKey] = data;
261 261 query.callback({results: data.results});
262 262 },
263 263 error: function(data, textStatus, errorThrown) {
264 264 alert(
265 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 266 }
267 267 });
268 268 }
269 269 };
270 270
271 271 var queryTargetRefs = function(initialData, query) {
272 272 var data = {results: []};
273 273 // filter initialData
274 274 $.each(initialData, function() {
275 275 var section = this.text;
276 276 var children = [];
277 277 $.each(this.children, function() {
278 278 if (query.term.length === 0 ||
279 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 280 children.push({'id': this.id, 'text': this.text})
281 281 }
282 282 });
283 283 data.results.push({'text': section, 'children': children})
284 284 });
285 285 query.callback({results: data.results});
286 286 };
287 287
288 288 var loadRepoRefDiffPreview = function() {
289 289
290 290 var url_data = {
291 291 'repo_name': targetRepo(),
292 292 'target_repo': sourceRepo(),
293 293 'source_ref': targetRef()[2],
294 294 'source_ref_type': 'rev',
295 295 'target_ref': sourceRef()[2],
296 296 'target_ref_type': 'rev',
297 297 'merge': true,
298 298 '_': Date.now() // bypass browser caching
299 299 }; // gather the source/target ref and repo here
300 300
301 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 302 prButtonLock(true, "${_('Please select source and target')}");
303 303 return;
304 304 }
305 305 var url = pyroutes.url('repo_compare', url_data);
306 306
307 307 // lock PR button, so we cannot send PR before it's calculated
308 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309 309
310 310 if (loadRepoRefDiffPreview._currentRequest) {
311 311 loadRepoRefDiffPreview._currentRequest.abort();
312 312 }
313 313
314 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 315 .error(function(data, textStatus, errorThrown) {
316 316 alert(
317 317 "Error while processing request.\nError code {0} ({1}).".format(
318 318 data.status, data.statusText));
319 319 })
320 320 .done(function(data) {
321 321 loadRepoRefDiffPreview._currentRequest = null;
322 322 $('#pull_request_overview').html(data);
323 323
324 324 var commitElements = $(data).find('tr[commit_id]');
325 325
326 326 var prTitleAndDesc = getTitleAndDescription(
327 327 sourceRef()[1], commitElements, 5);
328 328
329 329 var title = prTitleAndDesc[0];
330 330 var proposedDescription = prTitleAndDesc[1];
331 331
332 332 var useGeneratedTitle = (
333 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 334 $('#pullrequest_title').val() === "");
335 335
336 336 if (title && useGeneratedTitle) {
337 337 // use generated title if we haven't specified our own
338 338 $('#pullrequest_title').val(title);
339 339 $('#pullrequest_title').addClass('autogenerated-title');
340 340
341 341 }
342 342
343 343 var useGeneratedDescription = (
344 344 !codeMirrorInstance._userDefinedDesc ||
345 345 codeMirrorInstance.getValue() === "");
346 346
347 347 if (proposedDescription && useGeneratedDescription) {
348 348 // set proposed content, if we haven't defined our own,
349 349 // or we don't have description written
350 350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 351 codeMirrorInstance.setValue(proposedDescription);
352 352 }
353 353
354 354 var msg = '';
355 355 if (commitElements.length === 1) {
356 356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 357 } else {
358 358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 359 }
360 360
361 361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362 362
363 363 if (commitElements.length) {
364 364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 366 }
367 367 else {
368 368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 369 }
370 370
371 371
372 372 });
373 373 };
374 374
375 375 var Select2Box = function(element, overrides) {
376 376 var globalDefaults = {
377 377 dropdownAutoWidth: true,
378 378 containerCssClass: "drop-menu",
379 379 dropdownCssClass: "drop-menu-dropdown"
380 380 };
381 381
382 382 var initSelect2 = function(defaultOptions) {
383 383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 384 element.select2(options);
385 385 };
386 386
387 387 return {
388 388 initRef: function() {
389 389 var defaultOptions = {
390 390 minimumResultsForSearch: 5,
391 391 formatSelection: formatRefSelection
392 392 };
393 393
394 394 initSelect2(defaultOptions);
395 395 },
396 396
397 397 initRepo: function(defaultValue, readOnly) {
398 398 var defaultOptions = {
399 399 initSelection : function (element, callback) {
400 400 var data = {id: defaultValue, text: defaultValue};
401 401 callback(data);
402 402 }
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406
407 407 element.select2('val', defaultSourceRepo);
408 408 if (readOnly === true) {
409 409 element.select2('readonly', true);
410 410 }
411 411 }
412 412 };
413 413 };
414 414
415 415 var initTargetRefs = function(refsData, selectedRef){
416 416 Select2Box($targetRef, {
417 417 query: function(query) {
418 418 queryTargetRefs(refsData, query);
419 419 },
420 420 initSelection : initRefSelection(selectedRef)
421 421 }).initRef();
422 422
423 423 if (!(selectedRef === undefined)) {
424 424 $targetRef.select2('val', selectedRef);
425 425 }
426 426 };
427 427
428 428 var targetRepoChanged = function(repoData) {
429 429 // generate new DESC of target repo displayed next to select
430 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
430 431 $('#target_repo_desc').html(
431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Use as source</a>".format(repoData['description'], prLink)
432 433 );
433 434
434 435 // generate dynamic select2 for refs.
435 436 initTargetRefs(repoData['refs']['select2_refs'],
436 437 repoData['refs']['selected_ref']);
437 438
438 439 };
439 440
440 441 var sourceRefSelect2 = Select2Box($sourceRef, {
441 442 placeholder: "${_('Select commit reference')}",
442 443 query: function(query) {
443 444 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 445 queryTargetRefs(initialData, query)
445 446 },
446 447 initSelection: initRefSelection()
447 448 }
448 449 );
449 450
450 451 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 452 query: function(query) {}
452 453 });
453 454
454 455 var targetRepoSelect2 = Select2Box($targetRepo, {
455 456 cachedDataSource: {},
456 457 query: $.debounce(250, function(query) {
457 458 queryTargetRepo(this, query);
458 459 }),
459 460 formatResult: formatResult
460 461 });
461 462
462 463 sourceRefSelect2.initRef();
463 464
464 465 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465 466
466 467 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467 468
468 469 $sourceRef.on('change', function(e){
469 470 loadRepoRefDiffPreview();
470 471 reviewersController.loadDefaultReviewers(
471 472 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 473 });
473 474
474 475 $targetRef.on('change', function(e){
475 476 loadRepoRefDiffPreview();
476 477 reviewersController.loadDefaultReviewers(
477 478 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 479 });
479 480
480 481 $targetRepo.on('change', function(e){
481 482 var repoName = $(this).val();
482 483 calculateContainerWidth();
483 484 $targetRef.select2('destroy');
484 485 $('#target_ref_loading').show();
485 486
486 487 $.ajax({
487 488 url: pyroutes.url('pullrequest_repo_refs',
488 489 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 490 data: {},
490 491 dataType: 'json',
491 492 type: 'GET',
492 493 success: function(data) {
493 494 $('#target_ref_loading').hide();
494 495 targetRepoChanged(data);
495 496 loadRepoRefDiffPreview();
496 497 },
497 498 error: function(data, textStatus, errorThrown) {
498 499 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 500 }
500 501 })
501 502
502 503 });
503 504
504 505 prButtonLock(true, "${_('Please select source and target')}", 'all');
505 506
506 507 // auto-load on init, the target refs select2
507 508 calculateContainerWidth();
508 509 targetRepoChanged(defaultTargetRepoData);
509 510
510 511 $('#pullrequest_title').on('keyup', function(e){
511 512 $(this).removeClass('autogenerated-title');
512 513 });
513 514
514 515 % if c.default_source_ref:
515 516 // in case we have a pre-selected value, use it now
516 517 $sourceRef.select2('val', '${c.default_source_ref}');
517 518 loadRepoRefDiffPreview();
518 519 reviewersController.loadDefaultReviewers(
519 520 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 521 % endif
521 522
522 523 ReviewerAutoComplete('#user');
523 524 });
524 525 </script>
525 526
526 527 </%def>
General Comments 0
You need to be logged in to leave comments. Login now