##// END OF EJS Templates
pull-request: lock button when updating reviewers to forbid multi-submit....
marcink -
r1578:3793854d default
parent child Browse files
Show More
@@ -1,1423 +1,1425 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 587 if not self.has_valid_update_type(pull_request):
588 588 log.debug(
589 589 "Skipping update of pull request %s due to ref type: %s",
590 590 pull_request, source_ref_type)
591 591 return UpdateResponse(
592 592 executed=False,
593 593 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 594 old=pull_request, new=None, changes=None)
595 595
596 596 source_repo = pull_request.source_repo.scm_instance()
597 597 try:
598 598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 599 except CommitDoesNotExistError:
600 600 return UpdateResponse(
601 601 executed=False,
602 602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 603 old=pull_request, new=None, changes=None)
604 604
605 605 if source_ref_id == source_commit.raw_id:
606 606 log.debug("Nothing changed in pull request %s", pull_request)
607 607 return UpdateResponse(
608 608 executed=False,
609 609 reason=UpdateFailureReason.NO_CHANGE,
610 610 old=pull_request, new=None, changes=None)
611 611
612 612 # Finally there is a need for an update
613 613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 614 self._link_comments_to_version(pull_request_version)
615 615
616 616 target_ref_type = pull_request.target_ref_parts.type
617 617 target_ref_name = pull_request.target_ref_parts.name
618 618 target_ref_id = pull_request.target_ref_parts.commit_id
619 619 target_repo = pull_request.target_repo.scm_instance()
620 620
621 621 try:
622 622 if target_ref_type in ('tag', 'branch', 'book'):
623 623 target_commit = target_repo.get_commit(target_ref_name)
624 624 else:
625 625 target_commit = target_repo.get_commit(target_ref_id)
626 626 except CommitDoesNotExistError:
627 627 return UpdateResponse(
628 628 executed=False,
629 629 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 630 old=pull_request, new=None, changes=None)
631 631
632 632 # re-compute commit ids
633 633 old_commit_ids = pull_request.revisions
634 634 pre_load = ["author", "branch", "date", "message"]
635 635 commit_ranges = target_repo.compare(
636 636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 637 pre_load=pre_load)
638 638
639 639 ancestor = target_repo.get_common_ancestor(
640 640 target_commit.raw_id, source_commit.raw_id, source_repo)
641 641
642 642 pull_request.source_ref = '%s:%s:%s' % (
643 643 source_ref_type, source_ref_name, source_commit.raw_id)
644 644 pull_request.target_ref = '%s:%s:%s' % (
645 645 target_ref_type, target_ref_name, ancestor)
646 646 pull_request.revisions = [
647 647 commit.raw_id for commit in reversed(commit_ranges)]
648 648 pull_request.updated_on = datetime.datetime.now()
649 649 Session().add(pull_request)
650 650 new_commit_ids = pull_request.revisions
651 651
652 652 changes = self._calculate_commit_id_changes(
653 653 old_commit_ids, new_commit_ids)
654 654
655 655 old_diff_data, new_diff_data = self._generate_update_diffs(
656 656 pull_request, pull_request_version)
657 657
658 658 CommentsModel().outdate_comments(
659 659 pull_request, old_diff_data=old_diff_data,
660 660 new_diff_data=new_diff_data)
661 661
662 662 file_changes = self._calculate_file_changes(
663 663 old_diff_data, new_diff_data)
664 664
665 665 # Add an automatic comment to the pull request
666 666 update_comment = CommentsModel().create(
667 667 text=self._render_update_message(changes, file_changes),
668 668 repo=pull_request.target_repo,
669 669 user=pull_request.author,
670 670 pull_request=pull_request,
671 671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672 672
673 673 # Update status to "Under Review" for added commits
674 674 for commit_id in changes.added:
675 675 ChangesetStatusModel().set_status(
676 676 repo=pull_request.source_repo,
677 677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 678 comment=update_comment,
679 679 user=pull_request.author,
680 680 pull_request=pull_request,
681 681 revision=commit_id)
682 682
683 683 log.debug(
684 684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 685 'removed_ids: %s', pull_request.pull_request_id,
686 686 changes.added, changes.common, changes.removed)
687 687 log.debug('Updated pull request with the following file changes: %s',
688 688 file_changes)
689 689
690 690 log.info(
691 691 "Updated pull request %s from commit %s to commit %s, "
692 692 "stored new version %s of this pull request.",
693 693 pull_request.pull_request_id, source_ref_id,
694 694 pull_request.source_ref_parts.commit_id,
695 695 pull_request_version.pull_request_version_id)
696 696 Session().commit()
697 697 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 698 'update')
699 699
700 700 return UpdateResponse(
701 701 executed=True, reason=UpdateFailureReason.NONE,
702 702 old=pull_request, new=pull_request_version, changes=changes)
703 703
704 704 def _create_version_from_snapshot(self, pull_request):
705 705 version = PullRequestVersion()
706 706 version.title = pull_request.title
707 707 version.description = pull_request.description
708 708 version.status = pull_request.status
709 709 version.created_on = datetime.datetime.now()
710 710 version.updated_on = pull_request.updated_on
711 711 version.user_id = pull_request.user_id
712 712 version.source_repo = pull_request.source_repo
713 713 version.source_ref = pull_request.source_ref
714 714 version.target_repo = pull_request.target_repo
715 715 version.target_ref = pull_request.target_ref
716 716
717 717 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 718 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 719 version._last_merge_status = pull_request._last_merge_status
720 720 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 721 version.merge_rev = pull_request.merge_rev
722 722
723 723 version.revisions = pull_request.revisions
724 724 version.pull_request = pull_request
725 725 Session().add(version)
726 726 Session().flush()
727 727
728 728 return version
729 729
730 730 def _generate_update_diffs(self, pull_request, pull_request_version):
731 731
732 732 diff_context = (
733 733 self.DIFF_CONTEXT +
734 734 CommentsModel.needed_extra_diff_context())
735 735
736 736 source_repo = pull_request_version.source_repo
737 737 source_ref_id = pull_request_version.source_ref_parts.commit_id
738 738 target_ref_id = pull_request_version.target_ref_parts.commit_id
739 739 old_diff = self._get_diff_from_pr_or_version(
740 740 source_repo, source_ref_id, target_ref_id, context=diff_context)
741 741
742 742 source_repo = pull_request.source_repo
743 743 source_ref_id = pull_request.source_ref_parts.commit_id
744 744 target_ref_id = pull_request.target_ref_parts.commit_id
745 745
746 746 new_diff = self._get_diff_from_pr_or_version(
747 747 source_repo, source_ref_id, target_ref_id, context=diff_context)
748 748
749 749 old_diff_data = diffs.DiffProcessor(old_diff)
750 750 old_diff_data.prepare()
751 751 new_diff_data = diffs.DiffProcessor(new_diff)
752 752 new_diff_data.prepare()
753 753
754 754 return old_diff_data, new_diff_data
755 755
756 756 def _link_comments_to_version(self, pull_request_version):
757 757 """
758 758 Link all unlinked comments of this pull request to the given version.
759 759
760 760 :param pull_request_version: The `PullRequestVersion` to which
761 761 the comments shall be linked.
762 762
763 763 """
764 764 pull_request = pull_request_version.pull_request
765 765 comments = ChangesetComment.query().filter(
766 766 # TODO: johbo: Should we query for the repo at all here?
767 767 # Pending decision on how comments of PRs are to be related
768 768 # to either the source repo, the target repo or no repo at all.
769 769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
770 770 ChangesetComment.pull_request == pull_request,
771 771 ChangesetComment.pull_request_version == None)
772 772
773 773 # TODO: johbo: Find out why this breaks if it is done in a bulk
774 774 # operation.
775 775 for comment in comments:
776 776 comment.pull_request_version_id = (
777 777 pull_request_version.pull_request_version_id)
778 778 Session().add(comment)
779 779
780 780 def _calculate_commit_id_changes(self, old_ids, new_ids):
781 781 added = [x for x in new_ids if x not in old_ids]
782 782 common = [x for x in new_ids if x in old_ids]
783 783 removed = [x for x in old_ids if x not in new_ids]
784 784 total = new_ids
785 785 return ChangeTuple(added, common, removed, total)
786 786
787 787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
788 788
789 789 old_files = OrderedDict()
790 790 for diff_data in old_diff_data.parsed_diff:
791 791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
792 792
793 793 added_files = []
794 794 modified_files = []
795 795 removed_files = []
796 796 for diff_data in new_diff_data.parsed_diff:
797 797 new_filename = diff_data['filename']
798 798 new_hash = md5_safe(diff_data['raw_diff'])
799 799
800 800 old_hash = old_files.get(new_filename)
801 801 if not old_hash:
802 802 # file is not present in old diff, means it's added
803 803 added_files.append(new_filename)
804 804 else:
805 805 if new_hash != old_hash:
806 806 modified_files.append(new_filename)
807 807 # now remove a file from old, since we have seen it already
808 808 del old_files[new_filename]
809 809
810 810 # removed files is when there are present in old, but not in NEW,
811 811 # since we remove old files that are present in new diff, left-overs
812 812 # if any should be the removed files
813 813 removed_files.extend(old_files.keys())
814 814
815 815 return FileChangeTuple(added_files, modified_files, removed_files)
816 816
817 817 def _render_update_message(self, changes, file_changes):
818 818 """
819 819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
820 820 so it's always looking the same disregarding on which default
821 821 renderer system is using.
822 822
823 823 :param changes: changes named tuple
824 824 :param file_changes: file changes named tuple
825 825
826 826 """
827 827 new_status = ChangesetStatus.get_status_lbl(
828 828 ChangesetStatus.STATUS_UNDER_REVIEW)
829 829
830 830 changed_files = (
831 831 file_changes.added + file_changes.modified + file_changes.removed)
832 832
833 833 params = {
834 834 'under_review_label': new_status,
835 835 'added_commits': changes.added,
836 836 'removed_commits': changes.removed,
837 837 'changed_files': changed_files,
838 838 'added_files': file_changes.added,
839 839 'modified_files': file_changes.modified,
840 840 'removed_files': file_changes.removed,
841 841 }
842 842 renderer = RstTemplateRenderer()
843 843 return renderer.render('pull_request_update.mako', **params)
844 844
845 845 def edit(self, pull_request, title, description):
846 846 pull_request = self.__get_pull_request(pull_request)
847 847 if pull_request.is_closed():
848 848 raise ValueError('This pull request is closed')
849 849 if title:
850 850 pull_request.title = title
851 851 pull_request.description = description
852 852 pull_request.updated_on = datetime.datetime.now()
853 853 Session().add(pull_request)
854 854
855 855 def update_reviewers(self, pull_request, reviewer_data):
856 856 """
857 857 Update the reviewers in the pull request
858 858
859 859 :param pull_request: the pr to update
860 860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
861 861 """
862 862
863 863 reviewers_reasons = {}
864 864 for user_id, reasons in reviewer_data:
865 865 if isinstance(user_id, (int, basestring)):
866 866 user_id = self._get_user(user_id).user_id
867 867 reviewers_reasons[user_id] = reasons
868 868
869 869 reviewers_ids = set(reviewers_reasons.keys())
870 870 pull_request = self.__get_pull_request(pull_request)
871 871 current_reviewers = PullRequestReviewers.query()\
872 872 .filter(PullRequestReviewers.pull_request ==
873 873 pull_request).all()
874 874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
875 875
876 876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
877 877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
878 878
879 879 log.debug("Adding %s reviewers", ids_to_add)
880 880 log.debug("Removing %s reviewers", ids_to_remove)
881 881 changed = False
882 882 for uid in ids_to_add:
883 883 changed = True
884 884 _usr = self._get_user(uid)
885 885 reasons = reviewers_reasons[uid]
886 886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
887 887 Session().add(reviewer)
888 888
889 self.notify_reviewers(pull_request, ids_to_add)
890
891 889 for uid in ids_to_remove:
892 890 changed = True
893 reviewer = PullRequestReviewers.query()\
891 reviewers = PullRequestReviewers.query()\
894 892 .filter(PullRequestReviewers.user_id == uid,
895 893 PullRequestReviewers.pull_request == pull_request)\
896 .scalar()
897 if reviewer:
898 Session().delete(reviewer)
894 .all()
895 # use .all() in case we accidentally added the same person twice
896 # this CAN happen due to the lack of DB checks
897 for obj in reviewers:
898 Session().delete(obj)
899
899 900 if changed:
900 901 pull_request.updated_on = datetime.datetime.now()
901 902 Session().add(pull_request)
902 903
904 self.notify_reviewers(pull_request, ids_to_add)
903 905 return ids_to_add, ids_to_remove
904 906
905 907 def get_url(self, pull_request):
906 908 return h.url('pullrequest_show',
907 909 repo_name=safe_str(pull_request.target_repo.repo_name),
908 910 pull_request_id=pull_request.pull_request_id,
909 911 qualified=True)
910 912
911 913 def get_shadow_clone_url(self, pull_request):
912 914 """
913 915 Returns qualified url pointing to the shadow repository. If this pull
914 916 request is closed there is no shadow repository and ``None`` will be
915 917 returned.
916 918 """
917 919 if pull_request.is_closed():
918 920 return None
919 921 else:
920 922 pr_url = urllib.unquote(self.get_url(pull_request))
921 923 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
922 924
923 925 def notify_reviewers(self, pull_request, reviewers_ids):
924 926 # notification to reviewers
925 927 if not reviewers_ids:
926 928 return
927 929
928 930 pull_request_obj = pull_request
929 931 # get the current participants of this pull request
930 932 recipients = reviewers_ids
931 933 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
932 934
933 935 pr_source_repo = pull_request_obj.source_repo
934 936 pr_target_repo = pull_request_obj.target_repo
935 937
936 938 pr_url = h.url(
937 939 'pullrequest_show',
938 940 repo_name=pr_target_repo.repo_name,
939 941 pull_request_id=pull_request_obj.pull_request_id,
940 942 qualified=True,)
941 943
942 944 # set some variables for email notification
943 945 pr_target_repo_url = h.url(
944 946 'summary_home',
945 947 repo_name=pr_target_repo.repo_name,
946 948 qualified=True)
947 949
948 950 pr_source_repo_url = h.url(
949 951 'summary_home',
950 952 repo_name=pr_source_repo.repo_name,
951 953 qualified=True)
952 954
953 955 # pull request specifics
954 956 pull_request_commits = [
955 957 (x.raw_id, x.message)
956 958 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
957 959
958 960 kwargs = {
959 961 'user': pull_request.author,
960 962 'pull_request': pull_request_obj,
961 963 'pull_request_commits': pull_request_commits,
962 964
963 965 'pull_request_target_repo': pr_target_repo,
964 966 'pull_request_target_repo_url': pr_target_repo_url,
965 967
966 968 'pull_request_source_repo': pr_source_repo,
967 969 'pull_request_source_repo_url': pr_source_repo_url,
968 970
969 971 'pull_request_url': pr_url,
970 972 }
971 973
972 974 # pre-generate the subject for notification itself
973 975 (subject,
974 976 _h, _e, # we don't care about those
975 977 body_plaintext) = EmailNotificationModel().render_email(
976 978 notification_type, **kwargs)
977 979
978 980 # create notification objects, and emails
979 981 NotificationModel().create(
980 982 created_by=pull_request.author,
981 983 notification_subject=subject,
982 984 notification_body=body_plaintext,
983 985 notification_type=notification_type,
984 986 recipients=recipients,
985 987 email_kwargs=kwargs,
986 988 )
987 989
988 990 def delete(self, pull_request):
989 991 pull_request = self.__get_pull_request(pull_request)
990 992 self._cleanup_merge_workspace(pull_request)
991 993 Session().delete(pull_request)
992 994
993 995 def close_pull_request(self, pull_request, user):
994 996 pull_request = self.__get_pull_request(pull_request)
995 997 self._cleanup_merge_workspace(pull_request)
996 998 pull_request.status = PullRequest.STATUS_CLOSED
997 999 pull_request.updated_on = datetime.datetime.now()
998 1000 Session().add(pull_request)
999 1001 self._trigger_pull_request_hook(
1000 1002 pull_request, pull_request.author, 'close')
1001 1003 self._log_action('user_closed_pull_request', user, pull_request)
1002 1004
1003 1005 def close_pull_request_with_comment(self, pull_request, user, repo,
1004 1006 message=None):
1005 1007 status = ChangesetStatus.STATUS_REJECTED
1006 1008
1007 1009 if not message:
1008 1010 message = (
1009 1011 _('Status change %(transition_icon)s %(status)s') % {
1010 1012 'transition_icon': '>',
1011 1013 'status': ChangesetStatus.get_status_lbl(status)})
1012 1014
1013 1015 internal_message = _('Closing with') + ' ' + message
1014 1016
1015 1017 comm = CommentsModel().create(
1016 1018 text=internal_message,
1017 1019 repo=repo.repo_id,
1018 1020 user=user.user_id,
1019 1021 pull_request=pull_request.pull_request_id,
1020 1022 f_path=None,
1021 1023 line_no=None,
1022 1024 status_change=ChangesetStatus.get_status_lbl(status),
1023 1025 status_change_type=status,
1024 1026 closing_pr=True
1025 1027 )
1026 1028
1027 1029 ChangesetStatusModel().set_status(
1028 1030 repo.repo_id,
1029 1031 status,
1030 1032 user.user_id,
1031 1033 comm,
1032 1034 pull_request=pull_request.pull_request_id
1033 1035 )
1034 1036 Session().flush()
1035 1037
1036 1038 PullRequestModel().close_pull_request(
1037 1039 pull_request.pull_request_id, user)
1038 1040
1039 1041 def merge_status(self, pull_request):
1040 1042 if not self._is_merge_enabled(pull_request):
1041 1043 return False, _('Server-side pull request merging is disabled.')
1042 1044 if pull_request.is_closed():
1043 1045 return False, _('This pull request is closed.')
1044 1046 merge_possible, msg = self._check_repo_requirements(
1045 1047 target=pull_request.target_repo, source=pull_request.source_repo)
1046 1048 if not merge_possible:
1047 1049 return merge_possible, msg
1048 1050
1049 1051 try:
1050 1052 resp = self._try_merge(pull_request)
1051 1053 log.debug("Merge response: %s", resp)
1052 1054 status = resp.possible, self.merge_status_message(
1053 1055 resp.failure_reason)
1054 1056 except NotImplementedError:
1055 1057 status = False, _('Pull request merging is not supported.')
1056 1058
1057 1059 return status
1058 1060
1059 1061 def _check_repo_requirements(self, target, source):
1060 1062 """
1061 1063 Check if `target` and `source` have compatible requirements.
1062 1064
1063 1065 Currently this is just checking for largefiles.
1064 1066 """
1065 1067 target_has_largefiles = self._has_largefiles(target)
1066 1068 source_has_largefiles = self._has_largefiles(source)
1067 1069 merge_possible = True
1068 1070 message = u''
1069 1071
1070 1072 if target_has_largefiles != source_has_largefiles:
1071 1073 merge_possible = False
1072 1074 if source_has_largefiles:
1073 1075 message = _(
1074 1076 'Target repository large files support is disabled.')
1075 1077 else:
1076 1078 message = _(
1077 1079 'Source repository large files support is disabled.')
1078 1080
1079 1081 return merge_possible, message
1080 1082
1081 1083 def _has_largefiles(self, repo):
1082 1084 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1083 1085 'extensions', 'largefiles')
1084 1086 return largefiles_ui and largefiles_ui[0].active
1085 1087
1086 1088 def _try_merge(self, pull_request):
1087 1089 """
1088 1090 Try to merge the pull request and return the merge status.
1089 1091 """
1090 1092 log.debug(
1091 1093 "Trying out if the pull request %s can be merged.",
1092 1094 pull_request.pull_request_id)
1093 1095 target_vcs = pull_request.target_repo.scm_instance()
1094 1096
1095 1097 # Refresh the target reference.
1096 1098 try:
1097 1099 target_ref = self._refresh_reference(
1098 1100 pull_request.target_ref_parts, target_vcs)
1099 1101 except CommitDoesNotExistError:
1100 1102 merge_state = MergeResponse(
1101 1103 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1102 1104 return merge_state
1103 1105
1104 1106 target_locked = pull_request.target_repo.locked
1105 1107 if target_locked and target_locked[0]:
1106 1108 log.debug("The target repository is locked.")
1107 1109 merge_state = MergeResponse(
1108 1110 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1109 1111 elif self._needs_merge_state_refresh(pull_request, target_ref):
1110 1112 log.debug("Refreshing the merge status of the repository.")
1111 1113 merge_state = self._refresh_merge_state(
1112 1114 pull_request, target_vcs, target_ref)
1113 1115 else:
1114 1116 possible = pull_request.\
1115 1117 _last_merge_status == MergeFailureReason.NONE
1116 1118 merge_state = MergeResponse(
1117 1119 possible, False, None, pull_request._last_merge_status)
1118 1120
1119 1121 return merge_state
1120 1122
1121 1123 def _refresh_reference(self, reference, vcs_repository):
1122 1124 if reference.type in ('branch', 'book'):
1123 1125 name_or_id = reference.name
1124 1126 else:
1125 1127 name_or_id = reference.commit_id
1126 1128 refreshed_commit = vcs_repository.get_commit(name_or_id)
1127 1129 refreshed_reference = Reference(
1128 1130 reference.type, reference.name, refreshed_commit.raw_id)
1129 1131 return refreshed_reference
1130 1132
1131 1133 def _needs_merge_state_refresh(self, pull_request, target_reference):
1132 1134 return not(
1133 1135 pull_request.revisions and
1134 1136 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1135 1137 target_reference.commit_id == pull_request._last_merge_target_rev)
1136 1138
1137 1139 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1138 1140 workspace_id = self._workspace_id(pull_request)
1139 1141 source_vcs = pull_request.source_repo.scm_instance()
1140 1142 use_rebase = self._use_rebase_for_merging(pull_request)
1141 1143 merge_state = target_vcs.merge(
1142 1144 target_reference, source_vcs, pull_request.source_ref_parts,
1143 1145 workspace_id, dry_run=True, use_rebase=use_rebase)
1144 1146
1145 1147 # Do not store the response if there was an unknown error.
1146 1148 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1147 1149 pull_request._last_merge_source_rev = \
1148 1150 pull_request.source_ref_parts.commit_id
1149 1151 pull_request._last_merge_target_rev = target_reference.commit_id
1150 1152 pull_request._last_merge_status = merge_state.failure_reason
1151 1153 pull_request.shadow_merge_ref = merge_state.merge_ref
1152 1154 Session().add(pull_request)
1153 1155 Session().commit()
1154 1156
1155 1157 return merge_state
1156 1158
1157 1159 def _workspace_id(self, pull_request):
1158 1160 workspace_id = 'pr-%s' % pull_request.pull_request_id
1159 1161 return workspace_id
1160 1162
1161 1163 def merge_status_message(self, status_code):
1162 1164 """
1163 1165 Return a human friendly error message for the given merge status code.
1164 1166 """
1165 1167 return self.MERGE_STATUS_MESSAGES[status_code]
1166 1168
1167 1169 def generate_repo_data(self, repo, commit_id=None, branch=None,
1168 1170 bookmark=None):
1169 1171 all_refs, selected_ref = \
1170 1172 self._get_repo_pullrequest_sources(
1171 1173 repo.scm_instance(), commit_id=commit_id,
1172 1174 branch=branch, bookmark=bookmark)
1173 1175
1174 1176 refs_select2 = []
1175 1177 for element in all_refs:
1176 1178 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1177 1179 refs_select2.append({'text': element[1], 'children': children})
1178 1180
1179 1181 return {
1180 1182 'user': {
1181 1183 'user_id': repo.user.user_id,
1182 1184 'username': repo.user.username,
1183 1185 'firstname': repo.user.firstname,
1184 1186 'lastname': repo.user.lastname,
1185 1187 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1186 1188 },
1187 1189 'description': h.chop_at_smart(repo.description, '\n'),
1188 1190 'refs': {
1189 1191 'all_refs': all_refs,
1190 1192 'selected_ref': selected_ref,
1191 1193 'select2_refs': refs_select2
1192 1194 }
1193 1195 }
1194 1196
1195 1197 def generate_pullrequest_title(self, source, source_ref, target):
1196 1198 return u'{source}#{at_ref} to {target}'.format(
1197 1199 source=source,
1198 1200 at_ref=source_ref,
1199 1201 target=target,
1200 1202 )
1201 1203
1202 1204 def _cleanup_merge_workspace(self, pull_request):
1203 1205 # Merging related cleanup
1204 1206 target_scm = pull_request.target_repo.scm_instance()
1205 1207 workspace_id = 'pr-%s' % pull_request.pull_request_id
1206 1208
1207 1209 try:
1208 1210 target_scm.cleanup_merge_workspace(workspace_id)
1209 1211 except NotImplementedError:
1210 1212 pass
1211 1213
1212 1214 def _get_repo_pullrequest_sources(
1213 1215 self, repo, commit_id=None, branch=None, bookmark=None):
1214 1216 """
1215 1217 Return a structure with repo's interesting commits, suitable for
1216 1218 the selectors in pullrequest controller
1217 1219
1218 1220 :param commit_id: a commit that must be in the list somehow
1219 1221 and selected by default
1220 1222 :param branch: a branch that must be in the list and selected
1221 1223 by default - even if closed
1222 1224 :param bookmark: a bookmark that must be in the list and selected
1223 1225 """
1224 1226
1225 1227 commit_id = safe_str(commit_id) if commit_id else None
1226 1228 branch = safe_str(branch) if branch else None
1227 1229 bookmark = safe_str(bookmark) if bookmark else None
1228 1230
1229 1231 selected = None
1230 1232
1231 1233 # order matters: first source that has commit_id in it will be selected
1232 1234 sources = []
1233 1235 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1234 1236 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1235 1237
1236 1238 if commit_id:
1237 1239 ref_commit = (h.short_id(commit_id), commit_id)
1238 1240 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1239 1241
1240 1242 sources.append(
1241 1243 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1242 1244 )
1243 1245
1244 1246 groups = []
1245 1247 for group_key, ref_list, group_name, match in sources:
1246 1248 group_refs = []
1247 1249 for ref_name, ref_id in ref_list:
1248 1250 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1249 1251 group_refs.append((ref_key, ref_name))
1250 1252
1251 1253 if not selected:
1252 1254 if set([commit_id, match]) & set([ref_id, ref_name]):
1253 1255 selected = ref_key
1254 1256
1255 1257 if group_refs:
1256 1258 groups.append((group_refs, group_name))
1257 1259
1258 1260 if not selected:
1259 1261 ref = commit_id or branch or bookmark
1260 1262 if ref:
1261 1263 raise CommitDoesNotExistError(
1262 1264 'No commit refs could be found matching: %s' % ref)
1263 1265 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1264 1266 selected = 'branch:%s:%s' % (
1265 1267 repo.DEFAULT_BRANCH_NAME,
1266 1268 repo.branches[repo.DEFAULT_BRANCH_NAME]
1267 1269 )
1268 1270 elif repo.commit_ids:
1269 1271 rev = repo.commit_ids[0]
1270 1272 selected = 'rev:%s:%s' % (rev, rev)
1271 1273 else:
1272 1274 raise EmptyRepositoryError()
1273 1275 return groups, selected
1274 1276
1275 1277 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1276 1278 return self._get_diff_from_pr_or_version(
1277 1279 source_repo, source_ref_id, target_ref_id, context=context)
1278 1280
1279 1281 def _get_diff_from_pr_or_version(
1280 1282 self, source_repo, source_ref_id, target_ref_id, context):
1281 1283 target_commit = source_repo.get_commit(
1282 1284 commit_id=safe_str(target_ref_id))
1283 1285 source_commit = source_repo.get_commit(
1284 1286 commit_id=safe_str(source_ref_id))
1285 1287 if isinstance(source_repo, Repository):
1286 1288 vcs_repo = source_repo.scm_instance()
1287 1289 else:
1288 1290 vcs_repo = source_repo
1289 1291
1290 1292 # TODO: johbo: In the context of an update, we cannot reach
1291 1293 # the old commit anymore with our normal mechanisms. It needs
1292 1294 # some sort of special support in the vcs layer to avoid this
1293 1295 # workaround.
1294 1296 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1295 1297 vcs_repo.alias == 'git'):
1296 1298 source_commit.raw_id = safe_str(source_ref_id)
1297 1299
1298 1300 log.debug('calculating diff between '
1299 1301 'source_ref:%s and target_ref:%s for repo `%s`',
1300 1302 target_ref_id, source_ref_id,
1301 1303 safe_unicode(vcs_repo.path))
1302 1304
1303 1305 vcs_diff = vcs_repo.get_diff(
1304 1306 commit1=target_commit, commit2=source_commit, context=context)
1305 1307 return vcs_diff
1306 1308
1307 1309 def _is_merge_enabled(self, pull_request):
1308 1310 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1309 1311 settings = settings_model.get_general_settings()
1310 1312 return settings.get('rhodecode_pr_merge_enabled', False)
1311 1313
1312 1314 def _use_rebase_for_merging(self, pull_request):
1313 1315 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1314 1316 settings = settings_model.get_general_settings()
1315 1317 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1316 1318
1317 1319 def _log_action(self, action, user, pull_request):
1318 1320 action_logger(
1319 1321 user,
1320 1322 '{action}:{pr_id}'.format(
1321 1323 action=action, pr_id=pull_request.pull_request_id),
1322 1324 pull_request.target_repo)
1323 1325
1324 1326
1325 1327 class MergeCheck(object):
1326 1328 """
1327 1329 Perform Merge Checks and returns a check object which stores information
1328 1330 about merge errors, and merge conditions
1329 1331 """
1330 1332 TODO_CHECK = 'todo'
1331 1333 PERM_CHECK = 'perm'
1332 1334 REVIEW_CHECK = 'review'
1333 1335 MERGE_CHECK = 'merge'
1334 1336
1335 1337 def __init__(self):
1336 1338 self.review_status = None
1337 1339 self.merge_possible = None
1338 1340 self.merge_msg = ''
1339 1341 self.failed = None
1340 1342 self.errors = []
1341 1343 self.error_details = OrderedDict()
1342 1344
1343 1345 def push_error(self, error_type, message, error_key, details):
1344 1346 self.failed = True
1345 1347 self.errors.append([error_type, message])
1346 1348 self.error_details[error_key] = dict(
1347 1349 details=details,
1348 1350 error_type=error_type,
1349 1351 message=message
1350 1352 )
1351 1353
1352 1354 @classmethod
1353 1355 def validate(cls, pull_request, user, fail_early=False, translator=None):
1354 1356 # if migrated to pyramid...
1355 1357 # _ = lambda: translator or _ # use passed in translator if any
1356 1358
1357 1359 merge_check = cls()
1358 1360
1359 1361 # permissions to merge
1360 1362 user_allowed_to_merge = PullRequestModel().check_user_merge(
1361 1363 pull_request, user)
1362 1364 if not user_allowed_to_merge:
1363 1365 log.debug("MergeCheck: cannot merge, approval is pending.")
1364 1366
1365 1367 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1366 1368 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1367 1369 if fail_early:
1368 1370 return merge_check
1369 1371
1370 1372 # review status, must be always present
1371 1373 review_status = pull_request.calculated_review_status()
1372 1374 merge_check.review_status = review_status
1373 1375
1374 1376 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1375 1377 if not status_approved:
1376 1378 log.debug("MergeCheck: cannot merge, approval is pending.")
1377 1379
1378 1380 msg = _('Pull request reviewer approval is pending.')
1379 1381
1380 1382 merge_check.push_error(
1381 1383 'warning', msg, cls.REVIEW_CHECK, review_status)
1382 1384
1383 1385 if fail_early:
1384 1386 return merge_check
1385 1387
1386 1388 # left over TODOs
1387 1389 todos = CommentsModel().get_unresolved_todos(pull_request)
1388 1390 if todos:
1389 1391 log.debug("MergeCheck: cannot merge, {} "
1390 1392 "unresolved todos left.".format(len(todos)))
1391 1393
1392 1394 if len(todos) == 1:
1393 1395 msg = _('Cannot merge, {} TODO still not resolved.').format(
1394 1396 len(todos))
1395 1397 else:
1396 1398 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1397 1399 len(todos))
1398 1400
1399 1401 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1400 1402
1401 1403 if fail_early:
1402 1404 return merge_check
1403 1405
1404 1406 # merge possible
1405 1407 merge_status, msg = PullRequestModel().merge_status(pull_request)
1406 1408 merge_check.merge_possible = merge_status
1407 1409 merge_check.merge_msg = msg
1408 1410 if not merge_status:
1409 1411 log.debug(
1410 1412 "MergeCheck: cannot merge, pull request merge not possible.")
1411 1413 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1412 1414
1413 1415 if fail_early:
1414 1416 return merge_check
1415 1417
1416 1418 return merge_check
1417 1419
1418 1420
1419 1421 ChangeTuple = namedtuple('ChangeTuple',
1420 1422 ['added', 'common', 'removed', 'total'])
1421 1423
1422 1424 FileChangeTuple = namedtuple('FileChangeTuple',
1423 1425 ['added', 'modified', 'removed'])
@@ -1,2349 +1,2342 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wide-mode-wrapper {
113 113 max-width:4000px !important;
114 114 }
115 115
116 116 .wrapper {
117 117 position: relative;
118 118 max-width: @wrapper-maxwidth;
119 119 margin: 0 auto;
120 120 }
121 121
122 122 #content {
123 123 clear: both;
124 124 padding: 0 @contentpadding;
125 125 }
126 126
127 127 .advanced-settings-fields{
128 128 input{
129 129 margin-left: @textmargin;
130 130 margin-right: @padding/2;
131 131 }
132 132 }
133 133
134 134 .cs_files_title {
135 135 margin: @pagepadding 0 0;
136 136 }
137 137
138 138 input.inline[type="file"] {
139 139 display: inline;
140 140 }
141 141
142 142 .error_page {
143 143 margin: 10% auto;
144 144
145 145 h1 {
146 146 color: @grey2;
147 147 }
148 148
149 149 .alert {
150 150 margin: @padding 0;
151 151 }
152 152
153 153 .error-branding {
154 154 font-family: @text-semibold;
155 155 color: @grey4;
156 156 }
157 157
158 158 .error_message {
159 159 font-family: @text-regular;
160 160 }
161 161
162 162 .sidebar {
163 163 min-height: 275px;
164 164 margin: 0;
165 165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 166 border: none;
167 167 }
168 168
169 169 .main-content {
170 170 position: relative;
171 171 margin: 0 @sidebarpadding @sidebarpadding;
172 172 padding: 0 0 0 @sidebarpadding;
173 173 border-left: @border-thickness solid @grey5;
174 174
175 175 @media (max-width:767px) {
176 176 clear: both;
177 177 width: 100%;
178 178 margin: 0;
179 179 border: none;
180 180 }
181 181 }
182 182
183 183 .inner-column {
184 184 float: left;
185 185 width: 29.75%;
186 186 min-height: 150px;
187 187 margin: @sidebarpadding 2% 0 0;
188 188 padding: 0 2% 0 0;
189 189 border-right: @border-thickness solid @grey5;
190 190
191 191 @media (max-width:767px) {
192 192 clear: both;
193 193 width: 100%;
194 194 border: none;
195 195 }
196 196
197 197 ul {
198 198 padding-left: 1.25em;
199 199 }
200 200
201 201 &:last-child {
202 202 margin: @sidebarpadding 0 0;
203 203 border: none;
204 204 }
205 205
206 206 h4 {
207 207 margin: 0 0 @padding;
208 208 font-family: @text-semibold;
209 209 }
210 210 }
211 211 }
212 212 .error-page-logo {
213 213 width: 130px;
214 214 height: 160px;
215 215 }
216 216
217 217 // HEADER
218 218 .header {
219 219
220 220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 221 // for the header and then remove the min-height. I chose a smaller value
222 222 // intentionally here to avoid rendering issues in the main navigation.
223 223 min-height: 49px;
224 224
225 225 position: relative;
226 226 vertical-align: bottom;
227 227 padding: 0 @header-padding;
228 228 background-color: @grey2;
229 229 color: @grey5;
230 230
231 231 .title {
232 232 overflow: visible;
233 233 }
234 234
235 235 &:before,
236 236 &:after {
237 237 content: "";
238 238 clear: both;
239 239 width: 100%;
240 240 }
241 241
242 242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 243 .select2-container .select2-choice .select2-arrow {
244 244 display: none;
245 245 }
246 246 }
247 247
248 248 #header-inner {
249 249 &.title {
250 250 margin: 0;
251 251 }
252 252 &:before,
253 253 &:after {
254 254 content: "";
255 255 clear: both;
256 256 }
257 257 }
258 258
259 259 // Gists
260 260 #files_data {
261 261 clear: both; //for firefox
262 262 }
263 263 #gistid {
264 264 margin-right: @padding;
265 265 }
266 266
267 267 // Global Settings Editor
268 268 .textarea.editor {
269 269 float: left;
270 270 position: relative;
271 271 max-width: @texteditor-width;
272 272
273 273 select {
274 274 position: absolute;
275 275 top:10px;
276 276 right:0;
277 277 }
278 278
279 279 .CodeMirror {
280 280 margin: 0;
281 281 }
282 282
283 283 .help-block {
284 284 margin: 0 0 @padding;
285 285 padding:.5em;
286 286 background-color: @grey6;
287 287 }
288 288 }
289 289
290 290 ul.auth_plugins {
291 291 margin: @padding 0 @padding @legend-width;
292 292 padding: 0;
293 293
294 294 li {
295 295 margin-bottom: @padding;
296 296 line-height: 1em;
297 297 list-style-type: none;
298 298
299 299 .auth_buttons .btn {
300 300 margin-right: @padding;
301 301 }
302 302
303 303 &:before { content: none; }
304 304 }
305 305 }
306 306
307 307
308 308 // My Account PR list
309 309
310 310 #show_closed {
311 311 margin: 0 1em 0 0;
312 312 }
313 313
314 314 .pullrequestlist {
315 315 .closed {
316 316 background-color: @grey6;
317 317 }
318 318 .td-status {
319 319 padding-left: .5em;
320 320 }
321 321 .log-container .truncate {
322 322 height: 2.75em;
323 323 white-space: pre-line;
324 324 }
325 325 table.rctable .user {
326 326 padding-left: 0;
327 327 }
328 328 table.rctable {
329 329 td.td-description,
330 330 .rc-user {
331 331 min-width: auto;
332 332 }
333 333 }
334 334 }
335 335
336 336 // Pull Requests
337 337
338 338 .pullrequests_section_head {
339 339 display: block;
340 340 clear: both;
341 341 margin: @padding 0;
342 342 font-family: @text-bold;
343 343 }
344 344
345 345 .pr-origininfo, .pr-targetinfo {
346 346 position: relative;
347 347
348 348 .tag {
349 349 display: inline-block;
350 350 margin: 0 1em .5em 0;
351 351 }
352 352
353 353 .clone-url {
354 354 display: inline-block;
355 355 margin: 0 0 .5em 0;
356 356 padding: 0;
357 357 line-height: 1.2em;
358 358 }
359 359 }
360 360
361 361 .pr-pullinfo {
362 362 clear: both;
363 363 margin: .5em 0;
364 364 }
365 365
366 366 #pr-title-input {
367 367 width: 72%;
368 368 font-size: 1em;
369 369 font-family: @text-bold;
370 370 margin: 0;
371 371 padding: 0 0 0 @padding/4;
372 372 line-height: 1.7em;
373 373 color: @text-color;
374 374 letter-spacing: .02em;
375 375 }
376 376
377 377 #pullrequest_title {
378 378 width: 100%;
379 379 box-sizing: border-box;
380 380 }
381 381
382 382 #pr_open_message {
383 383 border: @border-thickness solid #fff;
384 384 border-radius: @border-radius;
385 385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 386 text-align: left;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .pr-submit-button {
391 391 float: right;
392 392 margin: 0 0 0 5px;
393 393 }
394 394
395 395 .pr-spacing-container {
396 396 padding: 20px;
397 397 clear: both
398 398 }
399 399
400 400 #pr-description-input {
401 401 margin-bottom: 0;
402 402 }
403 403
404 404 .pr-description-label {
405 405 vertical-align: top;
406 406 }
407 407
408 408 .perms_section_head {
409 409 min-width: 625px;
410 410
411 411 h2 {
412 412 margin-bottom: 0;
413 413 }
414 414
415 415 .label-checkbox {
416 416 float: left;
417 417 }
418 418
419 419 &.field {
420 420 margin: @space 0 @padding;
421 421 }
422 422
423 423 &:first-child.field {
424 424 margin-top: 0;
425 425
426 426 .label {
427 427 margin-top: 0;
428 428 padding-top: 0;
429 429 }
430 430
431 431 .radios {
432 432 padding-top: 0;
433 433 }
434 434 }
435 435
436 436 .radios {
437 437 float: right;
438 438 position: relative;
439 439 width: 405px;
440 440 }
441 441 }
442 442
443 443 //--- MODULES ------------------//
444 444
445 445
446 446 // Server Announcement
447 447 #server-announcement {
448 448 width: 95%;
449 449 margin: @padding auto;
450 450 padding: @padding;
451 451 border-width: 2px;
452 452 border-style: solid;
453 453 .border-radius(2px);
454 454 font-family: @text-bold;
455 455
456 456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 461 }
462 462
463 463 // Fixed Sidebar Column
464 464 .sidebar-col-wrapper {
465 465 padding-left: @sidebar-all-width;
466 466
467 467 .sidebar {
468 468 width: @sidebar-width;
469 469 margin-left: -@sidebar-all-width;
470 470 }
471 471 }
472 472
473 473 .sidebar-col-wrapper.scw-small {
474 474 padding-left: @sidebar-small-all-width;
475 475
476 476 .sidebar {
477 477 width: @sidebar-small-width;
478 478 margin-left: -@sidebar-small-all-width;
479 479 }
480 480 }
481 481
482 482
483 483 // FOOTER
484 484 #footer {
485 485 padding: 0;
486 486 text-align: center;
487 487 vertical-align: middle;
488 488 color: @grey2;
489 489 background-color: @grey6;
490 490
491 491 p {
492 492 margin: 0;
493 493 padding: 1em;
494 494 line-height: 1em;
495 495 }
496 496
497 497 .server-instance { //server instance
498 498 display: none;
499 499 }
500 500
501 501 .title {
502 502 float: none;
503 503 margin: 0 auto;
504 504 }
505 505 }
506 506
507 507 button.close {
508 508 padding: 0;
509 509 cursor: pointer;
510 510 background: transparent;
511 511 border: 0;
512 512 .box-shadow(none);
513 513 -webkit-appearance: none;
514 514 }
515 515
516 516 .close {
517 517 float: right;
518 518 font-size: 21px;
519 519 font-family: @text-bootstrap;
520 520 line-height: 1em;
521 521 font-weight: bold;
522 522 color: @grey2;
523 523
524 524 &:hover,
525 525 &:focus {
526 526 color: @grey1;
527 527 text-decoration: none;
528 528 cursor: pointer;
529 529 }
530 530 }
531 531
532 532 // GRID
533 533 .sorting,
534 534 .sorting_desc,
535 535 .sorting_asc {
536 536 cursor: pointer;
537 537 }
538 538 .sorting_desc:after {
539 539 content: "\00A0\25B2";
540 540 font-size: .75em;
541 541 }
542 542 .sorting_asc:after {
543 543 content: "\00A0\25BC";
544 544 font-size: .68em;
545 545 }
546 546
547 547
548 548 .user_auth_tokens {
549 549
550 550 &.truncate {
551 551 white-space: nowrap;
552 552 overflow: hidden;
553 553 text-overflow: ellipsis;
554 554 }
555 555
556 556 .fields .field .input {
557 557 margin: 0;
558 558 }
559 559
560 560 input#description {
561 561 width: 100px;
562 562 margin: 0;
563 563 }
564 564
565 565 .drop-menu {
566 566 // TODO: johbo: Remove this, should work out of the box when
567 567 // having multiple inputs inline
568 568 margin: 0 0 0 5px;
569 569 }
570 570 }
571 571 #user_list_table {
572 572 .closed {
573 573 background-color: @grey6;
574 574 }
575 575 }
576 576
577 577
578 578 input {
579 579 &.disabled {
580 580 opacity: .5;
581 581 }
582 582 }
583 583
584 584 // remove extra padding in firefox
585 585 input::-moz-focus-inner { border:0; padding:0 }
586 586
587 587 .adjacent input {
588 588 margin-bottom: @padding;
589 589 }
590 590
591 591 .permissions_boxes {
592 592 display: block;
593 593 }
594 594
595 595 //TODO: lisa: this should be in tables
596 596 .show_more_col {
597 597 width: 20px;
598 598 }
599 599
600 600 //FORMS
601 601
602 602 .medium-inline,
603 603 input#description.medium-inline {
604 604 display: inline;
605 605 width: @medium-inline-input-width;
606 606 min-width: 100px;
607 607 }
608 608
609 609 select {
610 610 //reset
611 611 -webkit-appearance: none;
612 612 -moz-appearance: none;
613 613
614 614 display: inline-block;
615 615 height: 28px;
616 616 width: auto;
617 617 margin: 0 @padding @padding 0;
618 618 padding: 0 18px 0 8px;
619 619 line-height:1em;
620 620 font-size: @basefontsize;
621 621 border: @border-thickness solid @rcblue;
622 622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 623 color: @rcblue;
624 624
625 625 &:after {
626 626 content: "\00A0\25BE";
627 627 }
628 628
629 629 &:focus {
630 630 outline: none;
631 631 }
632 632 }
633 633
634 634 option {
635 635 &:focus {
636 636 outline: none;
637 637 }
638 638 }
639 639
640 640 input,
641 641 textarea {
642 642 padding: @input-padding;
643 643 border: @input-border-thickness solid @border-highlight-color;
644 644 .border-radius (@border-radius);
645 645 font-family: @text-light;
646 646 font-size: @basefontsize;
647 647
648 648 &.input-sm {
649 649 padding: 5px;
650 650 }
651 651
652 652 &#description {
653 653 min-width: @input-description-minwidth;
654 654 min-height: 1em;
655 655 padding: 10px;
656 656 }
657 657 }
658 658
659 659 .field-sm {
660 660 input,
661 661 textarea {
662 662 padding: 5px;
663 663 }
664 664 }
665 665
666 666 textarea {
667 667 display: block;
668 668 clear: both;
669 669 width: 100%;
670 670 min-height: 100px;
671 671 margin-bottom: @padding;
672 672 .box-sizing(border-box);
673 673 overflow: auto;
674 674 }
675 675
676 676 label {
677 677 font-family: @text-light;
678 678 }
679 679
680 680 // GRAVATARS
681 681 // centers gravatar on username to the right
682 682
683 683 .gravatar {
684 684 display: inline;
685 685 min-width: 16px;
686 686 min-height: 16px;
687 687 margin: -5px 0;
688 688 padding: 0;
689 689 line-height: 1em;
690 690 border: 1px solid @grey4;
691 691 box-sizing: content-box;
692 692
693 693 &.gravatar-large {
694 694 margin: -0.5em .25em -0.5em 0;
695 695 }
696 696
697 697 & + .user {
698 698 display: inline;
699 699 margin: 0;
700 700 padding: 0 0 0 .17em;
701 701 line-height: 1em;
702 702 }
703 703 }
704 704
705 705 .user-inline-data {
706 706 display: inline-block;
707 707 float: left;
708 708 padding-left: .5em;
709 709 line-height: 1.3em;
710 710 }
711 711
712 712 .rc-user { // gravatar + user wrapper
713 713 float: left;
714 714 position: relative;
715 715 min-width: 100px;
716 716 max-width: 200px;
717 717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
718 718 display: block;
719 719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
720 720
721 721
722 722 .gravatar {
723 723 display: block;
724 724 position: absolute;
725 725 top: 0;
726 726 left: 0;
727 727 min-width: @gravatar-size;
728 728 min-height: @gravatar-size;
729 729 margin: 0;
730 730 }
731 731
732 732 .user {
733 733 display: block;
734 734 max-width: 175px;
735 735 padding-top: 2px;
736 736 overflow: hidden;
737 737 text-overflow: ellipsis;
738 738 }
739 739 }
740 740
741 741 .gist-gravatar,
742 742 .journal_container {
743 743 .gravatar-large {
744 744 margin: 0 .5em -10px 0;
745 745 }
746 746 }
747 747
748 748
749 749 // ADMIN SETTINGS
750 750
751 751 // Tag Patterns
752 752 .tag_patterns {
753 753 .tag_input {
754 754 margin-bottom: @padding;
755 755 }
756 756 }
757 757
758 758 .locked_input {
759 759 position: relative;
760 760
761 761 input {
762 762 display: inline;
763 763 margin-top: 3px;
764 764 }
765 765
766 766 br {
767 767 display: none;
768 768 }
769 769
770 770 .error-message {
771 771 float: left;
772 772 width: 100%;
773 773 }
774 774
775 775 .lock_input_button {
776 776 display: inline;
777 777 }
778 778
779 779 .help-block {
780 780 clear: both;
781 781 }
782 782 }
783 783
784 784 // Notifications
785 785
786 786 .notifications_buttons {
787 787 margin: 0 0 @space 0;
788 788 padding: 0;
789 789
790 790 .btn {
791 791 display: inline-block;
792 792 }
793 793 }
794 794
795 795 .notification-list {
796 796
797 797 div {
798 798 display: inline-block;
799 799 vertical-align: middle;
800 800 }
801 801
802 802 .container {
803 803 display: block;
804 804 margin: 0 0 @padding 0;
805 805 }
806 806
807 807 .delete-notifications {
808 808 margin-left: @padding;
809 809 text-align: right;
810 810 cursor: pointer;
811 811 }
812 812
813 813 .read-notifications {
814 814 margin-left: @padding/2;
815 815 text-align: right;
816 816 width: 35px;
817 817 cursor: pointer;
818 818 }
819 819
820 820 .icon-minus-sign {
821 821 color: @alert2;
822 822 }
823 823
824 824 .icon-ok-sign {
825 825 color: @alert1;
826 826 }
827 827 }
828 828
829 829 .user_settings {
830 830 float: left;
831 831 clear: both;
832 832 display: block;
833 833 width: 100%;
834 834
835 835 .gravatar_box {
836 836 margin-bottom: @padding;
837 837
838 838 &:after {
839 839 content: " ";
840 840 clear: both;
841 841 width: 100%;
842 842 }
843 843 }
844 844
845 845 .fields .field {
846 846 clear: both;
847 847 }
848 848 }
849 849
850 850 .advanced_settings {
851 851 margin-bottom: @space;
852 852
853 853 .help-block {
854 854 margin-left: 0;
855 855 }
856 856
857 857 button + .help-block {
858 858 margin-top: @padding;
859 859 }
860 860 }
861 861
862 862 // admin settings radio buttons and labels
863 863 .label-2 {
864 864 float: left;
865 865 width: @label2-width;
866 866
867 867 label {
868 868 color: @grey1;
869 869 }
870 870 }
871 871 .checkboxes {
872 872 float: left;
873 873 width: @checkboxes-width;
874 874 margin-bottom: @padding;
875 875
876 876 .checkbox {
877 877 width: 100%;
878 878
879 879 label {
880 880 margin: 0;
881 881 padding: 0;
882 882 }
883 883 }
884 884
885 885 .checkbox + .checkbox {
886 886 display: inline-block;
887 887 }
888 888
889 889 label {
890 890 margin-right: 1em;
891 891 }
892 892 }
893 893
894 894 // CHANGELOG
895 895 .container_header {
896 896 float: left;
897 897 display: block;
898 898 width: 100%;
899 899 margin: @padding 0 @padding;
900 900
901 901 #filter_changelog {
902 902 float: left;
903 903 margin-right: @padding;
904 904 }
905 905
906 906 .breadcrumbs_light {
907 907 display: inline-block;
908 908 }
909 909 }
910 910
911 911 .info_box {
912 912 float: right;
913 913 }
914 914
915 915
916 916 #graph_nodes {
917 917 padding-top: 43px;
918 918 }
919 919
920 920 #graph_content{
921 921
922 922 // adjust for table headers so that graph renders properly
923 923 // #graph_nodes padding - table cell padding
924 924 padding-top: (@space - (@basefontsize * 2.4));
925 925
926 926 &.graph_full_width {
927 927 width: 100%;
928 928 max-width: 100%;
929 929 }
930 930 }
931 931
932 932 #graph {
933 933 .flag_status {
934 934 margin: 0;
935 935 }
936 936
937 937 .pagination-left {
938 938 float: left;
939 939 clear: both;
940 940 }
941 941
942 942 .log-container {
943 943 max-width: 345px;
944 944
945 945 .message{
946 946 max-width: 340px;
947 947 }
948 948 }
949 949
950 950 .graph-col-wrapper {
951 951 padding-left: 110px;
952 952
953 953 #graph_nodes {
954 954 width: 100px;
955 955 margin-left: -110px;
956 956 float: left;
957 957 clear: left;
958 958 }
959 959 }
960 960
961 961 .load-more-commits {
962 962 text-align: center;
963 963 }
964 964 .load-more-commits:hover {
965 965 background-color: @grey7;
966 966 }
967 967 .load-more-commits {
968 968 a {
969 969 display: block;
970 970 }
971 971 }
972 972 }
973 973
974 974 #filter_changelog {
975 975 float: left;
976 976 }
977 977
978 978
979 979 //--- THEME ------------------//
980 980
981 981 #logo {
982 982 float: left;
983 983 margin: 9px 0 0 0;
984 984
985 985 .header {
986 986 background-color: transparent;
987 987 }
988 988
989 989 a {
990 990 display: inline-block;
991 991 }
992 992
993 993 img {
994 994 height:30px;
995 995 }
996 996 }
997 997
998 998 .logo-wrapper {
999 999 float:left;
1000 1000 }
1001 1001
1002 1002 .branding{
1003 1003 float: left;
1004 1004 padding: 9px 2px;
1005 1005 line-height: 1em;
1006 1006 font-size: @navigation-fontsize;
1007 1007 }
1008 1008
1009 1009 img {
1010 1010 border: none;
1011 1011 outline: none;
1012 1012 }
1013 1013 user-profile-header
1014 1014 label {
1015 1015
1016 1016 input[type="checkbox"] {
1017 1017 margin-right: 1em;
1018 1018 }
1019 1019 input[type="radio"] {
1020 1020 margin-right: 1em;
1021 1021 }
1022 1022 }
1023 1023
1024 1024 .flag_status {
1025 1025 margin: 2px 8px 6px 2px;
1026 1026 &.under_review {
1027 1027 .circle(5px, @alert3);
1028 1028 }
1029 1029 &.approved {
1030 1030 .circle(5px, @alert1);
1031 1031 }
1032 1032 &.rejected,
1033 1033 &.forced_closed{
1034 1034 .circle(5px, @alert2);
1035 1035 }
1036 1036 &.not_reviewed {
1037 1037 .circle(5px, @grey5);
1038 1038 }
1039 1039 }
1040 1040
1041 1041 .flag_status_comment_box {
1042 1042 margin: 5px 6px 0px 2px;
1043 1043 }
1044 1044 .test_pattern_preview {
1045 1045 margin: @space 0;
1046 1046
1047 1047 p {
1048 1048 margin-bottom: 0;
1049 1049 border-bottom: @border-thickness solid @border-default-color;
1050 1050 color: @grey3;
1051 1051 }
1052 1052
1053 1053 .btn {
1054 1054 margin-bottom: @padding;
1055 1055 }
1056 1056 }
1057 1057 #test_pattern_result {
1058 1058 display: none;
1059 1059 &:extend(pre);
1060 1060 padding: .9em;
1061 1061 color: @grey3;
1062 1062 background-color: @grey7;
1063 1063 border-right: @border-thickness solid @border-default-color;
1064 1064 border-bottom: @border-thickness solid @border-default-color;
1065 1065 border-left: @border-thickness solid @border-default-color;
1066 1066 }
1067 1067
1068 1068 #repo_vcs_settings {
1069 1069 #inherit_overlay_vcs_default {
1070 1070 display: none;
1071 1071 }
1072 1072 #inherit_overlay_vcs_custom {
1073 1073 display: custom;
1074 1074 }
1075 1075 &.inherited {
1076 1076 #inherit_overlay_vcs_default {
1077 1077 display: block;
1078 1078 }
1079 1079 #inherit_overlay_vcs_custom {
1080 1080 display: none;
1081 1081 }
1082 1082 }
1083 1083 }
1084 1084
1085 1085 .issue-tracker-link {
1086 1086 color: @rcblue;
1087 1087 }
1088 1088
1089 1089 // Issue Tracker Table Show/Hide
1090 1090 #repo_issue_tracker {
1091 1091 #inherit_overlay {
1092 1092 display: none;
1093 1093 }
1094 1094 #custom_overlay {
1095 1095 display: custom;
1096 1096 }
1097 1097 &.inherited {
1098 1098 #inherit_overlay {
1099 1099 display: block;
1100 1100 }
1101 1101 #custom_overlay {
1102 1102 display: none;
1103 1103 }
1104 1104 }
1105 1105 }
1106 1106 table.issuetracker {
1107 1107 &.readonly {
1108 1108 tr, td {
1109 1109 color: @grey3;
1110 1110 }
1111 1111 }
1112 1112 .edit {
1113 1113 display: none;
1114 1114 }
1115 1115 .editopen {
1116 1116 .edit {
1117 1117 display: inline;
1118 1118 }
1119 1119 .entry {
1120 1120 display: none;
1121 1121 }
1122 1122 }
1123 1123 tr td.td-action {
1124 1124 min-width: 117px;
1125 1125 }
1126 1126 td input {
1127 1127 max-width: none;
1128 1128 min-width: 30px;
1129 1129 width: 80%;
1130 1130 }
1131 1131 .issuetracker_pref input {
1132 1132 width: 40%;
1133 1133 }
1134 1134 input.edit_issuetracker_update {
1135 1135 margin-right: 0;
1136 1136 width: auto;
1137 1137 }
1138 1138 }
1139 1139
1140 1140 table.integrations {
1141 1141 .td-icon {
1142 1142 width: 20px;
1143 1143 .integration-icon {
1144 1144 height: 20px;
1145 1145 width: 20px;
1146 1146 }
1147 1147 }
1148 1148 }
1149 1149
1150 1150 .integrations {
1151 1151 a.integration-box {
1152 1152 color: @text-color;
1153 1153 &:hover {
1154 1154 .panel {
1155 1155 background: #fbfbfb;
1156 1156 }
1157 1157 }
1158 1158 .integration-icon {
1159 1159 width: 30px;
1160 1160 height: 30px;
1161 1161 margin-right: 20px;
1162 1162 float: left;
1163 1163 }
1164 1164
1165 1165 .panel-body {
1166 1166 padding: 10px;
1167 1167 }
1168 1168 .panel {
1169 1169 margin-bottom: 10px;
1170 1170 }
1171 1171 h2 {
1172 1172 display: inline-block;
1173 1173 margin: 0;
1174 1174 min-width: 140px;
1175 1175 }
1176 1176 }
1177 1177 }
1178 1178
1179 1179 //Permissions Settings
1180 1180 #add_perm {
1181 1181 margin: 0 0 @padding;
1182 1182 cursor: pointer;
1183 1183 }
1184 1184
1185 1185 .perm_ac {
1186 1186 input {
1187 1187 width: 95%;
1188 1188 }
1189 1189 }
1190 1190
1191 1191 .autocomplete-suggestions {
1192 1192 width: auto !important; // overrides autocomplete.js
1193 1193 margin: 0;
1194 1194 border: @border-thickness solid @rcblue;
1195 1195 border-radius: @border-radius;
1196 1196 color: @rcblue;
1197 1197 background-color: white;
1198 1198 }
1199 1199 .autocomplete-selected {
1200 1200 background: #F0F0F0;
1201 1201 }
1202 1202 .ac-container-wrap {
1203 1203 margin: 0;
1204 1204 padding: 8px;
1205 1205 border-bottom: @border-thickness solid @rclightblue;
1206 1206 list-style-type: none;
1207 1207 cursor: pointer;
1208 1208
1209 1209 &:hover {
1210 1210 background-color: @rclightblue;
1211 1211 }
1212 1212
1213 1213 img {
1214 1214 height: @gravatar-size;
1215 1215 width: @gravatar-size;
1216 1216 margin-right: 1em;
1217 1217 }
1218 1218
1219 1219 strong {
1220 1220 font-weight: normal;
1221 1221 }
1222 1222 }
1223 1223
1224 1224 // Settings Dropdown
1225 1225 .user-menu .container {
1226 1226 padding: 0 4px;
1227 1227 margin: 0;
1228 1228 }
1229 1229
1230 1230 .user-menu .gravatar {
1231 1231 cursor: pointer;
1232 1232 }
1233 1233
1234 1234 .codeblock {
1235 1235 margin-bottom: @padding;
1236 1236 clear: both;
1237 1237
1238 1238 .stats{
1239 1239 overflow: hidden;
1240 1240 }
1241 1241
1242 1242 .message{
1243 1243 textarea{
1244 1244 margin: 0;
1245 1245 }
1246 1246 }
1247 1247
1248 1248 .code-header {
1249 1249 .stats {
1250 1250 line-height: 2em;
1251 1251
1252 1252 .revision_id {
1253 1253 margin-left: 0;
1254 1254 }
1255 1255 .buttons {
1256 1256 padding-right: 0;
1257 1257 }
1258 1258 }
1259 1259
1260 1260 .item{
1261 1261 margin-right: 0.5em;
1262 1262 }
1263 1263 }
1264 1264
1265 1265 #editor_container{
1266 1266 position: relative;
1267 1267 margin: @padding;
1268 1268 }
1269 1269 }
1270 1270
1271 1271 #file_history_container {
1272 1272 display: none;
1273 1273 }
1274 1274
1275 1275 .file-history-inner {
1276 1276 margin-bottom: 10px;
1277 1277 }
1278 1278
1279 1279 // Pull Requests
1280 1280 .summary-details {
1281 1281 width: 72%;
1282 1282 }
1283 1283 .pr-summary {
1284 1284 border-bottom: @border-thickness solid @grey5;
1285 1285 margin-bottom: @space;
1286 1286 }
1287 1287 .reviewers-title {
1288 1288 width: 25%;
1289 1289 min-width: 200px;
1290 1290 }
1291 1291 .reviewers {
1292 1292 width: 25%;
1293 1293 min-width: 200px;
1294 1294 }
1295 1295 .reviewers ul li {
1296 1296 position: relative;
1297 1297 width: 100%;
1298 1298 margin-bottom: 8px;
1299 1299 }
1300 1300 .reviewers_member {
1301 1301 width: 100%;
1302 1302 overflow: auto;
1303 1303 }
1304 1304 .reviewer_reason {
1305 1305 padding-left: 20px;
1306 1306 }
1307 1307 .reviewer_status {
1308 1308 display: inline-block;
1309 1309 vertical-align: top;
1310 1310 width: 7%;
1311 1311 min-width: 20px;
1312 1312 height: 1.2em;
1313 1313 margin-top: 3px;
1314 1314 line-height: 1em;
1315 1315 }
1316 1316
1317 1317 .reviewer_name {
1318 1318 display: inline-block;
1319 1319 max-width: 83%;
1320 1320 padding-right: 20px;
1321 1321 vertical-align: middle;
1322 1322 line-height: 1;
1323 1323
1324 1324 .rc-user {
1325 1325 min-width: 0;
1326 1326 margin: -2px 1em 0 0;
1327 1327 }
1328 1328
1329 1329 .reviewer {
1330 1330 float: left;
1331 1331 }
1332
1333 &.to-delete {
1334 .user,
1335 .reviewer {
1336 text-decoration: line-through;
1337 }
1338 }
1339 1332 }
1340 1333
1341 1334 .reviewer_member_remove {
1342 1335 position: absolute;
1343 1336 right: 0;
1344 1337 top: 0;
1345 1338 width: 16px;
1346 1339 margin-bottom: 10px;
1347 1340 padding: 0;
1348 1341 color: black;
1349 1342 }
1350 1343 .reviewer_member_status {
1351 1344 margin-top: 5px;
1352 1345 }
1353 1346 .pr-summary #summary{
1354 1347 width: 100%;
1355 1348 }
1356 1349 .pr-summary .action_button:hover {
1357 1350 border: 0;
1358 1351 cursor: pointer;
1359 1352 }
1360 1353 .pr-details-title {
1361 1354 padding-bottom: 8px;
1362 1355 border-bottom: @border-thickness solid @grey5;
1363 1356
1364 1357 .action_button.disabled {
1365 1358 color: @grey4;
1366 1359 cursor: inherit;
1367 1360 }
1368 1361 .action_button {
1369 1362 color: @rcblue;
1370 1363 }
1371 1364 }
1372 1365 .pr-details-content {
1373 1366 margin-top: @textmargin;
1374 1367 margin-bottom: @textmargin;
1375 1368 }
1376 1369 .pr-description {
1377 1370 white-space:pre-wrap;
1378 1371 }
1379 1372 .group_members {
1380 1373 margin-top: 0;
1381 1374 padding: 0;
1382 1375 list-style: outside none none;
1383 1376
1384 1377 img {
1385 1378 height: @gravatar-size;
1386 1379 width: @gravatar-size;
1387 1380 margin-right: .5em;
1388 1381 margin-left: 3px;
1389 1382 }
1390 1383
1391 1384 .to-delete {
1392 1385 .user {
1393 1386 text-decoration: line-through;
1394 1387 }
1395 1388 }
1396 1389 }
1397 1390
1398 1391 .compare_view_commits_title {
1399 1392 .disabled {
1400 1393 cursor: inherit;
1401 1394 &:hover{
1402 1395 background-color: inherit;
1403 1396 color: inherit;
1404 1397 }
1405 1398 }
1406 1399 }
1407 1400
1408 1401 .subtitle-compare {
1409 1402 margin: -15px 0px 0px 0px;
1410 1403 }
1411 1404
1412 1405 .comments-summary-td {
1413 1406 border-top: 1px dashed @grey5;
1414 1407 }
1415 1408
1416 1409 // new entry in group_members
1417 1410 .td-author-new-entry {
1418 1411 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1419 1412 }
1420 1413
1421 1414 .usergroup_member_remove {
1422 1415 width: 16px;
1423 1416 margin-bottom: 10px;
1424 1417 padding: 0;
1425 1418 color: black !important;
1426 1419 cursor: pointer;
1427 1420 }
1428 1421
1429 1422 .reviewer_ac .ac-input {
1430 1423 width: 92%;
1431 1424 margin-bottom: 1em;
1432 1425 }
1433 1426
1434 1427 .compare_view_commits tr{
1435 1428 height: 20px;
1436 1429 }
1437 1430 .compare_view_commits td {
1438 1431 vertical-align: top;
1439 1432 padding-top: 10px;
1440 1433 }
1441 1434 .compare_view_commits .author {
1442 1435 margin-left: 5px;
1443 1436 }
1444 1437
1445 1438 .compare_view_commits {
1446 1439 .color-a {
1447 1440 color: @alert1;
1448 1441 }
1449 1442
1450 1443 .color-c {
1451 1444 color: @color3;
1452 1445 }
1453 1446
1454 1447 .color-r {
1455 1448 color: @color5;
1456 1449 }
1457 1450
1458 1451 .color-a-bg {
1459 1452 background-color: @alert1;
1460 1453 }
1461 1454
1462 1455 .color-c-bg {
1463 1456 background-color: @alert3;
1464 1457 }
1465 1458
1466 1459 .color-r-bg {
1467 1460 background-color: @alert2;
1468 1461 }
1469 1462
1470 1463 .color-a-border {
1471 1464 border: 1px solid @alert1;
1472 1465 }
1473 1466
1474 1467 .color-c-border {
1475 1468 border: 1px solid @alert3;
1476 1469 }
1477 1470
1478 1471 .color-r-border {
1479 1472 border: 1px solid @alert2;
1480 1473 }
1481 1474
1482 1475 .commit-change-indicator {
1483 1476 width: 15px;
1484 1477 height: 15px;
1485 1478 position: relative;
1486 1479 left: 15px;
1487 1480 }
1488 1481
1489 1482 .commit-change-content {
1490 1483 text-align: center;
1491 1484 vertical-align: middle;
1492 1485 line-height: 15px;
1493 1486 }
1494 1487 }
1495 1488
1496 1489 .compare_view_files {
1497 1490 width: 100%;
1498 1491
1499 1492 td {
1500 1493 vertical-align: middle;
1501 1494 }
1502 1495 }
1503 1496
1504 1497 .compare_view_filepath {
1505 1498 color: @grey1;
1506 1499 }
1507 1500
1508 1501 .show_more {
1509 1502 display: inline-block;
1510 1503 position: relative;
1511 1504 vertical-align: middle;
1512 1505 width: 4px;
1513 1506 height: @basefontsize;
1514 1507
1515 1508 &:after {
1516 1509 content: "\00A0\25BE";
1517 1510 display: inline-block;
1518 1511 width:10px;
1519 1512 line-height: 5px;
1520 1513 font-size: 12px;
1521 1514 cursor: pointer;
1522 1515 }
1523 1516 }
1524 1517
1525 1518 .journal_more .show_more {
1526 1519 display: inline;
1527 1520
1528 1521 &:after {
1529 1522 content: none;
1530 1523 }
1531 1524 }
1532 1525
1533 1526 .open .show_more:after,
1534 1527 .select2-dropdown-open .show_more:after {
1535 1528 .rotate(180deg);
1536 1529 margin-left: 4px;
1537 1530 }
1538 1531
1539 1532
1540 1533 .compare_view_commits .collapse_commit:after {
1541 1534 cursor: pointer;
1542 1535 content: "\00A0\25B4";
1543 1536 margin-left: -3px;
1544 1537 font-size: 17px;
1545 1538 color: @grey4;
1546 1539 }
1547 1540
1548 1541 .diff_links {
1549 1542 margin-left: 8px;
1550 1543 }
1551 1544
1552 1545 div.ancestor {
1553 1546 margin: -30px 0px;
1554 1547 }
1555 1548
1556 1549 .cs_icon_td input[type="checkbox"] {
1557 1550 display: none;
1558 1551 }
1559 1552
1560 1553 .cs_icon_td .expand_file_icon:after {
1561 1554 cursor: pointer;
1562 1555 content: "\00A0\25B6";
1563 1556 font-size: 12px;
1564 1557 color: @grey4;
1565 1558 }
1566 1559
1567 1560 .cs_icon_td .collapse_file_icon:after {
1568 1561 cursor: pointer;
1569 1562 content: "\00A0\25BC";
1570 1563 font-size: 12px;
1571 1564 color: @grey4;
1572 1565 }
1573 1566
1574 1567 /*new binary
1575 1568 NEW_FILENODE = 1
1576 1569 DEL_FILENODE = 2
1577 1570 MOD_FILENODE = 3
1578 1571 RENAMED_FILENODE = 4
1579 1572 COPIED_FILENODE = 5
1580 1573 CHMOD_FILENODE = 6
1581 1574 BIN_FILENODE = 7
1582 1575 */
1583 1576 .cs_files_expand {
1584 1577 font-size: @basefontsize + 5px;
1585 1578 line-height: 1.8em;
1586 1579 float: right;
1587 1580 }
1588 1581
1589 1582 .cs_files_expand span{
1590 1583 color: @rcblue;
1591 1584 cursor: pointer;
1592 1585 }
1593 1586 .cs_files {
1594 1587 clear: both;
1595 1588 padding-bottom: @padding;
1596 1589
1597 1590 .cur_cs {
1598 1591 margin: 10px 2px;
1599 1592 font-weight: bold;
1600 1593 }
1601 1594
1602 1595 .node {
1603 1596 float: left;
1604 1597 }
1605 1598
1606 1599 .changes {
1607 1600 float: right;
1608 1601 color: white;
1609 1602 font-size: @basefontsize - 4px;
1610 1603 margin-top: 4px;
1611 1604 opacity: 0.6;
1612 1605 filter: Alpha(opacity=60); /* IE8 and earlier */
1613 1606
1614 1607 .added {
1615 1608 background-color: @alert1;
1616 1609 float: left;
1617 1610 text-align: center;
1618 1611 }
1619 1612
1620 1613 .deleted {
1621 1614 background-color: @alert2;
1622 1615 float: left;
1623 1616 text-align: center;
1624 1617 }
1625 1618
1626 1619 .bin {
1627 1620 background-color: @alert1;
1628 1621 text-align: center;
1629 1622 }
1630 1623
1631 1624 /*new binary*/
1632 1625 .bin.bin1 {
1633 1626 background-color: @alert1;
1634 1627 text-align: center;
1635 1628 }
1636 1629
1637 1630 /*deleted binary*/
1638 1631 .bin.bin2 {
1639 1632 background-color: @alert2;
1640 1633 text-align: center;
1641 1634 }
1642 1635
1643 1636 /*mod binary*/
1644 1637 .bin.bin3 {
1645 1638 background-color: @grey2;
1646 1639 text-align: center;
1647 1640 }
1648 1641
1649 1642 /*rename file*/
1650 1643 .bin.bin4 {
1651 1644 background-color: @alert4;
1652 1645 text-align: center;
1653 1646 }
1654 1647
1655 1648 /*copied file*/
1656 1649 .bin.bin5 {
1657 1650 background-color: @alert4;
1658 1651 text-align: center;
1659 1652 }
1660 1653
1661 1654 /*chmod file*/
1662 1655 .bin.bin6 {
1663 1656 background-color: @grey2;
1664 1657 text-align: center;
1665 1658 }
1666 1659 }
1667 1660 }
1668 1661
1669 1662 .cs_files .cs_added, .cs_files .cs_A,
1670 1663 .cs_files .cs_added, .cs_files .cs_M,
1671 1664 .cs_files .cs_added, .cs_files .cs_D {
1672 1665 height: 16px;
1673 1666 padding-right: 10px;
1674 1667 margin-top: 7px;
1675 1668 text-align: left;
1676 1669 }
1677 1670
1678 1671 .cs_icon_td {
1679 1672 min-width: 16px;
1680 1673 width: 16px;
1681 1674 }
1682 1675
1683 1676 .pull-request-merge {
1684 1677 border: 1px solid @grey5;
1685 1678 padding: 10px 0px 20px;
1686 1679 margin-top: 10px;
1687 1680 margin-bottom: 20px;
1688 1681 }
1689 1682
1690 1683 .pull-request-merge ul {
1691 1684 padding: 0px 0px;
1692 1685 }
1693 1686
1694 1687 .pull-request-merge li:before{
1695 1688 content:none;
1696 1689 }
1697 1690
1698 1691 .pull-request-merge .pull-request-wrap {
1699 1692 height: auto;
1700 1693 padding: 0px 0px;
1701 1694 text-align: right;
1702 1695 }
1703 1696
1704 1697 .pull-request-merge span {
1705 1698 margin-right: 5px;
1706 1699 }
1707 1700
1708 1701 .pull-request-merge-actions {
1709 1702 height: 30px;
1710 1703 padding: 0px 0px;
1711 1704 }
1712 1705
1713 1706 .merge-status {
1714 1707 margin-right: 5px;
1715 1708 }
1716 1709
1717 1710 .merge-message {
1718 1711 font-size: 1.2em
1719 1712 }
1720 1713
1721 1714 .merge-message.success i,
1722 1715 .merge-icon.success i {
1723 1716 color:@alert1;
1724 1717 }
1725 1718
1726 1719 .merge-message.warning i,
1727 1720 .merge-icon.warning i {
1728 1721 color: @alert3;
1729 1722 }
1730 1723
1731 1724 .merge-message.error i,
1732 1725 .merge-icon.error i {
1733 1726 color:@alert2;
1734 1727 }
1735 1728
1736 1729 .pr-versions {
1737 1730 font-size: 1.1em;
1738 1731
1739 1732 table {
1740 1733 padding: 0px 5px;
1741 1734 }
1742 1735
1743 1736 td {
1744 1737 line-height: 15px;
1745 1738 }
1746 1739
1747 1740 .flag_status {
1748 1741 margin: 0;
1749 1742 }
1750 1743
1751 1744 .compare-radio-button {
1752 1745 position: relative;
1753 1746 top: -3px;
1754 1747 }
1755 1748 }
1756 1749
1757 1750
1758 1751 #close_pull_request {
1759 1752 margin-right: 0px;
1760 1753 }
1761 1754
1762 1755 .empty_data {
1763 1756 color: @grey4;
1764 1757 }
1765 1758
1766 1759 #changeset_compare_view_content {
1767 1760 margin-bottom: @space;
1768 1761 clear: both;
1769 1762 width: 100%;
1770 1763 box-sizing: border-box;
1771 1764 .border-radius(@border-radius);
1772 1765
1773 1766 .help-block {
1774 1767 margin: @padding 0;
1775 1768 color: @text-color;
1776 1769 }
1777 1770
1778 1771 .empty_data {
1779 1772 margin: @padding 0;
1780 1773 }
1781 1774
1782 1775 .alert {
1783 1776 margin-bottom: @space;
1784 1777 }
1785 1778 }
1786 1779
1787 1780 .table_disp {
1788 1781 .status {
1789 1782 width: auto;
1790 1783
1791 1784 .flag_status {
1792 1785 float: left;
1793 1786 }
1794 1787 }
1795 1788 }
1796 1789
1797 1790 .status_box_menu {
1798 1791 margin: 0;
1799 1792 }
1800 1793
1801 1794 .notification-table{
1802 1795 margin-bottom: @space;
1803 1796 display: table;
1804 1797 width: 100%;
1805 1798
1806 1799 .container{
1807 1800 display: table-row;
1808 1801
1809 1802 .notification-header{
1810 1803 border-bottom: @border-thickness solid @border-default-color;
1811 1804 }
1812 1805
1813 1806 .notification-subject{
1814 1807 display: table-cell;
1815 1808 }
1816 1809 }
1817 1810 }
1818 1811
1819 1812 // Notifications
1820 1813 .notification-header{
1821 1814 display: table;
1822 1815 width: 100%;
1823 1816 padding: floor(@basefontsize/2) 0;
1824 1817 line-height: 1em;
1825 1818
1826 1819 .desc, .delete-notifications, .read-notifications{
1827 1820 display: table-cell;
1828 1821 text-align: left;
1829 1822 }
1830 1823
1831 1824 .desc{
1832 1825 width: 1163px;
1833 1826 }
1834 1827
1835 1828 .delete-notifications, .read-notifications{
1836 1829 width: 35px;
1837 1830 min-width: 35px; //fixes when only one button is displayed
1838 1831 }
1839 1832 }
1840 1833
1841 1834 .notification-body {
1842 1835 .markdown-block,
1843 1836 .rst-block {
1844 1837 padding: @padding 0;
1845 1838 }
1846 1839
1847 1840 .notification-subject {
1848 1841 padding: @textmargin 0;
1849 1842 border-bottom: @border-thickness solid @border-default-color;
1850 1843 }
1851 1844 }
1852 1845
1853 1846
1854 1847 .notifications_buttons{
1855 1848 float: right;
1856 1849 }
1857 1850
1858 1851 #notification-status{
1859 1852 display: inline;
1860 1853 }
1861 1854
1862 1855 // Repositories
1863 1856
1864 1857 #summary.fields{
1865 1858 display: table;
1866 1859
1867 1860 .field{
1868 1861 display: table-row;
1869 1862
1870 1863 .label-summary{
1871 1864 display: table-cell;
1872 1865 min-width: @label-summary-minwidth;
1873 1866 padding-top: @padding/2;
1874 1867 padding-bottom: @padding/2;
1875 1868 padding-right: @padding/2;
1876 1869 }
1877 1870
1878 1871 .input{
1879 1872 display: table-cell;
1880 1873 padding: @padding/2;
1881 1874
1882 1875 input{
1883 1876 min-width: 29em;
1884 1877 padding: @padding/4;
1885 1878 }
1886 1879 }
1887 1880 .statistics, .downloads{
1888 1881 .disabled{
1889 1882 color: @grey4;
1890 1883 }
1891 1884 }
1892 1885 }
1893 1886 }
1894 1887
1895 1888 #summary{
1896 1889 width: 70%;
1897 1890 }
1898 1891
1899 1892
1900 1893 // Journal
1901 1894 .journal.title {
1902 1895 h5 {
1903 1896 float: left;
1904 1897 margin: 0;
1905 1898 width: 70%;
1906 1899 }
1907 1900
1908 1901 ul {
1909 1902 float: right;
1910 1903 display: inline-block;
1911 1904 margin: 0;
1912 1905 width: 30%;
1913 1906 text-align: right;
1914 1907
1915 1908 li {
1916 1909 display: inline;
1917 1910 font-size: @journal-fontsize;
1918 1911 line-height: 1em;
1919 1912
1920 1913 &:before { content: none; }
1921 1914 }
1922 1915 }
1923 1916 }
1924 1917
1925 1918 .filterexample {
1926 1919 position: absolute;
1927 1920 top: 95px;
1928 1921 left: @contentpadding;
1929 1922 color: @rcblue;
1930 1923 font-size: 11px;
1931 1924 font-family: @text-regular;
1932 1925 cursor: help;
1933 1926
1934 1927 &:hover {
1935 1928 color: @rcdarkblue;
1936 1929 }
1937 1930
1938 1931 @media (max-width:768px) {
1939 1932 position: relative;
1940 1933 top: auto;
1941 1934 left: auto;
1942 1935 display: block;
1943 1936 }
1944 1937 }
1945 1938
1946 1939
1947 1940 #journal{
1948 1941 margin-bottom: @space;
1949 1942
1950 1943 .journal_day{
1951 1944 margin-bottom: @textmargin/2;
1952 1945 padding-bottom: @textmargin/2;
1953 1946 font-size: @journal-fontsize;
1954 1947 border-bottom: @border-thickness solid @border-default-color;
1955 1948 }
1956 1949
1957 1950 .journal_container{
1958 1951 margin-bottom: @space;
1959 1952
1960 1953 .journal_user{
1961 1954 display: inline-block;
1962 1955 }
1963 1956 .journal_action_container{
1964 1957 display: block;
1965 1958 margin-top: @textmargin;
1966 1959
1967 1960 div{
1968 1961 display: inline;
1969 1962 }
1970 1963
1971 1964 div.journal_action_params{
1972 1965 display: block;
1973 1966 }
1974 1967
1975 1968 div.journal_repo:after{
1976 1969 content: "\A";
1977 1970 white-space: pre;
1978 1971 }
1979 1972
1980 1973 div.date{
1981 1974 display: block;
1982 1975 margin-bottom: @textmargin;
1983 1976 }
1984 1977 }
1985 1978 }
1986 1979 }
1987 1980
1988 1981 // Files
1989 1982 .edit-file-title {
1990 1983 border-bottom: @border-thickness solid @border-default-color;
1991 1984
1992 1985 .breadcrumbs {
1993 1986 margin-bottom: 0;
1994 1987 }
1995 1988 }
1996 1989
1997 1990 .edit-file-fieldset {
1998 1991 margin-top: @sidebarpadding;
1999 1992
2000 1993 .fieldset {
2001 1994 .left-label {
2002 1995 width: 13%;
2003 1996 }
2004 1997 .right-content {
2005 1998 width: 87%;
2006 1999 max-width: 100%;
2007 2000 }
2008 2001 .filename-label {
2009 2002 margin-top: 13px;
2010 2003 }
2011 2004 .commit-message-label {
2012 2005 margin-top: 4px;
2013 2006 }
2014 2007 .file-upload-input {
2015 2008 input {
2016 2009 display: none;
2017 2010 }
2018 2011 }
2019 2012 p {
2020 2013 margin-top: 5px;
2021 2014 }
2022 2015
2023 2016 }
2024 2017 .custom-path-link {
2025 2018 margin-left: 5px;
2026 2019 }
2027 2020 #commit {
2028 2021 resize: vertical;
2029 2022 }
2030 2023 }
2031 2024
2032 2025 .delete-file-preview {
2033 2026 max-height: 250px;
2034 2027 }
2035 2028
2036 2029 .new-file,
2037 2030 #filter_activate,
2038 2031 #filter_deactivate {
2039 2032 float: left;
2040 2033 margin: 0 0 0 15px;
2041 2034 }
2042 2035
2043 2036 h3.files_location{
2044 2037 line-height: 2.4em;
2045 2038 }
2046 2039
2047 2040 .browser-nav {
2048 2041 display: table;
2049 2042 margin-bottom: @space;
2050 2043
2051 2044
2052 2045 .info_box {
2053 2046 display: inline-table;
2054 2047 height: 2.5em;
2055 2048
2056 2049 .browser-cur-rev, .info_box_elem {
2057 2050 display: table-cell;
2058 2051 vertical-align: middle;
2059 2052 }
2060 2053
2061 2054 .info_box_elem {
2062 2055 border-top: @border-thickness solid @rcblue;
2063 2056 border-bottom: @border-thickness solid @rcblue;
2064 2057
2065 2058 #at_rev, a {
2066 2059 padding: 0.6em 0.9em;
2067 2060 margin: 0;
2068 2061 .box-shadow(none);
2069 2062 border: 0;
2070 2063 height: 12px;
2071 2064 }
2072 2065
2073 2066 input#at_rev {
2074 2067 max-width: 50px;
2075 2068 text-align: right;
2076 2069 }
2077 2070
2078 2071 &.previous {
2079 2072 border: @border-thickness solid @rcblue;
2080 2073 .disabled {
2081 2074 color: @grey4;
2082 2075 cursor: not-allowed;
2083 2076 }
2084 2077 }
2085 2078
2086 2079 &.next {
2087 2080 border: @border-thickness solid @rcblue;
2088 2081 .disabled {
2089 2082 color: @grey4;
2090 2083 cursor: not-allowed;
2091 2084 }
2092 2085 }
2093 2086 }
2094 2087
2095 2088 .browser-cur-rev {
2096 2089
2097 2090 span{
2098 2091 margin: 0;
2099 2092 color: @rcblue;
2100 2093 height: 12px;
2101 2094 display: inline-block;
2102 2095 padding: 0.7em 1em ;
2103 2096 border: @border-thickness solid @rcblue;
2104 2097 margin-right: @padding;
2105 2098 }
2106 2099 }
2107 2100 }
2108 2101
2109 2102 .search_activate {
2110 2103 display: table-cell;
2111 2104 vertical-align: middle;
2112 2105
2113 2106 input, label{
2114 2107 margin: 0;
2115 2108 padding: 0;
2116 2109 }
2117 2110
2118 2111 input{
2119 2112 margin-left: @textmargin;
2120 2113 }
2121 2114
2122 2115 }
2123 2116 }
2124 2117
2125 2118 .browser-cur-rev{
2126 2119 margin-bottom: @textmargin;
2127 2120 }
2128 2121
2129 2122 #node_filter_box_loading{
2130 2123 .info_text;
2131 2124 }
2132 2125
2133 2126 .browser-search {
2134 2127 margin: -25px 0px 5px 0px;
2135 2128 }
2136 2129
2137 2130 .node-filter {
2138 2131 font-size: @repo-title-fontsize;
2139 2132 padding: 4px 0px 0px 0px;
2140 2133
2141 2134 .node-filter-path {
2142 2135 float: left;
2143 2136 color: @grey4;
2144 2137 }
2145 2138 .node-filter-input {
2146 2139 float: left;
2147 2140 margin: -2px 0px 0px 2px;
2148 2141 input {
2149 2142 padding: 2px;
2150 2143 border: none;
2151 2144 font-size: @repo-title-fontsize;
2152 2145 }
2153 2146 }
2154 2147 }
2155 2148
2156 2149
2157 2150 .browser-result{
2158 2151 td a{
2159 2152 margin-left: 0.5em;
2160 2153 display: inline-block;
2161 2154
2162 2155 em{
2163 2156 font-family: @text-bold;
2164 2157 }
2165 2158 }
2166 2159 }
2167 2160
2168 2161 .browser-highlight{
2169 2162 background-color: @grey5-alpha;
2170 2163 }
2171 2164
2172 2165
2173 2166 // Search
2174 2167
2175 2168 .search-form{
2176 2169 #q {
2177 2170 width: @search-form-width;
2178 2171 }
2179 2172 .fields{
2180 2173 margin: 0 0 @space;
2181 2174 }
2182 2175
2183 2176 label{
2184 2177 display: inline-block;
2185 2178 margin-right: @textmargin;
2186 2179 padding-top: 0.25em;
2187 2180 }
2188 2181
2189 2182
2190 2183 .results{
2191 2184 clear: both;
2192 2185 margin: 0 0 @padding;
2193 2186 }
2194 2187 }
2195 2188
2196 2189 div.search-feedback-items {
2197 2190 display: inline-block;
2198 2191 padding:0px 0px 0px 96px;
2199 2192 }
2200 2193
2201 2194 div.search-code-body {
2202 2195 background-color: #ffffff; padding: 5px 0 5px 10px;
2203 2196 pre {
2204 2197 .match { background-color: #faffa6;}
2205 2198 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2206 2199 }
2207 2200 }
2208 2201
2209 2202 .expand_commit.search {
2210 2203 .show_more.open {
2211 2204 height: auto;
2212 2205 max-height: none;
2213 2206 }
2214 2207 }
2215 2208
2216 2209 .search-results {
2217 2210
2218 2211 h2 {
2219 2212 margin-bottom: 0;
2220 2213 }
2221 2214 .codeblock {
2222 2215 border: none;
2223 2216 background: transparent;
2224 2217 }
2225 2218
2226 2219 .codeblock-header {
2227 2220 border: none;
2228 2221 background: transparent;
2229 2222 }
2230 2223
2231 2224 .code-body {
2232 2225 border: @border-thickness solid @border-default-color;
2233 2226 .border-radius(@border-radius);
2234 2227 }
2235 2228
2236 2229 .td-commit {
2237 2230 &:extend(pre);
2238 2231 border-bottom: @border-thickness solid @border-default-color;
2239 2232 }
2240 2233
2241 2234 .message {
2242 2235 height: auto;
2243 2236 max-width: 350px;
2244 2237 white-space: normal;
2245 2238 text-overflow: initial;
2246 2239 overflow: visible;
2247 2240
2248 2241 .match { background-color: #faffa6;}
2249 2242 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2250 2243 }
2251 2244
2252 2245 }
2253 2246
2254 2247 table.rctable td.td-search-results div {
2255 2248 max-width: 100%;
2256 2249 }
2257 2250
2258 2251 #tip-box, .tip-box{
2259 2252 padding: @menupadding/2;
2260 2253 display: block;
2261 2254 border: @border-thickness solid @border-highlight-color;
2262 2255 .border-radius(@border-radius);
2263 2256 background-color: white;
2264 2257 z-index: 99;
2265 2258 white-space: pre-wrap;
2266 2259 }
2267 2260
2268 2261 #linktt {
2269 2262 width: 79px;
2270 2263 }
2271 2264
2272 2265 #help_kb .modal-content{
2273 2266 max-width: 750px;
2274 2267 margin: 10% auto;
2275 2268
2276 2269 table{
2277 2270 td,th{
2278 2271 border-bottom: none;
2279 2272 line-height: 2.5em;
2280 2273 }
2281 2274 th{
2282 2275 padding-bottom: @textmargin/2;
2283 2276 }
2284 2277 td.keys{
2285 2278 text-align: center;
2286 2279 }
2287 2280 }
2288 2281
2289 2282 .block-left{
2290 2283 width: 45%;
2291 2284 margin-right: 5%;
2292 2285 }
2293 2286 .modal-footer{
2294 2287 clear: both;
2295 2288 }
2296 2289 .key.tag{
2297 2290 padding: 0.5em;
2298 2291 background-color: @rcblue;
2299 2292 color: white;
2300 2293 border-color: @rcblue;
2301 2294 .box-shadow(none);
2302 2295 }
2303 2296 }
2304 2297
2305 2298
2306 2299
2307 2300 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2308 2301
2309 2302 @import 'statistics-graph';
2310 2303 @import 'tables';
2311 2304 @import 'forms';
2312 2305 @import 'diff';
2313 2306 @import 'summary';
2314 2307 @import 'navigation';
2315 2308
2316 2309 //--- SHOW/HIDE SECTIONS --//
2317 2310
2318 2311 .btn-collapse {
2319 2312 float: right;
2320 2313 text-align: right;
2321 2314 font-family: @text-light;
2322 2315 font-size: @basefontsize;
2323 2316 cursor: pointer;
2324 2317 border: none;
2325 2318 color: @rcblue;
2326 2319 }
2327 2320
2328 2321 table.rctable,
2329 2322 table.dataTable {
2330 2323 .btn-collapse {
2331 2324 float: right;
2332 2325 text-align: right;
2333 2326 }
2334 2327 }
2335 2328
2336 2329
2337 2330 // TODO: johbo: Fix for IE10, this avoids that we see a border
2338 2331 // and padding around checkboxes and radio boxes. Move to the right place,
2339 2332 // or better: Remove this once we did the form refactoring.
2340 2333 input[type=checkbox],
2341 2334 input[type=radio] {
2342 2335 padding: 0;
2343 2336 border: none;
2344 2337 }
2345 2338
2346 2339 .toggle-ajax-spinner{
2347 2340 height: 16px;
2348 2341 width: 16px;
2349 2342 }
@@ -1,342 +1,343 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Pull request reviewers
21 21 */
22 22 var removeReviewMember = function(reviewer_id, mark_delete){
23 23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 24
25 25 if(typeof(mark_delete) === undefined){
26 26 mark_delete = false;
27 27 }
28 28
29 29 if(mark_delete === true){
30 30 if (reviewer){
31 // mark as to-remove
31 // now delete the input
32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
33 // mark as to-delete
32 34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 35 obj.addClass('to-delete');
34 // now delete the input
35 $('#reviewer_{0} input'.format(reviewer_id)).remove();
36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
36 37 }
37 38 }
38 39 else{
39 40 $('#reviewer_{0}'.format(reviewer_id)).remove();
40 41 }
41 42 };
42 43
43 44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 45 var members = $('#review_members').get(0);
45 46 var reasons_html = '';
46 47 var reasons_inputs = '';
47 48 var reasons = reasons || [];
48 49 if (reasons) {
49 50 for (var i = 0; i < reasons.length; i++) {
50 51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
51 52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
52 53 }
53 54 }
54 55 var tmpl = '<li id="reviewer_{2}">'+
55 56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
56 57 '<div class="reviewer_status">'+
57 58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
58 59 '</div>'+
59 60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
60 61 '<span class="reviewer_name user">{1}</span>'+
61 62 reasons_html +
62 63 '<input type="hidden" name="user_id" value="{2}">'+
63 64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
64 65 '{3}'+
65 66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
66 67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
67 68 '<i class="icon-remove-sign"></i>'+
68 69 '</div>'+
69 70 '</div>'+
70 71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
71 72 '</li>' ;
72 73
73 74 var displayname = "{0} ({1} {2})".format(
74 75 nname, escapeHtml(fname), escapeHtml(lname));
75 76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
76 77 // check if we don't have this ID already in
77 78 var ids = [];
78 79 var _els = $('#review_members li').toArray();
79 80 for (el in _els){
80 81 ids.push(_els[el].id)
81 82 }
82 83 if(ids.indexOf('reviewer_'+id) == -1){
83 84 // only add if it's not there
84 85 members.innerHTML += element;
85 86 }
86 87
87 88 };
88 89
89 90 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
90 91 var url = pyroutes.url(
91 92 'pullrequest_update',
92 93 {"repo_name": repo_name, "pull_request_id": pull_request_id});
93 94 if (typeof postData === 'string' ) {
94 95 postData += '&csrf_token=' + CSRF_TOKEN;
95 96 } else {
96 97 postData.csrf_token = CSRF_TOKEN;
97 98 }
98 99 var success = function(o) {
99 100 window.location.reload();
100 101 };
101 102 ajaxPOST(url, postData, success);
102 103 };
103 104
104 105 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
105 106 if (reviewers_ids === undefined){
106 107 var postData = '_method=put&' + $('#reviewers input').serialize();
107 108 _updatePullRequest(repo_name, pull_request_id, postData);
108 109 }
109 110 };
110 111
111 112 /**
112 113 * PULL REQUEST reject & close
113 114 */
114 115 var closePullRequest = function(repo_name, pull_request_id) {
115 116 var postData = {
116 117 '_method': 'put',
117 118 'close_pull_request': true};
118 119 _updatePullRequest(repo_name, pull_request_id, postData);
119 120 };
120 121
121 122 /**
122 123 * PULL REQUEST update commits
123 124 */
124 125 var updateCommits = function(repo_name, pull_request_id) {
125 126 var postData = {
126 127 '_method': 'put',
127 128 'update_commits': true};
128 129 _updatePullRequest(repo_name, pull_request_id, postData);
129 130 };
130 131
131 132
132 133 /**
133 134 * PULL REQUEST edit info
134 135 */
135 136 var editPullRequest = function(repo_name, pull_request_id, title, description) {
136 137 var url = pyroutes.url(
137 138 'pullrequest_update',
138 139 {"repo_name": repo_name, "pull_request_id": pull_request_id});
139 140
140 141 var postData = {
141 142 '_method': 'put',
142 143 'title': title,
143 144 'description': description,
144 145 'edit_pull_request': true,
145 146 'csrf_token': CSRF_TOKEN
146 147 };
147 148 var success = function(o) {
148 149 window.location.reload();
149 150 };
150 151 ajaxPOST(url, postData, success);
151 152 };
152 153
153 154 var initPullRequestsCodeMirror = function (textAreaId) {
154 155 var ta = $(textAreaId).get(0);
155 156 var initialHeight = '100px';
156 157
157 158 // default options
158 159 var codeMirrorOptions = {
159 160 mode: "text",
160 161 lineNumbers: false,
161 162 indentUnit: 4,
162 163 theme: 'rc-input'
163 164 };
164 165
165 166 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
166 167 // marker for manually set description
167 168 codeMirrorInstance._userDefinedDesc = false;
168 169 codeMirrorInstance.setSize(null, initialHeight);
169 170 codeMirrorInstance.on("change", function(instance, changeObj) {
170 171 var height = initialHeight;
171 172 var lines = instance.lineCount();
172 173 if (lines > 6 && lines < 20) {
173 174 height = "auto"
174 175 }
175 176 else if (lines >= 20) {
176 177 height = 20 * 15;
177 178 }
178 179 instance.setSize(null, height);
179 180
180 181 // detect if the change was trigger by auto desc, or user input
181 182 changeOrigin = changeObj.origin;
182 183
183 184 if (changeOrigin === "setValue") {
184 185 cmLog.debug('Change triggered by setValue');
185 186 }
186 187 else {
187 188 cmLog.debug('user triggered change !');
188 189 // set special marker to indicate user has created an input.
189 190 instance._userDefinedDesc = true;
190 191 }
191 192
192 193 });
193 194
194 195 return codeMirrorInstance
195 196 };
196 197
197 198 /**
198 199 * Reviewer autocomplete
199 200 */
200 201 var ReviewerAutoComplete = function(input_id) {
201 202 $('#'+input_id).autocomplete({
202 203 serviceUrl: pyroutes.url('user_autocomplete_data'),
203 204 minChars:2,
204 205 maxHeight:400,
205 206 deferRequestBy: 300, //miliseconds
206 207 showNoSuggestionNotice: true,
207 208 tabDisabled: true,
208 209 autoSelectFirst: true,
209 210 formatResult: autocompleteFormatResult,
210 211 lookupFilter: autocompleteFilterResult,
211 212 onSelect: function(suggestion, data){
212 213 var msg = _gettext('added manually by "{0}"');
213 214 var reasons = [msg.format(templateContext.rhodecode_user.username)];
214 215 addReviewMember(data.id, data.first_name, data.last_name,
215 216 data.username, data.icon_link, reasons);
216 217 $('#'+input_id).val('');
217 218 }
218 219 });
219 220 };
220 221
221 222
222 223 VersionController = function () {
223 224 var self = this;
224 225 this.$verSource = $('input[name=ver_source]');
225 226 this.$verTarget = $('input[name=ver_target]');
226 227 this.$showVersionDiff = $('#show-version-diff');
227 228
228 229 this.adjustRadioSelectors = function (curNode) {
229 230 var getVal = function (item) {
230 231 if (item == 'latest') {
231 232 return Number.MAX_SAFE_INTEGER
232 233 }
233 234 else {
234 235 return parseInt(item)
235 236 }
236 237 };
237 238
238 239 var curVal = getVal($(curNode).val());
239 240 var cleared = false;
240 241
241 242 $.each(self.$verSource, function (index, value) {
242 243 var elVal = getVal($(value).val());
243 244
244 245 if (elVal > curVal) {
245 246 if ($(value).is(':checked')) {
246 247 cleared = true;
247 248 }
248 249 $(value).attr('disabled', 'disabled');
249 250 $(value).removeAttr('checked');
250 251 $(value).css({'opacity': 0.1});
251 252 }
252 253 else {
253 254 $(value).css({'opacity': 1});
254 255 $(value).removeAttr('disabled');
255 256 }
256 257 });
257 258
258 259 if (cleared) {
259 260 // if we unchecked an active, set the next one to same loc.
260 261 $(this.$verSource).filter('[value={0}]'.format(
261 262 curVal)).attr('checked', 'checked');
262 263 }
263 264
264 265 self.setLockAction(false,
265 266 $(curNode).data('verPos'),
266 267 $(this.$verSource).filter(':checked').data('verPos')
267 268 );
268 269 };
269 270
270 271
271 272 this.attachVersionListener = function () {
272 273 self.$verTarget.change(function (e) {
273 274 self.adjustRadioSelectors(this)
274 275 });
275 276 self.$verSource.change(function (e) {
276 277 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
277 278 });
278 279 };
279 280
280 281 this.init = function () {
281 282
282 283 var curNode = self.$verTarget.filter(':checked');
283 284 self.adjustRadioSelectors(curNode);
284 285 self.setLockAction(true);
285 286 self.attachVersionListener();
286 287
287 288 };
288 289
289 290 this.setLockAction = function (state, selectedVersion, otherVersion) {
290 291 var $showVersionDiff = this.$showVersionDiff;
291 292
292 293 if (state) {
293 294 $showVersionDiff.attr('disabled', 'disabled');
294 295 $showVersionDiff.addClass('disabled');
295 296 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
296 297 }
297 298 else {
298 299 $showVersionDiff.removeAttr('disabled');
299 300 $showVersionDiff.removeClass('disabled');
300 301
301 302 if (selectedVersion == otherVersion) {
302 303 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
303 304 } else {
304 305 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
305 306 }
306 307 }
307 308
308 309 };
309 310
310 311 this.showVersionDiff = function () {
311 312 var target = self.$verTarget.filter(':checked');
312 313 var source = self.$verSource.filter(':checked');
313 314
314 315 if (target.val() && source.val()) {
315 316 var params = {
316 317 'pull_request_id': templateContext.pull_request_data.pull_request_id,
317 318 'repo_name': templateContext.repo_name,
318 319 'version': target.val(),
319 320 'from_version': source.val()
320 321 };
321 322 window.location = pyroutes.url('pullrequest_show', params)
322 323 }
323 324
324 325 return false;
325 326 };
326 327
327 328 this.toggleVersionView = function (elem) {
328 329
329 330 if (this.$showVersionDiff.is(':visible')) {
330 331 $('.version-pr').hide();
331 332 this.$showVersionDiff.hide();
332 333 $(elem).html($(elem).data('toggleOn'))
333 334 } else {
334 335 $('.version-pr').show();
335 336 this.$showVersionDiff.show();
336 337 $(elem).html($(elem).data('toggleOff'))
337 338 }
338 339
339 340 return false
340 341 }
341 342
342 343 }; No newline at end of file
@@ -1,818 +1,821 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 </div>
87 87 <div class="pr-pullinfo">
88 88 %if h.is_hg(c.pull_request.source_repo):
89 89 <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">
90 90 %elif h.is_git(c.pull_request.source_repo):
91 91 <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">
92 92 %endif
93 93 </div>
94 94 </div>
95 95 </div>
96 96 <div class="field">
97 97 <div class="label-summary">
98 98 <label>${_('Target')}:</label>
99 99 </div>
100 100 <div class="input">
101 101 <div class="pr-targetinfo">
102 102 ## branch link is only valid if it is a branch
103 103 <span class="tag">
104 104 %if c.pull_request.target_ref_parts.type == 'branch':
105 105 <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>
106 106 %else:
107 107 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
108 108 %endif
109 109 </span>
110 110 <span class="clone-url">
111 111 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
112 112 </span>
113 113 </div>
114 114 </div>
115 115 </div>
116 116
117 117 ## Link to the shadow repository.
118 118 <div class="field">
119 119 <div class="label-summary">
120 120 <label>${_('Merge')}:</label>
121 121 </div>
122 122 <div class="input">
123 123 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
124 124 <div class="pr-mergeinfo">
125 125 %if h.is_hg(c.pull_request.target_repo):
126 126 <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">
127 127 %elif h.is_git(c.pull_request.target_repo):
128 128 <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">
129 129 %endif
130 130 </div>
131 131 % else:
132 132 <div class="">
133 133 ${_('Shadow repository data not available')}.
134 134 </div>
135 135 % endif
136 136 </div>
137 137 </div>
138 138
139 139 <div class="field">
140 140 <div class="label-summary">
141 141 <label>${_('Review')}:</label>
142 142 </div>
143 143 <div class="input">
144 144 %if c.pull_request_review_status:
145 145 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
146 146 <span class="changeset-status-lbl tooltip">
147 147 %if c.pull_request.is_closed():
148 148 ${_('Closed')},
149 149 %endif
150 150 ${h.commit_status_lbl(c.pull_request_review_status)}
151 151 </span>
152 152 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
153 153 %endif
154 154 </div>
155 155 </div>
156 156 <div class="field">
157 157 <div class="pr-description-label label-summary">
158 158 <label>${_('Description')}:</label>
159 159 </div>
160 160 <div id="pr-desc" class="input">
161 161 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
162 162 </div>
163 163 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
164 164 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
165 165 </div>
166 166 </div>
167 167
168 168 <div class="field">
169 169 <div class="label-summary">
170 170 <label>${_('Versions')}:</label>
171 171 </div>
172 172
173 173 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
174 174 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
175 175
176 176 <div class="pr-versions">
177 177 % if c.show_version_changes:
178 178 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
180 180 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
181 181 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))}"
182 182 data-toggle-off="${_('Hide all versions of this pull request')}">
183 183 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
184 184 </a>
185 185 <table>
186 186 ## SHOW ALL VERSIONS OF PR
187 187 <% ver_pr = None %>
188 188
189 189 % for data in reversed(list(enumerate(c.versions, 1))):
190 190 <% ver_pos = data[0] %>
191 191 <% ver = data[1] %>
192 192 <% ver_pr = ver.pull_request_version_id %>
193 193 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
194 194
195 195 <tr class="version-pr" style="display: ${display_row}">
196 196 <td>
197 197 <code>
198 198 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
199 199 </code>
200 200 </td>
201 201 <td>
202 202 <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}"/>
203 203 <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}"/>
204 204 </td>
205 205 <td>
206 206 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
207 207 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
208 208 </div>
209 209 </td>
210 210 <td>
211 211 % if c.at_version_num != ver_pr:
212 212 <i class="icon-comment"></i>
213 213 <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']))}">
214 214 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
215 215 </code>
216 216 % endif
217 217 </td>
218 218 <td>
219 219 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
220 220 </td>
221 221 <td>
222 222 ${h.age_component(ver.updated_on, time_is_local=True)}
223 223 </td>
224 224 </tr>
225 225 % endfor
226 226
227 227 <tr>
228 228 <td colspan="6">
229 229 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
230 230 data-label-text-locked="${_('select versions to show changes')}"
231 231 data-label-text-diff="${_('show changes between versions')}"
232 232 data-label-text-show="${_('show pull request for this version')}"
233 233 >
234 234 ${_('select versions to show changes')}
235 235 </button>
236 236 </td>
237 237 </tr>
238 238
239 239 ## show comment/inline comments summary
240 240 <%def name="comments_summary()">
241 241 <tr>
242 242 <td colspan="6" class="comments-summary-td">
243 243
244 244 % if c.at_version:
245 245 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
246 246 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
247 247 ${_('Comments at this version')}:
248 248 % else:
249 249 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
250 250 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
251 251 ${_('Comments for this pull request')}:
252 252 % endif
253 253
254 254
255 255 %if general_comm_count_ver:
256 256 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
257 257 %else:
258 258 ${_("%d General ") % general_comm_count_ver}
259 259 %endif
260 260
261 261 %if inline_comm_count_ver:
262 262 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
263 263 %else:
264 264 , ${_("%d Inline") % inline_comm_count_ver}
265 265 %endif
266 266
267 267 %if outdated_comm_count_ver:
268 268 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
269 269 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
270 270 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
271 271 %else:
272 272 , ${_("%d Outdated") % outdated_comm_count_ver}
273 273 %endif
274 274 </td>
275 275 </tr>
276 276 </%def>
277 277 ${comments_summary()}
278 278 </table>
279 279 % else:
280 280 <div class="input">
281 281 ${_('Pull request versions not available')}.
282 282 </div>
283 283 <div>
284 284 <table>
285 285 ${comments_summary()}
286 286 </table>
287 287 </div>
288 288 % endif
289 289 </div>
290 290 </div>
291 291
292 292 <div id="pr-save" class="field" style="display: none;">
293 293 <div class="label-summary"></div>
294 294 <div class="input">
295 295 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
296 296 </div>
297 297 </div>
298 298 </div>
299 299 </div>
300 300 <div>
301 301 ## AUTHOR
302 302 <div class="reviewers-title block-right">
303 303 <div class="pr-details-title">
304 304 ${_('Author')}
305 305 </div>
306 306 </div>
307 307 <div class="block-right pr-details-content reviewers">
308 308 <ul class="group_members">
309 309 <li>
310 310 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
311 311 </li>
312 312 </ul>
313 313 </div>
314 314 ## REVIEWERS
315 315 <div class="reviewers-title block-right">
316 316 <div class="pr-details-title">
317 317 ${_('Pull request reviewers')}
318 318 %if c.allowed_to_update:
319 319 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
320 320 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
321 321 %endif
322 322 </div>
323 323 </div>
324 324 <div id="reviewers" class="block-right pr-details-content reviewers">
325 325 ## members goes here !
326 326 <input type="hidden" name="__start__" value="review_members:sequence">
327 327 <ul id="review_members" class="group_members">
328 328 %for member,reasons,status in c.pull_request_reviewers:
329 329 <li id="reviewer_${member.user_id}">
330 330 <div class="reviewers_member">
331 331 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
332 332 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
333 333 </div>
334 334 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
335 335 ${self.gravatar_with_user(member.email, 16)}
336 336 </div>
337 337 <input type="hidden" name="__start__" value="reviewer:mapping">
338 338 <input type="hidden" name="__start__" value="reasons:sequence">
339 339 %for reason in reasons:
340 340 <div class="reviewer_reason">- ${reason}</div>
341 341 <input type="hidden" name="reason" value="${reason}">
342 342
343 343 %endfor
344 344 <input type="hidden" name="__end__" value="reasons:sequence">
345 345 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
346 346 <input type="hidden" name="__end__" value="reviewer:mapping">
347 347 %if c.allowed_to_update:
348 348 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
349 349 <i class="icon-remove-sign" ></i>
350 350 </div>
351 351 %endif
352 352 </div>
353 353 </li>
354 354 %endfor
355 355 </ul>
356 356 <input type="hidden" name="__end__" value="review_members:sequence">
357 357 %if not c.pull_request.is_closed():
358 358 <div id="add_reviewer_input" class='ac' style="display: none;">
359 359 %if c.allowed_to_update:
360 360 <div class="reviewer_ac">
361 361 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
362 362 <div id="reviewers_container"></div>
363 363 </div>
364 364 <div>
365 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
365 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
366 366 </div>
367 367 %endif
368 368 </div>
369 369 %endif
370 370 </div>
371 371 </div>
372 372 </div>
373 373 <div class="box">
374 374 ##DIFF
375 375 <div class="table" >
376 376 <div id="changeset_compare_view_content">
377 377 ##CS
378 378 % if c.missing_requirements:
379 379 <div class="box">
380 380 <div class="alert alert-warning">
381 381 <div>
382 382 <strong>${_('Missing requirements:')}</strong>
383 383 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
384 384 </div>
385 385 </div>
386 386 </div>
387 387 % elif c.missing_commits:
388 388 <div class="box">
389 389 <div class="alert alert-warning">
390 390 <div>
391 391 <strong>${_('Missing commits')}:</strong>
392 392 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
393 393 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
394 394 </div>
395 395 </div>
396 396 </div>
397 397 % endif
398 398
399 399 <div class="compare_view_commits_title">
400 400 % if not c.compare_mode:
401 401
402 402 % if c.at_version_pos:
403 403 <h4>
404 404 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
405 405 </h4>
406 406 % endif
407 407
408 408 <div class="pull-left">
409 409 <div class="btn-group">
410 410 <a
411 411 class="btn"
412 412 href="#"
413 413 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
414 414 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
415 415 </a>
416 416 <a
417 417 class="btn"
418 418 href="#"
419 419 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
420 420 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
421 421 </a>
422 422 </div>
423 423 </div>
424 424
425 425 <div class="pull-right">
426 426 % if c.allowed_to_update and not c.pull_request.is_closed():
427 427 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
428 428 % else:
429 429 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
430 430 % endif
431 431
432 432 </div>
433 433 % endif
434 434 </div>
435 435
436 436 % if not c.missing_commits:
437 437 % if c.compare_mode:
438 438 % if c.at_version:
439 439 <h4>
440 440 ${_('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')}:
441 441 </h4>
442 442
443 443 <div class="subtitle-compare">
444 444 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
445 445 </div>
446 446
447 447 <div class="container">
448 448 <table class="rctable compare_view_commits">
449 449 <tr>
450 450 <th></th>
451 451 <th>${_('Time')}</th>
452 452 <th>${_('Author')}</th>
453 453 <th>${_('Commit')}</th>
454 454 <th></th>
455 455 <th>${_('Description')}</th>
456 456 </tr>
457 457
458 458 % for c_type, commit in c.commit_changes:
459 459 % if c_type in ['a', 'r']:
460 460 <%
461 461 if c_type == 'a':
462 462 cc_title = _('Commit added in displayed changes')
463 463 elif c_type == 'r':
464 464 cc_title = _('Commit removed in displayed changes')
465 465 else:
466 466 cc_title = ''
467 467 %>
468 468 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
469 469 <td>
470 470 <div class="commit-change-indicator color-${c_type}-border">
471 471 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
472 472 ${c_type.upper()}
473 473 </div>
474 474 </div>
475 475 </td>
476 476 <td class="td-time">
477 477 ${h.age_component(commit.date)}
478 478 </td>
479 479 <td class="td-user">
480 480 ${base.gravatar_with_user(commit.author, 16)}
481 481 </td>
482 482 <td class="td-hash">
483 483 <code>
484 484 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
485 485 r${commit.revision}:${h.short_id(commit.raw_id)}
486 486 </a>
487 487 ${h.hidden('revisions', commit.raw_id)}
488 488 </code>
489 489 </td>
490 490 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
491 491 <div class="show_more_col">
492 492 <i class="show_more"></i>
493 493 </div>
494 494 </td>
495 495 <td class="mid td-description">
496 496 <div class="log-container truncate-wrap">
497 497 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
498 498 ${h.urlify_commit_message(commit.message, c.repo_name)}
499 499 </div>
500 500 </div>
501 501 </td>
502 502 </tr>
503 503 % endif
504 504 % endfor
505 505 </table>
506 506 </div>
507 507
508 508 <script>
509 509 $('.expand_commit').on('click',function(e){
510 510 var target_expand = $(this);
511 511 var cid = target_expand.data('commitId');
512 512
513 513 if (target_expand.hasClass('open')){
514 514 $('#c-'+cid).css({
515 515 'height': '1.5em',
516 516 'white-space': 'nowrap',
517 517 'text-overflow': 'ellipsis',
518 518 'overflow':'hidden'
519 519 });
520 520 target_expand.removeClass('open');
521 521 }
522 522 else {
523 523 $('#c-'+cid).css({
524 524 'height': 'auto',
525 525 'white-space': 'pre-line',
526 526 'text-overflow': 'initial',
527 527 'overflow':'visible'
528 528 });
529 529 target_expand.addClass('open');
530 530 }
531 531 });
532 532 </script>
533 533
534 534 % endif
535 535
536 536 % else:
537 537 <%include file="/compare/compare_commits.mako" />
538 538 % endif
539 539
540 540 <div class="cs_files">
541 541 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
542 542 ${cbdiffs.render_diffset_menu()}
543 543 ${cbdiffs.render_diffset(
544 544 c.diffset, use_comments=True,
545 545 collapse_when_files_over=30,
546 546 disable_new_comments=not c.allowed_to_comment,
547 547 deleted_files_comments=c.deleted_files_comments)}
548 548 </div>
549 549 % else:
550 550 ## skipping commits we need to clear the view for missing commits
551 551 <div style="clear:both;"></div>
552 552 % endif
553 553
554 554 </div>
555 555 </div>
556 556
557 557 ## template for inline comment form
558 558 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
559 559
560 560 ## render general comments
561 561
562 562 <div id="comment-tr-show">
563 563 <div class="comment">
564 564 % if general_outdated_comm_count_ver:
565 565 <div class="meta">
566 566 % if general_outdated_comm_count_ver == 1:
567 567 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
568 568 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
569 569 % else:
570 570 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
571 571 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
572 572 % endif
573 573 </div>
574 574 % endif
575 575 </div>
576 576 </div>
577 577
578 578 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
579 579
580 580 % if not c.pull_request.is_closed():
581 581 ## merge status, and merge action
582 582 <div class="pull-request-merge">
583 583 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
584 584 </div>
585 585
586 586 ## main comment form and it status
587 587 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
588 588 pull_request_id=c.pull_request.pull_request_id),
589 589 c.pull_request_review_status,
590 590 is_pull_request=True, change_status=c.allowed_to_change_status)}
591 591 %endif
592 592
593 593 <script type="text/javascript">
594 594 if (location.hash) {
595 595 var result = splitDelimitedHash(location.hash);
596 596 var line = $('html').find(result.loc);
597 597 // show hidden comments if we use location.hash
598 598 if (line.hasClass('comment-general')) {
599 599 $(line).show();
600 600 } else if (line.hasClass('comment-inline')) {
601 601 $(line).show();
602 602 var $cb = $(line).closest('.cb');
603 603 $cb.removeClass('cb-collapsed')
604 604 }
605 605 if (line.length > 0){
606 606 offsetScroll(line, 70);
607 607 }
608 608 }
609 609
610 610 versionController = new VersionController();
611 611 versionController.init();
612 612
613 613
614 614 $(function(){
615 615 ReviewerAutoComplete('user');
616 616 // custom code mirror
617 617 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
618 618
619 619 var PRDetails = {
620 620 editButton: $('#open_edit_pullrequest'),
621 621 closeButton: $('#close_edit_pullrequest'),
622 622 deleteButton: $('#delete_pullrequest'),
623 623 viewFields: $('#pr-desc, #pr-title'),
624 624 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
625 625
626 626 init: function() {
627 627 var that = this;
628 628 this.editButton.on('click', function(e) { that.edit(); });
629 629 this.closeButton.on('click', function(e) { that.view(); });
630 630 },
631 631
632 632 edit: function(event) {
633 633 this.viewFields.hide();
634 634 this.editButton.hide();
635 635 this.deleteButton.hide();
636 636 this.closeButton.show();
637 637 this.editFields.show();
638 638 codeMirrorInstance.refresh();
639 639 },
640 640
641 641 view: function(event) {
642 642 this.editButton.show();
643 643 this.deleteButton.show();
644 644 this.editFields.hide();
645 645 this.closeButton.hide();
646 646 this.viewFields.show();
647 647 }
648 648 };
649 649
650 650 var ReviewersPanel = {
651 651 editButton: $('#open_edit_reviewers'),
652 652 closeButton: $('#close_edit_reviewers'),
653 653 addButton: $('#add_reviewer_input'),
654 654 removeButtons: $('.reviewer_member_remove'),
655 655
656 656 init: function() {
657 657 var that = this;
658 658 this.editButton.on('click', function(e) { that.edit(); });
659 659 this.closeButton.on('click', function(e) { that.close(); });
660 660 },
661 661
662 662 edit: function(event) {
663 663 this.editButton.hide();
664 664 this.closeButton.show();
665 665 this.addButton.show();
666 666 this.removeButtons.css('visibility', 'visible');
667 667 },
668 668
669 669 close: function(event) {
670 670 this.editButton.show();
671 671 this.closeButton.hide();
672 672 this.addButton.hide();
673 673 this.removeButtons.css('visibility', 'hidden');
674 674 }
675 675 };
676 676
677 677 PRDetails.init();
678 678 ReviewersPanel.init();
679 679
680 680 showOutdated = function(self){
681 681 $('.comment-inline.comment-outdated').show();
682 682 $('.filediff-outdated').show();
683 683 $('.showOutdatedComments').hide();
684 684 $('.hideOutdatedComments').show();
685 685 };
686 686
687 687 hideOutdated = function(self){
688 688 $('.comment-inline.comment-outdated').hide();
689 689 $('.filediff-outdated').hide();
690 690 $('.hideOutdatedComments').hide();
691 691 $('.showOutdatedComments').show();
692 692 };
693 693
694 694 refreshMergeChecks = function(){
695 695 var loadUrl = "${h.url.current(merge_checks=1)}";
696 696 $('.pull-request-merge').css('opacity', 0.3);
697 697 $('.action-buttons-extra').css('opacity', 0.3);
698 698
699 699 $('.pull-request-merge').load(
700 700 loadUrl, function() {
701 701 $('.pull-request-merge').css('opacity', 1);
702 702
703 703 $('.action-buttons-extra').css('opacity', 1);
704 704 injectCloseAction();
705 705 }
706 706 );
707 707 };
708 708
709 709 injectCloseAction = function() {
710 710 var closeAction = $('#close-pull-request-action').html();
711 711 var $actionButtons = $('.action-buttons-extra');
712 712 // clear the action before
713 713 $actionButtons.html("");
714 714 $actionButtons.html(closeAction);
715 715 };
716 716
717 717 closePullRequest = function (status) {
718 718 // inject closing flag
719 719 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
720 720 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
721 721 $(generalCommentForm.submitForm).submit();
722 722 };
723 723
724 724 $('#show-outdated-comments').on('click', function(e){
725 725 var button = $(this);
726 726 var outdated = $('.comment-outdated');
727 727
728 728 if (button.html() === "(Show)") {
729 729 button.html("(Hide)");
730 730 outdated.show();
731 731 } else {
732 732 button.html("(Show)");
733 733 outdated.hide();
734 734 }
735 735 });
736 736
737 737 $('.show-inline-comments').on('change', function(e){
738 738 var show = 'none';
739 739 var target = e.currentTarget;
740 740 if(target.checked){
741 741 show = ''
742 742 }
743 743 var boxid = $(target).attr('id_for');
744 744 var comments = $('#{0} .inline-comments'.format(boxid));
745 745 var fn_display = function(idx){
746 746 $(this).css('display', show);
747 747 };
748 748 $(comments).each(fn_display);
749 749 var btns = $('#{0} .inline-comments-button'.format(boxid));
750 750 $(btns).each(fn_display);
751 751 });
752 752
753 753 $('#merge_pull_request_form').submit(function() {
754 754 if (!$('#merge_pull_request').attr('disabled')) {
755 755 $('#merge_pull_request').attr('disabled', 'disabled');
756 756 }
757 757 return true;
758 758 });
759 759
760 760 $('#edit_pull_request').on('click', function(e){
761 761 var title = $('#pr-title-input').val();
762 762 var description = codeMirrorInstance.getValue();
763 763 editPullRequest(
764 764 "${c.repo_name}", "${c.pull_request.pull_request_id}",
765 765 title, description);
766 766 });
767 767
768 768 $('#update_pull_request').on('click', function(e){
769 $(this).attr('disabled', 'disabled');
770 $(this).addClass('disabled');
771 $(this).html(_gettext('saving...'));
769 772 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
770 773 });
771 774
772 775 $('#update_commits').on('click', function(e){
773 776 var isDisabled = !$(e.currentTarget).attr('disabled');
774 777 $(e.currentTarget).text(_gettext('Updating...'));
775 778 $(e.currentTarget).attr('disabled', 'disabled');
776 779 if(isDisabled){
777 780 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
778 781 }
779 782
780 783 });
781 784 // fixing issue with caches on firefox
782 785 $('#update_commits').removeAttr("disabled");
783 786
784 787 $('#close_pull_request').on('click', function(e){
785 788 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
786 789 });
787 790
788 791 $('.show-inline-comments').on('click', function(e){
789 792 var boxid = $(this).attr('data-comment-id');
790 793 var button = $(this);
791 794
792 795 if(button.hasClass("comments-visible")) {
793 796 $('#{0} .inline-comments'.format(boxid)).each(function(index){
794 797 $(this).hide();
795 798 });
796 799 button.removeClass("comments-visible");
797 800 } else {
798 801 $('#{0} .inline-comments'.format(boxid)).each(function(index){
799 802 $(this).show();
800 803 });
801 804 button.addClass("comments-visible");
802 805 }
803 806 });
804 807
805 808 // register submit callback on commentForm form to track TODOs
806 809 window.commentFormGlobalSubmitSuccessCallback = function(){
807 810 refreshMergeChecks();
808 811 };
809 812 // initial injection
810 813 injectCloseAction();
811 814
812 815 })
813 816 </script>
814 817
815 818 </div>
816 819 </div>
817 820
818 821 </%def>
General Comments 0
You need to be logged in to leave comments. Login now