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