##// END OF EJS Templates
pull-request: force update pull-request in case of the target repo reference changes....
marcink -
r1595:c00c09dd default
parent child Browse files
Show More
@@ -1,1425 +1,1450 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 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 from sqlalchemy import or_
35 35
36 36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 37 from rhodecode.lib.compat import OrderedDict
38 38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 39 from rhodecode.lib.markup_renderer import (
40 40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 41 from rhodecode.lib.utils import action_logger
42 42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 CommitDoesNotExistError, EmptyRepositoryError)
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 50 from rhodecode.model.comment import CommentsModel
51 51 from rhodecode.model.db import (
52 52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 53 PullRequestVersion, ChangesetComment, Repository)
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.notification import NotificationModel, \
56 56 EmailNotificationModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.settings import VcsSettingsModel
59 59
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 # Data structure to hold the response data when updating commits during a pull
65 65 # request update.
66 66 UpdateResponse = namedtuple(
67 67 'UpdateResponse', 'executed, reason, new, old, changes')
68 68
69 69
70 70 class PullRequestModel(BaseModel):
71 71
72 72 cls = PullRequest
73 73
74 74 DIFF_CONTEXT = 3
75 75
76 76 MERGE_STATUS_MESSAGES = {
77 77 MergeFailureReason.NONE: lazy_ugettext(
78 78 'This pull request can be automatically merged.'),
79 79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 80 'This pull request cannot be merged because of an unhandled'
81 81 ' exception.'),
82 82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 83 'This pull request cannot be merged because of merge conflicts.'),
84 84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 85 'This pull request could not be merged because push to target'
86 86 ' failed.'),
87 87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 88 'This pull request cannot be merged because the target is not a'
89 89 ' head.'),
90 90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 91 'This pull request cannot be merged because the source contains'
92 92 ' more branches than the target.'),
93 93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 94 'This pull request cannot be merged because the target has'
95 95 ' multiple heads.'),
96 96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 97 'This pull request cannot be merged because the target repository'
98 98 ' is locked.'),
99 99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 100 'This pull request cannot be merged because the target or the '
101 101 'source reference is missing.'),
102 102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 103 'This pull request cannot be merged because the target '
104 104 'reference is missing.'),
105 105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 106 'This pull request cannot be merged because the source '
107 107 'reference is missing.'),
108 108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 109 'This pull request cannot be merged because of conflicts related '
110 110 'to sub repositories.'),
111 111 }
112 112
113 113 UPDATE_STATUS_MESSAGES = {
114 114 UpdateFailureReason.NONE: lazy_ugettext(
115 115 'Pull request update successful.'),
116 116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 117 'Pull request update failed because of an unknown error.'),
118 118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 119 'No update needed because the source reference is already '
120 120 'up to date.'),
121 121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 122 'Pull request cannot be updated because the reference type is '
123 123 'not supported for an update.'),
124 124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 125 'This pull request cannot be updated because the target '
126 126 'reference is missing.'),
127 127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 128 'This pull request cannot be updated because the source '
129 129 'reference is missing.'),
130 130 }
131 131
132 132 def __get_pull_request(self, pull_request):
133 133 return self._get_instance((
134 134 PullRequest, PullRequestVersion), pull_request)
135 135
136 136 def _check_perms(self, perms, pull_request, user, api=False):
137 137 if not api:
138 138 return h.HasRepoPermissionAny(*perms)(
139 139 user=user, repo_name=pull_request.target_repo.repo_name)
140 140 else:
141 141 return h.HasRepoPermissionAnyApi(*perms)(
142 142 user=user, repo_name=pull_request.target_repo.repo_name)
143 143
144 144 def check_user_read(self, pull_request, user, api=False):
145 145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 146 return self._check_perms(_perms, pull_request, user, api)
147 147
148 148 def check_user_merge(self, pull_request, user, api=False):
149 149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 150 return self._check_perms(_perms, pull_request, user, api)
151 151
152 152 def check_user_update(self, pull_request, user, api=False):
153 153 owner = user.user_id == pull_request.user_id
154 154 return self.check_user_merge(pull_request, user, api) or owner
155 155
156 156 def check_user_delete(self, pull_request, user):
157 157 owner = user.user_id == pull_request.user_id
158 158 _perms = ('repository.admin',)
159 159 return self._check_perms(_perms, pull_request, user) or owner
160 160
161 161 def check_user_change_status(self, pull_request, user, api=False):
162 162 reviewer = user.user_id in [x.user_id for x in
163 163 pull_request.reviewers]
164 164 return self.check_user_update(pull_request, user, api) or reviewer
165 165
166 166 def get(self, pull_request):
167 167 return self.__get_pull_request(pull_request)
168 168
169 169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 170 opened_by=None, order_by=None,
171 171 order_dir='desc'):
172 172 repo = None
173 173 if repo_name:
174 174 repo = self._get_repo(repo_name)
175 175
176 176 q = PullRequest.query()
177 177
178 178 # source or target
179 179 if repo and source:
180 180 q = q.filter(PullRequest.source_repo == repo)
181 181 elif repo:
182 182 q = q.filter(PullRequest.target_repo == repo)
183 183
184 184 # closed,opened
185 185 if statuses:
186 186 q = q.filter(PullRequest.status.in_(statuses))
187 187
188 188 # opened by filter
189 189 if opened_by:
190 190 q = q.filter(PullRequest.user_id.in_(opened_by))
191 191
192 192 if order_by:
193 193 order_map = {
194 194 'name_raw': PullRequest.pull_request_id,
195 195 'title': PullRequest.title,
196 196 'updated_on_raw': PullRequest.updated_on,
197 197 'target_repo': PullRequest.target_repo_id
198 198 }
199 199 if order_dir == 'asc':
200 200 q = q.order_by(order_map[order_by].asc())
201 201 else:
202 202 q = q.order_by(order_map[order_by].desc())
203 203
204 204 return q
205 205
206 206 def count_all(self, repo_name, source=False, statuses=None,
207 207 opened_by=None):
208 208 """
209 209 Count the number of pull requests for a specific repository.
210 210
211 211 :param repo_name: target or source repo
212 212 :param source: boolean flag to specify if repo_name refers to source
213 213 :param statuses: list of pull request statuses
214 214 :param opened_by: author user of the pull request
215 215 :returns: int number of pull requests
216 216 """
217 217 q = self._prepare_get_all_query(
218 218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219 219
220 220 return q.count()
221 221
222 222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 223 offset=0, length=None, order_by=None, order_dir='desc'):
224 224 """
225 225 Get all pull requests for a specific repository.
226 226
227 227 :param repo_name: target or source repo
228 228 :param source: boolean flag to specify if repo_name refers to source
229 229 :param statuses: list of pull request statuses
230 230 :param opened_by: author user of the pull request
231 231 :param offset: pagination offset
232 232 :param length: length of returned list
233 233 :param order_by: order of the returned list
234 234 :param order_dir: 'asc' or 'desc' ordering direction
235 235 :returns: list of pull requests
236 236 """
237 237 q = self._prepare_get_all_query(
238 238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 239 order_by=order_by, order_dir=order_dir)
240 240
241 241 if length:
242 242 pull_requests = q.limit(length).offset(offset).all()
243 243 else:
244 244 pull_requests = q.all()
245 245
246 246 return pull_requests
247 247
248 248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 249 opened_by=None):
250 250 """
251 251 Count the number of pull requests for a specific repository that are
252 252 awaiting review.
253 253
254 254 :param repo_name: target or source repo
255 255 :param source: boolean flag to specify if repo_name refers to source
256 256 :param statuses: list of pull request statuses
257 257 :param opened_by: author user of the pull request
258 258 :returns: int number of pull requests
259 259 """
260 260 pull_requests = self.get_awaiting_review(
261 261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262 262
263 263 return len(pull_requests)
264 264
265 265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 266 opened_by=None, offset=0, length=None,
267 267 order_by=None, order_dir='desc'):
268 268 """
269 269 Get all pull requests for a specific repository that are awaiting
270 270 review.
271 271
272 272 :param repo_name: target or source repo
273 273 :param source: boolean flag to specify if repo_name refers to source
274 274 :param statuses: list of pull request statuses
275 275 :param opened_by: author user of the pull request
276 276 :param offset: pagination offset
277 277 :param length: length of returned list
278 278 :param order_by: order of the returned list
279 279 :param order_dir: 'asc' or 'desc' ordering direction
280 280 :returns: list of pull requests
281 281 """
282 282 pull_requests = self.get_all(
283 283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 284 order_by=order_by, order_dir=order_dir)
285 285
286 286 _filtered_pull_requests = []
287 287 for pr in pull_requests:
288 288 status = pr.calculated_review_status()
289 289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 291 _filtered_pull_requests.append(pr)
292 292 if length:
293 293 return _filtered_pull_requests[offset:offset+length]
294 294 else:
295 295 return _filtered_pull_requests
296 296
297 297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 298 opened_by=None, user_id=None):
299 299 """
300 300 Count the number of pull requests for a specific repository that are
301 301 awaiting review from a specific user.
302 302
303 303 :param repo_name: target or source repo
304 304 :param source: boolean flag to specify if repo_name refers to source
305 305 :param statuses: list of pull request statuses
306 306 :param opened_by: author user of the pull request
307 307 :param user_id: reviewer user of the pull request
308 308 :returns: int number of pull requests
309 309 """
310 310 pull_requests = self.get_awaiting_my_review(
311 311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 312 user_id=user_id)
313 313
314 314 return len(pull_requests)
315 315
316 316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 317 opened_by=None, user_id=None, offset=0,
318 318 length=None, order_by=None, order_dir='desc'):
319 319 """
320 320 Get all pull requests for a specific repository that are awaiting
321 321 review from a specific user.
322 322
323 323 :param repo_name: target or source repo
324 324 :param source: boolean flag to specify if repo_name refers to source
325 325 :param statuses: list of pull request statuses
326 326 :param opened_by: author user of the pull request
327 327 :param user_id: reviewer user of the pull request
328 328 :param offset: pagination offset
329 329 :param length: length of returned list
330 330 :param order_by: order of the returned list
331 331 :param order_dir: 'asc' or 'desc' ordering direction
332 332 :returns: list of pull requests
333 333 """
334 334 pull_requests = self.get_all(
335 335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 336 order_by=order_by, order_dir=order_dir)
337 337
338 338 _my = PullRequestModel().get_not_reviewed(user_id)
339 339 my_participation = []
340 340 for pr in pull_requests:
341 341 if pr in _my:
342 342 my_participation.append(pr)
343 343 _filtered_pull_requests = my_participation
344 344 if length:
345 345 return _filtered_pull_requests[offset:offset+length]
346 346 else:
347 347 return _filtered_pull_requests
348 348
349 349 def get_not_reviewed(self, user_id):
350 350 return [
351 351 x.pull_request for x in PullRequestReviewers.query().filter(
352 352 PullRequestReviewers.user_id == user_id).all()
353 353 ]
354 354
355 355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 356 order_by=None, order_dir='desc'):
357 357 q = PullRequest.query()
358 358 if user_id:
359 359 reviewers_subquery = Session().query(
360 360 PullRequestReviewers.pull_request_id).filter(
361 361 PullRequestReviewers.user_id == user_id).subquery()
362 362 user_filter= or_(
363 363 PullRequest.user_id == user_id,
364 364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 365 )
366 366 q = PullRequest.query().filter(user_filter)
367 367
368 368 # closed,opened
369 369 if statuses:
370 370 q = q.filter(PullRequest.status.in_(statuses))
371 371
372 372 if order_by:
373 373 order_map = {
374 374 'name_raw': PullRequest.pull_request_id,
375 375 'title': PullRequest.title,
376 376 'updated_on_raw': PullRequest.updated_on,
377 377 'target_repo': PullRequest.target_repo_id
378 378 }
379 379 if order_dir == 'asc':
380 380 q = q.order_by(order_map[order_by].asc())
381 381 else:
382 382 q = q.order_by(order_map[order_by].desc())
383 383
384 384 return q
385 385
386 386 def count_im_participating_in(self, user_id=None, statuses=None):
387 387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 388 return q.count()
389 389
390 390 def get_im_participating_in(
391 391 self, user_id=None, statuses=None, offset=0,
392 392 length=None, order_by=None, order_dir='desc'):
393 393 """
394 394 Get all Pull requests that i'm participating in, or i have opened
395 395 """
396 396
397 397 q = self._prepare_participating_query(
398 398 user_id, statuses=statuses, order_by=order_by,
399 399 order_dir=order_dir)
400 400
401 401 if length:
402 402 pull_requests = q.limit(length).offset(offset).all()
403 403 else:
404 404 pull_requests = q.all()
405 405
406 406 return pull_requests
407 407
408 408 def get_versions(self, pull_request):
409 409 """
410 410 returns version of pull request sorted by ID descending
411 411 """
412 412 return PullRequestVersion.query()\
413 413 .filter(PullRequestVersion.pull_request == pull_request)\
414 414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 415 .all()
416 416
417 417 def create(self, created_by, source_repo, source_ref, target_repo,
418 418 target_ref, revisions, reviewers, title, description=None):
419 419 created_by_user = self._get_user(created_by)
420 420 source_repo = self._get_repo(source_repo)
421 421 target_repo = self._get_repo(target_repo)
422 422
423 423 pull_request = PullRequest()
424 424 pull_request.source_repo = source_repo
425 425 pull_request.source_ref = source_ref
426 426 pull_request.target_repo = target_repo
427 427 pull_request.target_ref = target_ref
428 428 pull_request.revisions = revisions
429 429 pull_request.title = title
430 430 pull_request.description = description
431 431 pull_request.author = created_by_user
432 432
433 433 Session().add(pull_request)
434 434 Session().flush()
435 435
436 436 reviewer_ids = set()
437 437 # members / reviewers
438 438 for reviewer_object in reviewers:
439 439 if isinstance(reviewer_object, tuple):
440 440 user_id, reasons = reviewer_object
441 441 else:
442 442 user_id, reasons = reviewer_object, []
443 443
444 444 user = self._get_user(user_id)
445 445 reviewer_ids.add(user.user_id)
446 446
447 447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 448 Session().add(reviewer)
449 449
450 450 # Set approval status to "Under Review" for all commits which are
451 451 # part of this pull request.
452 452 ChangesetStatusModel().set_status(
453 453 repo=target_repo,
454 454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 455 user=created_by_user,
456 456 pull_request=pull_request
457 457 )
458 458
459 459 self.notify_reviewers(pull_request, reviewer_ids)
460 460 self._trigger_pull_request_hook(
461 461 pull_request, created_by_user, 'create')
462 462
463 463 return pull_request
464 464
465 465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 466 pull_request = self.__get_pull_request(pull_request)
467 467 target_scm = pull_request.target_repo.scm_instance()
468 468 if action == 'create':
469 469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 470 elif action == 'merge':
471 471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 472 elif action == 'close':
473 473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 474 elif action == 'review_status_change':
475 475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 476 elif action == 'update':
477 477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 478 else:
479 479 return
480 480
481 481 trigger_hook(
482 482 username=user.username,
483 483 repo_name=pull_request.target_repo.repo_name,
484 484 repo_alias=target_scm.alias,
485 485 pull_request=pull_request)
486 486
487 487 def _get_commit_ids(self, pull_request):
488 488 """
489 489 Return the commit ids of the merged pull request.
490 490
491 491 This method is not dealing correctly yet with the lack of autoupdates
492 492 nor with the implicit target updates.
493 493 For example: if a commit in the source repo is already in the target it
494 494 will be reported anyways.
495 495 """
496 496 merge_rev = pull_request.merge_rev
497 497 if merge_rev is None:
498 498 raise ValueError('This pull request was not merged yet')
499 499
500 500 commit_ids = list(pull_request.revisions)
501 501 if merge_rev not in commit_ids:
502 502 commit_ids.append(merge_rev)
503 503
504 504 return commit_ids
505 505
506 506 def merge(self, pull_request, user, extras):
507 507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 509 if merge_state.executed:
510 510 log.debug(
511 511 "Merge was successful, updating the pull request comments.")
512 512 self._comment_and_close_pr(pull_request, user, merge_state)
513 513 self._log_action('user_merged_pull_request', user, pull_request)
514 514 else:
515 515 log.warn("Merge failed, not updating the pull request.")
516 516 return merge_state
517 517
518 518 def _merge_pull_request(self, pull_request, user, extras):
519 519 target_vcs = pull_request.target_repo.scm_instance()
520 520 source_vcs = pull_request.source_repo.scm_instance()
521 521 target_ref = self._refresh_reference(
522 522 pull_request.target_ref_parts, target_vcs)
523 523
524 524 message = _(
525 525 'Merge pull request #%(pr_id)s from '
526 526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 527 'pr_id': pull_request.pull_request_id,
528 528 'source_repo': source_vcs.name,
529 529 'source_ref_name': pull_request.source_ref_parts.name,
530 530 'pr_title': pull_request.title
531 531 }
532 532
533 533 workspace_id = self._workspace_id(pull_request)
534 534 use_rebase = self._use_rebase_for_merging(pull_request)
535 535
536 536 callback_daemon, extras = prepare_callback_daemon(
537 537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539 539
540 540 with callback_daemon:
541 541 # TODO: johbo: Implement a clean way to run a config_override
542 542 # for a single call.
543 543 target_vcs.config.set(
544 544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 545 merge_state = target_vcs.merge(
546 546 target_ref, source_vcs, pull_request.source_ref_parts,
547 547 workspace_id, user_name=user.username,
548 548 user_email=user.email, message=message, use_rebase=use_rebase)
549 549 return merge_state
550 550
551 551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 553 pull_request.updated_on = datetime.datetime.now()
554 554
555 555 CommentsModel().create(
556 556 text=unicode(_('Pull request merged and closed')),
557 557 repo=pull_request.target_repo.repo_id,
558 558 user=user.user_id,
559 559 pull_request=pull_request.pull_request_id,
560 560 f_path=None,
561 561 line_no=None,
562 562 closing_pr=True
563 563 )
564 564
565 565 Session().add(pull_request)
566 566 Session().flush()
567 567 # TODO: paris: replace invalidation with less radical solution
568 568 ScmModel().mark_for_invalidation(
569 569 pull_request.target_repo.repo_name)
570 570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571 571
572 572 def has_valid_update_type(self, pull_request):
573 573 source_ref_type = pull_request.source_ref_parts.type
574 574 return source_ref_type in ['book', 'branch', 'tag']
575 575
576 576 def update_commits(self, pull_request):
577 577 """
578 578 Get the updated list of commits for the pull request
579 579 and return the new pull request version and the list
580 580 of commits processed by this update action
581 581 """
582 582 pull_request = self.__get_pull_request(pull_request)
583 583 source_ref_type = pull_request.source_ref_parts.type
584 584 source_ref_name = pull_request.source_ref_parts.name
585 585 source_ref_id = pull_request.source_ref_parts.commit_id
586 586
587 target_ref_type = pull_request.target_ref_parts.type
588 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
590
587 591 if not self.has_valid_update_type(pull_request):
588 592 log.debug(
589 593 "Skipping update of pull request %s due to ref type: %s",
590 594 pull_request, source_ref_type)
591 595 return UpdateResponse(
592 596 executed=False,
593 597 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 598 old=pull_request, new=None, changes=None)
595 599
600 # source repo
596 601 source_repo = pull_request.source_repo.scm_instance()
597 602 try:
598 603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 604 except CommitDoesNotExistError:
600 605 return UpdateResponse(
601 606 executed=False,
602 607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 608 old=pull_request, new=None, changes=None)
604 609
605 if source_ref_id == source_commit.raw_id:
610 source_changed = source_ref_id != source_commit.raw_id
611
612 # target repo
613 target_repo = pull_request.target_repo.scm_instance()
614 try:
615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
616 except CommitDoesNotExistError:
617 return UpdateResponse(
618 executed=False,
619 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 old=pull_request, new=None, changes=None)
621 target_changed = target_ref_id != target_commit.raw_id
622
623 if not (source_changed or target_changed):
606 624 log.debug("Nothing changed in pull request %s", pull_request)
607 625 return UpdateResponse(
608 626 executed=False,
609 627 reason=UpdateFailureReason.NO_CHANGE,
610 628 old=pull_request, new=None, changes=None)
611 629
612 # Finally there is a need for an update
613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 self._link_comments_to_version(pull_request_version)
630 change_in_found = 'target repo' if target_changed else 'source repo'
631 log.debug('Updating pull request because of change in %s detected',
632 change_in_found)
615 633
616 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_repo = pull_request.target_repo.scm_instance()
634 # Finally there is a need for an update, in case of source change
635 # we create a new version, else just an update
636 if source_changed:
637 pull_request_version = self._create_version_from_snapshot(pull_request)
638 self._link_comments_to_version(pull_request_version)
639 else:
640 ver = pull_request.versions[-1]
641 pull_request.pull_request_version_id = \
642 ver.pull_request_version_id if ver else None
643 pull_request_version = pull_request
620 644
621 645 try:
622 646 if target_ref_type in ('tag', 'branch', 'book'):
623 647 target_commit = target_repo.get_commit(target_ref_name)
624 648 else:
625 649 target_commit = target_repo.get_commit(target_ref_id)
626 650 except CommitDoesNotExistError:
627 651 return UpdateResponse(
628 652 executed=False,
629 653 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 654 old=pull_request, new=None, changes=None)
631 655
632 656 # re-compute commit ids
633 657 old_commit_ids = pull_request.revisions
634 658 pre_load = ["author", "branch", "date", "message"]
635 659 commit_ranges = target_repo.compare(
636 660 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 661 pre_load=pre_load)
638 662
639 663 ancestor = target_repo.get_common_ancestor(
640 664 target_commit.raw_id, source_commit.raw_id, source_repo)
641 665
642 666 pull_request.source_ref = '%s:%s:%s' % (
643 667 source_ref_type, source_ref_name, source_commit.raw_id)
644 668 pull_request.target_ref = '%s:%s:%s' % (
645 669 target_ref_type, target_ref_name, ancestor)
670
646 671 pull_request.revisions = [
647 672 commit.raw_id for commit in reversed(commit_ranges)]
648 673 pull_request.updated_on = datetime.datetime.now()
649 674 Session().add(pull_request)
650 675 new_commit_ids = pull_request.revisions
651 676
652 677 changes = self._calculate_commit_id_changes(
653 678 old_commit_ids, new_commit_ids)
654 679
655 680 old_diff_data, new_diff_data = self._generate_update_diffs(
656 681 pull_request, pull_request_version)
657 682
658 683 CommentsModel().outdate_comments(
659 684 pull_request, old_diff_data=old_diff_data,
660 685 new_diff_data=new_diff_data)
661 686
662 687 file_changes = self._calculate_file_changes(
663 688 old_diff_data, new_diff_data)
664 689
665 690 # Add an automatic comment to the pull request
666 691 update_comment = CommentsModel().create(
667 692 text=self._render_update_message(changes, file_changes),
668 693 repo=pull_request.target_repo,
669 694 user=pull_request.author,
670 695 pull_request=pull_request,
671 696 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672 697
673 698 # Update status to "Under Review" for added commits
674 699 for commit_id in changes.added:
675 700 ChangesetStatusModel().set_status(
676 701 repo=pull_request.source_repo,
677 702 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 703 comment=update_comment,
679 704 user=pull_request.author,
680 705 pull_request=pull_request,
681 706 revision=commit_id)
682 707
683 708 log.debug(
684 709 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 710 'removed_ids: %s', pull_request.pull_request_id,
686 711 changes.added, changes.common, changes.removed)
687 712 log.debug('Updated pull request with the following file changes: %s',
688 713 file_changes)
689 714
690 715 log.info(
691 716 "Updated pull request %s from commit %s to commit %s, "
692 717 "stored new version %s of this pull request.",
693 718 pull_request.pull_request_id, source_ref_id,
694 719 pull_request.source_ref_parts.commit_id,
695 720 pull_request_version.pull_request_version_id)
696 721 Session().commit()
697 722 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 723 'update')
699 724
700 725 return UpdateResponse(
701 726 executed=True, reason=UpdateFailureReason.NONE,
702 727 old=pull_request, new=pull_request_version, changes=changes)
703 728
704 729 def _create_version_from_snapshot(self, pull_request):
705 730 version = PullRequestVersion()
706 731 version.title = pull_request.title
707 732 version.description = pull_request.description
708 733 version.status = pull_request.status
709 734 version.created_on = datetime.datetime.now()
710 735 version.updated_on = pull_request.updated_on
711 736 version.user_id = pull_request.user_id
712 737 version.source_repo = pull_request.source_repo
713 738 version.source_ref = pull_request.source_ref
714 739 version.target_repo = pull_request.target_repo
715 740 version.target_ref = pull_request.target_ref
716 741
717 742 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 743 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 744 version._last_merge_status = pull_request._last_merge_status
720 745 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 746 version.merge_rev = pull_request.merge_rev
722 747
723 748 version.revisions = pull_request.revisions
724 749 version.pull_request = pull_request
725 750 Session().add(version)
726 751 Session().flush()
727 752
728 753 return version
729 754
730 755 def _generate_update_diffs(self, pull_request, pull_request_version):
731 756
732 757 diff_context = (
733 758 self.DIFF_CONTEXT +
734 759 CommentsModel.needed_extra_diff_context())
735 760
736 761 source_repo = pull_request_version.source_repo
737 762 source_ref_id = pull_request_version.source_ref_parts.commit_id
738 763 target_ref_id = pull_request_version.target_ref_parts.commit_id
739 764 old_diff = self._get_diff_from_pr_or_version(
740 765 source_repo, source_ref_id, target_ref_id, context=diff_context)
741 766
742 767 source_repo = pull_request.source_repo
743 768 source_ref_id = pull_request.source_ref_parts.commit_id
744 769 target_ref_id = pull_request.target_ref_parts.commit_id
745 770
746 771 new_diff = self._get_diff_from_pr_or_version(
747 772 source_repo, source_ref_id, target_ref_id, context=diff_context)
748 773
749 774 old_diff_data = diffs.DiffProcessor(old_diff)
750 775 old_diff_data.prepare()
751 776 new_diff_data = diffs.DiffProcessor(new_diff)
752 777 new_diff_data.prepare()
753 778
754 779 return old_diff_data, new_diff_data
755 780
756 781 def _link_comments_to_version(self, pull_request_version):
757 782 """
758 783 Link all unlinked comments of this pull request to the given version.
759 784
760 785 :param pull_request_version: The `PullRequestVersion` to which
761 786 the comments shall be linked.
762 787
763 788 """
764 789 pull_request = pull_request_version.pull_request
765 790 comments = ChangesetComment.query().filter(
766 791 # TODO: johbo: Should we query for the repo at all here?
767 792 # Pending decision on how comments of PRs are to be related
768 793 # to either the source repo, the target repo or no repo at all.
769 794 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
770 795 ChangesetComment.pull_request == pull_request,
771 796 ChangesetComment.pull_request_version == None)
772 797
773 798 # TODO: johbo: Find out why this breaks if it is done in a bulk
774 799 # operation.
775 800 for comment in comments:
776 801 comment.pull_request_version_id = (
777 802 pull_request_version.pull_request_version_id)
778 803 Session().add(comment)
779 804
780 805 def _calculate_commit_id_changes(self, old_ids, new_ids):
781 806 added = [x for x in new_ids if x not in old_ids]
782 807 common = [x for x in new_ids if x in old_ids]
783 808 removed = [x for x in old_ids if x not in new_ids]
784 809 total = new_ids
785 810 return ChangeTuple(added, common, removed, total)
786 811
787 812 def _calculate_file_changes(self, old_diff_data, new_diff_data):
788 813
789 814 old_files = OrderedDict()
790 815 for diff_data in old_diff_data.parsed_diff:
791 816 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
792 817
793 818 added_files = []
794 819 modified_files = []
795 820 removed_files = []
796 821 for diff_data in new_diff_data.parsed_diff:
797 822 new_filename = diff_data['filename']
798 823 new_hash = md5_safe(diff_data['raw_diff'])
799 824
800 825 old_hash = old_files.get(new_filename)
801 826 if not old_hash:
802 827 # file is not present in old diff, means it's added
803 828 added_files.append(new_filename)
804 829 else:
805 830 if new_hash != old_hash:
806 831 modified_files.append(new_filename)
807 832 # now remove a file from old, since we have seen it already
808 833 del old_files[new_filename]
809 834
810 835 # removed files is when there are present in old, but not in NEW,
811 836 # since we remove old files that are present in new diff, left-overs
812 837 # if any should be the removed files
813 838 removed_files.extend(old_files.keys())
814 839
815 840 return FileChangeTuple(added_files, modified_files, removed_files)
816 841
817 842 def _render_update_message(self, changes, file_changes):
818 843 """
819 844 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
820 845 so it's always looking the same disregarding on which default
821 846 renderer system is using.
822 847
823 848 :param changes: changes named tuple
824 849 :param file_changes: file changes named tuple
825 850
826 851 """
827 852 new_status = ChangesetStatus.get_status_lbl(
828 853 ChangesetStatus.STATUS_UNDER_REVIEW)
829 854
830 855 changed_files = (
831 856 file_changes.added + file_changes.modified + file_changes.removed)
832 857
833 858 params = {
834 859 'under_review_label': new_status,
835 860 'added_commits': changes.added,
836 861 'removed_commits': changes.removed,
837 862 'changed_files': changed_files,
838 863 'added_files': file_changes.added,
839 864 'modified_files': file_changes.modified,
840 865 'removed_files': file_changes.removed,
841 866 }
842 867 renderer = RstTemplateRenderer()
843 868 return renderer.render('pull_request_update.mako', **params)
844 869
845 870 def edit(self, pull_request, title, description):
846 871 pull_request = self.__get_pull_request(pull_request)
847 872 if pull_request.is_closed():
848 873 raise ValueError('This pull request is closed')
849 874 if title:
850 875 pull_request.title = title
851 876 pull_request.description = description
852 877 pull_request.updated_on = datetime.datetime.now()
853 878 Session().add(pull_request)
854 879
855 880 def update_reviewers(self, pull_request, reviewer_data):
856 881 """
857 882 Update the reviewers in the pull request
858 883
859 884 :param pull_request: the pr to update
860 885 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
861 886 """
862 887
863 888 reviewers_reasons = {}
864 889 for user_id, reasons in reviewer_data:
865 890 if isinstance(user_id, (int, basestring)):
866 891 user_id = self._get_user(user_id).user_id
867 892 reviewers_reasons[user_id] = reasons
868 893
869 894 reviewers_ids = set(reviewers_reasons.keys())
870 895 pull_request = self.__get_pull_request(pull_request)
871 896 current_reviewers = PullRequestReviewers.query()\
872 897 .filter(PullRequestReviewers.pull_request ==
873 898 pull_request).all()
874 899 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
875 900
876 901 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
877 902 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
878 903
879 904 log.debug("Adding %s reviewers", ids_to_add)
880 905 log.debug("Removing %s reviewers", ids_to_remove)
881 906 changed = False
882 907 for uid in ids_to_add:
883 908 changed = True
884 909 _usr = self._get_user(uid)
885 910 reasons = reviewers_reasons[uid]
886 911 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
887 912 Session().add(reviewer)
888 913
889 914 for uid in ids_to_remove:
890 915 changed = True
891 916 reviewers = PullRequestReviewers.query()\
892 917 .filter(PullRequestReviewers.user_id == uid,
893 918 PullRequestReviewers.pull_request == pull_request)\
894 919 .all()
895 920 # use .all() in case we accidentally added the same person twice
896 921 # this CAN happen due to the lack of DB checks
897 922 for obj in reviewers:
898 923 Session().delete(obj)
899 924
900 925 if changed:
901 926 pull_request.updated_on = datetime.datetime.now()
902 927 Session().add(pull_request)
903 928
904 929 self.notify_reviewers(pull_request, ids_to_add)
905 930 return ids_to_add, ids_to_remove
906 931
907 932 def get_url(self, pull_request):
908 933 return h.url('pullrequest_show',
909 934 repo_name=safe_str(pull_request.target_repo.repo_name),
910 935 pull_request_id=pull_request.pull_request_id,
911 936 qualified=True)
912 937
913 938 def get_shadow_clone_url(self, pull_request):
914 939 """
915 940 Returns qualified url pointing to the shadow repository. If this pull
916 941 request is closed there is no shadow repository and ``None`` will be
917 942 returned.
918 943 """
919 944 if pull_request.is_closed():
920 945 return None
921 946 else:
922 947 pr_url = urllib.unquote(self.get_url(pull_request))
923 948 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
924 949
925 950 def notify_reviewers(self, pull_request, reviewers_ids):
926 951 # notification to reviewers
927 952 if not reviewers_ids:
928 953 return
929 954
930 955 pull_request_obj = pull_request
931 956 # get the current participants of this pull request
932 957 recipients = reviewers_ids
933 958 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
934 959
935 960 pr_source_repo = pull_request_obj.source_repo
936 961 pr_target_repo = pull_request_obj.target_repo
937 962
938 963 pr_url = h.url(
939 964 'pullrequest_show',
940 965 repo_name=pr_target_repo.repo_name,
941 966 pull_request_id=pull_request_obj.pull_request_id,
942 967 qualified=True,)
943 968
944 969 # set some variables for email notification
945 970 pr_target_repo_url = h.url(
946 971 'summary_home',
947 972 repo_name=pr_target_repo.repo_name,
948 973 qualified=True)
949 974
950 975 pr_source_repo_url = h.url(
951 976 'summary_home',
952 977 repo_name=pr_source_repo.repo_name,
953 978 qualified=True)
954 979
955 980 # pull request specifics
956 981 pull_request_commits = [
957 982 (x.raw_id, x.message)
958 983 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
959 984
960 985 kwargs = {
961 986 'user': pull_request.author,
962 987 'pull_request': pull_request_obj,
963 988 'pull_request_commits': pull_request_commits,
964 989
965 990 'pull_request_target_repo': pr_target_repo,
966 991 'pull_request_target_repo_url': pr_target_repo_url,
967 992
968 993 'pull_request_source_repo': pr_source_repo,
969 994 'pull_request_source_repo_url': pr_source_repo_url,
970 995
971 996 'pull_request_url': pr_url,
972 997 }
973 998
974 999 # pre-generate the subject for notification itself
975 1000 (subject,
976 1001 _h, _e, # we don't care about those
977 1002 body_plaintext) = EmailNotificationModel().render_email(
978 1003 notification_type, **kwargs)
979 1004
980 1005 # create notification objects, and emails
981 1006 NotificationModel().create(
982 1007 created_by=pull_request.author,
983 1008 notification_subject=subject,
984 1009 notification_body=body_plaintext,
985 1010 notification_type=notification_type,
986 1011 recipients=recipients,
987 1012 email_kwargs=kwargs,
988 1013 )
989 1014
990 1015 def delete(self, pull_request):
991 1016 pull_request = self.__get_pull_request(pull_request)
992 1017 self._cleanup_merge_workspace(pull_request)
993 1018 Session().delete(pull_request)
994 1019
995 1020 def close_pull_request(self, pull_request, user):
996 1021 pull_request = self.__get_pull_request(pull_request)
997 1022 self._cleanup_merge_workspace(pull_request)
998 1023 pull_request.status = PullRequest.STATUS_CLOSED
999 1024 pull_request.updated_on = datetime.datetime.now()
1000 1025 Session().add(pull_request)
1001 1026 self._trigger_pull_request_hook(
1002 1027 pull_request, pull_request.author, 'close')
1003 1028 self._log_action('user_closed_pull_request', user, pull_request)
1004 1029
1005 1030 def close_pull_request_with_comment(self, pull_request, user, repo,
1006 1031 message=None):
1007 1032 status = ChangesetStatus.STATUS_REJECTED
1008 1033
1009 1034 if not message:
1010 1035 message = (
1011 1036 _('Status change %(transition_icon)s %(status)s') % {
1012 1037 'transition_icon': '>',
1013 1038 'status': ChangesetStatus.get_status_lbl(status)})
1014 1039
1015 1040 internal_message = _('Closing with') + ' ' + message
1016 1041
1017 1042 comm = CommentsModel().create(
1018 1043 text=internal_message,
1019 1044 repo=repo.repo_id,
1020 1045 user=user.user_id,
1021 1046 pull_request=pull_request.pull_request_id,
1022 1047 f_path=None,
1023 1048 line_no=None,
1024 1049 status_change=ChangesetStatus.get_status_lbl(status),
1025 1050 status_change_type=status,
1026 1051 closing_pr=True
1027 1052 )
1028 1053
1029 1054 ChangesetStatusModel().set_status(
1030 1055 repo.repo_id,
1031 1056 status,
1032 1057 user.user_id,
1033 1058 comm,
1034 1059 pull_request=pull_request.pull_request_id
1035 1060 )
1036 1061 Session().flush()
1037 1062
1038 1063 PullRequestModel().close_pull_request(
1039 1064 pull_request.pull_request_id, user)
1040 1065
1041 1066 def merge_status(self, pull_request):
1042 1067 if not self._is_merge_enabled(pull_request):
1043 1068 return False, _('Server-side pull request merging is disabled.')
1044 1069 if pull_request.is_closed():
1045 1070 return False, _('This pull request is closed.')
1046 1071 merge_possible, msg = self._check_repo_requirements(
1047 1072 target=pull_request.target_repo, source=pull_request.source_repo)
1048 1073 if not merge_possible:
1049 1074 return merge_possible, msg
1050 1075
1051 1076 try:
1052 1077 resp = self._try_merge(pull_request)
1053 1078 log.debug("Merge response: %s", resp)
1054 1079 status = resp.possible, self.merge_status_message(
1055 1080 resp.failure_reason)
1056 1081 except NotImplementedError:
1057 1082 status = False, _('Pull request merging is not supported.')
1058 1083
1059 1084 return status
1060 1085
1061 1086 def _check_repo_requirements(self, target, source):
1062 1087 """
1063 1088 Check if `target` and `source` have compatible requirements.
1064 1089
1065 1090 Currently this is just checking for largefiles.
1066 1091 """
1067 1092 target_has_largefiles = self._has_largefiles(target)
1068 1093 source_has_largefiles = self._has_largefiles(source)
1069 1094 merge_possible = True
1070 1095 message = u''
1071 1096
1072 1097 if target_has_largefiles != source_has_largefiles:
1073 1098 merge_possible = False
1074 1099 if source_has_largefiles:
1075 1100 message = _(
1076 1101 'Target repository large files support is disabled.')
1077 1102 else:
1078 1103 message = _(
1079 1104 'Source repository large files support is disabled.')
1080 1105
1081 1106 return merge_possible, message
1082 1107
1083 1108 def _has_largefiles(self, repo):
1084 1109 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1085 1110 'extensions', 'largefiles')
1086 1111 return largefiles_ui and largefiles_ui[0].active
1087 1112
1088 1113 def _try_merge(self, pull_request):
1089 1114 """
1090 1115 Try to merge the pull request and return the merge status.
1091 1116 """
1092 1117 log.debug(
1093 1118 "Trying out if the pull request %s can be merged.",
1094 1119 pull_request.pull_request_id)
1095 1120 target_vcs = pull_request.target_repo.scm_instance()
1096 1121
1097 1122 # Refresh the target reference.
1098 1123 try:
1099 1124 target_ref = self._refresh_reference(
1100 1125 pull_request.target_ref_parts, target_vcs)
1101 1126 except CommitDoesNotExistError:
1102 1127 merge_state = MergeResponse(
1103 1128 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1104 1129 return merge_state
1105 1130
1106 1131 target_locked = pull_request.target_repo.locked
1107 1132 if target_locked and target_locked[0]:
1108 1133 log.debug("The target repository is locked.")
1109 1134 merge_state = MergeResponse(
1110 1135 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1111 1136 elif self._needs_merge_state_refresh(pull_request, target_ref):
1112 1137 log.debug("Refreshing the merge status of the repository.")
1113 1138 merge_state = self._refresh_merge_state(
1114 1139 pull_request, target_vcs, target_ref)
1115 1140 else:
1116 1141 possible = pull_request.\
1117 1142 _last_merge_status == MergeFailureReason.NONE
1118 1143 merge_state = MergeResponse(
1119 1144 possible, False, None, pull_request._last_merge_status)
1120 1145
1121 1146 return merge_state
1122 1147
1123 1148 def _refresh_reference(self, reference, vcs_repository):
1124 1149 if reference.type in ('branch', 'book'):
1125 1150 name_or_id = reference.name
1126 1151 else:
1127 1152 name_or_id = reference.commit_id
1128 1153 refreshed_commit = vcs_repository.get_commit(name_or_id)
1129 1154 refreshed_reference = Reference(
1130 1155 reference.type, reference.name, refreshed_commit.raw_id)
1131 1156 return refreshed_reference
1132 1157
1133 1158 def _needs_merge_state_refresh(self, pull_request, target_reference):
1134 1159 return not(
1135 1160 pull_request.revisions and
1136 1161 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1137 1162 target_reference.commit_id == pull_request._last_merge_target_rev)
1138 1163
1139 1164 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1140 1165 workspace_id = self._workspace_id(pull_request)
1141 1166 source_vcs = pull_request.source_repo.scm_instance()
1142 1167 use_rebase = self._use_rebase_for_merging(pull_request)
1143 1168 merge_state = target_vcs.merge(
1144 1169 target_reference, source_vcs, pull_request.source_ref_parts,
1145 1170 workspace_id, dry_run=True, use_rebase=use_rebase)
1146 1171
1147 1172 # Do not store the response if there was an unknown error.
1148 1173 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1149 1174 pull_request._last_merge_source_rev = \
1150 1175 pull_request.source_ref_parts.commit_id
1151 1176 pull_request._last_merge_target_rev = target_reference.commit_id
1152 1177 pull_request._last_merge_status = merge_state.failure_reason
1153 1178 pull_request.shadow_merge_ref = merge_state.merge_ref
1154 1179 Session().add(pull_request)
1155 1180 Session().commit()
1156 1181
1157 1182 return merge_state
1158 1183
1159 1184 def _workspace_id(self, pull_request):
1160 1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1161 1186 return workspace_id
1162 1187
1163 1188 def merge_status_message(self, status_code):
1164 1189 """
1165 1190 Return a human friendly error message for the given merge status code.
1166 1191 """
1167 1192 return self.MERGE_STATUS_MESSAGES[status_code]
1168 1193
1169 1194 def generate_repo_data(self, repo, commit_id=None, branch=None,
1170 1195 bookmark=None):
1171 1196 all_refs, selected_ref = \
1172 1197 self._get_repo_pullrequest_sources(
1173 1198 repo.scm_instance(), commit_id=commit_id,
1174 1199 branch=branch, bookmark=bookmark)
1175 1200
1176 1201 refs_select2 = []
1177 1202 for element in all_refs:
1178 1203 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1179 1204 refs_select2.append({'text': element[1], 'children': children})
1180 1205
1181 1206 return {
1182 1207 'user': {
1183 1208 'user_id': repo.user.user_id,
1184 1209 'username': repo.user.username,
1185 1210 'firstname': repo.user.firstname,
1186 1211 'lastname': repo.user.lastname,
1187 1212 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1188 1213 },
1189 1214 'description': h.chop_at_smart(repo.description, '\n'),
1190 1215 'refs': {
1191 1216 'all_refs': all_refs,
1192 1217 'selected_ref': selected_ref,
1193 1218 'select2_refs': refs_select2
1194 1219 }
1195 1220 }
1196 1221
1197 1222 def generate_pullrequest_title(self, source, source_ref, target):
1198 1223 return u'{source}#{at_ref} to {target}'.format(
1199 1224 source=source,
1200 1225 at_ref=source_ref,
1201 1226 target=target,
1202 1227 )
1203 1228
1204 1229 def _cleanup_merge_workspace(self, pull_request):
1205 1230 # Merging related cleanup
1206 1231 target_scm = pull_request.target_repo.scm_instance()
1207 1232 workspace_id = 'pr-%s' % pull_request.pull_request_id
1208 1233
1209 1234 try:
1210 1235 target_scm.cleanup_merge_workspace(workspace_id)
1211 1236 except NotImplementedError:
1212 1237 pass
1213 1238
1214 1239 def _get_repo_pullrequest_sources(
1215 1240 self, repo, commit_id=None, branch=None, bookmark=None):
1216 1241 """
1217 1242 Return a structure with repo's interesting commits, suitable for
1218 1243 the selectors in pullrequest controller
1219 1244
1220 1245 :param commit_id: a commit that must be in the list somehow
1221 1246 and selected by default
1222 1247 :param branch: a branch that must be in the list and selected
1223 1248 by default - even if closed
1224 1249 :param bookmark: a bookmark that must be in the list and selected
1225 1250 """
1226 1251
1227 1252 commit_id = safe_str(commit_id) if commit_id else None
1228 1253 branch = safe_str(branch) if branch else None
1229 1254 bookmark = safe_str(bookmark) if bookmark else None
1230 1255
1231 1256 selected = None
1232 1257
1233 1258 # order matters: first source that has commit_id in it will be selected
1234 1259 sources = []
1235 1260 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1236 1261 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1237 1262
1238 1263 if commit_id:
1239 1264 ref_commit = (h.short_id(commit_id), commit_id)
1240 1265 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1241 1266
1242 1267 sources.append(
1243 1268 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1244 1269 )
1245 1270
1246 1271 groups = []
1247 1272 for group_key, ref_list, group_name, match in sources:
1248 1273 group_refs = []
1249 1274 for ref_name, ref_id in ref_list:
1250 1275 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1251 1276 group_refs.append((ref_key, ref_name))
1252 1277
1253 1278 if not selected:
1254 1279 if set([commit_id, match]) & set([ref_id, ref_name]):
1255 1280 selected = ref_key
1256 1281
1257 1282 if group_refs:
1258 1283 groups.append((group_refs, group_name))
1259 1284
1260 1285 if not selected:
1261 1286 ref = commit_id or branch or bookmark
1262 1287 if ref:
1263 1288 raise CommitDoesNotExistError(
1264 1289 'No commit refs could be found matching: %s' % ref)
1265 1290 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1266 1291 selected = 'branch:%s:%s' % (
1267 1292 repo.DEFAULT_BRANCH_NAME,
1268 1293 repo.branches[repo.DEFAULT_BRANCH_NAME]
1269 1294 )
1270 1295 elif repo.commit_ids:
1271 1296 rev = repo.commit_ids[0]
1272 1297 selected = 'rev:%s:%s' % (rev, rev)
1273 1298 else:
1274 1299 raise EmptyRepositoryError()
1275 1300 return groups, selected
1276 1301
1277 1302 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1278 1303 return self._get_diff_from_pr_or_version(
1279 1304 source_repo, source_ref_id, target_ref_id, context=context)
1280 1305
1281 1306 def _get_diff_from_pr_or_version(
1282 1307 self, source_repo, source_ref_id, target_ref_id, context):
1283 1308 target_commit = source_repo.get_commit(
1284 1309 commit_id=safe_str(target_ref_id))
1285 1310 source_commit = source_repo.get_commit(
1286 1311 commit_id=safe_str(source_ref_id))
1287 1312 if isinstance(source_repo, Repository):
1288 1313 vcs_repo = source_repo.scm_instance()
1289 1314 else:
1290 1315 vcs_repo = source_repo
1291 1316
1292 1317 # TODO: johbo: In the context of an update, we cannot reach
1293 1318 # the old commit anymore with our normal mechanisms. It needs
1294 1319 # some sort of special support in the vcs layer to avoid this
1295 1320 # workaround.
1296 1321 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1297 1322 vcs_repo.alias == 'git'):
1298 1323 source_commit.raw_id = safe_str(source_ref_id)
1299 1324
1300 1325 log.debug('calculating diff between '
1301 1326 'source_ref:%s and target_ref:%s for repo `%s`',
1302 1327 target_ref_id, source_ref_id,
1303 1328 safe_unicode(vcs_repo.path))
1304 1329
1305 1330 vcs_diff = vcs_repo.get_diff(
1306 1331 commit1=target_commit, commit2=source_commit, context=context)
1307 1332 return vcs_diff
1308 1333
1309 1334 def _is_merge_enabled(self, pull_request):
1310 1335 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1311 1336 settings = settings_model.get_general_settings()
1312 1337 return settings.get('rhodecode_pr_merge_enabled', False)
1313 1338
1314 1339 def _use_rebase_for_merging(self, pull_request):
1315 1340 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1316 1341 settings = settings_model.get_general_settings()
1317 1342 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1318 1343
1319 1344 def _log_action(self, action, user, pull_request):
1320 1345 action_logger(
1321 1346 user,
1322 1347 '{action}:{pr_id}'.format(
1323 1348 action=action, pr_id=pull_request.pull_request_id),
1324 1349 pull_request.target_repo)
1325 1350
1326 1351
1327 1352 class MergeCheck(object):
1328 1353 """
1329 1354 Perform Merge Checks and returns a check object which stores information
1330 1355 about merge errors, and merge conditions
1331 1356 """
1332 1357 TODO_CHECK = 'todo'
1333 1358 PERM_CHECK = 'perm'
1334 1359 REVIEW_CHECK = 'review'
1335 1360 MERGE_CHECK = 'merge'
1336 1361
1337 1362 def __init__(self):
1338 1363 self.review_status = None
1339 1364 self.merge_possible = None
1340 1365 self.merge_msg = ''
1341 1366 self.failed = None
1342 1367 self.errors = []
1343 1368 self.error_details = OrderedDict()
1344 1369
1345 1370 def push_error(self, error_type, message, error_key, details):
1346 1371 self.failed = True
1347 1372 self.errors.append([error_type, message])
1348 1373 self.error_details[error_key] = dict(
1349 1374 details=details,
1350 1375 error_type=error_type,
1351 1376 message=message
1352 1377 )
1353 1378
1354 1379 @classmethod
1355 1380 def validate(cls, pull_request, user, fail_early=False, translator=None):
1356 1381 # if migrated to pyramid...
1357 1382 # _ = lambda: translator or _ # use passed in translator if any
1358 1383
1359 1384 merge_check = cls()
1360 1385
1361 1386 # permissions to merge
1362 1387 user_allowed_to_merge = PullRequestModel().check_user_merge(
1363 1388 pull_request, user)
1364 1389 if not user_allowed_to_merge:
1365 1390 log.debug("MergeCheck: cannot merge, approval is pending.")
1366 1391
1367 1392 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1368 1393 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1369 1394 if fail_early:
1370 1395 return merge_check
1371 1396
1372 1397 # review status, must be always present
1373 1398 review_status = pull_request.calculated_review_status()
1374 1399 merge_check.review_status = review_status
1375 1400
1376 1401 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1377 1402 if not status_approved:
1378 1403 log.debug("MergeCheck: cannot merge, approval is pending.")
1379 1404
1380 1405 msg = _('Pull request reviewer approval is pending.')
1381 1406
1382 1407 merge_check.push_error(
1383 1408 'warning', msg, cls.REVIEW_CHECK, review_status)
1384 1409
1385 1410 if fail_early:
1386 1411 return merge_check
1387 1412
1388 1413 # left over TODOs
1389 1414 todos = CommentsModel().get_unresolved_todos(pull_request)
1390 1415 if todos:
1391 1416 log.debug("MergeCheck: cannot merge, {} "
1392 1417 "unresolved todos left.".format(len(todos)))
1393 1418
1394 1419 if len(todos) == 1:
1395 1420 msg = _('Cannot merge, {} TODO still not resolved.').format(
1396 1421 len(todos))
1397 1422 else:
1398 1423 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1399 1424 len(todos))
1400 1425
1401 1426 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1402 1427
1403 1428 if fail_early:
1404 1429 return merge_check
1405 1430
1406 1431 # merge possible
1407 1432 merge_status, msg = PullRequestModel().merge_status(pull_request)
1408 1433 merge_check.merge_possible = merge_status
1409 1434 merge_check.merge_msg = msg
1410 1435 if not merge_status:
1411 1436 log.debug(
1412 1437 "MergeCheck: cannot merge, pull request merge not possible.")
1413 1438 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1414 1439
1415 1440 if fail_early:
1416 1441 return merge_check
1417 1442
1418 1443 return merge_check
1419 1444
1420 1445
1421 1446 ChangeTuple = namedtuple('ChangeTuple',
1422 1447 ['added', 'common', 'removed', 'total'])
1423 1448
1424 1449 FileChangeTuple = namedtuple('FileChangeTuple',
1425 1450 ['added', 'modified', 'removed'])
@@ -1,826 +1,827 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Origin')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 <div class="pr-pullinfo">
93 93 %if h.is_hg(c.pull_request.source_repo):
94 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 95 %elif h.is_git(c.pull_request.source_repo):
96 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 97 %endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label-summary">
103 103 <label>${_('Target')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 <div class="pr-targetinfo">
107 107 ## branch link is only valid if it is a branch
108 108 <span class="tag">
109 109 %if c.pull_request.target_ref_parts.type == 'branch':
110 110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 111 %else:
112 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 113 %endif
114 114 </span>
115 115 <span class="clone-url">
116 116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 117 </span>
118 118 </div>
119 119 </div>
120 120 </div>
121 121
122 122 ## Link to the shadow repository.
123 123 <div class="field">
124 124 <div class="label-summary">
125 125 <label>${_('Merge')}:</label>
126 126 </div>
127 127 <div class="input">
128 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 129 <div class="pr-mergeinfo">
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 134 %endif
135 135 </div>
136 136 % else:
137 137 <div class="">
138 138 ${_('Shadow repository data not available')}.
139 139 </div>
140 140 % endif
141 141 </div>
142 142 </div>
143 143
144 144 <div class="field">
145 145 <div class="label-summary">
146 146 <label>${_('Review')}:</label>
147 147 </div>
148 148 <div class="input">
149 149 %if c.pull_request_review_status:
150 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 151 <span class="changeset-status-lbl tooltip">
152 152 %if c.pull_request.is_closed():
153 153 ${_('Closed')},
154 154 %endif
155 155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 156 </span>
157 157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 158 %endif
159 159 </div>
160 160 </div>
161 161 <div class="field">
162 162 <div class="pr-description-label label-summary">
163 163 <label>${_('Description')}:</label>
164 164 </div>
165 165 <div id="pr-desc" class="input">
166 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 167 </div>
168 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 170 </div>
171 171 </div>
172 172
173 173 <div class="field">
174 174 <div class="label-summary">
175 175 <label>${_('Versions')}:</label>
176 176 </div>
177 177
178 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180 180
181 181 <div class="pr-versions">
182 182 % if c.show_version_changes:
183 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 189 </a>
190 190 <table>
191 191 ## SHOW ALL VERSIONS OF PR
192 192 <% ver_pr = None %>
193 193
194 194 % for data in reversed(list(enumerate(c.versions, 1))):
195 195 <% ver_pos = data[0] %>
196 196 <% ver = data[1] %>
197 197 <% ver_pr = ver.pull_request_version_id %>
198 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199 199
200 200 <tr class="version-pr" style="display: ${display_row}">
201 201 <td>
202 202 <code>
203 203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 204 </code>
205 205 </td>
206 206 <td>
207 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 209 </td>
210 210 <td>
211 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 213 </div>
214 214 </td>
215 215 <td>
216 216 % if c.at_version_num != ver_pr:
217 217 <i class="icon-comment"></i>
218 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 220 </code>
221 221 % endif
222 222 </td>
223 223 <td>
224 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 225 </td>
226 226 <td>
227 227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 228 </td>
229 229 </tr>
230 230 % endfor
231 231
232 232 <tr>
233 233 <td colspan="6">
234 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 235 data-label-text-locked="${_('select versions to show changes')}"
236 236 data-label-text-diff="${_('show changes between versions')}"
237 237 data-label-text-show="${_('show pull request for this version')}"
238 238 >
239 239 ${_('select versions to show changes')}
240 240 </button>
241 241 </td>
242 242 </tr>
243 243
244 244 ## show comment/inline comments summary
245 245 <%def name="comments_summary()">
246 246 <tr>
247 247 <td colspan="6" class="comments-summary-td">
248 248
249 249 % if c.at_version:
250 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 252 ${_('Comments at this version')}:
253 253 % else:
254 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 256 ${_('Comments for this pull request')}:
257 257 % endif
258 258
259 259
260 260 %if general_comm_count_ver:
261 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 262 %else:
263 263 ${_("%d General ") % general_comm_count_ver}
264 264 %endif
265 265
266 266 %if inline_comm_count_ver:
267 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 268 %else:
269 269 , ${_("%d Inline") % inline_comm_count_ver}
270 270 %endif
271 271
272 272 %if outdated_comm_count_ver:
273 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 276 %else:
277 277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 278 %endif
279 279 </td>
280 280 </tr>
281 281 </%def>
282 282 ${comments_summary()}
283 283 </table>
284 284 % else:
285 285 <div class="input">
286 286 ${_('Pull request versions not available')}.
287 287 </div>
288 288 <div>
289 289 <table>
290 290 ${comments_summary()}
291 291 </table>
292 292 </div>
293 293 % endif
294 294 </div>
295 295 </div>
296 296
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
304 304 </div>
305 305 <div>
306 306 ## AUTHOR
307 307 <div class="reviewers-title block-right">
308 308 <div class="pr-details-title">
309 309 ${_('Author')}
310 310 </div>
311 311 </div>
312 312 <div class="block-right pr-details-content reviewers">
313 313 <ul class="group_members">
314 314 <li>
315 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 316 </li>
317 317 </ul>
318 318 </div>
319 319 ## REVIEWERS
320 320 <div class="reviewers-title block-right">
321 321 <div class="pr-details-title">
322 322 ${_('Pull request reviewers')}
323 323 %if c.allowed_to_update:
324 324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
325 325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
326 326 %endif
327 327 </div>
328 328 </div>
329 329 <div id="reviewers" class="block-right pr-details-content reviewers">
330 330 ## members goes here !
331 331 <input type="hidden" name="__start__" value="review_members:sequence">
332 332 <ul id="review_members" class="group_members">
333 333 %for member,reasons,status in c.pull_request_reviewers:
334 334 <li id="reviewer_${member.user_id}">
335 335 <div class="reviewers_member">
336 336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
337 337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
338 338 </div>
339 339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
340 340 ${self.gravatar_with_user(member.email, 16)}
341 341 </div>
342 342 <input type="hidden" name="__start__" value="reviewer:mapping">
343 343 <input type="hidden" name="__start__" value="reasons:sequence">
344 344 %for reason in reasons:
345 345 <div class="reviewer_reason">- ${reason}</div>
346 346 <input type="hidden" name="reason" value="${reason}">
347 347
348 348 %endfor
349 349 <input type="hidden" name="__end__" value="reasons:sequence">
350 350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
351 351 <input type="hidden" name="__end__" value="reviewer:mapping">
352 352 %if c.allowed_to_update:
353 353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
354 354 <i class="icon-remove-sign" ></i>
355 355 </div>
356 356 %endif
357 357 </div>
358 358 </li>
359 359 %endfor
360 360 </ul>
361 361 <input type="hidden" name="__end__" value="review_members:sequence">
362 362 %if not c.pull_request.is_closed():
363 363 <div id="add_reviewer_input" class='ac' style="display: none;">
364 364 %if c.allowed_to_update:
365 365 <div class="reviewer_ac">
366 366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
367 367 <div id="reviewers_container"></div>
368 368 </div>
369 369 <div>
370 370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
371 371 </div>
372 372 %endif
373 373 </div>
374 374 %endif
375 375 </div>
376 376 </div>
377 377 </div>
378 378 <div class="box">
379 379 ##DIFF
380 380 <div class="table" >
381 381 <div id="changeset_compare_view_content">
382 382 ##CS
383 383 % if c.missing_requirements:
384 384 <div class="box">
385 385 <div class="alert alert-warning">
386 386 <div>
387 387 <strong>${_('Missing requirements:')}</strong>
388 388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
389 389 </div>
390 390 </div>
391 391 </div>
392 392 % elif c.missing_commits:
393 393 <div class="box">
394 394 <div class="alert alert-warning">
395 395 <div>
396 396 <strong>${_('Missing commits')}:</strong>
397 397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
398 398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
399 399 </div>
400 400 </div>
401 401 </div>
402 402 % endif
403 403
404 404 <div class="compare_view_commits_title">
405 405 % if not c.compare_mode:
406 406
407 407 % if c.at_version_pos:
408 408 <h4>
409 409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
410 410 </h4>
411 411 % endif
412 412
413 413 <div class="pull-left">
414 414 <div class="btn-group">
415 415 <a
416 416 class="btn"
417 417 href="#"
418 418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
419 419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
420 420 </a>
421 421 <a
422 422 class="btn"
423 423 href="#"
424 424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
425 425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
426 426 </a>
427 427 </div>
428 428 </div>
429 429
430 430 <div class="pull-right">
431 431 % if c.allowed_to_update and not c.pull_request.is_closed():
432 432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
433 433 % else:
434 434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
435 435 % endif
436 436
437 437 </div>
438 438 % endif
439 439 </div>
440 440
441 441 % if not c.missing_commits:
442 442 % if c.compare_mode:
443 443 % if c.at_version:
444 444 <h4>
445 445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
446 446 </h4>
447 447
448 448 <div class="subtitle-compare">
449 449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
450 450 </div>
451 451
452 452 <div class="container">
453 453 <table class="rctable compare_view_commits">
454 454 <tr>
455 455 <th></th>
456 456 <th>${_('Time')}</th>
457 457 <th>${_('Author')}</th>
458 458 <th>${_('Commit')}</th>
459 459 <th></th>
460 460 <th>${_('Description')}</th>
461 461 </tr>
462 462
463 463 % for c_type, commit in c.commit_changes:
464 464 % if c_type in ['a', 'r']:
465 465 <%
466 466 if c_type == 'a':
467 467 cc_title = _('Commit added in displayed changes')
468 468 elif c_type == 'r':
469 469 cc_title = _('Commit removed in displayed changes')
470 470 else:
471 471 cc_title = ''
472 472 %>
473 473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
474 474 <td>
475 475 <div class="commit-change-indicator color-${c_type}-border">
476 476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
477 477 ${c_type.upper()}
478 478 </div>
479 479 </div>
480 480 </td>
481 481 <td class="td-time">
482 482 ${h.age_component(commit.date)}
483 483 </td>
484 484 <td class="td-user">
485 485 ${base.gravatar_with_user(commit.author, 16)}
486 486 </td>
487 487 <td class="td-hash">
488 488 <code>
489 489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
490 490 r${commit.revision}:${h.short_id(commit.raw_id)}
491 491 </a>
492 492 ${h.hidden('revisions', commit.raw_id)}
493 493 </code>
494 494 </td>
495 495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
496 496 <div class="show_more_col">
497 497 <i class="show_more"></i>
498 498 </div>
499 499 </td>
500 500 <td class="mid td-description">
501 501 <div class="log-container truncate-wrap">
502 502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
503 503 ${h.urlify_commit_message(commit.message, c.repo_name)}
504 504 </div>
505 505 </div>
506 506 </td>
507 507 </tr>
508 508 % endif
509 509 % endfor
510 510 </table>
511 511 </div>
512 512
513 513 <script>
514 514 $('.expand_commit').on('click',function(e){
515 515 var target_expand = $(this);
516 516 var cid = target_expand.data('commitId');
517 517
518 518 if (target_expand.hasClass('open')){
519 519 $('#c-'+cid).css({
520 520 'height': '1.5em',
521 521 'white-space': 'nowrap',
522 522 'text-overflow': 'ellipsis',
523 523 'overflow':'hidden'
524 524 });
525 525 target_expand.removeClass('open');
526 526 }
527 527 else {
528 528 $('#c-'+cid).css({
529 529 'height': 'auto',
530 530 'white-space': 'pre-line',
531 531 'text-overflow': 'initial',
532 532 'overflow':'visible'
533 533 });
534 534 target_expand.addClass('open');
535 535 }
536 536 });
537 537 </script>
538 538
539 539 % endif
540 540
541 541 % else:
542 542 <%include file="/compare/compare_commits.mako" />
543 543 % endif
544 544
545 545 <div class="cs_files">
546 546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
547 547 ${cbdiffs.render_diffset_menu()}
548 548 ${cbdiffs.render_diffset(
549 549 c.diffset, use_comments=True,
550 550 collapse_when_files_over=30,
551 551 disable_new_comments=not c.allowed_to_comment,
552 552 deleted_files_comments=c.deleted_files_comments)}
553 553 </div>
554 554 % else:
555 555 ## skipping commits we need to clear the view for missing commits
556 556 <div style="clear:both;"></div>
557 557 % endif
558 558
559 559 </div>
560 560 </div>
561 561
562 562 ## template for inline comment form
563 563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
564 564
565 565 ## render general comments
566 566
567 567 <div id="comment-tr-show">
568 568 <div class="comment">
569 569 % if general_outdated_comm_count_ver:
570 570 <div class="meta">
571 571 % if general_outdated_comm_count_ver == 1:
572 572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
573 573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
574 574 % else:
575 575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
576 576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
577 577 % endif
578 578 </div>
579 579 % endif
580 580 </div>
581 581 </div>
582 582
583 583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
584 584
585 585 % if not c.pull_request.is_closed():
586 586 ## merge status, and merge action
587 587 <div class="pull-request-merge">
588 588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
589 589 </div>
590 590
591 591 ## main comment form and it status
592 592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
593 593 pull_request_id=c.pull_request.pull_request_id),
594 594 c.pull_request_review_status,
595 595 is_pull_request=True, change_status=c.allowed_to_change_status)}
596 596 %endif
597 597
598 598 <script type="text/javascript">
599 599 if (location.hash) {
600 600 var result = splitDelimitedHash(location.hash);
601 601 var line = $('html').find(result.loc);
602 602 // show hidden comments if we use location.hash
603 603 if (line.hasClass('comment-general')) {
604 604 $(line).show();
605 605 } else if (line.hasClass('comment-inline')) {
606 606 $(line).show();
607 607 var $cb = $(line).closest('.cb');
608 608 $cb.removeClass('cb-collapsed')
609 609 }
610 610 if (line.length > 0){
611 611 offsetScroll(line, 70);
612 612 }
613 613 }
614 614
615 615 versionController = new VersionController();
616 616 versionController.init();
617 617
618 618
619 619 $(function(){
620 620 ReviewerAutoComplete('user');
621 621 // custom code mirror
622 622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
623 623
624 624 var PRDetails = {
625 625 editButton: $('#open_edit_pullrequest'),
626 626 closeButton: $('#close_edit_pullrequest'),
627 627 deleteButton: $('#delete_pullrequest'),
628 628 viewFields: $('#pr-desc, #pr-title'),
629 629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
630 630
631 631 init: function() {
632 632 var that = this;
633 633 this.editButton.on('click', function(e) { that.edit(); });
634 634 this.closeButton.on('click', function(e) { that.view(); });
635 635 },
636 636
637 637 edit: function(event) {
638 638 this.viewFields.hide();
639 639 this.editButton.hide();
640 640 this.deleteButton.hide();
641 641 this.closeButton.show();
642 642 this.editFields.show();
643 643 codeMirrorInstance.refresh();
644 644 },
645 645
646 646 view: function(event) {
647 647 this.editButton.show();
648 648 this.deleteButton.show();
649 649 this.editFields.hide();
650 650 this.closeButton.hide();
651 651 this.viewFields.show();
652 652 }
653 653 };
654 654
655 655 var ReviewersPanel = {
656 656 editButton: $('#open_edit_reviewers'),
657 657 closeButton: $('#close_edit_reviewers'),
658 658 addButton: $('#add_reviewer_input'),
659 659 removeButtons: $('.reviewer_member_remove'),
660 660
661 661 init: function() {
662 662 var that = this;
663 663 this.editButton.on('click', function(e) { that.edit(); });
664 664 this.closeButton.on('click', function(e) { that.close(); });
665 665 },
666 666
667 667 edit: function(event) {
668 668 this.editButton.hide();
669 669 this.closeButton.show();
670 670 this.addButton.show();
671 671 this.removeButtons.css('visibility', 'visible');
672 672 },
673 673
674 674 close: function(event) {
675 675 this.editButton.show();
676 676 this.closeButton.hide();
677 677 this.addButton.hide();
678 678 this.removeButtons.css('visibility', 'hidden');
679 679 }
680 680 };
681 681
682 682 PRDetails.init();
683 683 ReviewersPanel.init();
684 684
685 685 showOutdated = function(self){
686 686 $('.comment-inline.comment-outdated').show();
687 687 $('.filediff-outdated').show();
688 688 $('.showOutdatedComments').hide();
689 689 $('.hideOutdatedComments').show();
690 690 };
691 691
692 692 hideOutdated = function(self){
693 693 $('.comment-inline.comment-outdated').hide();
694 694 $('.filediff-outdated').hide();
695 695 $('.hideOutdatedComments').hide();
696 696 $('.showOutdatedComments').show();
697 697 };
698 698
699 699 refreshMergeChecks = function(){
700 700 var loadUrl = "${h.url.current(merge_checks=1)}";
701 701 $('.pull-request-merge').css('opacity', 0.3);
702 702 $('.action-buttons-extra').css('opacity', 0.3);
703 703
704 704 $('.pull-request-merge').load(
705 705 loadUrl, function() {
706 706 $('.pull-request-merge').css('opacity', 1);
707 707
708 708 $('.action-buttons-extra').css('opacity', 1);
709 709 injectCloseAction();
710 710 }
711 711 );
712 712 };
713 713
714 714 injectCloseAction = function() {
715 715 var closeAction = $('#close-pull-request-action').html();
716 716 var $actionButtons = $('.action-buttons-extra');
717 717 // clear the action before
718 718 $actionButtons.html("");
719 719 $actionButtons.html(closeAction);
720 720 };
721 721
722 722 closePullRequest = function (status) {
723 723 // inject closing flag
724 724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
725 725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
726 726 $(generalCommentForm.submitForm).submit();
727 727 };
728 728
729 729 $('#show-outdated-comments').on('click', function(e){
730 730 var button = $(this);
731 731 var outdated = $('.comment-outdated');
732 732
733 733 if (button.html() === "(Show)") {
734 734 button.html("(Hide)");
735 735 outdated.show();
736 736 } else {
737 737 button.html("(Show)");
738 738 outdated.hide();
739 739 }
740 740 });
741 741
742 742 $('.show-inline-comments').on('change', function(e){
743 743 var show = 'none';
744 744 var target = e.currentTarget;
745 745 if(target.checked){
746 746 show = ''
747 747 }
748 748 var boxid = $(target).attr('id_for');
749 749 var comments = $('#{0} .inline-comments'.format(boxid));
750 750 var fn_display = function(idx){
751 751 $(this).css('display', show);
752 752 };
753 753 $(comments).each(fn_display);
754 754 var btns = $('#{0} .inline-comments-button'.format(boxid));
755 755 $(btns).each(fn_display);
756 756 });
757 757
758 758 $('#merge_pull_request_form').submit(function() {
759 759 if (!$('#merge_pull_request').attr('disabled')) {
760 760 $('#merge_pull_request').attr('disabled', 'disabled');
761 761 }
762 762 return true;
763 763 });
764 764
765 765 $('#edit_pull_request').on('click', function(e){
766 766 var title = $('#pr-title-input').val();
767 767 var description = codeMirrorInstance.getValue();
768 768 editPullRequest(
769 769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
770 770 title, description);
771 771 });
772 772
773 773 $('#update_pull_request').on('click', function(e){
774 774 $(this).attr('disabled', 'disabled');
775 775 $(this).addClass('disabled');
776 $(this).html(_gettext('saving...'));
776 $(this).html(_gettext('Saving...'));
777 777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
778 778 });
779 779
780 780 $('#update_commits').on('click', function(e){
781 781 var isDisabled = !$(e.currentTarget).attr('disabled');
782 $(e.currentTarget).attr('disabled', 'disabled');
783 $(e.currentTarget).addClass('disabled');
784 $(e.currentTarget).removeClass('btn-primary');
782 785 $(e.currentTarget).text(_gettext('Updating...'));
783 $(e.currentTarget).attr('disabled', 'disabled');
784 786 if(isDisabled){
785 787 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
786 788 }
787
788 789 });
789 790 // fixing issue with caches on firefox
790 791 $('#update_commits').removeAttr("disabled");
791 792
792 793 $('#close_pull_request').on('click', function(e){
793 794 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
794 795 });
795 796
796 797 $('.show-inline-comments').on('click', function(e){
797 798 var boxid = $(this).attr('data-comment-id');
798 799 var button = $(this);
799 800
800 801 if(button.hasClass("comments-visible")) {
801 802 $('#{0} .inline-comments'.format(boxid)).each(function(index){
802 803 $(this).hide();
803 804 });
804 805 button.removeClass("comments-visible");
805 806 } else {
806 807 $('#{0} .inline-comments'.format(boxid)).each(function(index){
807 808 $(this).show();
808 809 });
809 810 button.addClass("comments-visible");
810 811 }
811 812 });
812 813
813 814 // register submit callback on commentForm form to track TODOs
814 815 window.commentFormGlobalSubmitSuccessCallback = function(){
815 816 refreshMergeChecks();
816 817 };
817 818 // initial injection
818 819 injectCloseAction();
819 820
820 821 })
821 822 </script>
822 823
823 824 </div>
824 825 </div>
825 826
826 827 </%def>
General Comments 0
You need to be logged in to leave comments. Login now