##// END OF EJS Templates
translation: unified usage of pluralize function ungettext....
marcink -
r1945:8e51a936 default
parent child Browse files
Show More
@@ -1,634 +1,660 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako, pylons_globals, literal, cached_template
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 # hack to make the migration to pyramid easier
69 def render(template_name, extra_vars=None, cache_key=None,
70 cache_type=None, cache_expire=None):
71 """Render a template with Mako
72
73 Accepts the cache options ``cache_key``, ``cache_type``, and
74 ``cache_expire``.
75
76 """
77 # Create a render callable for the cache function
78 def render_template():
79 # Pull in extra vars if needed
80 globs = extra_vars or {}
81
82 # Second, get the globals
83 globs.update(pylons_globals())
84
85 globs['_ungettext'] = globs['ungettext']
86 # Grab a template reference
87 template = globs['app_globals'].mako_lookup.get_template(template_name)
88
89 return literal(template.render_unicode(**globs))
90
91 return cached_template(template_name, render_template, cache_key=cache_key,
92 cache_type=cache_type, cache_expire=cache_expire)
93
68 94 def _filter_proxy(ip):
69 95 """
70 96 Passed in IP addresses in HEADERS can be in a special format of multiple
71 97 ips. Those comma separated IPs are passed from various proxies in the
72 98 chain of request processing. The left-most being the original client.
73 99 We only care about the first IP which came from the org. client.
74 100
75 101 :param ip: ip string from headers
76 102 """
77 103 if ',' in ip:
78 104 _ips = ip.split(',')
79 105 _first_ip = _ips[0].strip()
80 106 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 107 return _first_ip
82 108 return ip
83 109
84 110
85 111 def _filter_port(ip):
86 112 """
87 113 Removes a port from ip, there are 4 main cases to handle here.
88 114 - ipv4 eg. 127.0.0.1
89 115 - ipv6 eg. ::1
90 116 - ipv4+port eg. 127.0.0.1:8080
91 117 - ipv6+port eg. [::1]:8080
92 118
93 119 :param ip:
94 120 """
95 121 def is_ipv6(ip_addr):
96 122 if hasattr(socket, 'inet_pton'):
97 123 try:
98 124 socket.inet_pton(socket.AF_INET6, ip_addr)
99 125 except socket.error:
100 126 return False
101 127 else:
102 128 # fallback to ipaddress
103 129 try:
104 130 ipaddress.IPv6Address(safe_unicode(ip_addr))
105 131 except Exception:
106 132 return False
107 133 return True
108 134
109 135 if ':' not in ip: # must be ipv4 pure ip
110 136 return ip
111 137
112 138 if '[' in ip and ']' in ip: # ipv6 with port
113 139 return ip.split(']')[0][1:].lower()
114 140
115 141 # must be ipv6 or ipv4 with port
116 142 if is_ipv6(ip):
117 143 return ip
118 144 else:
119 145 ip, _port = ip.split(':')[:2] # means ipv4+port
120 146 return ip
121 147
122 148
123 149 def get_ip_addr(environ):
124 150 proxy_key = 'HTTP_X_REAL_IP'
125 151 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 152 def_key = 'REMOTE_ADDR'
127 153 _filters = lambda x: _filter_port(_filter_proxy(x))
128 154
129 155 ip = environ.get(proxy_key)
130 156 if ip:
131 157 return _filters(ip)
132 158
133 159 ip = environ.get(proxy_key2)
134 160 if ip:
135 161 return _filters(ip)
136 162
137 163 ip = environ.get(def_key, '0.0.0.0')
138 164 return _filters(ip)
139 165
140 166
141 167 def get_server_ip_addr(environ, log_errors=True):
142 168 hostname = environ.get('SERVER_NAME')
143 169 try:
144 170 return socket.gethostbyname(hostname)
145 171 except Exception as e:
146 172 if log_errors:
147 173 # in some cases this lookup is not possible, and we don't want to
148 174 # make it an exception in logs
149 175 log.exception('Could not retrieve server ip address: %s', e)
150 176 return hostname
151 177
152 178
153 179 def get_server_port(environ):
154 180 return environ.get('SERVER_PORT')
155 181
156 182
157 183 def get_access_path(environ):
158 184 path = environ.get('PATH_INFO')
159 185 org_req = environ.get('pylons.original_request')
160 186 if org_req:
161 187 path = org_req.environ.get('PATH_INFO')
162 188 return path
163 189
164 190
165 191 def get_user_agent(environ):
166 192 return environ.get('HTTP_USER_AGENT')
167 193
168 194
169 195 def vcs_operation_context(
170 196 environ, repo_name, username, action, scm, check_locking=True,
171 197 is_shadow_repo=False):
172 198 """
173 199 Generate the context for a vcs operation, e.g. push or pull.
174 200
175 201 This context is passed over the layers so that hooks triggered by the
176 202 vcs operation know details like the user, the user's IP address etc.
177 203
178 204 :param check_locking: Allows to switch of the computation of the locking
179 205 data. This serves mainly the need of the simplevcs middleware to be
180 206 able to disable this for certain operations.
181 207
182 208 """
183 209 # Tri-state value: False: unlock, None: nothing, True: lock
184 210 make_lock = None
185 211 locked_by = [None, None, None]
186 212 is_anonymous = username == User.DEFAULT_USER
187 213 if not is_anonymous and check_locking:
188 214 log.debug('Checking locking on repository "%s"', repo_name)
189 215 user = User.get_by_username(username)
190 216 repo = Repository.get_by_repo_name(repo_name)
191 217 make_lock, __, locked_by = repo.get_locking_state(
192 218 action, user.user_id)
193 219
194 220 settings_model = VcsSettingsModel(repo=repo_name)
195 221 ui_settings = settings_model.get_ui_settings()
196 222
197 223 extras = {
198 224 'ip': get_ip_addr(environ),
199 225 'username': username,
200 226 'action': action,
201 227 'repository': repo_name,
202 228 'scm': scm,
203 229 'config': rhodecode.CONFIG['__file__'],
204 230 'make_lock': make_lock,
205 231 'locked_by': locked_by,
206 232 'server_url': utils2.get_server_url(environ),
207 233 'user_agent': get_user_agent(environ),
208 234 'hooks': get_enabled_hook_classes(ui_settings),
209 235 'is_shadow_repo': is_shadow_repo,
210 236 }
211 237 return extras
212 238
213 239
214 240 class BasicAuth(AuthBasicAuthenticator):
215 241
216 242 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 243 initial_call_detection=False, acl_repo_name=None):
218 244 self.realm = realm
219 245 self.initial_call = initial_call_detection
220 246 self.authfunc = authfunc
221 247 self.registry = registry
222 248 self.acl_repo_name = acl_repo_name
223 249 self._rc_auth_http_code = auth_http_code
224 250
225 251 def _get_response_from_code(self, http_code):
226 252 try:
227 253 return get_exception(safe_int(http_code))
228 254 except Exception:
229 255 log.exception('Failed to fetch response for code %s' % http_code)
230 256 return HTTPForbidden
231 257
232 258 def build_authentication(self):
233 259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 260 if self._rc_auth_http_code and not self.initial_call:
235 261 # return alternative HTTP code if alternative http return code
236 262 # is specified in RhodeCode config, but ONLY if it's not the
237 263 # FIRST call
238 264 custom_response_klass = self._get_response_from_code(
239 265 self._rc_auth_http_code)
240 266 return custom_response_klass(headers=head)
241 267 return HTTPUnauthorized(headers=head)
242 268
243 269 def authenticate(self, environ):
244 270 authorization = AUTHORIZATION(environ)
245 271 if not authorization:
246 272 return self.build_authentication()
247 273 (authmeth, auth) = authorization.split(' ', 1)
248 274 if 'basic' != authmeth.lower():
249 275 return self.build_authentication()
250 276 auth = auth.strip().decode('base64')
251 277 _parts = auth.split(':', 1)
252 278 if len(_parts) == 2:
253 279 username, password = _parts
254 280 if self.authfunc(
255 281 username, password, environ, VCS_TYPE,
256 282 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 283 return username
258 284 if username and password:
259 285 # we mark that we actually executed authentication once, at
260 286 # that point we can use the alternative auth code
261 287 self.initial_call = False
262 288
263 289 return self.build_authentication()
264 290
265 291 __call__ = authenticate
266 292
267 293
268 294 def calculate_version_hash():
269 295 return md5(
270 296 config.get('beaker.session.secret', '') +
271 297 rhodecode.__version__)[:8]
272 298
273 299
274 300 def get_current_lang(request):
275 301 # NOTE(marcink): remove after pyramid move
276 302 try:
277 303 return translation.get_lang()[0]
278 304 except:
279 305 pass
280 306
281 307 return getattr(request, '_LOCALE_', request.locale_name)
282 308
283 309
284 310 def attach_context_attributes(context, request, user_id):
285 311 """
286 312 Attach variables into template context called `c`, please note that
287 313 request could be pylons or pyramid request in here.
288 314 """
289 315
290 316 rc_config = SettingsModel().get_all_settings(cache=True)
291 317
292 318 context.rhodecode_version = rhodecode.__version__
293 319 context.rhodecode_edition = config.get('rhodecode.edition')
294 320 # unique secret + version does not leak the version but keep consistency
295 321 context.rhodecode_version_hash = calculate_version_hash()
296 322
297 323 # Default language set for the incoming request
298 324 context.language = get_current_lang(request)
299 325
300 326 # Visual options
301 327 context.visual = AttributeDict({})
302 328
303 329 # DB stored Visual Items
304 330 context.visual.show_public_icon = str2bool(
305 331 rc_config.get('rhodecode_show_public_icon'))
306 332 context.visual.show_private_icon = str2bool(
307 333 rc_config.get('rhodecode_show_private_icon'))
308 334 context.visual.stylify_metatags = str2bool(
309 335 rc_config.get('rhodecode_stylify_metatags'))
310 336 context.visual.dashboard_items = safe_int(
311 337 rc_config.get('rhodecode_dashboard_items', 100))
312 338 context.visual.admin_grid_items = safe_int(
313 339 rc_config.get('rhodecode_admin_grid_items', 100))
314 340 context.visual.repository_fields = str2bool(
315 341 rc_config.get('rhodecode_repository_fields'))
316 342 context.visual.show_version = str2bool(
317 343 rc_config.get('rhodecode_show_version'))
318 344 context.visual.use_gravatar = str2bool(
319 345 rc_config.get('rhodecode_use_gravatar'))
320 346 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
321 347 context.visual.default_renderer = rc_config.get(
322 348 'rhodecode_markup_renderer', 'rst')
323 349 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
324 350 context.visual.rhodecode_support_url = \
325 351 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
326 352
327 353 context.visual.affected_files_cut_off = 60
328 354
329 355 context.pre_code = rc_config.get('rhodecode_pre_code')
330 356 context.post_code = rc_config.get('rhodecode_post_code')
331 357 context.rhodecode_name = rc_config.get('rhodecode_title')
332 358 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
333 359 # if we have specified default_encoding in the request, it has more
334 360 # priority
335 361 if request.GET.get('default_encoding'):
336 362 context.default_encodings.insert(0, request.GET.get('default_encoding'))
337 363 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
338 364
339 365 # INI stored
340 366 context.labs_active = str2bool(
341 367 config.get('labs_settings_active', 'false'))
342 368 context.visual.allow_repo_location_change = str2bool(
343 369 config.get('allow_repo_location_change', True))
344 370 context.visual.allow_custom_hooks_settings = str2bool(
345 371 config.get('allow_custom_hooks_settings', True))
346 372 context.debug_style = str2bool(config.get('debug_style', False))
347 373
348 374 context.rhodecode_instanceid = config.get('instance_id')
349 375
350 376 context.visual.cut_off_limit_diff = safe_int(
351 377 config.get('cut_off_limit_diff'))
352 378 context.visual.cut_off_limit_file = safe_int(
353 379 config.get('cut_off_limit_file'))
354 380
355 381 # AppEnlight
356 382 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
357 383 context.appenlight_api_public_key = config.get(
358 384 'appenlight.api_public_key', '')
359 385 context.appenlight_server_url = config.get('appenlight.server_url', '')
360 386
361 387 # JS template context
362 388 context.template_context = {
363 389 'repo_name': None,
364 390 'repo_type': None,
365 391 'repo_landing_commit': None,
366 392 'rhodecode_user': {
367 393 'username': None,
368 394 'email': None,
369 395 'notification_status': False
370 396 },
371 397 'visual': {
372 398 'default_renderer': None
373 399 },
374 400 'commit_data': {
375 401 'commit_id': None
376 402 },
377 403 'pull_request_data': {'pull_request_id': None},
378 404 'timeago': {
379 405 'refresh_time': 120 * 1000,
380 406 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
381 407 },
382 408 'pylons_dispatch': {
383 409 # 'controller': request.environ['pylons.routes_dict']['controller'],
384 410 # 'action': request.environ['pylons.routes_dict']['action'],
385 411 },
386 412 'pyramid_dispatch': {
387 413
388 414 },
389 415 'extra': {'plugins': {}}
390 416 }
391 417 # END CONFIG VARS
392 418
393 419 # TODO: This dosn't work when called from pylons compatibility tween.
394 420 # Fix this and remove it from base controller.
395 421 # context.repo_name = get_repo_slug(request) # can be empty
396 422
397 423 diffmode = 'sideside'
398 424 if request.GET.get('diffmode'):
399 425 if request.GET['diffmode'] == 'unified':
400 426 diffmode = 'unified'
401 427 elif request.session.get('diffmode'):
402 428 diffmode = request.session['diffmode']
403 429
404 430 context.diffmode = diffmode
405 431
406 432 if request.session.get('diffmode') != diffmode:
407 433 request.session['diffmode'] = diffmode
408 434
409 435 context.csrf_token = auth.get_csrf_token(session=request.session)
410 436 context.backends = rhodecode.BACKENDS.keys()
411 437 context.backends.sort()
412 438 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
413 439
414 440 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
415 441 # given request will ALWAYS be pyramid one
416 442 pyramid_request = pyramid.threadlocal.get_current_request()
417 443 context.pyramid_request = pyramid_request
418 444
419 445 # web case
420 446 if hasattr(pyramid_request, 'user'):
421 447 context.auth_user = pyramid_request.user
422 448 context.rhodecode_user = pyramid_request.user
423 449
424 450 # api case
425 451 if hasattr(pyramid_request, 'rpc_user'):
426 452 context.auth_user = pyramid_request.rpc_user
427 453 context.rhodecode_user = pyramid_request.rpc_user
428 454
429 455 # attach the whole call context to the request
430 456 request.call_context = context
431 457
432 458
433 459 def get_auth_user(request):
434 460 environ = request.environ
435 461 session = request.session
436 462
437 463 ip_addr = get_ip_addr(environ)
438 464 # make sure that we update permissions each time we call controller
439 465 _auth_token = (request.GET.get('auth_token', '') or
440 466 request.GET.get('api_key', ''))
441 467
442 468 if _auth_token:
443 469 # when using API_KEY we assume user exists, and
444 470 # doesn't need auth based on cookies.
445 471 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
446 472 authenticated = False
447 473 else:
448 474 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
449 475 try:
450 476 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
451 477 ip_addr=ip_addr)
452 478 except UserCreationError as e:
453 479 h.flash(e, 'error')
454 480 # container auth or other auth functions that create users
455 481 # on the fly can throw this exception signaling that there's
456 482 # issue with user creation, explanation should be provided
457 483 # in Exception itself. We then create a simple blank
458 484 # AuthUser
459 485 auth_user = AuthUser(ip_addr=ip_addr)
460 486
461 487 if password_changed(auth_user, session):
462 488 session.invalidate()
463 489 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
464 490 auth_user = AuthUser(ip_addr=ip_addr)
465 491
466 492 authenticated = cookie_store.get('is_authenticated')
467 493
468 494 if not auth_user.is_authenticated and auth_user.is_user_object:
469 495 # user is not authenticated and not empty
470 496 auth_user.set_authenticated(authenticated)
471 497
472 498 return auth_user
473 499
474 500
475 501 class BaseController(WSGIController):
476 502
477 503 def __before__(self):
478 504 """
479 505 __before__ is called before controller methods and after __call__
480 506 """
481 507 # on each call propagate settings calls into global settings.
482 508 set_rhodecode_config(config)
483 509 attach_context_attributes(c, request, self._rhodecode_user.user_id)
484 510
485 511 # TODO: Remove this when fixed in attach_context_attributes()
486 512 c.repo_name = get_repo_slug(request) # can be empty
487 513
488 514 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
489 515 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
490 516 self.sa = meta.Session
491 517 self.scm_model = ScmModel(self.sa)
492 518
493 519 # set user language
494 520 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
495 521 if user_lang:
496 522 translation.set_lang(user_lang)
497 523 log.debug('set language to %s for user %s',
498 524 user_lang, self._rhodecode_user)
499 525
500 526 def _dispatch_redirect(self, with_url, environ, start_response):
501 527 resp = HTTPFound(with_url)
502 528 environ['SCRIPT_NAME'] = '' # handle prefix middleware
503 529 environ['PATH_INFO'] = with_url
504 530 return resp(environ, start_response)
505 531
506 532 def __call__(self, environ, start_response):
507 533 """Invoke the Controller"""
508 534 # WSGIController.__call__ dispatches to the Controller method
509 535 # the request is routed to. This routing information is
510 536 # available in environ['pylons.routes_dict']
511 537 from rhodecode.lib import helpers as h
512 538
513 539 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
514 540 if environ.get('debugtoolbar.wants_pylons_context', False):
515 541 environ['debugtoolbar.pylons_context'] = c._current_obj()
516 542
517 543 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
518 544 environ['pylons.routes_dict']['action']])
519 545
520 546 self.rc_config = SettingsModel().get_all_settings(cache=True)
521 547 self.ip_addr = get_ip_addr(environ)
522 548
523 549 # The rhodecode auth user is looked up and passed through the
524 550 # environ by the pylons compatibility tween in pyramid.
525 551 # So we can just grab it from there.
526 552 auth_user = environ['rc_auth_user']
527 553
528 554 # set globals for auth user
529 555 request.user = auth_user
530 556 self._rhodecode_user = auth_user
531 557
532 558 log.info('IP: %s User: %s accessed %s [%s]' % (
533 559 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
534 560 _route_name)
535 561 )
536 562
537 563 user_obj = auth_user.get_instance()
538 564 if user_obj and user_obj.user_data.get('force_password_change'):
539 565 h.flash('You are required to change your password', 'warning',
540 566 ignore_duplicate=True)
541 567 return self._dispatch_redirect(
542 568 url('my_account_password'), environ, start_response)
543 569
544 570 return WSGIController.__call__(self, environ, start_response)
545 571
546 572
547 573 class BaseRepoController(BaseController):
548 574 """
549 575 Base class for controllers responsible for loading all needed data for
550 576 repository loaded items are
551 577
552 578 c.rhodecode_repo: instance of scm repository
553 579 c.rhodecode_db_repo: instance of db
554 580 c.repository_requirements_missing: shows that repository specific data
555 581 could not be displayed due to the missing requirements
556 582 c.repository_pull_requests: show number of open pull requests
557 583 """
558 584
559 585 def __before__(self):
560 586 super(BaseRepoController, self).__before__()
561 587 if c.repo_name: # extracted from routes
562 588 db_repo = Repository.get_by_repo_name(c.repo_name)
563 589 if not db_repo:
564 590 return
565 591
566 592 log.debug(
567 593 'Found repository in database %s with state `%s`',
568 594 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
569 595 route = getattr(request.environ.get('routes.route'), 'name', '')
570 596
571 597 # allow to delete repos that are somehow damages in filesystem
572 598 if route in ['delete_repo']:
573 599 return
574 600
575 601 if db_repo.repo_state in [Repository.STATE_PENDING]:
576 602 if route in ['repo_creating_home']:
577 603 return
578 604 check_url = url('repo_creating_home', repo_name=c.repo_name)
579 605 return redirect(check_url)
580 606
581 607 self.rhodecode_db_repo = db_repo
582 608
583 609 missing_requirements = False
584 610 try:
585 611 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
586 612 except RepositoryRequirementError as e:
587 613 missing_requirements = True
588 614 self._handle_missing_requirements(e)
589 615
590 616 if self.rhodecode_repo is None and not missing_requirements:
591 617 log.error('%s this repository is present in database but it '
592 618 'cannot be created as an scm instance', c.repo_name)
593 619
594 620 h.flash(_(
595 621 "The repository at %(repo_name)s cannot be located.") %
596 622 {'repo_name': c.repo_name},
597 623 category='error', ignore_duplicate=True)
598 624 redirect(h.route_path('home'))
599 625
600 626 # update last change according to VCS data
601 627 if not missing_requirements:
602 628 commit = db_repo.get_commit(
603 629 pre_load=["author", "date", "message", "parents"])
604 630 db_repo.update_commit_cache(commit)
605 631
606 632 # Prepare context
607 633 c.rhodecode_db_repo = db_repo
608 634 c.rhodecode_repo = self.rhodecode_repo
609 635 c.repository_requirements_missing = missing_requirements
610 636
611 637 self._update_global_counters(self.scm_model, db_repo)
612 638
613 639 def _update_global_counters(self, scm_model, db_repo):
614 640 """
615 641 Base variables that are exposed to every page of repository
616 642 """
617 643 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
618 644
619 645 def _handle_missing_requirements(self, error):
620 646 self.rhodecode_repo = None
621 647 log.error(
622 648 'Requirements are missing for repository %s: %s',
623 649 c.repo_name, error.message)
624 650
625 651 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
626 652 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
627 653 settings_update_url = url('repo', repo_name=c.repo_name)
628 654 path = request.path
629 655 should_redirect = (
630 656 path not in (summary_url, settings_update_url)
631 657 and '/settings' not in path or path == statistics_url
632 658 )
633 659 if should_redirect:
634 660 redirect(summary_url)
@@ -1,97 +1,97 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 import logging
23 23 from mako import exceptions
24 24 from pyramid.renderers import get_renderer
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 def get_partial_renderer(request, tmpl_name):
30 30 return PyramidPartialRenderer(request, tmpl_name=tmpl_name)
31 31
32 32
33 33 class PyramidPartialRenderer(object):
34 34
35 35 """
36 36 Partial renderer used to render chunks of html used in datagrids
37 37 use like::
38 38
39 39 _renderer = request.get_partial_renderer('_dt/template_base.mako')
40 40 _render('quick_menu', args, kwargs)
41 41
42 42 :param tmpl_name: template path relate to /templates/ dir
43 43 """
44 44
45 45 def __init__(self, request, tmpl_name):
46 46 self.tmpl_name = tmpl_name
47 47 self.request = request
48 48
49 49 def _mako_lookup(self):
50 50 _tmpl_lookup = get_renderer('root.mako').lookup
51 51 return _tmpl_lookup.get_template(self.tmpl_name)
52 52
53 53 def get_call_context(self):
54 54 return self.request.call_context
55 55
56 56 def get_helpers(self):
57 57 from rhodecode.lib import helpers
58 58 return helpers
59 59
60 60 def _update_kwargs_for_render(self, kwargs):
61 61 """
62 62 Inject params required for Mako rendering
63 63 """
64 64
65 65 _kwargs = {
66 66 '_': self.request.translate,
67 'ungettext': self.request.plularize,
67 '_ungettext': self.request.plularize,
68 68 'h': self.get_helpers(),
69 69 'c': self.get_call_context(),
70 70
71 71 'request': self.request,
72 72 }
73 73 _kwargs.update(kwargs)
74 74 return _kwargs
75 75
76 76 def _render_with_exc(self, render_func, args, kwargs):
77 77 try:
78 78 return render_func.render(*args, **kwargs)
79 79 except:
80 80 log.error(exceptions.text_error_template().render())
81 81 raise
82 82
83 83 def _get_template(self, template_obj, def_name):
84 84 if def_name:
85 85 tmpl = template_obj.get_def(def_name)
86 86 else:
87 87 tmpl = template_obj
88 88 return tmpl
89 89
90 90 def render(self, def_name, *args, **kwargs):
91 91 lookup_obj = self._mako_lookup()
92 92 tmpl = self._get_template(lookup_obj, def_name=def_name)
93 93 kwargs = self._update_kwargs_for_render(kwargs)
94 94 return self._render_with_exc(tmpl, args, kwargs)
95 95
96 96 def __call__(self, tmpl, *args, **kwargs):
97 97 return self.render(tmpl, *args, **kwargs)
@@ -1,982 +1,982 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 import hashlib
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from paste.script.command import Command, BadCommand
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from mako import exceptions
44 44 from pyramid.threadlocal import get_current_registry
45 45 from pyramid.request import Request
46 46
47 47 from rhodecode.lib.fakemod import create_module
48 48 from rhodecode.lib.vcs.backends.base import Config
49 49 from rhodecode.lib.vcs.exceptions import VCSError
50 50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 51 from rhodecode.lib.utils2 import (
52 52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 53 from rhodecode.model import meta
54 54 from rhodecode.model.db import (
55 55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 62
63 63 # String which contains characters that are not allowed in slug names for
64 64 # repositories or repository groups. It is properly escaped to use it in
65 65 # regular expressions.
66 66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67 67
68 68 # Regex that matches forbidden characters in repo/group slugs.
69 69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70 70
71 71 # Regex that matches allowed characters in repo/group slugs.
72 72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73 73
74 74 # Regex that matches whole repo/group slugs.
75 75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76 76
77 77 _license_cache = None
78 78
79 79
80 80 def repo_name_slug(value):
81 81 """
82 82 Return slug of name of repository
83 83 This function is called on each creation/modification
84 84 of repository to prevent bad names in repo
85 85 """
86 86 replacement_char = '-'
87 87
88 88 slug = remove_formatting(value)
89 89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 90 slug = re.sub('[\s]+', '-', slug)
91 91 slug = collapse(slug, replacement_char)
92 92 return slug
93 93
94 94
95 95 #==============================================================================
96 96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 97 #==============================================================================
98 98 def get_repo_slug(request):
99 99 if isinstance(request, Request) and getattr(request, 'db_repo', None):
100 100 # pyramid
101 101 _repo = request.db_repo.repo_name
102 102 else:
103 103 # TODO(marcink): remove after pylons migration...
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105
106 106 if _repo:
107 107 _repo = _repo.rstrip('/')
108 108 return _repo
109 109
110 110
111 111 def get_repo_group_slug(request):
112 112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
113 113 # pyramid
114 114 _group = request.matchdict.get('repo_group_name')
115 115 else:
116 116 _group = request.environ['pylons.routes_dict'].get('group_name')
117 117
118 118 if _group:
119 119 _group = _group.rstrip('/')
120 120 return _group
121 121
122 122
123 123 def get_user_group_slug(request):
124 124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
125 125 # pyramid
126 126 _group = request.matchdict.get('user_group_id')
127 127 else:
128 128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
129 129
130 130 try:
131 131 _group = UserGroup.get(_group)
132 132 if _group:
133 133 _group = _group.users_group_name
134 134 except Exception:
135 135 log.debug(traceback.format_exc())
136 136 # catch all failures here
137 137 pass
138 138
139 139 return _group
140 140
141 141
142 142 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
143 143 """
144 144 Scans given path for repos and return (name,(type,path)) tuple
145 145
146 146 :param path: path to scan for repositories
147 147 :param recursive: recursive search and return names with subdirs in front
148 148 """
149 149
150 150 # remove ending slash for better results
151 151 path = path.rstrip(os.sep)
152 152 log.debug('now scanning in %s location recursive:%s...', path, recursive)
153 153
154 154 def _get_repos(p):
155 155 dirpaths = _get_dirpaths(p)
156 156 if not _is_dir_writable(p):
157 157 log.warning('repo path without write access: %s', p)
158 158
159 159 for dirpath in dirpaths:
160 160 if os.path.isfile(os.path.join(p, dirpath)):
161 161 continue
162 162 cur_path = os.path.join(p, dirpath)
163 163
164 164 # skip removed repos
165 165 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
166 166 continue
167 167
168 168 #skip .<somethin> dirs
169 169 if dirpath.startswith('.'):
170 170 continue
171 171
172 172 try:
173 173 scm_info = get_scm(cur_path)
174 174 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
175 175 except VCSError:
176 176 if not recursive:
177 177 continue
178 178 #check if this dir containts other repos for recursive scan
179 179 rec_path = os.path.join(p, dirpath)
180 180 if os.path.isdir(rec_path):
181 181 for inner_scm in _get_repos(rec_path):
182 182 yield inner_scm
183 183
184 184 return _get_repos(path)
185 185
186 186
187 187 def _get_dirpaths(p):
188 188 try:
189 189 # OS-independable way of checking if we have at least read-only
190 190 # access or not.
191 191 dirpaths = os.listdir(p)
192 192 except OSError:
193 193 log.warning('ignoring repo path without read access: %s', p)
194 194 return []
195 195
196 196 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
197 197 # decode paths and suddenly returns unicode objects itself. The items it
198 198 # cannot decode are returned as strings and cause issues.
199 199 #
200 200 # Those paths are ignored here until a solid solution for path handling has
201 201 # been built.
202 202 expected_type = type(p)
203 203
204 204 def _has_correct_type(item):
205 205 if type(item) is not expected_type:
206 206 log.error(
207 207 u"Ignoring path %s since it cannot be decoded into unicode.",
208 208 # Using "repr" to make sure that we see the byte value in case
209 209 # of support.
210 210 repr(item))
211 211 return False
212 212 return True
213 213
214 214 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
215 215
216 216 return dirpaths
217 217
218 218
219 219 def _is_dir_writable(path):
220 220 """
221 221 Probe if `path` is writable.
222 222
223 223 Due to trouble on Cygwin / Windows, this is actually probing if it is
224 224 possible to create a file inside of `path`, stat does not produce reliable
225 225 results in this case.
226 226 """
227 227 try:
228 228 with tempfile.TemporaryFile(dir=path):
229 229 pass
230 230 except OSError:
231 231 return False
232 232 return True
233 233
234 234
235 235 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
236 236 """
237 237 Returns True if given path is a valid repository False otherwise.
238 238 If expect_scm param is given also, compare if given scm is the same
239 239 as expected from scm parameter. If explicit_scm is given don't try to
240 240 detect the scm, just use the given one to check if repo is valid
241 241
242 242 :param repo_name:
243 243 :param base_path:
244 244 :param expect_scm:
245 245 :param explicit_scm:
246 246
247 247 :return True: if given path is a valid repository
248 248 """
249 249 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
250 250 log.debug('Checking if `%s` is a valid path for repository. '
251 251 'Explicit type: %s', repo_name, explicit_scm)
252 252
253 253 try:
254 254 if explicit_scm:
255 255 detected_scms = [get_scm_backend(explicit_scm)]
256 256 else:
257 257 detected_scms = get_scm(full_path)
258 258
259 259 if expect_scm:
260 260 return detected_scms[0] == expect_scm
261 261 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
262 262 return True
263 263 except VCSError:
264 264 log.debug('path: %s is not a valid repo !', full_path)
265 265 return False
266 266
267 267
268 268 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
269 269 """
270 270 Returns True if given path is a repository group, False otherwise
271 271
272 272 :param repo_name:
273 273 :param base_path:
274 274 """
275 275 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
276 276 log.debug('Checking if `%s` is a valid path for repository group',
277 277 repo_group_name)
278 278
279 279 # check if it's not a repo
280 280 if is_valid_repo(repo_group_name, base_path):
281 281 log.debug('Repo called %s exist, it is not a valid '
282 282 'repo group' % repo_group_name)
283 283 return False
284 284
285 285 try:
286 286 # we need to check bare git repos at higher level
287 287 # since we might match branches/hooks/info/objects or possible
288 288 # other things inside bare git repo
289 289 scm_ = get_scm(os.path.dirname(full_path))
290 290 log.debug('path: %s is a vcs object:%s, not valid '
291 291 'repo group' % (full_path, scm_))
292 292 return False
293 293 except VCSError:
294 294 pass
295 295
296 296 # check if it's a valid path
297 297 if skip_path_check or os.path.isdir(full_path):
298 298 log.debug('path: %s is a valid repo group !', full_path)
299 299 return True
300 300
301 301 log.debug('path: %s is not a valid repo group !', full_path)
302 302 return False
303 303
304 304
305 305 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
306 306 while True:
307 307 ok = raw_input(prompt)
308 308 if ok.lower() in ('y', 'ye', 'yes'):
309 309 return True
310 310 if ok.lower() in ('n', 'no', 'nop', 'nope'):
311 311 return False
312 312 retries = retries - 1
313 313 if retries < 0:
314 314 raise IOError
315 315 print(complaint)
316 316
317 317 # propagated from mercurial documentation
318 318 ui_sections = [
319 319 'alias', 'auth',
320 320 'decode/encode', 'defaults',
321 321 'diff', 'email',
322 322 'extensions', 'format',
323 323 'merge-patterns', 'merge-tools',
324 324 'hooks', 'http_proxy',
325 325 'smtp', 'patch',
326 326 'paths', 'profiling',
327 327 'server', 'trusted',
328 328 'ui', 'web', ]
329 329
330 330
331 331 def config_data_from_db(clear_session=True, repo=None):
332 332 """
333 333 Read the configuration data from the database and return configuration
334 334 tuples.
335 335 """
336 336 from rhodecode.model.settings import VcsSettingsModel
337 337
338 338 config = []
339 339
340 340 sa = meta.Session()
341 341 settings_model = VcsSettingsModel(repo=repo, sa=sa)
342 342
343 343 ui_settings = settings_model.get_ui_settings()
344 344
345 345 for setting in ui_settings:
346 346 if setting.active:
347 347 log.debug(
348 348 'settings ui from db: [%s] %s=%s',
349 349 setting.section, setting.key, setting.value)
350 350 config.append((
351 351 safe_str(setting.section), safe_str(setting.key),
352 352 safe_str(setting.value)))
353 353 if setting.key == 'push_ssl':
354 354 # force set push_ssl requirement to False, rhodecode
355 355 # handles that
356 356 config.append((
357 357 safe_str(setting.section), safe_str(setting.key), False))
358 358 if clear_session:
359 359 meta.Session.remove()
360 360
361 361 # TODO: mikhail: probably it makes no sense to re-read hooks information.
362 362 # It's already there and activated/deactivated
363 363 skip_entries = []
364 364 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
365 365 if 'pull' not in enabled_hook_classes:
366 366 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
367 367 if 'push' not in enabled_hook_classes:
368 368 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
369 369 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
370 370 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
371 371
372 372 config = [entry for entry in config if entry[:2] not in skip_entries]
373 373
374 374 return config
375 375
376 376
377 377 def make_db_config(clear_session=True, repo=None):
378 378 """
379 379 Create a :class:`Config` instance based on the values in the database.
380 380 """
381 381 config = Config()
382 382 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
383 383 for section, option, value in config_data:
384 384 config.set(section, option, value)
385 385 return config
386 386
387 387
388 388 def get_enabled_hook_classes(ui_settings):
389 389 """
390 390 Return the enabled hook classes.
391 391
392 392 :param ui_settings: List of ui_settings as returned
393 393 by :meth:`VcsSettingsModel.get_ui_settings`
394 394
395 395 :return: a list with the enabled hook classes. The order is not guaranteed.
396 396 :rtype: list
397 397 """
398 398 enabled_hooks = []
399 399 active_hook_keys = [
400 400 key for section, key, value, active in ui_settings
401 401 if section == 'hooks' and active]
402 402
403 403 hook_names = {
404 404 RhodeCodeUi.HOOK_PUSH: 'push',
405 405 RhodeCodeUi.HOOK_PULL: 'pull',
406 406 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
407 407 }
408 408
409 409 for key in active_hook_keys:
410 410 hook = hook_names.get(key)
411 411 if hook:
412 412 enabled_hooks.append(hook)
413 413
414 414 return enabled_hooks
415 415
416 416
417 417 def set_rhodecode_config(config):
418 418 """
419 419 Updates pylons config with new settings from database
420 420
421 421 :param config:
422 422 """
423 423 from rhodecode.model.settings import SettingsModel
424 424 app_settings = SettingsModel().get_all_settings()
425 425
426 426 for k, v in app_settings.items():
427 427 config[k] = v
428 428
429 429
430 430 def get_rhodecode_realm():
431 431 """
432 432 Return the rhodecode realm from database.
433 433 """
434 434 from rhodecode.model.settings import SettingsModel
435 435 realm = SettingsModel().get_setting_by_name('realm')
436 436 return safe_str(realm.app_settings_value)
437 437
438 438
439 439 def get_rhodecode_base_path():
440 440 """
441 441 Returns the base path. The base path is the filesystem path which points
442 442 to the repository store.
443 443 """
444 444 from rhodecode.model.settings import SettingsModel
445 445 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
446 446 return safe_str(paths_ui.ui_value)
447 447
448 448
449 449 def map_groups(path):
450 450 """
451 451 Given a full path to a repository, create all nested groups that this
452 452 repo is inside. This function creates parent-child relationships between
453 453 groups and creates default perms for all new groups.
454 454
455 455 :param paths: full path to repository
456 456 """
457 457 from rhodecode.model.repo_group import RepoGroupModel
458 458 sa = meta.Session()
459 459 groups = path.split(Repository.NAME_SEP)
460 460 parent = None
461 461 group = None
462 462
463 463 # last element is repo in nested groups structure
464 464 groups = groups[:-1]
465 465 rgm = RepoGroupModel(sa)
466 466 owner = User.get_first_super_admin()
467 467 for lvl, group_name in enumerate(groups):
468 468 group_name = '/'.join(groups[:lvl] + [group_name])
469 469 group = RepoGroup.get_by_group_name(group_name)
470 470 desc = '%s group' % group_name
471 471
472 472 # skip folders that are now removed repos
473 473 if REMOVED_REPO_PAT.match(group_name):
474 474 break
475 475
476 476 if group is None:
477 477 log.debug('creating group level: %s group_name: %s',
478 478 lvl, group_name)
479 479 group = RepoGroup(group_name, parent)
480 480 group.group_description = desc
481 481 group.user = owner
482 482 sa.add(group)
483 483 perm_obj = rgm._create_default_perms(group)
484 484 sa.add(perm_obj)
485 485 sa.flush()
486 486
487 487 parent = group
488 488 return group
489 489
490 490
491 491 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
492 492 """
493 493 maps all repos given in initial_repo_list, non existing repositories
494 494 are created, if remove_obsolete is True it also checks for db entries
495 495 that are not in initial_repo_list and removes them.
496 496
497 497 :param initial_repo_list: list of repositories found by scanning methods
498 498 :param remove_obsolete: check for obsolete entries in database
499 499 """
500 500 from rhodecode.model.repo import RepoModel
501 501 from rhodecode.model.scm import ScmModel
502 502 from rhodecode.model.repo_group import RepoGroupModel
503 503 from rhodecode.model.settings import SettingsModel
504 504
505 505 sa = meta.Session()
506 506 repo_model = RepoModel()
507 507 user = User.get_first_super_admin()
508 508 added = []
509 509
510 510 # creation defaults
511 511 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
512 512 enable_statistics = defs.get('repo_enable_statistics')
513 513 enable_locking = defs.get('repo_enable_locking')
514 514 enable_downloads = defs.get('repo_enable_downloads')
515 515 private = defs.get('repo_private')
516 516
517 517 for name, repo in initial_repo_list.items():
518 518 group = map_groups(name)
519 519 unicode_name = safe_unicode(name)
520 520 db_repo = repo_model.get_by_repo_name(unicode_name)
521 521 # found repo that is on filesystem not in RhodeCode database
522 522 if not db_repo:
523 523 log.info('repository %s not found, creating now', name)
524 524 added.append(name)
525 525 desc = (repo.description
526 526 if repo.description != 'unknown'
527 527 else '%s repository' % name)
528 528
529 529 db_repo = repo_model._create_repo(
530 530 repo_name=name,
531 531 repo_type=repo.alias,
532 532 description=desc,
533 533 repo_group=getattr(group, 'group_id', None),
534 534 owner=user,
535 535 enable_locking=enable_locking,
536 536 enable_downloads=enable_downloads,
537 537 enable_statistics=enable_statistics,
538 538 private=private,
539 539 state=Repository.STATE_CREATED
540 540 )
541 541 sa.commit()
542 542 # we added that repo just now, and make sure we updated server info
543 543 if db_repo.repo_type == 'git':
544 544 git_repo = db_repo.scm_instance()
545 545 # update repository server-info
546 546 log.debug('Running update server info')
547 547 git_repo._update_server_info()
548 548
549 549 db_repo.update_commit_cache()
550 550
551 551 config = db_repo._config
552 552 config.set('extensions', 'largefiles', '')
553 553 ScmModel().install_hooks(
554 554 db_repo.scm_instance(config=config),
555 555 repo_type=db_repo.repo_type)
556 556
557 557 removed = []
558 558 if remove_obsolete:
559 559 # remove from database those repositories that are not in the filesystem
560 560 for repo in sa.query(Repository).all():
561 561 if repo.repo_name not in initial_repo_list.keys():
562 562 log.debug("Removing non-existing repository found in db `%s`",
563 563 repo.repo_name)
564 564 try:
565 565 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
566 566 sa.commit()
567 567 removed.append(repo.repo_name)
568 568 except Exception:
569 569 # don't hold further removals on error
570 570 log.error(traceback.format_exc())
571 571 sa.rollback()
572 572
573 573 def splitter(full_repo_name):
574 574 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
575 575 gr_name = None
576 576 if len(_parts) == 2:
577 577 gr_name = _parts[0]
578 578 return gr_name
579 579
580 580 initial_repo_group_list = [splitter(x) for x in
581 581 initial_repo_list.keys() if splitter(x)]
582 582
583 583 # remove from database those repository groups that are not in the
584 584 # filesystem due to parent child relationships we need to delete them
585 585 # in a specific order of most nested first
586 586 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
587 587 nested_sort = lambda gr: len(gr.split('/'))
588 588 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
589 589 if group_name not in initial_repo_group_list:
590 590 repo_group = RepoGroup.get_by_group_name(group_name)
591 591 if (repo_group.children.all() or
592 592 not RepoGroupModel().check_exist_filesystem(
593 593 group_name=group_name, exc_on_failure=False)):
594 594 continue
595 595
596 596 log.info(
597 597 'Removing non-existing repository group found in db `%s`',
598 598 group_name)
599 599 try:
600 600 RepoGroupModel(sa).delete(group_name, fs_remove=False)
601 601 sa.commit()
602 602 removed.append(group_name)
603 603 except Exception:
604 604 # don't hold further removals on error
605 605 log.exception(
606 606 'Unable to remove repository group `%s`',
607 607 group_name)
608 608 sa.rollback()
609 609 raise
610 610
611 611 return added, removed
612 612
613 613
614 614 def get_default_cache_settings(settings):
615 615 cache_settings = {}
616 616 for key in settings.keys():
617 617 for prefix in ['beaker.cache.', 'cache.']:
618 618 if key.startswith(prefix):
619 619 name = key.split(prefix)[1].strip()
620 620 cache_settings[name] = settings[key].strip()
621 621 return cache_settings
622 622
623 623
624 624 # set cache regions for beaker so celery can utilise it
625 625 def add_cache(settings):
626 626 from rhodecode.lib import caches
627 627 cache_settings = {'regions': None}
628 628 # main cache settings used as default ...
629 629 cache_settings.update(get_default_cache_settings(settings))
630 630
631 631 if cache_settings['regions']:
632 632 for region in cache_settings['regions'].split(','):
633 633 region = region.strip()
634 634 region_settings = {}
635 635 for key, value in cache_settings.items():
636 636 if key.startswith(region):
637 637 region_settings[key.split('.')[1]] = value
638 638
639 639 caches.configure_cache_region(
640 640 region, region_settings, cache_settings)
641 641
642 642
643 643 def load_rcextensions(root_path):
644 644 import rhodecode
645 645 from rhodecode.config import conf
646 646
647 647 path = os.path.join(root_path, 'rcextensions', '__init__.py')
648 648 if os.path.isfile(path):
649 649 rcext = create_module('rc', path)
650 650 EXT = rhodecode.EXTENSIONS = rcext
651 651 log.debug('Found rcextensions now loading %s...', rcext)
652 652
653 653 # Additional mappings that are not present in the pygments lexers
654 654 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
655 655
656 656 # auto check if the module is not missing any data, set to default if is
657 657 # this will help autoupdate new feature of rcext module
658 658 #from rhodecode.config import rcextensions
659 659 #for k in dir(rcextensions):
660 660 # if not k.startswith('_') and not hasattr(EXT, k):
661 661 # setattr(EXT, k, getattr(rcextensions, k))
662 662
663 663
664 664 def get_custom_lexer(extension):
665 665 """
666 666 returns a custom lexer if it is defined in rcextensions module, or None
667 667 if there's no custom lexer defined
668 668 """
669 669 import rhodecode
670 670 from pygments import lexers
671 671
672 672 # custom override made by RhodeCode
673 673 if extension in ['mako']:
674 674 return lexers.get_lexer_by_name('html+mako')
675 675
676 676 # check if we didn't define this extension as other lexer
677 677 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
678 678 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
679 679 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
680 680 return lexers.get_lexer_by_name(_lexer_name)
681 681
682 682
683 683 #==============================================================================
684 684 # TEST FUNCTIONS AND CREATORS
685 685 #==============================================================================
686 686 def create_test_index(repo_location, config):
687 687 """
688 688 Makes default test index.
689 689 """
690 690 import rc_testdata
691 691
692 692 rc_testdata.extract_search_index(
693 693 'vcs_search_index', os.path.dirname(config['search.location']))
694 694
695 695
696 696 def create_test_directory(test_path):
697 697 """
698 698 Create test directory if it doesn't exist.
699 699 """
700 700 if not os.path.isdir(test_path):
701 701 log.debug('Creating testdir %s', test_path)
702 702 os.makedirs(test_path)
703 703
704 704
705 705 def create_test_database(test_path, config):
706 706 """
707 707 Makes a fresh database.
708 708 """
709 709 from rhodecode.lib.db_manage import DbManage
710 710
711 711 # PART ONE create db
712 712 dbconf = config['sqlalchemy.db1.url']
713 713 log.debug('making test db %s', dbconf)
714 714
715 715 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
716 716 tests=True, cli_args={'force_ask': True})
717 717 dbmanage.create_tables(override=True)
718 718 dbmanage.set_db_version()
719 719 # for tests dynamically set new root paths based on generated content
720 720 dbmanage.create_settings(dbmanage.config_prompt(test_path))
721 721 dbmanage.create_default_user()
722 722 dbmanage.create_test_admin_and_users()
723 723 dbmanage.create_permissions()
724 724 dbmanage.populate_default_permissions()
725 725 Session().commit()
726 726
727 727
728 728 def create_test_repositories(test_path, config):
729 729 """
730 730 Creates test repositories in the temporary directory. Repositories are
731 731 extracted from archives within the rc_testdata package.
732 732 """
733 733 import rc_testdata
734 734 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
735 735
736 736 log.debug('making test vcs repositories')
737 737
738 738 idx_path = config['search.location']
739 739 data_path = config['cache_dir']
740 740
741 741 # clean index and data
742 742 if idx_path and os.path.exists(idx_path):
743 743 log.debug('remove %s', idx_path)
744 744 shutil.rmtree(idx_path)
745 745
746 746 if data_path and os.path.exists(data_path):
747 747 log.debug('remove %s', data_path)
748 748 shutil.rmtree(data_path)
749 749
750 750 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
751 751 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
752 752
753 753 # Note: Subversion is in the process of being integrated with the system,
754 754 # until we have a properly packed version of the test svn repository, this
755 755 # tries to copy over the repo from a package "rc_testdata"
756 756 svn_repo_path = rc_testdata.get_svn_repo_archive()
757 757 with tarfile.open(svn_repo_path) as tar:
758 758 tar.extractall(jn(test_path, SVN_REPO))
759 759
760 760
761 761 #==============================================================================
762 762 # PASTER COMMANDS
763 763 #==============================================================================
764 764 class BasePasterCommand(Command):
765 765 """
766 766 Abstract Base Class for paster commands.
767 767
768 768 The celery commands are somewhat aggressive about loading
769 769 celery.conf, and since our module sets the `CELERY_LOADER`
770 770 environment variable to our loader, we have to bootstrap a bit and
771 771 make sure we've had a chance to load the pylons config off of the
772 772 command line, otherwise everything fails.
773 773 """
774 774 min_args = 1
775 775 min_args_error = "Please provide a paster config file as an argument."
776 776 takes_config_file = 1
777 777 requires_config_file = True
778 778
779 779 def notify_msg(self, msg, log=False):
780 780 """Make a notification to user, additionally if logger is passed
781 781 it logs this action using given logger
782 782
783 783 :param msg: message that will be printed to user
784 784 :param log: logging instance, to use to additionally log this message
785 785
786 786 """
787 787 if log and isinstance(log, logging):
788 788 log(msg)
789 789
790 790 def run(self, args):
791 791 """
792 792 Overrides Command.run
793 793
794 794 Checks for a config file argument and loads it.
795 795 """
796 796 if len(args) < self.min_args:
797 797 raise BadCommand(
798 798 self.min_args_error % {'min_args': self.min_args,
799 799 'actual_args': len(args)})
800 800
801 801 # Decrement because we're going to lob off the first argument.
802 802 # @@ This is hacky
803 803 self.min_args -= 1
804 804 self.bootstrap_config(args[0])
805 805 self.update_parser()
806 806 return super(BasePasterCommand, self).run(args[1:])
807 807
808 808 def update_parser(self):
809 809 """
810 810 Abstract method. Allows for the class' parser to be updated
811 811 before the superclass' `run` method is called. Necessary to
812 812 allow options/arguments to be passed through to the underlying
813 813 celery command.
814 814 """
815 815 raise NotImplementedError("Abstract Method.")
816 816
817 817 def bootstrap_config(self, conf):
818 818 """
819 819 Loads the pylons configuration.
820 820 """
821 821 from pylons import config as pylonsconfig
822 822
823 823 self.path_to_ini_file = os.path.realpath(conf)
824 824 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
825 825 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
826 826
827 827 def _init_session(self):
828 828 """
829 829 Inits SqlAlchemy Session
830 830 """
831 831 logging.config.fileConfig(self.path_to_ini_file)
832 832 from pylons import config
833 833 from rhodecode.config.utils import initialize_database
834 834
835 835 # get to remove repos !!
836 836 add_cache(config)
837 837 initialize_database(config)
838 838
839 839
840 840 @decorator.decorator
841 841 def jsonify(func, *args, **kwargs):
842 842 """Action decorator that formats output for JSON
843 843
844 844 Given a function that will return content, this decorator will turn
845 845 the result into JSON, with a content-type of 'application/json' and
846 846 output it.
847 847
848 848 """
849 849 from pylons.decorators.util import get_pylons
850 850 from rhodecode.lib.ext_json import json
851 851 pylons = get_pylons(args)
852 852 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
853 853 data = func(*args, **kwargs)
854 854 if isinstance(data, (list, tuple)):
855 855 msg = "JSON responses with Array envelopes are susceptible to " \
856 856 "cross-site data leak attacks, see " \
857 857 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
858 858 warnings.warn(msg, Warning, 2)
859 859 log.warning(msg)
860 860 log.debug("Returning JSON wrapped action output")
861 861 return json.dumps(data, encoding='utf-8')
862 862
863 863
864 864 class PartialRenderer(object):
865 865 """
866 866 Partial renderer used to render chunks of html used in datagrids
867 867 use like::
868 868
869 869 _render = PartialRenderer('data_table/_dt_elements.mako')
870 870 _render('quick_menu', args, kwargs)
871 871 PartialRenderer.h,
872 872 c,
873 873 _,
874 874 ungettext
875 875 are the template stuff initialized inside and can be re-used later
876 876
877 877 :param tmpl_name: template path relate to /templates/ dir
878 878 """
879 879
880 880 def __init__(self, tmpl_name):
881 881 import rhodecode
882 882 from pylons import request, tmpl_context as c
883 883 from pylons.i18n.translation import _, ungettext
884 884 from rhodecode.lib import helpers as h
885 885
886 886 self.tmpl_name = tmpl_name
887 887 self.rhodecode = rhodecode
888 888 self.c = c
889 889 self._ = _
890 890 self.ungettext = ungettext
891 891 self.h = h
892 892 self.request = request
893 893
894 894 def _mako_lookup(self):
895 895 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
896 896 return _tmpl_lookup.get_template(self.tmpl_name)
897 897
898 898 def _update_kwargs_for_render(self, kwargs):
899 899 """
900 900 Inject params required for Mako rendering
901 901 """
902 902 _kwargs = {
903 903 '_': self._,
904 904 'h': self.h,
905 905 'c': self.c,
906 906 'request': self.request,
907 'ungettext': self.ungettext,
907 '_ungettext': self.ungettext,
908 908 }
909 909 _kwargs.update(kwargs)
910 910 return _kwargs
911 911
912 912 def _render_with_exc(self, render_func, args, kwargs):
913 913 try:
914 914 return render_func.render(*args, **kwargs)
915 915 except:
916 916 log.error(exceptions.text_error_template().render())
917 917 raise
918 918
919 919 def _get_template(self, template_obj, def_name):
920 920 if def_name:
921 921 tmpl = template_obj.get_def(def_name)
922 922 else:
923 923 tmpl = template_obj
924 924 return tmpl
925 925
926 926 def render(self, def_name, *args, **kwargs):
927 927 lookup_obj = self._mako_lookup()
928 928 tmpl = self._get_template(lookup_obj, def_name=def_name)
929 929 kwargs = self._update_kwargs_for_render(kwargs)
930 930 return self._render_with_exc(tmpl, args, kwargs)
931 931
932 932 def __call__(self, tmpl, *args, **kwargs):
933 933 return self.render(tmpl, *args, **kwargs)
934 934
935 935
936 936 def password_changed(auth_user, session):
937 937 # Never report password change in case of default user or anonymous user.
938 938 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
939 939 return False
940 940
941 941 password_hash = md5(auth_user.password) if auth_user.password else None
942 942 rhodecode_user = session.get('rhodecode_user', {})
943 943 session_password_hash = rhodecode_user.get('password', '')
944 944 return password_hash != session_password_hash
945 945
946 946
947 947 def read_opensource_licenses():
948 948 global _license_cache
949 949
950 950 if not _license_cache:
951 951 licenses = pkg_resources.resource_string(
952 952 'rhodecode', 'config/licenses.json')
953 953 _license_cache = json.loads(licenses)
954 954
955 955 return _license_cache
956 956
957 957
958 958 def get_registry(request):
959 959 """
960 960 Utility to get the pyramid registry from a request. During migration to
961 961 pyramid we sometimes want to use the pyramid registry from pylons context.
962 962 Therefore this utility returns `request.registry` for pyramid requests and
963 963 uses `get_current_registry()` for pylons requests.
964 964 """
965 965 try:
966 966 return request.registry
967 967 except AttributeError:
968 968 return get_current_registry()
969 969
970 970
971 971 def generate_platform_uuid():
972 972 """
973 973 Generates platform UUID based on it's name
974 974 """
975 975 import platform
976 976
977 977 try:
978 978 uuid_list = [platform.platform()]
979 979 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
980 980 except Exception as e:
981 981 log.error('Failed to generate host uuid: %s' % e)
982 982 return 'UNDEFINED'
@@ -1,64 +1,64 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email), '', ''),
6 6 (_('Created on'), h.format_date(c.repo_group.created_on), '', ''),
7 7 (_('Is Personal Group'), c.repo_group.personal or False, '', ''),
8 8
9 9 (_('Total repositories'), c.repo_group.repositories_recursive_count, '', ''),
10 10 (_('Top level repositories'), c.repo_group.repositories.count(), '', c.repo_group.repositories.all()),
11 11
12 12 (_('Children groups'), c.repo_group.children.count(), '', c.repo_group.children.all()),
13 13 ]
14 14 %>
15 15
16 16 <div class="panel panel-default">
17 17 <div class="panel-heading">
18 18 <h3 class="panel-title">${_('Repository Group: %s') % c.repo_group.group_name}</h3>
19 19 </div>
20 20 <div class="panel-body">
21 21 ${base.dt_info_panel(elems)}
22 22 </div>
23 23
24 24 </div>
25 25
26 26 <div class="panel panel-danger">
27 27 <div class="panel-heading">
28 28 <h3 class="panel-title">${_('Delete repository group')}</h3>
29 29 </div>
30 30 <div class="panel-body">
31 31 ${h.secure_form(h.url('delete_repo_group', group_name=c.repo_group.group_name),method='delete')}
32 32 <table class="display">
33 33
34 34 <tr>
35 35 <td>
36 ${ungettext('This repository group includes %s children repository group.', 'This repository group includes %s children repository groups.', c.repo_group.children.count()) % c.repo_group.children.count()}
36 ${_ungettext('This repository group includes %s children repository group.', 'This repository group includes %s children repository groups.', c.repo_group.children.count()) % c.repo_group.children.count()}
37 37 </td>
38 38 <td>
39 39 </td>
40 40 <td>
41 41 </td>
42 42 </tr>
43 43 <tr>
44 44 <td>
45 ${ungettext('This repository group includes %s repository.', 'This repository group includes %s repositories.', c.repo_group.repositories_recursive_count) % c.repo_group.repositories_recursive_count}
45 ${_ungettext('This repository group includes %s repository.', 'This repository group includes %s repositories.', c.repo_group.repositories_recursive_count) % c.repo_group.repositories_recursive_count}
46 46 </td>
47 47 <td>
48 48 </td>
49 49 <td>
50 50 </td>
51 51 </tr>
52 52
53 53 </table>
54 54 <div style="margin: 0 0 20px 0" class="fake-space"></div>
55 55
56 56 <button class="btn btn-small btn-danger" type="submit"
57 57 onclick="return confirm('${_('Confirm to delete this group: %s') % (c.repo_group.group_name)}');">
58 58 ${_('Delete this repository group')}
59 59 </button>
60 60 ${h.end_form()}
61 61 </div>
62 62 </div>
63 63
64 64
@@ -1,159 +1,159 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 6 (_('Source of Record'), c.user.extern_type, '', ''),
7 7
8 8 (_('Last login'), c.user.last_login or '-', '', ''),
9 9 (_('Last activity'), c.user.last_activity, '', ''),
10 10
11 11 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
12 12 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 13 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 14
15 15 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
16 16 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
17 17 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
18 18 ]
19 19 %>
20 20
21 21 <div class="panel panel-default">
22 22 <div class="panel-heading">
23 23 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
24 24 </div>
25 25 <div class="panel-body">
26 26 ${base.dt_info_panel(elems)}
27 27 </div>
28 28 </div>
29 29
30 30 <div class="panel panel-default">
31 31 <div class="panel-heading">
32 32 <h3 class="panel-title">${_('Force Password Reset')}</h3>
33 33 </div>
34 34 <div class="panel-body">
35 35 ${h.secure_form(h.url('force_password_reset_user', user_id=c.user.user_id), method='post')}
36 36 <div class="field">
37 37 <button class="btn btn-default" type="submit">
38 38 <i class="icon-lock"></i>
39 39 %if c.user.user_data.get('force_password_change'):
40 40 ${_('Disable forced password reset')}
41 41 %else:
42 42 ${_('Enable forced password reset')}
43 43 %endif
44 44 </button>
45 45 </div>
46 46 <div class="field">
47 47 <span class="help-block">
48 48 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
49 49 </span>
50 50 </div>
51 51 ${h.end_form()}
52 52 </div>
53 53 </div>
54 54
55 55 <div class="panel panel-default">
56 56 <div class="panel-heading">
57 57 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
58 58 </div>
59 59 <div class="panel-body">
60 60 ${h.secure_form(h.url('create_personal_repo_group', user_id=c.user.user_id), method='post')}
61 61
62 62 %if c.personal_repo_group:
63 63 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
64 64 %else:
65 65 <div class="panel-body-title-text">
66 66 ${_('This user currently does not have a personal repository group')}
67 67 <br/>
68 68 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
69 69 </div>
70 70 %endif
71 71 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
72 72 <i class="icon-folder-close"></i>
73 73 ${_('Create personal repository group')}
74 74 </button>
75 75 ${h.end_form()}
76 76 </div>
77 77 </div>
78 78
79 79
80 80 <div class="panel panel-danger">
81 81 <div class="panel-heading">
82 82 <h3 class="panel-title">${_('Delete User')}</h3>
83 83 </div>
84 84 <div class="panel-body">
85 85 ${h.secure_form(h.url('delete_user', user_id=c.user.user_id), method='delete')}
86 86
87 87 <table class="display">
88 88 <tr>
89 89 <td>
90 ${ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
90 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
91 91 </td>
92 92 <td>
93 93 %if len(c.user.repositories) > 0:
94 94 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked"/> <label for="user_repos_1">${_('Detach repositories')}</label>
95 95 %endif
96 96 </td>
97 97 <td>
98 98 %if len(c.user.repositories) > 0:
99 99 <input type="radio" id="user_repos_2" name="user_repos" value="delete" /> <label for="user_repos_2">${_('Delete repositories')}</label>
100 100 %endif
101 101 </td>
102 102 </tr>
103 103
104 104 <tr>
105 105 <td>
106 ${ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
106 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
107 107 </td>
108 108 <td>
109 109 %if len(c.user.repository_groups) > 0:
110 110 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked"/> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
111 111 %endif
112 112 </td>
113 113 <td>
114 114 %if len(c.user.repository_groups) > 0:
115 115 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" /> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
116 116 %endif
117 117 </td>
118 118 </tr>
119 119
120 120 <tr>
121 121 <td>
122 ${ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
122 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
123 123 </td>
124 124 <td>
125 125 %if len(c.user.user_groups) > 0:
126 126 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked"/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
127 127 %endif
128 128 </td>
129 129 <td>
130 130 %if len(c.user.user_groups) > 0:
131 131 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" /> <label for="user_user_groups_2">${_('Delete repositories')}</label>
132 132 %endif
133 133 </td>
134 134 </tr>
135 135 </table>
136 136 <div style="margin: 0 0 20px 0" class="fake-space"></div>
137 137
138 138 <div class="field">
139 139 <button class="btn btn-small btn-danger" type="submit"
140 140 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
141 141 ${"disabled" if not c.can_delete_user else ""}>
142 142 ${_('Delete this user')}
143 143 </button>
144 144 </div>
145 145 % if c.can_delete_user_message:
146 146 <p class="help-block pre-formatting">${c.can_delete_user_message}</p>
147 147 % endif
148 148
149 149 <div class="field">
150 150 <span class="help-block">
151 151 %if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
152 152 <p class="help-block">${_("When selecting the detach option, the depending objects owned by this user will be assigned to the `%s` super admin in the system. The delete option will delete the user's repositories!") % (c.first_admin.full_name)}</p>
153 153 %endif
154 154 </span>
155 155 </div>
156 156
157 157 ${h.end_form()}
158 158 </div>
159 159 </div>
@@ -1,207 +1,207 b''
1 1 ## snippet for displaying permissions overview for users
2 2 ## usage:
3 3 ## <%namespace name="p" file="/base/perms_summary.mako"/>
4 4 ## ${p.perms_summary(c.perm_user.permissions)}
5 5
6 6 <%def name="perms_summary(permissions, show_all=False, actions=True)">
7 7 <div id="perms" class="table fields">
8 8 %for section in sorted(permissions.keys()):
9 9 <div class="panel panel-default">
10 10 <div class="panel-heading">
11 11 <h3 class="panel-title">${section.replace("_"," ").capitalize()}</h3>
12 12 </div>
13 13 <div class="panel-body">
14 14 <div class="perms_section_head field">
15 15 <div class="radios">
16 16 %if section != 'global':
17 17 <span class="permissions_boxes">
18 18 <span class="desc">${_('show')}: </span>
19 19 ${h.checkbox('perms_filter_none_%s' % section, 'none', 'checked', class_='perm_filter filter_%s' % section, section=section, perm_type='none')} <label for="${'perms_filter_none_%s' % section}"><span class="perm_tag none">${_('none')}</span></label>
20 20 ${h.checkbox('perms_filter_read_%s' % section, 'read', 'checked', class_='perm_filter filter_%s' % section, section=section, perm_type='read')} <label for="${'perms_filter_read_%s' % section}"><span class="perm_tag read">${_('read')}</span></label>
21 21 ${h.checkbox('perms_filter_write_%s' % section, 'write', 'checked', class_='perm_filter filter_%s' % section, section=section, perm_type='write')} <label for="${'perms_filter_write_%s' % section}"> <span class="perm_tag write">${_('write')}</span></label>
22 22 ${h.checkbox('perms_filter_admin_%s' % section, 'admin', 'checked', class_='perm_filter filter_%s' % section, section=section, perm_type='admin')} <label for="${'perms_filter_admin_%s' % section}"><span class="perm_tag admin">${_('admin')}</span></label>
23 23 </span>
24 24 %endif
25 25 </div>
26 26 </div>
27 27 <div class="field">
28 28 %if not permissions[section]:
29 29 <p class="empty_data help-block">${_('No permissions defined')}</p>
30 30 %else:
31 31 <div id='tbl_list_wrap_${section}'>
32 32 <table id="tbl_list_${section}" class="rctable">
33 33 ## global permission box
34 34 %if section == 'global':
35 35 <thead>
36 36 <tr>
37 37 <th colspan="2" class="left">${_('Permission')}</th>
38 38 %if actions:
39 39 <th>${_('Edit Permission')}</th>
40 40 %endif
41 41 </thead>
42 42 <tbody>
43 43
44 44 <%
45 45 def get_section_perms(prefix, opts):
46 46 _selected = []
47 47 for op in opts:
48 48 if op.startswith(prefix) and not op.startswith('hg.create.write_on_repogroup'):
49 49 _selected.append(op)
50 50 admin = 'hg.admin' in opts
51 51 _selected_vals = [x.partition(prefix)[-1] for x in _selected]
52 52 return admin, _selected_vals, _selected
53 53 %>
54 54 <%def name="glob(lbl, val, val_lbl=None, custom_url=None)">
55 55 <tr>
56 56 <td class="td-tags">
57 57 ${lbl}
58 58 </td>
59 59 <td class="td-tags">
60 60 %if val[0]:
61 61 %if not val_lbl:
62 62 ${h.bool2icon(True)}
63 63 %else:
64 64 <span class="perm_tag admin">${val_lbl}.admin</span>
65 65 %endif
66 66 %else:
67 67 %if not val_lbl:
68 68 ${h.bool2icon({'false': False,
69 69 'true': True,
70 70 'none': False,
71 71 'repository': True}.get(val[1][0] if 0 < len(val[1]) else 'false'))}
72 72 %else:
73 73 <span class="perm_tag ${val[1][0]}">${val_lbl}.${val[1][0]}</span>
74 74 %endif
75 75 %endif
76 76 </td>
77 77 %if actions:
78 78 <td class="td-action">
79 79 <a href="${custom_url or h.route_path('admin_permissions_global')}">${_('edit')}</a>
80 80 </td>
81 81 %endif
82 82 </tr>
83 83 </%def>
84 84
85 85 ${glob(_('Super admin'), get_section_perms('hg.admin', permissions[section]))}
86 86
87 87 ${glob(_('Repository default permission'), get_section_perms('repository.', permissions[section]), 'repository', h.route_path('admin_permissions_object'))}
88 88 ${glob(_('Repository group default permission'), get_section_perms('group.', permissions[section]), 'group', h.route_path('admin_permissions_object'))}
89 89 ${glob(_('User group default permission'), get_section_perms('usergroup.', permissions[section]), 'usergroup', h.route_path('admin_permissions_object'))}
90 90
91 91 ${glob(_('Create repositories'), get_section_perms('hg.create.', permissions[section]), custom_url=h.route_path('admin_permissions_global'))}
92 92 ${glob(_('Fork repositories'), get_section_perms('hg.fork.', permissions[section]), custom_url=h.route_path('admin_permissions_global'))}
93 93 ${glob(_('Create repository groups'), get_section_perms('hg.repogroup.create.', permissions[section]), custom_url=h.route_path('admin_permissions_global'))}
94 94 ${glob(_('Create user groups'), get_section_perms('hg.usergroup.create.', permissions[section]), custom_url=h.route_path('admin_permissions_global'))}
95 95
96 96
97 97 </tbody>
98 98 %else:
99 99 ## none/read/write/admin permissions on groups/repos etc
100 100 <thead>
101 101 <tr>
102 102 <th>${_('Name')}</th>
103 103 <th>${_('Permission')}</th>
104 104 %if actions:
105 105 <th>${_('Edit Permission')}</th>
106 106 %endif
107 107 </thead>
108 108 <tbody class="section_${section}">
109 109 <%
110 110 def sorter(permissions):
111 111 def custom_sorter(item):
112 112 ## read/write/admin
113 113 section = item[1].split('.')[-1]
114 114 section_importance = {'none': u'0',
115 115 'read': u'1',
116 116 'write':u'2',
117 117 'admin':u'3'}.get(section)
118 118 ## sort by group importance+name
119 119 return section_importance+item[0]
120 120 return sorted(permissions, key=custom_sorter)
121 121 %>
122 122 %for k, section_perm in sorter(permissions[section].items()):
123 123 %if section_perm.split('.')[-1] != 'none' or show_all:
124 124 <tr class="perm_row ${'%s_%s' % (section, section_perm.split('.')[-1])}">
125 125 <td class="td-componentname">
126 126 %if section == 'repositories':
127 127 <a href="${h.route_path('repo_summary',repo_name=k)}">${k}</a>
128 128 %elif section == 'repositories_groups':
129 129 <a href="${h.route_path('repo_group_home', repo_group_name=k)}">${k}</a>
130 130 %elif section == 'user_groups':
131 131 ##<a href="${h.url('edit_users_group',user_group_id=k)}">${k}</a>
132 132 ${k}
133 133 %endif
134 134 </td>
135 135 <td class="td-tags">
136 136 %if hasattr(permissions[section], 'perm_origin_stack'):
137 137 %for i, (perm, origin) in enumerate(reversed(permissions[section].perm_origin_stack[k])):
138 138 <span class="${i > 0 and 'perm_overriden' or ''} perm_tag ${perm.split('.')[-1]}">
139 139 ${perm} (${origin})
140 140 </span>
141 141 %endfor
142 142 %else:
143 143 <span class="perm_tag ${section_perm.split('.')[-1]}">${section_perm}</span>
144 144 %endif
145 145 </td>
146 146 %if actions:
147 147 <td class="td-action">
148 148 %if section == 'repositories':
149 <a href="${h.route_path('edit_repo_perms',repo_name=k,anchor='permissions_manage')}">${_('edit')}</a>
149 <a href="${h.route_path('edit_repo_perms',repo_name=k,_anchor='permissions_manage')}">${_('edit')}</a>
150 150 %elif section == 'repositories_groups':
151 151 <a href="${h.url('edit_repo_group_perms',group_name=k,anchor='permissions_manage')}">${_('edit')}</a>
152 152 %elif section == 'user_groups':
153 153 ##<a href="${h.url('edit_users_group',user_group_id=k)}">${_('edit')}</a>
154 154 %endif
155 155 </td>
156 156 %endif
157 157 </tr>
158 158 %endif
159 159 %endfor
160 160
161 161 <tr id="empty_${section}" class="noborder" style="display:none;">
162 162 <td colspan="6">${_('No permission defined')}</td>
163 163 </tr>
164 164
165 165 </tbody>
166 166 %endif
167 167 </table>
168 168 </div>
169 169 %endif
170 170 </div>
171 171 </div>
172 172 </div>
173 173 %endfor
174 174 </div>
175 175
176 176 <script>
177 177 $(document).ready(function(){
178 178 var show_empty = function(section){
179 179 var visible = $('.section_{0} tr.perm_row:visible'.format(section)).length;
180 180 if(visible == 0){
181 181 $('#empty_{0}'.format(section)).show();
182 182 }
183 183 else{
184 184 $('#empty_{0}'.format(section)).hide();
185 185 }
186 186 };
187 187 $('.perm_filter').on('change', function(e){
188 188 var self = this;
189 189 var section = $(this).attr('section');
190 190
191 191 var opts = {};
192 192 var elems = $('.filter_' + section).each(function(el){
193 193 var perm_type = $(this).attr('perm_type');
194 194 var checked = this.checked;
195 195 opts[perm_type] = checked;
196 196 if(checked){
197 197 $('.'+section+'_'+perm_type).show();
198 198 }
199 199 else{
200 200 $('.'+section+'_'+perm_type).hide();
201 201 }
202 202 });
203 203 show_empty(section);
204 204 })
205 205 })
206 206 </script>
207 207 </%def>
@@ -1,352 +1,352 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 5
6 6 <%def name="title()">
7 7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 8 %if c.rhodecode_name:
9 9 &middot; ${h.branding(c.rhodecode_name)}
10 10 %endif
11 11 </%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <script>
23 23 // TODO: marcink switch this to pyroutes
24 24 AJAX_COMMENT_DELETE_URL = "${h.url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 26 </script>
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 30 </div>
31 31
32 32 <div id="changeset_compare_view_content" class="summary changeset">
33 33 <div class="summary-detail">
34 34 <div class="summary-detail-header">
35 35 <span class="breadcrumbs files_location">
36 36 <h4>${_('Commit')}
37 37 <code>
38 38 ${h.show_id(c.commit)}
39 39 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
40 40 % if hasattr(c.commit, 'phase'):
41 41 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">${c.commit.phase}</span>
42 42 % endif
43 43
44 44 ## obsolete commits
45 45 % if hasattr(c.commit, 'obsolete'):
46 46 % if c.commit.obsolete:
47 47 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
48 48 % endif
49 49 % endif
50 50
51 51 ## hidden commits
52 52 % if hasattr(c.commit, 'hidden'):
53 53 % if c.commit.hidden:
54 54 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
55 55 % endif
56 56 % endif
57 57
58 58 </code>
59 59 </h4>
60 60 </span>
61 61 <div class="pull-right">
62 62 <span id="parent_link">
63 63 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
64 64 </span>
65 65 |
66 66 <span id="child_link">
67 67 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
68 68 </span>
69 69 </div>
70 70 </div>
71 71
72 72 <div class="fieldset">
73 73 <div class="left-label">
74 74 ${_('Description')}:
75 75 </div>
76 76 <div class="right-content">
77 77 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
78 78 <div id="message_expand" style="display:none;">
79 79 ${_('Expand')}
80 80 </div>
81 81 </div>
82 82 </div>
83 83
84 84 %if c.statuses:
85 85 <div class="fieldset">
86 86 <div class="left-label">
87 87 ${_('Commit status')}:
88 88 </div>
89 89 <div class="right-content">
90 90 <div class="changeset-status-ico">
91 91 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
92 92 </div>
93 93 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
94 94 </div>
95 95 </div>
96 96 %endif
97 97
98 98 <div class="fieldset">
99 99 <div class="left-label">
100 100 ${_('References')}:
101 101 </div>
102 102 <div class="right-content">
103 103 <div class="tags">
104 104
105 105 %if c.commit.merge:
106 106 <span class="mergetag tag">
107 107 <i class="icon-merge"></i>${_('merge')}
108 108 </span>
109 109 %endif
110 110
111 111 %if h.is_hg(c.rhodecode_repo):
112 112 %for book in c.commit.bookmarks:
113 113 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
114 114 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
115 115 </span>
116 116 %endfor
117 117 %endif
118 118
119 119 %for tag in c.commit.tags:
120 120 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
121 121 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
122 122 </span>
123 123 %endfor
124 124
125 125 %if c.commit.branch:
126 126 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % c.commit.branch)}">
127 127 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=c.commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
128 128 </span>
129 129 %endif
130 130 </div>
131 131 </div>
132 132 </div>
133 133
134 134 <div class="fieldset">
135 135 <div class="left-label">
136 136 ${_('Diff options')}:
137 137 </div>
138 138 <div class="right-content">
139 139 <div class="diff-actions">
140 140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 141 ${_('Raw Diff')}
142 142 </a>
143 143 |
144 144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 145 ${_('Patch Diff')}
146 146 </a>
147 147 |
148 148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 149 ${_('Download Diff')}
150 150 </a>
151 151 |
152 152 ${c.ignorews_url(request.GET)}
153 153 |
154 154 ${c.context_url(request.GET)}
155 155 </div>
156 156 </div>
157 157 </div>
158 158
159 159 <div class="fieldset">
160 160 <div class="left-label">
161 161 ${_('Comments')}:
162 162 </div>
163 163 <div class="right-content">
164 164 <div class="comments-number">
165 165 %if c.comments:
166 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
166 <a href="#comments">${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
167 167 %else:
168 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
168 ${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
169 169 %endif
170 170 %if c.inline_cnt:
171 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
171 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
172 172 %else:
173 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
173 ${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
174 174 %endif
175 175 </div>
176 176 </div>
177 177 </div>
178 178
179 179 <div class="fieldset">
180 180 <div class="left-label">
181 181 ${_('Unresolved TODOs')}:
182 182 </div>
183 183 <div class="right-content">
184 184 <div class="comments-number">
185 185 % if c.unresolved_comments:
186 186 % for co in c.unresolved_comments:
187 187 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
188 188 % endfor
189 189 % else:
190 190 ${_('There are no unresolved TODOs')}
191 191 % endif
192 192 </div>
193 193 </div>
194 194 </div>
195 195
196 196 </div> <!-- end summary-detail -->
197 197
198 198 <div id="commit-stats" class="sidebar-right">
199 199 <div class="summary-detail-header">
200 200 <h4 class="item">
201 201 ${_('Author')}
202 202 </h4>
203 203 </div>
204 204 <div class="sidebar-right-content">
205 205 ${self.gravatar_with_user(c.commit.author)}
206 206 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
207 207 </div>
208 208 </div><!-- end sidebar -->
209 209 </div> <!-- end summary -->
210 210 <div class="cs_files">
211 211 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
212 212 ${cbdiffs.render_diffset_menu()}
213 213 ${cbdiffs.render_diffset(
214 214 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
215 215 </div>
216 216
217 217 ## template for inline comment form
218 218 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
219 219
220 220 ## render comments
221 221 ${comment.generate_comments(c.comments)}
222 222
223 223 ## main comment form and it status
224 224 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
225 225 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
226 226 </div>
227 227
228 228 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
229 229 <script type="text/javascript">
230 230
231 231 $(document).ready(function() {
232 232
233 233 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
234 234 if($('#trimmed_message_box').height() === boxmax){
235 235 $('#message_expand').show();
236 236 }
237 237
238 238 $('#message_expand').on('click', function(e){
239 239 $('#trimmed_message_box').css('max-height', 'none');
240 240 $(this).hide();
241 241 });
242 242
243 243 $('.show-inline-comments').on('click', function(e){
244 244 var boxid = $(this).attr('data-comment-id');
245 245 var button = $(this);
246 246
247 247 if(button.hasClass("comments-visible")) {
248 248 $('#{0} .inline-comments'.format(boxid)).each(function(index){
249 249 $(this).hide();
250 250 });
251 251 button.removeClass("comments-visible");
252 252 } else {
253 253 $('#{0} .inline-comments'.format(boxid)).each(function(index){
254 254 $(this).show();
255 255 });
256 256 button.addClass("comments-visible");
257 257 }
258 258 });
259 259
260 260
261 261 // next links
262 262 $('#child_link').on('click', function(e){
263 263 // fetch via ajax what is going to be the next link, if we have
264 264 // >1 links show them to user to choose
265 265 if(!$('#child_link').hasClass('disabled')){
266 266 $.ajax({
267 267 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
268 268 success: function(data) {
269 269 if(data.results.length === 0){
270 270 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
271 271 }
272 272 if(data.results.length === 1){
273 273 var commit = data.results[0];
274 274 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
275 275 }
276 276 else if(data.results.length === 2){
277 277 $('#child_link').addClass('disabled');
278 278 $('#child_link').addClass('double');
279 279 var _html = '';
280 280 _html +='<a title="__title__" href="__url__">__rev__</a> '
281 281 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
282 282 .replace('__title__', data.results[0].message)
283 283 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
284 284 _html +=' | ';
285 285 _html +='<a title="__title__" href="__url__">__rev__</a> '
286 286 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
287 287 .replace('__title__', data.results[1].message)
288 288 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
289 289 $('#child_link').html(_html);
290 290 }
291 291 }
292 292 });
293 293 e.preventDefault();
294 294 }
295 295 });
296 296
297 297 // prev links
298 298 $('#parent_link').on('click', function(e){
299 299 // fetch via ajax what is going to be the next link, if we have
300 300 // >1 links show them to user to choose
301 301 if(!$('#parent_link').hasClass('disabled')){
302 302 $.ajax({
303 303 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
304 304 success: function(data) {
305 305 if(data.results.length === 0){
306 306 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
307 307 }
308 308 if(data.results.length === 1){
309 309 var commit = data.results[0];
310 310 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
311 311 }
312 312 else if(data.results.length === 2){
313 313 $('#parent_link').addClass('disabled');
314 314 $('#parent_link').addClass('double');
315 315 var _html = '';
316 316 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
317 317 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
318 318 .replace('__title__', data.results[0].message)
319 319 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
320 320 _html +=' | ';
321 321 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
322 322 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
323 323 .replace('__title__', data.results[1].message)
324 324 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
325 325 $('#parent_link').html(_html);
326 326 }
327 327 }
328 328 });
329 329 e.preventDefault();
330 330 }
331 331 });
332 332
333 333 if (location.hash) {
334 334 var result = splitDelimitedHash(location.hash);
335 335 var line = $('html').find(result.loc);
336 336 if (line.length > 0){
337 337 offsetScroll(line, 70);
338 338 }
339 339 }
340 340
341 341 // browse tree @ revision
342 342 $('#files_link').on('click', function(e){
343 343 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
344 344 e.preventDefault();
345 345 });
346 346
347 347 // inject comments into their proper positions
348 348 var file_comments = $('.inline-comment-placeholder');
349 349 })
350 350 </script>
351 351
352 352 </%def>
@@ -1,125 +1,125 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Commits') % c.repo_name} -
6 6 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
7 7 ...
8 8 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 ${_('Commits')} -
17 17 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
18 18 ...
19 19 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
20 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
20 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
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='changelog')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32 <div class="summary-header">
33 33 <div class="title">
34 34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 35 </div>
36 36 </div>
37 37
38 38
39 39 <div class="summary changeset">
40 40 <div class="summary-detail">
41 41 <div class="summary-detail-header">
42 42 <span class="breadcrumbs files_location">
43 43 <h4>
44 44 ${_('Commit Range')}
45 45 <code>
46 46 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
47 47 </code>
48 48 </h4>
49 49 </span>
50 50 </div>
51 51
52 52 <div class="fieldset">
53 53 <div class="left-label">
54 54 ${_('Diff option')}:
55 55 </div>
56 56 <div class="right-content">
57 57 <div class="header-buttons">
58 58 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}">
59 59 ${_('Show combined compare')}
60 60 </a>
61 61 </div>
62 62 </div>
63 63 </div>
64 64
65 65 <%doc>
66 66 ##TODO(marcink): implement this and diff menus
67 67 <div class="fieldset">
68 68 <div class="left-label">
69 69 ${_('Diff options')}:
70 70 </div>
71 71 <div class="right-content">
72 72 <div class="diff-actions">
73 73 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
74 74 ${_('Raw Diff')}
75 75 </a>
76 76 |
77 77 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
78 78 ${_('Patch Diff')}
79 79 </a>
80 80 |
81 81 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
82 82 ${_('Download Diff')}
83 83 </a>
84 84 </div>
85 85 </div>
86 86 </div>
87 87 </%doc>
88 88 </div> <!-- end summary-detail -->
89 89
90 90 </div> <!-- end summary -->
91 91
92 92 <div id="changeset_compare_view_content">
93 93 <div class="pull-left">
94 94 <div class="btn-group">
95 95 <a
96 96 class="btn"
97 97 href="#"
98 98 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
99 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
99 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
100 100 </a>
101 101 <a
102 102 class="btn"
103 103 href="#"
104 104 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
105 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
105 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
106 106 </a>
107 107 </div>
108 108 </div>
109 109 ## Commit range generated below
110 110 <%include file="../compare/compare_commits.mako"/>
111 111 <div class="cs_files">
112 112 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
113 113 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
114 114 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
115 115 ${cbdiffs.render_diffset_menu()}
116 116 %for commit in c.commit_ranges:
117 117 ${cbdiffs.render_diffset(
118 118 diffset=c.changes[commit.raw_id],
119 119 collapse_when_files_over=5,
120 120 commit=commit,
121 121 )}
122 122 %endfor
123 123 </div>
124 124 </div>
125 125 </%def>
@@ -1,64 +1,64 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##usage:
3 3 ## <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
4 4 ## ${diff_block.diff_block_changeset_table(change)}
5 5 ##
6 6 <%def name="changeset_message()">
7 7 <h5>${_('The requested commit is too big and content was truncated.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
8 8 </%def>
9 9 <%def name="file_message()">
10 10 <h5>${_('The requested file is too big and its content is not shown.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
11 11 </%def>
12 12
13 13 <%def name="diff_block_changeset_table(change)">
14 14 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
15 15 %for FID,(cs1, cs2, change, filenode_path, diff, stats, file_data) in change.iteritems():
16 16 <div id="${h.FID('',filenode_path)}_target" ></div>
17 17 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
18 18 <div class="code-body">
19 19 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}" style="display: none"></div>
20 20 ${diff|n}
21 21 % if file_data["is_limited_diff"]:
22 22 % if file_data["exceeds_limit"]:
23 23 ${self.file_message()}
24 24 % else:
25 25 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
26 26 % endif
27 27 % endif
28 28 </div>
29 29 </div>
30 30 %endfor
31 31 </div>
32 32 </%def>
33 33
34 34 <%def name="diff_block_simple(change)">
35 35 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
36 36 %for op,filenode_path,diff,file_data in change:
37 37 <div id="${h.FID('',filenode_path)}_target" ></div>
38 38 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm" >
39 39 <div class="code-body">
40 40 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}" style="display: none;"></div>
41 41 ${diff|n}
42 42 % if file_data["is_limited_diff"]:
43 43 % if file_data["exceeds_limit"]:
44 44 ${self.file_message()}
45 45 % else:
46 46 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
47 47 % endif
48 48 % endif
49 49 </div>
50 50 </div>
51 51 %endfor
52 52 </div>
53 53 </%def>
54 54
55 55
56 56 <%def name="diff_summary_text(changed_files, lines_added, lines_deleted, limited_diff=False)">
57 57 % if limited_diff:
58 ${ungettext('%(num)s file changed', '%(num)s files changed', changed_files) % {'num': changed_files}}
58 ${_ungettext('%(num)s file changed', '%(num)s files changed', changed_files) % {'num': changed_files}}
59 59 % else:
60 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
60 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
61 61 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', changed_files) % {'num': changed_files, 'linesadd': lines_added, 'linesdel': lines_deleted}}
62 62 %endif
63 63 </%def>
64 64
@@ -1,671 +1,671 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26 <%def name="link_for(**kw)">
27 27 <%
28 28 new_args = request.GET.mixed()
29 29 new_args.update(kw)
30 30 return h.url('', **new_args)
31 31 %>
32 32 </%def>
33 33
34 34 <%def name="render_diffset(diffset, commit=None,
35 35
36 36 # collapse all file diff entries when there are more than this amount of files in the diff
37 37 collapse_when_files_over=20,
38 38
39 39 # collapse lines in the diff when more than this amount of lines changed in the file diff
40 40 lines_changed_limit=500,
41 41
42 42 # add a ruler at to the output
43 43 ruler_at_chars=0,
44 44
45 45 # show inline comments
46 46 use_comments=False,
47 47
48 48 # disable new comments
49 49 disable_new_comments=False,
50 50
51 51 # special file-comments that were deleted in previous versions
52 52 # it's used for showing outdated comments for deleted files in a PR
53 53 deleted_files_comments=None
54 54
55 55 )">
56 56
57 57 %if use_comments:
58 58 <div id="cb-comments-inline-container-template" class="js-template">
59 59 ${inline_comments_container([])}
60 60 </div>
61 61 <div class="js-template" id="cb-comment-inline-form-template">
62 62 <div class="comment-inline-form ac">
63 63
64 64 %if c.rhodecode_user.username != h.DEFAULT_USER:
65 65 ## render template for inline comments
66 66 ${commentblock.comment_form(form_type='inline')}
67 67 %else:
68 68 ${h.form('', class_='inline-form comment-form-login', method='get')}
69 69 <div class="pull-left">
70 70 <div class="comment-help pull-right">
71 71 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
72 72 </div>
73 73 </div>
74 74 <div class="comment-button pull-right">
75 75 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
76 76 ${_('Cancel')}
77 77 </button>
78 78 </div>
79 79 <div class="clearfix"></div>
80 80 ${h.end_form()}
81 81 %endif
82 82 </div>
83 83 </div>
84 84
85 85 %endif
86 86 <%
87 87 collapse_all = len(diffset.files) > collapse_when_files_over
88 88 %>
89 89
90 90 %if c.diffmode == 'sideside':
91 91 <style>
92 92 .wrapper {
93 93 max-width: 1600px !important;
94 94 }
95 95 </style>
96 96 %endif
97 97
98 98 %if ruler_at_chars:
99 99 <style>
100 100 .diff table.cb .cb-content:after {
101 101 content: "";
102 102 border-left: 1px solid blue;
103 103 position: absolute;
104 104 top: 0;
105 105 height: 18px;
106 106 opacity: .2;
107 107 z-index: 10;
108 108 //## +5 to account for diff action (+/-)
109 109 left: ${ruler_at_chars + 5}ch;
110 110 </style>
111 111 %endif
112 112
113 113 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
114 114 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
115 115 %if commit:
116 116 <div class="pull-right">
117 117 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
118 118 ${_('Browse Files')}
119 119 </a>
120 120 </div>
121 121 %endif
122 122 <h2 class="clearinner">
123 123 %if commit:
124 124 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
125 125 ${h.age_component(commit.date)} -
126 126 %endif
127 127 %if diffset.limited_diff:
128 128 ${_('The requested commit is too big and content was truncated.')}
129 129
130 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
130 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
131 131 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
132 132 %else:
133 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
133 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
134 134 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
135 135 %endif
136 136
137 137 </h2>
138 138 </div>
139 139
140 140 %if not diffset.files:
141 141 <p class="empty_data">${_('No files')}</p>
142 142 %endif
143 143
144 144 <div class="filediffs">
145 145 ## initial value could be marked as False later on
146 146 <% over_lines_changed_limit = False %>
147 147 %for i, filediff in enumerate(diffset.files):
148 148
149 149 <%
150 150 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
151 151 over_lines_changed_limit = lines_changed > lines_changed_limit
152 152 %>
153 153 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
154 154 <div
155 155 class="filediff"
156 156 data-f-path="${filediff.patch['filename']}"
157 157 id="a_${h.FID('', filediff.patch['filename'])}">
158 158 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
159 159 <div class="filediff-collapse-indicator"></div>
160 160 ${diff_ops(filediff)}
161 161 </label>
162 162 ${diff_menu(filediff, use_comments=use_comments)}
163 163 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
164 164 %if not filediff.hunks:
165 165 %for op_id, op_text in filediff.patch['stats']['ops'].items():
166 166 <tr>
167 167 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
168 168 %if op_id == DEL_FILENODE:
169 169 ${_('File was deleted')}
170 170 %elif op_id == BIN_FILENODE:
171 171 ${_('Binary file hidden')}
172 172 %else:
173 173 ${op_text}
174 174 %endif
175 175 </td>
176 176 </tr>
177 177 %endfor
178 178 %endif
179 179 %if filediff.limited_diff:
180 180 <tr class="cb-warning cb-collapser">
181 181 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
182 182 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
183 183 </td>
184 184 </tr>
185 185 %else:
186 186 %if over_lines_changed_limit:
187 187 <tr class="cb-warning cb-collapser">
188 188 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
189 189 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 190 <a href="#" class="cb-expand"
191 191 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
192 192 </a>
193 193 <a href="#" class="cb-collapse"
194 194 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
195 195 </a>
196 196 </td>
197 197 </tr>
198 198 %endif
199 199 %endif
200 200
201 201 %for hunk in filediff.hunks:
202 202 <tr class="cb-hunk">
203 203 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
204 204 ## TODO: dan: add ajax loading of more context here
205 205 ## <a href="#">
206 206 <i class="icon-more"></i>
207 207 ## </a>
208 208 </td>
209 209 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
210 210 @@
211 211 -${hunk.source_start},${hunk.source_length}
212 212 +${hunk.target_start},${hunk.target_length}
213 213 ${hunk.section_header}
214 214 </td>
215 215 </tr>
216 216 %if c.diffmode == 'unified':
217 217 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
218 218 %elif c.diffmode == 'sideside':
219 219 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
220 220 %else:
221 221 <tr class="cb-line">
222 222 <td>unknown diff mode</td>
223 223 </tr>
224 224 %endif
225 225 %endfor
226 226
227 227 ## outdated comments that do not fit into currently displayed lines
228 228 % for lineno, comments in filediff.left_comments.items():
229 229
230 230 %if c.diffmode == 'unified':
231 231 <tr class="cb-line">
232 232 <td class="cb-data cb-context"></td>
233 233 <td class="cb-lineno cb-context"></td>
234 234 <td class="cb-lineno cb-context"></td>
235 235 <td class="cb-content cb-context">
236 236 ${inline_comments_container(comments)}
237 237 </td>
238 238 </tr>
239 239 %elif c.diffmode == 'sideside':
240 240 <tr class="cb-line">
241 241 <td class="cb-data cb-context"></td>
242 242 <td class="cb-lineno cb-context"></td>
243 243 <td class="cb-content cb-context"></td>
244 244
245 245 <td class="cb-data cb-context"></td>
246 246 <td class="cb-lineno cb-context"></td>
247 247 <td class="cb-content cb-context">
248 248 ${inline_comments_container(comments)}
249 249 </td>
250 250 </tr>
251 251 %endif
252 252
253 253 % endfor
254 254
255 255 </table>
256 256 </div>
257 257 %endfor
258 258
259 259 ## outdated comments that are made for a file that has been deleted
260 260 % for filename, comments_dict in (deleted_files_comments or {}).items():
261 261
262 262 <div class="filediffs filediff-outdated" style="display: none">
263 263 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
264 264 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
265 265 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
266 266 <div class="filediff-collapse-indicator"></div>
267 267 <span class="pill">
268 268 ## file was deleted
269 269 <strong>${filename}</strong>
270 270 </span>
271 271 <span class="pill-group" style="float: left">
272 272 ## file op, doesn't need translation
273 273 <span class="pill" op="removed">removed in this version</span>
274 274 </span>
275 275 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
276 276 <span class="pill-group" style="float: right">
277 277 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
278 278 </span>
279 279 </label>
280 280
281 281 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
282 282 <tr>
283 283 % if c.diffmode == 'unified':
284 284 <td></td>
285 285 %endif
286 286
287 287 <td></td>
288 288 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
289 289 ${_('File was deleted in this version, and outdated comments were made on it')}
290 290 </td>
291 291 </tr>
292 292 %if c.diffmode == 'unified':
293 293 <tr class="cb-line">
294 294 <td class="cb-data cb-context"></td>
295 295 <td class="cb-lineno cb-context"></td>
296 296 <td class="cb-lineno cb-context"></td>
297 297 <td class="cb-content cb-context">
298 298 ${inline_comments_container(comments_dict['comments'])}
299 299 </td>
300 300 </tr>
301 301 %elif c.diffmode == 'sideside':
302 302 <tr class="cb-line">
303 303 <td class="cb-data cb-context"></td>
304 304 <td class="cb-lineno cb-context"></td>
305 305 <td class="cb-content cb-context"></td>
306 306
307 307 <td class="cb-data cb-context"></td>
308 308 <td class="cb-lineno cb-context"></td>
309 309 <td class="cb-content cb-context">
310 310 ${inline_comments_container(comments_dict['comments'])}
311 311 </td>
312 312 </tr>
313 313 %endif
314 314 </table>
315 315 </div>
316 316 </div>
317 317 % endfor
318 318
319 319 </div>
320 320 </div>
321 321 </%def>
322 322
323 323 <%def name="diff_ops(filediff)">
324 324 <%
325 325 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
326 326 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
327 327 %>
328 328 <span class="pill">
329 329 %if filediff.source_file_path and filediff.target_file_path:
330 330 %if filediff.source_file_path != filediff.target_file_path:
331 331 ## file was renamed, or copied
332 332 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
333 333 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
334 334 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
335 335 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
336 336 %endif
337 337 %else:
338 338 ## file was modified
339 339 <strong>${filediff.source_file_path}</strong>
340 340 %endif
341 341 %else:
342 342 %if filediff.source_file_path:
343 343 ## file was deleted
344 344 <strong>${filediff.source_file_path}</strong>
345 345 %else:
346 346 ## file was added
347 347 <strong>${filediff.target_file_path}</strong>
348 348 %endif
349 349 %endif
350 350 </span>
351 351 <span class="pill-group" style="float: left">
352 352 %if filediff.limited_diff:
353 353 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
354 354 %endif
355 355
356 356 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
357 357 <span class="pill" op="renamed">renamed</span>
358 358 %endif
359 359
360 360 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
361 361 <span class="pill" op="copied">copied</span>
362 362 %endif
363 363
364 364 %if NEW_FILENODE in filediff.patch['stats']['ops']:
365 365 <span class="pill" op="created">created</span>
366 366 %if filediff['target_mode'].startswith('120'):
367 367 <span class="pill" op="symlink">symlink</span>
368 368 %else:
369 369 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
370 370 %endif
371 371 %endif
372 372
373 373 %if DEL_FILENODE in filediff.patch['stats']['ops']:
374 374 <span class="pill" op="removed">removed</span>
375 375 %endif
376 376
377 377 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
378 378 <span class="pill" op="mode">
379 379 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
380 380 </span>
381 381 %endif
382 382 </span>
383 383
384 384 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
385 385
386 386 <span class="pill-group" style="float: right">
387 387 %if BIN_FILENODE in filediff.patch['stats']['ops']:
388 388 <span class="pill" op="binary">binary</span>
389 389 %if MOD_FILENODE in filediff.patch['stats']['ops']:
390 390 <span class="pill" op="modified">modified</span>
391 391 %endif
392 392 %endif
393 393 %if filediff.patch['stats']['added']:
394 394 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
395 395 %endif
396 396 %if filediff.patch['stats']['deleted']:
397 397 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
398 398 %endif
399 399 </span>
400 400
401 401 </%def>
402 402
403 403 <%def name="nice_mode(filemode)">
404 404 ${filemode.startswith('100') and filemode[3:] or filemode}
405 405 </%def>
406 406
407 407 <%def name="diff_menu(filediff, use_comments=False)">
408 408 <div class="filediff-menu">
409 409 %if filediff.diffset.source_ref:
410 410 %if filediff.operation in ['D', 'M']:
411 411 <a
412 412 class="tooltip"
413 413 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
414 414 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
415 415 >
416 416 ${_('Show file before')}
417 417 </a> |
418 418 %else:
419 419 <span
420 420 class="tooltip"
421 421 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
422 422 >
423 423 ${_('Show file before')}
424 424 </span> |
425 425 %endif
426 426 %if filediff.operation in ['A', 'M']:
427 427 <a
428 428 class="tooltip"
429 429 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
430 430 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
431 431 >
432 432 ${_('Show file after')}
433 433 </a> |
434 434 %else:
435 435 <span
436 436 class="tooltip"
437 437 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
438 438 >
439 439 ${_('Show file after')}
440 440 </span> |
441 441 %endif
442 442 <a
443 443 class="tooltip"
444 444 title="${h.tooltip(_('Raw diff'))}"
445 445 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
446 446 >
447 447 ${_('Raw diff')}
448 448 </a> |
449 449 <a
450 450 class="tooltip"
451 451 title="${h.tooltip(_('Download diff'))}"
452 452 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
453 453 >
454 454 ${_('Download diff')}
455 455 </a>
456 456 % if use_comments:
457 457 |
458 458 % endif
459 459
460 460 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
461 461 %if hasattr(c, 'ignorews_url'):
462 462 ${c.ignorews_url(request.GET, h.FID('', filediff.patch['filename']))}
463 463 %endif
464 464 %if hasattr(c, 'context_url'):
465 465 ${c.context_url(request.GET, h.FID('', filediff.patch['filename']))}
466 466 %endif
467 467
468 468 %if use_comments:
469 469 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
470 470 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
471 471 </a>
472 472 %endif
473 473 %endif
474 474 </div>
475 475 </%def>
476 476
477 477
478 478 <%def name="inline_comments_container(comments)">
479 479 <div class="inline-comments">
480 480 %for comment in comments:
481 481 ${commentblock.comment_block(comment, inline=True)}
482 482 %endfor
483 483
484 484 % if comments and comments[-1].outdated:
485 485 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
486 486 style="display: none;}">
487 487 ${_('Add another comment')}
488 488 </span>
489 489 % else:
490 490 <span onclick="return Rhodecode.comments.createComment(this)"
491 491 class="btn btn-secondary cb-comment-add-button">
492 492 ${_('Add another comment')}
493 493 </span>
494 494 % endif
495 495
496 496 </div>
497 497 </%def>
498 498
499 499
500 500 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
501 501 %for i, line in enumerate(hunk.sideside):
502 502 <%
503 503 old_line_anchor, new_line_anchor = None, None
504 504 if line.original.lineno:
505 505 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
506 506 if line.modified.lineno:
507 507 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
508 508 %>
509 509
510 510 <tr class="cb-line">
511 511 <td class="cb-data ${action_class(line.original.action)}"
512 512 data-line-number="${line.original.lineno}"
513 513 >
514 514 <div>
515 515 %if line.original.comments:
516 516 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
517 517 %endif
518 518 </div>
519 519 </td>
520 520 <td class="cb-lineno ${action_class(line.original.action)}"
521 521 data-line-number="${line.original.lineno}"
522 522 %if old_line_anchor:
523 523 id="${old_line_anchor}"
524 524 %endif
525 525 >
526 526 %if line.original.lineno:
527 527 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
528 528 %endif
529 529 </td>
530 530 <td class="cb-content ${action_class(line.original.action)}"
531 531 data-line-number="o${line.original.lineno}"
532 532 >
533 533 %if use_comments and line.original.lineno:
534 534 ${render_add_comment_button()}
535 535 %endif
536 536 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
537 537 %if use_comments and line.original.lineno and line.original.comments:
538 538 ${inline_comments_container(line.original.comments)}
539 539 %endif
540 540 </td>
541 541 <td class="cb-data ${action_class(line.modified.action)}"
542 542 data-line-number="${line.modified.lineno}"
543 543 >
544 544 <div>
545 545 %if line.modified.comments:
546 546 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
547 547 %endif
548 548 </div>
549 549 </td>
550 550 <td class="cb-lineno ${action_class(line.modified.action)}"
551 551 data-line-number="${line.modified.lineno}"
552 552 %if new_line_anchor:
553 553 id="${new_line_anchor}"
554 554 %endif
555 555 >
556 556 %if line.modified.lineno:
557 557 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
558 558 %endif
559 559 </td>
560 560 <td class="cb-content ${action_class(line.modified.action)}"
561 561 data-line-number="n${line.modified.lineno}"
562 562 >
563 563 %if use_comments and line.modified.lineno:
564 564 ${render_add_comment_button()}
565 565 %endif
566 566 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
567 567 %if use_comments and line.modified.lineno and line.modified.comments:
568 568 ${inline_comments_container(line.modified.comments)}
569 569 %endif
570 570 </td>
571 571 </tr>
572 572 %endfor
573 573 </%def>
574 574
575 575
576 576 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
577 577 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
578 578 <%
579 579 old_line_anchor, new_line_anchor = None, None
580 580 if old_line_no:
581 581 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
582 582 if new_line_no:
583 583 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
584 584 %>
585 585 <tr class="cb-line">
586 586 <td class="cb-data ${action_class(action)}">
587 587 <div>
588 588 %if comments:
589 589 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
590 590 %endif
591 591 </div>
592 592 </td>
593 593 <td class="cb-lineno ${action_class(action)}"
594 594 data-line-number="${old_line_no}"
595 595 %if old_line_anchor:
596 596 id="${old_line_anchor}"
597 597 %endif
598 598 >
599 599 %if old_line_anchor:
600 600 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
601 601 %endif
602 602 </td>
603 603 <td class="cb-lineno ${action_class(action)}"
604 604 data-line-number="${new_line_no}"
605 605 %if new_line_anchor:
606 606 id="${new_line_anchor}"
607 607 %endif
608 608 >
609 609 %if new_line_anchor:
610 610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
611 611 %endif
612 612 </td>
613 613 <td class="cb-content ${action_class(action)}"
614 614 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
615 615 >
616 616 %if use_comments:
617 617 ${render_add_comment_button()}
618 618 %endif
619 619 <span class="cb-code">${action} ${content or '' | n}</span>
620 620 %if use_comments and comments:
621 621 ${inline_comments_container(comments)}
622 622 %endif
623 623 </td>
624 624 </tr>
625 625 %endfor
626 626 </%def>
627 627
628 628 <%def name="render_add_comment_button()">
629 629 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
630 630 <span><i class="icon-comment"></i></span>
631 631 </button>
632 632 </%def>
633 633
634 634 <%def name="render_diffset_menu()">
635 635
636 636 <div class="diffset-menu clearinner">
637 637 <div class="pull-right">
638 638 <div class="btn-group">
639 639
640 640 <a
641 641 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
642 642 title="${h.tooltip(_('View side by side'))}"
643 643 href="${h.url_replace(diffmode='sideside')}">
644 644 <span>${_('Side by Side')}</span>
645 645 </a>
646 646 <a
647 647 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
648 648 title="${h.tooltip(_('View unified'))}" href="${h.url_replace(diffmode='unified')}">
649 649 <span>${_('Unified')}</span>
650 650 </a>
651 651 </div>
652 652 </div>
653 653
654 654 <div class="pull-left">
655 655 <div class="btn-group">
656 656 <a
657 657 class="btn"
658 658 href="#"
659 659 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
660 660 <a
661 661 class="btn"
662 662 href="#"
663 663 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
664 664 <a
665 665 class="btn"
666 666 href="#"
667 667 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
668 668 </div>
669 669 </div>
670 670 </div>
671 671 </%def>
@@ -1,114 +1,114 b''
1 1 ## Changesets table !
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 %if c.ancestor:
5 5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
7 7 ${h.short_id(c.ancestor)}
8 8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
10 10 </div>
11 11 %endif
12 12
13 13 <div class="container">
14 14 <input type="hidden" name="__start__" value="revisions:sequence">
15 15 <table class="rctable compare_view_commits">
16 16 <tr>
17 17 <th>${_('Time')}</th>
18 18 <th>${_('Author')}</th>
19 19 <th>${_('Commit')}</th>
20 20 <th></th>
21 21 <th>${_('Description')}</th>
22 22 </tr>
23 23 %for commit in c.commit_ranges:
24 24 <tr id="row-${commit.raw_id}"
25 25 commit_id="${commit.raw_id}"
26 26 class="compare_select"
27 27 style="${'display: none' if c.collapse_all_commits else ''}"
28 28 >
29 29 <td class="td-time">
30 30 ${h.age_component(commit.date)}
31 31 </td>
32 32 <td class="td-user">
33 33 ${base.gravatar_with_user(commit.author, 16)}
34 34 </td>
35 35 <td class="td-hash">
36 36 <code>
37 37 <a href="${h.url('changeset_home',
38 38 repo_name=c.target_repo.repo_name,
39 39 revision=commit.raw_id)}">
40 40 r${commit.revision}:${h.short_id(commit.raw_id)}
41 41 </a>
42 42 ${h.hidden('revisions',commit.raw_id)}
43 43 </code>
44 44 </td>
45 45 <td class="expand_commit"
46 46 data-commit-id="${commit.raw_id}"
47 47 title="${_( 'Expand commit message')}"
48 48 >
49 49 <div class="show_more_col">
50 50 <i class="show_more"></i>
51 51 </div>
52 52 </td>
53 53 <td class="mid td-description">
54 54 <div class="log-container truncate-wrap">
55 55 <div
56 56 id="c-${commit.raw_id}"
57 57 class="message truncate"
58 58 data-message-raw="${commit.message}"
59 59 >
60 60 ${h.urlify_commit_message(commit.message, c.repo_name)}
61 61 </div>
62 62 </div>
63 63 </td>
64 64 </tr>
65 65 %endfor
66 66 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
67 67 <td colspan="5">
68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
69 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
68 ${_ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
69 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${_ungettext('show it','show them', len(c.commit_ranges))}</a>
70 70 </td>
71 71 </tr>
72 72 % if not c.commit_ranges:
73 73 <tr class="compare_select">
74 74 <td colspan="5">
75 75 ${_('No commits in this compare')}
76 76 </td>
77 77 </tr>
78 78 % endif
79 79 </table>
80 80 <input type="hidden" name="__end__" value="revisions:sequence">
81 81
82 82 </div>
83 83
84 84 <script>
85 85 $('.expand_commit').on('click',function(e){
86 86 var target_expand = $(this);
87 87 var cid = target_expand.data('commitId');
88 88
89 89 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
90 90 if (target_expand.hasClass('open')){
91 91 $('#c-'+cid).css({
92 92 'height': '1.5em',
93 93 'white-space': 'nowrap',
94 94 'text-overflow': 'ellipsis',
95 95 'overflow':'hidden'
96 96 });
97 97 target_expand.removeClass('open');
98 98 }
99 99 else {
100 100 $('#c-'+cid).css({
101 101 'height': 'auto',
102 102 'white-space': 'pre-line',
103 103 'text-overflow': 'initial',
104 104 'overflow':'visible'
105 105 });
106 106 target_expand.addClass('open');
107 107 }
108 108 });
109 109
110 110 $('.compare_select').on('click',function(e){
111 111 var cid = $(this).attr('commit_id');
112 112 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
113 113 });
114 114 </script>
@@ -1,333 +1,333 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4 4
5 5 <%def name="title()">
6 6 %if c.compare_home:
7 7 ${_('%s Compare') % c.repo_name}
8 8 %else:
9 9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 10 %endif
11 11 %if c.rhodecode_name:
12 12 &middot; ${h.branding(c.rhodecode_name)}
13 13 %endif
14 14 </%def>
15 15
16 16 <%def name="breadcrumbs_links()">
17 ${ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
17 ${_ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
18 18 </%def>
19 19
20 20 <%def name="menu_bar_nav()">
21 21 ${self.menu_items(active='repositories')}
22 22 </%def>
23 23
24 24 <%def name="menu_bar_subnav()">
25 25 ${self.repo_menu(active='compare')}
26 26 </%def>
27 27
28 28 <%def name="main()">
29 29 <script type="text/javascript">
30 30 // set fake commitId on this commit-range page
31 31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
32 32 </script>
33 33
34 34 <div class="box">
35 35 <div class="title">
36 36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 37 </div>
38 38
39 39 <div class="summary changeset">
40 40 <div class="summary-detail">
41 41 <div class="summary-detail-header">
42 42 <span class="breadcrumbs files_location">
43 43 <h4>
44 44 ${_('Compare Commits')}
45 45 % if c.file_path:
46 46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 47 % endif
48 48
49 49 % if c.commit_ranges:
50 50 <code>
51 51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
52 52 </code>
53 53 % endif
54 54 </h4>
55 55 </span>
56 56 </div>
57 57
58 58 <div class="fieldset">
59 59 <div class="left-label">
60 60 ${_('Target')}:
61 61 </div>
62 62 <div class="right-content">
63 63 <div>
64 64 <div class="code-header" >
65 65 <div class="compare_header">
66 66 ## The hidden elements are replaced with a select2 widget
67 67 ${h.hidden('compare_source')}
68 68 </div>
69 69 </div>
70 70 </div>
71 71 </div>
72 72 </div>
73 73
74 74 <div class="fieldset">
75 75 <div class="left-label">
76 76 ${_('Source')}:
77 77 </div>
78 78 <div class="right-content">
79 79 <div>
80 80 <div class="code-header" >
81 81 <div class="compare_header">
82 82 ## The hidden elements are replaced with a select2 widget
83 83 ${h.hidden('compare_target')}
84 84 </div>
85 85 </div>
86 86 </div>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="fieldset">
91 91 <div class="left-label">
92 92 ${_('Actions')}:
93 93 </div>
94 94 <div class="right-content">
95 95 <div>
96 96 <div class="code-header" >
97 97 <div class="compare_header">
98 98
99 99 <div class="compare-buttons">
100 100 % if c.compare_home:
101 101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102 102
103 103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 105 <div id="changeset_compare_view_content">
106 106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 107 </div>
108 108
109 109 % elif c.preview_mode:
110 110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113 113
114 114 % else:
115 115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117 117
118 118 ## allow comment only if there are commits to comment on
119 119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 121 % else:
122 122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 123 % endif
124 124 % endif
125 125 </div>
126 126 </div>
127 127 </div>
128 128 </div>
129 129 </div>
130 130 </div>
131 131
132 132 <%doc>
133 133 ##TODO(marcink): implement this and diff menus
134 134 <div class="fieldset">
135 135 <div class="left-label">
136 136 ${_('Diff options')}:
137 137 </div>
138 138 <div class="right-content">
139 139 <div class="diff-actions">
140 140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 141 ${_('Raw Diff')}
142 142 </a>
143 143 |
144 144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 145 ${_('Patch Diff')}
146 146 </a>
147 147 |
148 148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 149 ${_('Download Diff')}
150 150 </a>
151 151 </div>
152 152 </div>
153 153 </div>
154 154 </%doc>
155 155
156 156 ## commit status form
157 157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
158 158 <div class="left-label">
159 159 ${_('Commit status')}:
160 160 </div>
161 161 <div class="right-content">
162 162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
163 163 ## main comment form and it status
164 164 <%
165 165 def revs(_revs):
166 166 form_inputs = []
167 167 for cs in _revs:
168 168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
169 169 form_inputs.append(tmpl)
170 170 return form_inputs
171 171 %>
172 172 <div>
173 173 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
174 174 </div>
175 175 </div>
176 176 </div>
177 177
178 178 </div> <!-- end summary-detail -->
179 179 </div> <!-- end summary -->
180 180
181 181 ## use JS script to load it quickly before potentially large diffs render long time
182 182 ## this prevents from situation when large diffs block rendering of select2 fields
183 183 <script type="text/javascript">
184 184
185 185 var cache = {};
186 186
187 187 var formatSelection = function(repoName){
188 188 return function(data, container, escapeMarkup) {
189 189 var selection = data ? this.text(data) : "";
190 190 return escapeMarkup('{0}@{1}'.format(repoName, selection));
191 191 }
192 192 };
193 193
194 194 var feedCompareData = function(query, cachedValue){
195 195 var data = {results: []};
196 196 //filter results
197 197 $.each(cachedValue.results, function() {
198 198 var section = this.text;
199 199 var children = [];
200 200 $.each(this.children, function() {
201 201 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
202 202 children.push({
203 203 'id': this.id,
204 204 'text': this.text,
205 205 'type': this.type
206 206 })
207 207 }
208 208 });
209 209 data.results.push({
210 210 'text': section,
211 211 'children': children
212 212 })
213 213 });
214 214 //push the typed in changeset
215 215 data.results.push({
216 216 'text': _gettext('specify commit'),
217 217 'children': [{
218 218 'id': query.term,
219 219 'text': query.term,
220 220 'type': 'rev'
221 221 }]
222 222 });
223 223 query.callback(data);
224 224 };
225 225
226 226 var loadCompareData = function(repoName, query, cache){
227 227 $.ajax({
228 228 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
229 229 data: {},
230 230 dataType: 'json',
231 231 type: 'GET',
232 232 success: function(data) {
233 233 cache[repoName] = data;
234 234 query.callback({results: data.results});
235 235 }
236 236 })
237 237 };
238 238
239 239 var enable_fields = ${"false" if c.preview_mode else "true"};
240 240 $("#compare_source").select2({
241 241 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
242 242 containerCssClass: "drop-menu",
243 243 dropdownCssClass: "drop-menu-dropdown",
244 244 formatSelection: formatSelection("${c.source_repo.repo_name}"),
245 245 dropdownAutoWidth: true,
246 246 query: function(query) {
247 247 var repoName = '${c.source_repo.repo_name}';
248 248 var cachedValue = cache[repoName];
249 249
250 250 if (cachedValue){
251 251 feedCompareData(query, cachedValue);
252 252 }
253 253 else {
254 254 loadCompareData(repoName, query, cache);
255 255 }
256 256 }
257 257 }).select2("enable", enable_fields);
258 258
259 259 $("#compare_target").select2({
260 260 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
261 261 dropdownAutoWidth: true,
262 262 containerCssClass: "drop-menu",
263 263 dropdownCssClass: "drop-menu-dropdown",
264 264 formatSelection: formatSelection("${c.target_repo.repo_name}"),
265 265 query: function(query) {
266 266 var repoName = '${c.target_repo.repo_name}';
267 267 var cachedValue = cache[repoName];
268 268
269 269 if (cachedValue){
270 270 feedCompareData(query, cachedValue);
271 271 }
272 272 else {
273 273 loadCompareData(repoName, query, cache);
274 274 }
275 275 }
276 276 }).select2("enable", enable_fields);
277 277 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
278 278 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
279 279
280 280 $('#compare_revs').on('click', function(e) {
281 281 var source = $('#compare_source').select2('data') || initial_compare_source;
282 282 var target = $('#compare_target').select2('data') || initial_compare_target;
283 283 if (source && target) {
284 284 var url_data = {
285 285 repo_name: "${c.repo_name}",
286 286 source_ref: source.id,
287 287 source_ref_type: source.type,
288 288 target_ref: target.id,
289 289 target_ref_type: target.type
290 290 };
291 291 window.location = pyroutes.url('compare_url', url_data);
292 292 }
293 293 });
294 294 $('#compare_changeset_status_toggle').on('click', function(e) {
295 295 $('#compare_changeset_status').toggle();
296 296 });
297 297
298 298 </script>
299 299
300 300 ## table diff data
301 301 <div class="table">
302 302
303 303
304 304 % if not c.compare_home:
305 305 <div id="changeset_compare_view_content">
306 306 <div class="pull-left">
307 307 <div class="btn-group">
308 308 <a
309 309 class="btn"
310 310 href="#"
311 311 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
312 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
312 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
313 313 </a>
314 314 <a
315 315 class="btn"
316 316 href="#"
317 317 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
318 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
318 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
319 319 </a>
320 320 </div>
321 321 </div>
322 322 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
323 323 ## commit compare generated below
324 324 <%include file="compare_commits.mako"/>
325 325 ${cbdiffs.render_diffset_menu()}
326 326 ${cbdiffs.render_diffset(c.diffset)}
327 327 </div>
328 328 % endif
329 329
330 330 </div>
331 331 </div>
332 332
333 333 </%def> No newline at end of file
@@ -1,317 +1,317 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.route_path('repo_changelog',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="repo_desc(description)">
76 76 <div class="truncate-wrap">${description}</div>
77 77 </%def>
78 78
79 79 <%def name="last_change(last_change)">
80 80 ${h.age_component(last_change)}
81 81 </%def>
82 82
83 83 <%def name="revision(name,rev,tip,author,last_msg)">
84 84 <div>
85 85 %if rev >= 0:
86 86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
87 87 %else:
88 88 ${_('No commits yet')}
89 89 %endif
90 90 </div>
91 91 </%def>
92 92
93 93 <%def name="rss(name)">
94 94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 95 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
96 96 %else:
97 97 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
98 98 %endif
99 99 </%def>
100 100
101 101 <%def name="atom(name)">
102 102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 103 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
104 104 %else:
105 105 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
106 106 %endif
107 107 </%def>
108 108
109 109 <%def name="user_gravatar(email, size=16)">
110 110 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
111 111 ${base.gravatar(email, 16)}
112 112 </div>
113 113 </%def>
114 114
115 115 <%def name="repo_actions(repo_name, super_user=True)">
116 116 <div>
117 117 <div class="grid_edit">
118 118 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
119 119 <i class="icon-pencil"></i>Edit</a>
120 120 </div>
121 121 <div class="grid_delete">
122 122 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST', request=request)}
123 123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
124 124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
125 125 ${h.end_form()}
126 126 </div>
127 127 </div>
128 128 </%def>
129 129
130 130 <%def name="repo_state(repo_state)">
131 131 <div>
132 132 %if repo_state == 'repo_state_pending':
133 133 <div class="tag tag4">${_('Creating')}</div>
134 134 %elif repo_state == 'repo_state_created':
135 135 <div class="tag tag1">${_('Created')}</div>
136 136 %else:
137 137 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
138 138 %endif
139 139 </div>
140 140 </%def>
141 141
142 142
143 143 ## REPO GROUP RENDERERS
144 144 <%def name="quick_repo_group_menu(repo_group_name)">
145 145 <i class="pointer icon-more"></i>
146 146 <div class="menu_items_container hidden">
147 147 <ul class="menu_items">
148 148 <li>
149 149 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
150 150 <span class="icon">
151 151 <i class="icon-file-text"></i>
152 152 </span>
153 153 <span>${_('Summary')}</span>
154 154 </a>
155 155 </li>
156 156
157 157 </ul>
158 158 </div>
159 159 </%def>
160 160
161 161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
162 162 <div>
163 163 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
164 164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
165 165 %if children_groups:
166 166 ${h.literal(' &raquo; '.join(children_groups))}
167 167 %else:
168 168 ${repo_group_name}
169 169 %endif
170 170 </a>
171 171 </div>
172 172 </%def>
173 173
174 174 <%def name="repo_group_desc(description)">
175 175 <div class="truncate-wrap">${description}</div>
176 176 </%def>
177 177
178 178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
179 179 <div class="grid_edit">
180 180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
181 181 </div>
182 182 <div class="grid_delete">
183 183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
184 184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
185 onclick="return confirm('"+_ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
186 186 ${h.end_form()}
187 187 </div>
188 188 </%def>
189 189
190 190
191 191 <%def name="user_actions(user_id, username)">
192 192 <div class="grid_edit">
193 193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
194 194 <i class="icon-pencil"></i>Edit</a>
195 195 </div>
196 196 <div class="grid_delete">
197 197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
198 198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
199 199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
200 200 ${h.end_form()}
201 201 </div>
202 202 </%def>
203 203
204 204 <%def name="user_group_actions(user_group_id, user_group_name)">
205 205 <div class="grid_edit">
206 206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
207 207 </div>
208 208 <div class="grid_delete">
209 209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
210 210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
211 211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
212 212 ${h.end_form()}
213 213 </div>
214 214 </%def>
215 215
216 216
217 217 <%def name="user_name(user_id, username)">
218 218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
219 219 </%def>
220 220
221 221 <%def name="user_profile(username)">
222 222 ${base.gravatar_with_user(username, 16)}
223 223 </%def>
224 224
225 225 <%def name="user_group_name(user_group_id, user_group_name)">
226 226 <div>
227 227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
228 228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
229 229 </div>
230 230 </%def>
231 231
232 232
233 233 ## GISTS
234 234
235 235 <%def name="gist_gravatar(full_contact)">
236 236 <div class="gist_gravatar">
237 237 ${base.gravatar(full_contact, 30)}
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_access_id(gist_access_id, full_contact)">
242 242 <div>
243 243 <b>
244 244 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
245 245 </b>
246 246 </div>
247 247 </%def>
248 248
249 249 <%def name="gist_author(full_contact, created_on, expires)">
250 250 ${base.gravatar_with_user(full_contact, 16)}
251 251 </%def>
252 252
253 253
254 254 <%def name="gist_created(created_on)">
255 255 <div class="created">
256 256 ${h.age_component(created_on, time_is_local=True)}
257 257 </div>
258 258 </%def>
259 259
260 260 <%def name="gist_expires(expires)">
261 261 <div class="created">
262 262 %if expires == -1:
263 263 ${_('never')}
264 264 %else:
265 265 ${h.age_component(h.time_to_utcdatetime(expires))}
266 266 %endif
267 267 </div>
268 268 </%def>
269 269
270 270 <%def name="gist_type(gist_type)">
271 271 %if gist_type != 'public':
272 272 <div class="tag">${_('Private')}</div>
273 273 %endif
274 274 </%def>
275 275
276 276 <%def name="gist_description(gist_description)">
277 277 ${gist_description}
278 278 </%def>
279 279
280 280
281 281 ## PULL REQUESTS GRID RENDERERS
282 282
283 283 <%def name="pullrequest_target_repo(repo_name)">
284 284 <div class="truncate">
285 285 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
286 286 </div>
287 287 </%def>
288 288 <%def name="pullrequest_status(status)">
289 289 <div class="${'flag_status %s' % status} pull-left"></div>
290 290 </%def>
291 291
292 292 <%def name="pullrequest_title(title, description)">
293 293 ${title} <br/>
294 294 ${h.shorter(description, 40)}
295 295 </%def>
296 296
297 297 <%def name="pullrequest_comments(comments_nr)">
298 298 <i class="icon-comment"></i> ${comments_nr}
299 299 </%def>
300 300
301 301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
302 302 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
303 303 % if short:
304 304 #${pull_request_id}
305 305 % else:
306 306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
307 307 % endif
308 308 </a>
309 309 </%def>
310 310
311 311 <%def name="pullrequest_updated_on(updated_on)">
312 312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
313 313 </%def>
314 314
315 315 <%def name="pullrequest_author(full_contact)">
316 316 ${base.gravatar_with_user(full_contact, 16)}
317 317 </%def>
@@ -1,197 +1,197 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/debug_style/index.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 ${h.link_to(_('Style'), h.route_path('debug_style_home'))}
6 6 &raquo;
7 7 ${c.active}
8 8 </%def>
9 9
10 10
11 11 <%def name="real_main()">
12 12 <div class="box">
13 13 <div class="title">
14 14 ${self.breadcrumbs()}
15 15 </div>
16 16
17 17 <div class='sidebar-col-wrapper'>
18 18 ${self.sidebar()}
19 19
20 20 <div class="main-content">
21 21
22 22 <h2>Buttons</h2>
23 23
24 24 <p>
25 25 Form buttons in various sizes. Buttons are always capitalised.
26 26 Use the following classes:
27 27 </p>
28 28
29 29 <ul>
30 30 ## TODO: lisa: Are we actually using three sizes of buttons??
31 31 <li><code>.btn-lg</code> for large buttons</li>
32 32 <li><code>.btn-sm</code> for small buttons</li>
33 33 <li><code>.btn-xs</code> for xtra small buttons</li>
34 34 </ul>
35 35
36 36 <p>Note that <code>.btn-mini</code> is supported for legacy reasons.</p>
37 37
38 38 <div class="bs-example">
39 39 ## TODO: johbo: Should also work without the form element
40 40 <form method='post' action=''>
41 41 <div class='form'>
42 42
43 43 <div class="buttons">
44 44 <input type="submit" value="Save .btn-lg" id="example_save" class="btn btn-lg">
45 45 <input type="reset" value="Reset" id="example_reset" class="btn btn-lg">
46 46 <button class="btn btn-lg">Large</button>
47 47 <a class="btn btn-lg" href="#">A link as button</a>
48 48 </div>
49 49
50 50 <div class="buttons">
51 51 <input type="submit" value="Save" id="example_save" class="btn">
52 52 <input type="reset" value="Reset" id="example_reset" class="btn">
53 53 <button class="btn">Normal</button>
54 54 <button class="btn btn-danger">Normal</button>
55 55 <a class="btn" href="#">A link as button</a>
56 56 </div>
57 57
58 58 <div class="buttons">
59 59 <input type="submit" value="Save .btn-sm" id="example_save" class="btn btn-sm">
60 60 <input type="reset" value="Reset" id="example_reset" class="btn btn-sm">
61 61 <button class="btn btn-sm">Small</button>
62 62 <button class="btn btn-sm btn-danger">Small</button>
63 63 <a class="btn btn-sm" href="#">A link as button</a>
64 64 </div>
65 65
66 66 <div class="buttons">
67 67 <input type="submit" value="Save .btn-xs" id="example_save" class="btn btn-xs">
68 68 <input type="reset" value="Reset" id="example_reset" class="btn btn-xs">
69 69 <button class="btn btn-xs">XSmall</button>
70 70 <button class="btn btn-xs btn-danger">XSmall</button>
71 71 <a class="btn btn-xs" href="#">A link as button</a>
72 72 </div>
73 73
74 74 <div class="buttons">
75 75 <input type="submit" value="Save .btn-mini" id="example_save" class="btn btn-mini">
76 76 <input type="reset" value="Reset" id="example_reset" class="btn btn-mini">
77 77 </div>
78 78
79 79 <div class="buttons">
80 80 Buttons of style <code>.btn-link</code>:
81 81 <input type="reset" value="Reset" id="example_reset" class="btn btn-link">
82 82 <button class="btn btn-link">Edit</button>
83 83 <button class="btn btn-danger btn-link">Delete</button>
84 84 </div>
85 85 </div>
86 86 </form>
87 87 </div>
88 88
89 89
90 90 <h2>Buttons as Links</h2>
91 91 <p>
92 92 Most of our Edit/Delete buttons come in the following form.
93 93 Inside of a table, these are "action buttons", and while an
94 94 Edit <em>link</em> is a typical blue link, a Delete <em>button</em>
95 95 is red as per the 'btn-danger' styling and use <code>.btn-link</code>.
96 96 </p>
97 97 <p>
98 98 We use "Delete" when the thing being deleted cannot be undone;
99 99 "Reset", and "Revoke" are used where applicable.
100 100 </p>
101 101 <p>
102 102 Note: Should there be a need for a change in the wording, be
103 103 aware that corresponding documentation may also need updating.
104 104 </p>
105 105 <div class="bs-example">
106 106 <table class="rctable edit_fields">
107 107 <tr><td></td><td></td></tr>
108 108 <tr>
109 109 <td></td>
110 110 <td class=" td-action">
111 111 <div class="grid_edit">
112 112 <a href="/_admin/repo_groups/breads/edit" title="Edit">Edit</a>
113 113 </div>
114 114 <div class="grid_delete">
115 115 <form action="/_admin/repo_groups/breads" method="post"><div style="display:none">
116 116 <input name="_method" type="hidden" value="delete">
117 117 </div>
118 118 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="03d6cc48726b885039b2f7675e85596b7dae6ecf"></div>
119 <button class="btn btn-link btn-danger" type="submit" onclick="return confirm('" +ungettext('confirm="" to="" delete="" this="" group:="" %s="" with="" repository','confirm="" repositories',gr_count)="" %="" (repo_group_name,="" gr_count)+"');"="">
119 <button class="btn btn-link btn-danger" type="submit" onclick="return confirm('" +_ungettext('confirm="" to="" delete="" this="" group:="" %s="" with="" repository','confirm="" repositories',gr_count)="" %="" (repo_group_name,="" gr_count)+"');"="">
120 120 Delete
121 121 </button>
122 122 </form>
123 123 </div>
124 124 </td>
125 125 </tr>
126 126 </table>
127 127 <div class="highlight-html"><xmp>
128 128 <a href="some-link" title="${_('Edit')}">${_('Edit')}</a>
129 129
130 130 <button class="btn btn-link btn-danger" type="submit"
131 131 onclick="return confirm('${_('Confirm to remove this field: Field')}');">
132 132 ${_('Delete')}
133 133 </button>
134 134 </xmp></div>
135 135 </div>
136 136
137 137
138 138 <h2>Buttons disabled</h2>
139 139
140 140 <p>Note that our application still uses the class <code>.disabled</code>
141 141 in some places. Interim we support both but prefer to use the
142 142 attribute <code>disabled</code> where possible.</p>
143 143
144 144 <div class="bs-example">
145 145 ## TODO: johbo: Should also work without the form element
146 146 <form method='post' action=''>
147 147 <div class='form'>
148 148
149 149 <div class="buttons">
150 150 <input type="submit" value="Save .btn-lg" id="example_save" class="btn btn-lg" disabled>
151 151 <input type="reset" value="Reset" id="example_reset" class="btn btn-lg" disabled>
152 152 <button class="btn btn-lg" disabled>Large</button>
153 153 </div>
154 154
155 155 <div class="buttons">
156 156 <input type="submit" value="Save" id="example_save" class="btn" disabled>
157 157 <input type="reset" value="Reset" id="example_reset" class="btn" disabled>
158 158 <button class="btn" disabled>Normal</button>
159 159 <button class="btn btn-danger" disabled>Normal</button>
160 160 </div>
161 161
162 162 <div class="buttons">
163 163 <input type="submit" value="Save .btn-sm" id="example_save" class="btn btn-sm" disabled>
164 164 <input type="reset" value="Reset" id="example_reset" class="btn btn-sm" disabled>
165 165 <button class="btn btn-sm" disabled>Small</button>
166 166 <button class="btn btn-sm btn-danger" disabled>Small</button>
167 167 </div>
168 168
169 169 <div class="buttons">
170 170 <input type="submit" value="Save .btn-xs" id="example_save" class="btn btn-xs" disabled>
171 171 <input type="reset" value="Reset" id="example_reset" class="btn btn-xs" disabled>
172 172 <button class="btn btn-xs" disabled>XSmall</button>
173 173 <button class="btn btn-xs btn-danger" disabled>XSmall</button>
174 174 </div>
175 175
176 176 <div class="buttons">
177 177 <input type="submit" value="Save .btn-mini" id="example_save" class="btn btn-mini" disabled>
178 178 <input type="reset" value="Reset" id="example_reset" class="btn btn-mini" disabled>
179 179 </div>
180 180
181 181 <div class="buttons">
182 182 Buttons of style <code>.btn-link</code>:
183 183 <input type="reset" value="Reset" id="example_reset" class="btn btn-link" disabled>
184 184 <button class="btn btn-link" disabled>Edit</button>
185 185 <button class="btn btn-link btn-danger" disabled>Delete</button>
186 186 </div>
187 187
188 188 </div>
189 189 </form>
190 190 </div>
191 191
192 192
193 193
194 194 </div>
195 195 </div> <!-- .main-content -->
196 196 </div> <!-- .box -->
197 197 </%def>
@@ -1,85 +1,85 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 <%def name="subject()" filter="n,trim,whitespace_filter">
6 6 <%
7 7 data = {
8 8 'user': h.person(user),
9 9 'pr_id': pull_request.pull_request_id,
10 10 'pr_title': pull_request.title,
11 11 }
12 12 %>
13 13
14 14 ${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n}
15 15 </%def>
16 16
17 17
18 18 <%def name="body_plaintext()" filter="n,trim">
19 19 <%
20 20 data = {
21 21 'user': h.person(user),
22 22 'pr_id': pull_request.pull_request_id,
23 23 'pr_title': pull_request.title,
24 24 'source_ref_type': pull_request.source_ref_parts.type,
25 25 'source_ref_name': pull_request.source_ref_parts.name,
26 26 'target_ref_type': pull_request.target_ref_parts.type,
27 27 'target_ref_name': pull_request.target_ref_parts.name,
28 28 'repo_url': pull_request_source_repo_url
29 29 }
30 30 %>
31 31 ${self.subject()}
32 32
33 33
34 34 ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)}
35 35
36 36
37 37 * ${_('Link')}: ${pull_request_url}
38 38
39 39 * ${_('Title')}: ${pull_request.title}
40 40
41 41 * ${_('Description')}:
42 42
43 43 ${pull_request.description}
44 44
45 45
46 * ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
46 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
47 47
48 48 % for commit_id, message in pull_request_commits:
49 49 - ${h.short_id(commit_id)}
50 50 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
51 51
52 52 % endfor
53 53
54 54 ${self.plaintext_footer()}
55 55 </%def>
56 56 <%
57 57 data = {
58 58 'user': h.person(user),
59 59 'pr_id': pull_request.pull_request_id,
60 60 'pr_title': pull_request.title,
61 61 'source_ref_type': pull_request.source_ref_parts.type,
62 62 'source_ref_name': pull_request.source_ref_parts.name,
63 63 'target_ref_type': pull_request.target_ref_parts.type,
64 64 'target_ref_name': pull_request.target_ref_parts.name,
65 65 'repo_url': pull_request_source_repo_url,
66 66 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
67 67 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url)
68 68 }
69 69 %>
70 70 <table style="text-align:left;vertical-align:middle;">
71 71 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }</a></h4></td></tr>
72 72 <tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
73 73 <tr><td style="padding-right:20px;">${_('Source')}</td><td>${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}</td></tr>
74 74 <tr><td style="padding-right:20px;">${_('Target')}</td><td>${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}</td></tr>
75 75 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
76 <tr><td style="padding-right:20px;">${ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
76 <tr><td style="padding-right:20px;">${_ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
77 77 <td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
78 78 % for commit_id, message in pull_request_commits:
79 79 <li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
80 80 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
81 81 </li>
82 82 % endfor
83 83 </ol></td>
84 84 </tr>
85 85 </table>
@@ -1,526 +1,526 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 25 ${h.secure_form(h.url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208 } else {
209 209 id = selectedRef;
210 210 refData = selectedRef.split(':');
211 211 }
212 212
213 213 var text = refData[1];
214 214 if (refData[0] === 'rev') {
215 215 text = text.substring(0, 12);
216 216 }
217 217
218 218 var data = {id: id, text: text};
219 219
220 220 callback(data);
221 221 };
222 222 };
223 223
224 224 var formatRefSelection = function(item) {
225 225 var prefix = '';
226 226 var refData = item.id.split(':');
227 227 if (refData[0] === 'branch') {
228 228 prefix = '<i class="icon-branch"></i>';
229 229 }
230 230 else if (refData[0] === 'book') {
231 231 prefix = '<i class="icon-bookmark"></i>';
232 232 }
233 233 else if (refData[0] === 'tag') {
234 234 prefix = '<i class="icon-tag"></i>';
235 235 }
236 236
237 237 var originalOption = item.element;
238 238 return prefix + item.text;
239 239 };
240 240
241 241 // custom code mirror
242 242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243 243
244 244 reviewersController = new ReviewersController();
245 245
246 246 var queryTargetRepo = function(self, query) {
247 247 // cache ALL results if query is empty
248 248 var cacheKey = query.term || '__';
249 249 var cachedData = self.cachedDataSource[cacheKey];
250 250
251 251 if (cachedData) {
252 252 query.callback({results: cachedData.results});
253 253 } else {
254 254 $.ajax({
255 255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 256 data: {query: query.term},
257 257 dataType: 'json',
258 258 type: 'GET',
259 259 success: function(data) {
260 260 self.cachedDataSource[cacheKey] = data;
261 261 query.callback({results: data.results});
262 262 },
263 263 error: function(data, textStatus, errorThrown) {
264 264 alert(
265 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 266 }
267 267 });
268 268 }
269 269 };
270 270
271 271 var queryTargetRefs = function(initialData, query) {
272 272 var data = {results: []};
273 273 // filter initialData
274 274 $.each(initialData, function() {
275 275 var section = this.text;
276 276 var children = [];
277 277 $.each(this.children, function() {
278 278 if (query.term.length === 0 ||
279 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 280 children.push({'id': this.id, 'text': this.text})
281 281 }
282 282 });
283 283 data.results.push({'text': section, 'children': children})
284 284 });
285 285 query.callback({results: data.results});
286 286 };
287 287
288 288 var loadRepoRefDiffPreview = function() {
289 289
290 290 var url_data = {
291 291 'repo_name': targetRepo(),
292 292 'target_repo': sourceRepo(),
293 293 'source_ref': targetRef()[2],
294 294 'source_ref_type': 'rev',
295 295 'target_ref': sourceRef()[2],
296 296 'target_ref_type': 'rev',
297 297 'merge': true,
298 298 '_': Date.now() // bypass browser caching
299 299 }; // gather the source/target ref and repo here
300 300
301 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 302 prButtonLock(true, "${_('Please select source and target')}");
303 303 return;
304 304 }
305 305 var url = pyroutes.url('compare_url', url_data);
306 306
307 307 // lock PR button, so we cannot send PR before it's calculated
308 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309 309
310 310 if (loadRepoRefDiffPreview._currentRequest) {
311 311 loadRepoRefDiffPreview._currentRequest.abort();
312 312 }
313 313
314 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 315 .error(function(data, textStatus, errorThrown) {
316 316 alert(
317 317 "Error while processing request.\nError code {0} ({1}).".format(
318 318 data.status, data.statusText));
319 319 })
320 320 .done(function(data) {
321 321 loadRepoRefDiffPreview._currentRequest = null;
322 322 $('#pull_request_overview').html(data);
323 323
324 324 var commitElements = $(data).find('tr[commit_id]');
325 325
326 326 var prTitleAndDesc = getTitleAndDescription(
327 327 sourceRef()[1], commitElements, 5);
328 328
329 329 var title = prTitleAndDesc[0];
330 330 var proposedDescription = prTitleAndDesc[1];
331 331
332 332 var useGeneratedTitle = (
333 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 334 $('#pullrequest_title').val() === "");
335 335
336 336 if (title && useGeneratedTitle) {
337 337 // use generated title if we haven't specified our own
338 338 $('#pullrequest_title').val(title);
339 339 $('#pullrequest_title').addClass('autogenerated-title');
340 340
341 341 }
342 342
343 343 var useGeneratedDescription = (
344 344 !codeMirrorInstance._userDefinedDesc ||
345 345 codeMirrorInstance.getValue() === "");
346 346
347 347 if (proposedDescription && useGeneratedDescription) {
348 348 // set proposed content, if we haven't defined our own,
349 349 // or we don't have description written
350 350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 351 codeMirrorInstance.setValue(proposedDescription);
352 352 }
353 353
354 354 var msg = '';
355 355 if (commitElements.length === 1) {
356 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 357 } else {
358 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 359 }
360 360
361 361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362 362
363 363 if (commitElements.length) {
364 364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 366 }
367 367 else {
368 368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 369 }
370 370
371 371
372 372 });
373 373 };
374 374
375 375 var Select2Box = function(element, overrides) {
376 376 var globalDefaults = {
377 377 dropdownAutoWidth: true,
378 378 containerCssClass: "drop-menu",
379 379 dropdownCssClass: "drop-menu-dropdown"
380 380 };
381 381
382 382 var initSelect2 = function(defaultOptions) {
383 383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 384 element.select2(options);
385 385 };
386 386
387 387 return {
388 388 initRef: function() {
389 389 var defaultOptions = {
390 390 minimumResultsForSearch: 5,
391 391 formatSelection: formatRefSelection
392 392 };
393 393
394 394 initSelect2(defaultOptions);
395 395 },
396 396
397 397 initRepo: function(defaultValue, readOnly) {
398 398 var defaultOptions = {
399 399 initSelection : function (element, callback) {
400 400 var data = {id: defaultValue, text: defaultValue};
401 401 callback(data);
402 402 }
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406
407 407 element.select2('val', defaultSourceRepo);
408 408 if (readOnly === true) {
409 409 element.select2('readonly', true);
410 410 }
411 411 }
412 412 };
413 413 };
414 414
415 415 var initTargetRefs = function(refsData, selectedRef){
416 416 Select2Box($targetRef, {
417 417 query: function(query) {
418 418 queryTargetRefs(refsData, query);
419 419 },
420 420 initSelection : initRefSelection(selectedRef)
421 421 }).initRef();
422 422
423 423 if (!(selectedRef === undefined)) {
424 424 $targetRef.select2('val', selectedRef);
425 425 }
426 426 };
427 427
428 428 var targetRepoChanged = function(repoData) {
429 429 // generate new DESC of target repo displayed next to select
430 430 $('#target_repo_desc').html(
431 431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 432 );
433 433
434 434 // generate dynamic select2 for refs.
435 435 initTargetRefs(repoData['refs']['select2_refs'],
436 436 repoData['refs']['selected_ref']);
437 437
438 438 };
439 439
440 440 var sourceRefSelect2 = Select2Box($sourceRef, {
441 441 placeholder: "${_('Select commit reference')}",
442 442 query: function(query) {
443 443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 444 queryTargetRefs(initialData, query)
445 445 },
446 446 initSelection: initRefSelection()
447 447 }
448 448 );
449 449
450 450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 451 query: function(query) {}
452 452 });
453 453
454 454 var targetRepoSelect2 = Select2Box($targetRepo, {
455 455 cachedDataSource: {},
456 456 query: $.debounce(250, function(query) {
457 457 queryTargetRepo(this, query);
458 458 }),
459 459 formatResult: formatResult
460 460 });
461 461
462 462 sourceRefSelect2.initRef();
463 463
464 464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465 465
466 466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467 467
468 468 $sourceRef.on('change', function(e){
469 469 loadRepoRefDiffPreview();
470 470 reviewersController.loadDefaultReviewers(
471 471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 472 });
473 473
474 474 $targetRef.on('change', function(e){
475 475 loadRepoRefDiffPreview();
476 476 reviewersController.loadDefaultReviewers(
477 477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 478 });
479 479
480 480 $targetRepo.on('change', function(e){
481 481 var repoName = $(this).val();
482 482 calculateContainerWidth();
483 483 $targetRef.select2('destroy');
484 484 $('#target_ref_loading').show();
485 485
486 486 $.ajax({
487 487 url: pyroutes.url('pullrequest_repo_refs',
488 488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 489 data: {},
490 490 dataType: 'json',
491 491 type: 'GET',
492 492 success: function(data) {
493 493 $('#target_ref_loading').hide();
494 494 targetRepoChanged(data);
495 495 loadRepoRefDiffPreview();
496 496 },
497 497 error: function(data, textStatus, errorThrown) {
498 498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 499 }
500 500 })
501 501
502 502 });
503 503
504 504 prButtonLock(true, "${_('Please select source and target')}", 'all');
505 505
506 506 // auto-load on init, the target refs select2
507 507 calculateContainerWidth();
508 508 targetRepoChanged(defaultTargetRepoData);
509 509
510 510 $('#pullrequest_title').on('keyup', function(e){
511 511 $(this).removeClass('autogenerated-title');
512 512 });
513 513
514 514 % if c.default_source_ref:
515 515 // in case we have a pre-selected value, use it now
516 516 $sourceRef.select2('val', '${c.default_source_ref}');
517 517 loadRepoRefDiffPreview();
518 518 reviewersController.loadDefaultReviewers(
519 519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 520 % endif
521 521
522 522 ReviewerAutoComplete('#user');
523 523 });
524 524 </script>
525 525
526 526 </%def>
@@ -1,860 +1,860 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 = "${h.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.route_path('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(h.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 no-margin",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>${_('Source')}:</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.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(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.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 <div class="pr-pullinfo">
93 93 %if h.is_hg(c.pull_request.source_repo):
94 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 95 %elif h.is_git(c.pull_request.source_repo):
96 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 97 %endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label-summary">
103 103 <label>${_('Target')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 <div class="pr-targetinfo">
107 107 ## branch link is only valid if it is a branch
108 108 <span class="tag">
109 109 %if c.pull_request.target_ref_parts.type == 'branch':
110 110 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 111 %else:
112 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 113 %endif
114 114 </span>
115 115 <span class="clone-url">
116 116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 117 </span>
118 118 </div>
119 119 </div>
120 120 </div>
121 121
122 122 ## Link to the shadow repository.
123 123 <div class="field">
124 124 <div class="label-summary">
125 125 <label>${_('Merge')}:</label>
126 126 </div>
127 127 <div class="input">
128 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 129 <div class="pr-mergeinfo">
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 134 %endif
135 135 </div>
136 136 % else:
137 137 <div class="">
138 138 ${_('Shadow repository data not available')}.
139 139 </div>
140 140 % endif
141 141 </div>
142 142 </div>
143 143
144 144 <div class="field">
145 145 <div class="label-summary">
146 146 <label>${_('Review')}:</label>
147 147 </div>
148 148 <div class="input">
149 149 %if c.pull_request_review_status:
150 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 151 <span class="changeset-status-lbl tooltip">
152 152 %if c.pull_request.is_closed():
153 153 ${_('Closed')},
154 154 %endif
155 155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 156 </span>
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
157 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 158 %endif
159 159 </div>
160 160 </div>
161 161 <div class="field">
162 162 <div class="pr-description-label label-summary">
163 163 <label>${_('Description')}:</label>
164 164 </div>
165 165 <div id="pr-desc" class="input">
166 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 167 </div>
168 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 170 </div>
171 171 </div>
172 172
173 173 <div class="field">
174 174 <div class="label-summary">
175 175 <label>${_('Versions')}:</label>
176 176 </div>
177 177
178 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180 180
181 181 <div class="pr-versions">
182 182 % if c.show_version_changes:
183 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 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))}"
186 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
188 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 189 </a>
190 190 <table>
191 191 ## SHOW ALL VERSIONS OF PR
192 192 <% ver_pr = None %>
193 193
194 194 % for data in reversed(list(enumerate(c.versions, 1))):
195 195 <% ver_pos = data[0] %>
196 196 <% ver = data[1] %>
197 197 <% ver_pr = ver.pull_request_version_id %>
198 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199 199
200 200 <tr class="version-pr" style="display: ${display_row}">
201 201 <td>
202 202 <code>
203 203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 204 </code>
205 205 </td>
206 206 <td>
207 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 209 </td>
210 210 <td>
211 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 213 </div>
214 214 </td>
215 215 <td>
216 216 % if c.at_version_num != ver_pr:
217 217 <i class="icon-comment"></i>
218 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 220 </code>
221 221 % endif
222 222 </td>
223 223 <td>
224 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 225 </td>
226 226 <td>
227 227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 228 </td>
229 229 </tr>
230 230 % endfor
231 231
232 232 <tr>
233 233 <td colspan="6">
234 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 235 data-label-text-locked="${_('select versions to show changes')}"
236 236 data-label-text-diff="${_('show changes between versions')}"
237 237 data-label-text-show="${_('show pull request for this version')}"
238 238 >
239 239 ${_('select versions to show changes')}
240 240 </button>
241 241 </td>
242 242 </tr>
243 243
244 244 ## show comment/inline comments summary
245 245 <%def name="comments_summary()">
246 246 <tr>
247 247 <td colspan="6" class="comments-summary-td">
248 248
249 249 % if c.at_version:
250 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 252 ${_('Comments at this version')}:
253 253 % else:
254 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 256 ${_('Comments for this pull request')}:
257 257 % endif
258 258
259 259
260 260 %if general_comm_count_ver:
261 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 262 %else:
263 263 ${_("%d General ") % general_comm_count_ver}
264 264 %endif
265 265
266 266 %if inline_comm_count_ver:
267 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 268 %else:
269 269 , ${_("%d Inline") % inline_comm_count_ver}
270 270 %endif
271 271
272 272 %if outdated_comm_count_ver:
273 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 276 %else:
277 277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 278 %endif
279 279 </td>
280 280 </tr>
281 281 </%def>
282 282 ${comments_summary()}
283 283 </table>
284 284 % else:
285 285 <div class="input">
286 286 ${_('Pull request versions not available')}.
287 287 </div>
288 288 <div>
289 289 <table>
290 290 ${comments_summary()}
291 291 </table>
292 292 </div>
293 293 % endif
294 294 </div>
295 295 </div>
296 296
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
304 304 </div>
305 305 <div>
306 306 ## AUTHOR
307 307 <div class="reviewers-title block-right">
308 308 <div class="pr-details-title">
309 309 ${_('Author of this pull request')}
310 310 </div>
311 311 </div>
312 312 <div class="block-right pr-details-content reviewers">
313 313 <ul class="group_members">
314 314 <li>
315 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 316 </li>
317 317 </ul>
318 318 </div>
319 319
320 320 ## REVIEW RULES
321 321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 322 <div class="pr-details-title">
323 323 ${_('Reviewer rules')}
324 324 %if c.allowed_to_update:
325 325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 326 %endif
327 327 </div>
328 328 <div class="pr-reviewer-rules">
329 329 ## review rules will be appended here, by default reviewers logic
330 330 </div>
331 331 <input id="review_data" type="hidden" name="review_data" value="">
332 332 </div>
333 333
334 334 ## REVIEWERS
335 335 <div class="reviewers-title block-right">
336 336 <div class="pr-details-title">
337 337 ${_('Pull request reviewers')}
338 338 %if c.allowed_to_update:
339 339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
340 340 %endif
341 341 </div>
342 342 </div>
343 343 <div id="reviewers" class="block-right pr-details-content reviewers">
344 344 ## members goes here !
345 345 <input type="hidden" name="__start__" value="review_members:sequence">
346 346 <ul id="review_members" class="group_members">
347 347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
349 349 <div class="reviewers_member">
350 350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
351 351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
352 352 </div>
353 353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
354 354 ${self.gravatar_with_user(member.email, 16)}
355 355 </div>
356 356 <input type="hidden" name="__start__" value="reviewer:mapping">
357 357 <input type="hidden" name="__start__" value="reasons:sequence">
358 358 %for reason in reasons:
359 359 <div class="reviewer_reason">- ${reason}</div>
360 360 <input type="hidden" name="reason" value="${reason}">
361 361
362 362 %endfor
363 363 <input type="hidden" name="__end__" value="reasons:sequence">
364 364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 365 <input type="hidden" name="mandatory" value="${mandatory}"/>
366 366 <input type="hidden" name="__end__" value="reviewer:mapping">
367 367 % if mandatory:
368 368 <div class="reviewer_member_mandatory_remove">
369 369 <i class="icon-remove-sign"></i>
370 370 </div>
371 371 <div class="reviewer_member_mandatory">
372 372 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
373 373 </div>
374 374 % else:
375 375 %if c.allowed_to_update:
376 376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 377 <i class="icon-remove-sign" ></i>
378 378 </div>
379 379 %endif
380 380 % endif
381 381 </div>
382 382 </li>
383 383 %endfor
384 384 </ul>
385 385 <input type="hidden" name="__end__" value="review_members:sequence">
386 386
387 387 %if not c.pull_request.is_closed():
388 388 <div id="add_reviewer" class="ac" style="display: none;">
389 389 %if c.allowed_to_update:
390 390 % if not c.forbid_adding_reviewers:
391 391 <div id="add_reviewer_input" class="reviewer_ac">
392 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
393 393 <div id="reviewers_container"></div>
394 394 </div>
395 395 % endif
396 396 <div class="pull-right">
397 397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
398 398 </div>
399 399 %endif
400 400 </div>
401 401 %endif
402 402 </div>
403 403 </div>
404 404 </div>
405 405 <div class="box">
406 406 ##DIFF
407 407 <div class="table" >
408 408 <div id="changeset_compare_view_content">
409 409 ##CS
410 410 % if c.missing_requirements:
411 411 <div class="box">
412 412 <div class="alert alert-warning">
413 413 <div>
414 414 <strong>${_('Missing requirements:')}</strong>
415 415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
416 416 </div>
417 417 </div>
418 418 </div>
419 419 % elif c.missing_commits:
420 420 <div class="box">
421 421 <div class="alert alert-warning">
422 422 <div>
423 423 <strong>${_('Missing commits')}:</strong>
424 424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
425 425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
426 426 </div>
427 427 </div>
428 428 </div>
429 429 % endif
430 430
431 431 <div class="compare_view_commits_title">
432 432 % if not c.compare_mode:
433 433
434 434 % if c.at_version_pos:
435 435 <h4>
436 436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
437 437 </h4>
438 438 % endif
439 439
440 440 <div class="pull-left">
441 441 <div class="btn-group">
442 442 <a
443 443 class="btn"
444 444 href="#"
445 445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
446 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
446 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 447 </a>
448 448 <a
449 449 class="btn"
450 450 href="#"
451 451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
452 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
452 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
453 453 </a>
454 454 </div>
455 455 </div>
456 456
457 457 <div class="pull-right">
458 458 % if c.allowed_to_update and not c.pull_request.is_closed():
459 459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
460 460 % else:
461 461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
462 462 % endif
463 463
464 464 </div>
465 465 % endif
466 466 </div>
467 467
468 468 % if not c.missing_commits:
469 469 % if c.compare_mode:
470 470 % if c.at_version:
471 471 <h4>
472 472 ${_('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')}:
473 473 </h4>
474 474
475 475 <div class="subtitle-compare">
476 476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
477 477 </div>
478 478
479 479 <div class="container">
480 480 <table class="rctable compare_view_commits">
481 481 <tr>
482 482 <th></th>
483 483 <th>${_('Time')}</th>
484 484 <th>${_('Author')}</th>
485 485 <th>${_('Commit')}</th>
486 486 <th></th>
487 487 <th>${_('Description')}</th>
488 488 </tr>
489 489
490 490 % for c_type, commit in c.commit_changes:
491 491 % if c_type in ['a', 'r']:
492 492 <%
493 493 if c_type == 'a':
494 494 cc_title = _('Commit added in displayed changes')
495 495 elif c_type == 'r':
496 496 cc_title = _('Commit removed in displayed changes')
497 497 else:
498 498 cc_title = ''
499 499 %>
500 500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
501 501 <td>
502 502 <div class="commit-change-indicator color-${c_type}-border">
503 503 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
504 504 ${c_type.upper()}
505 505 </div>
506 506 </div>
507 507 </td>
508 508 <td class="td-time">
509 509 ${h.age_component(commit.date)}
510 510 </td>
511 511 <td class="td-user">
512 512 ${base.gravatar_with_user(commit.author, 16)}
513 513 </td>
514 514 <td class="td-hash">
515 515 <code>
516 516 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
517 517 r${commit.revision}:${h.short_id(commit.raw_id)}
518 518 </a>
519 519 ${h.hidden('revisions', commit.raw_id)}
520 520 </code>
521 521 </td>
522 522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
523 523 <div class="show_more_col">
524 524 <i class="show_more"></i>
525 525 </div>
526 526 </td>
527 527 <td class="mid td-description">
528 528 <div class="log-container truncate-wrap">
529 529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
530 530 ${h.urlify_commit_message(commit.message, c.repo_name)}
531 531 </div>
532 532 </div>
533 533 </td>
534 534 </tr>
535 535 % endif
536 536 % endfor
537 537 </table>
538 538 </div>
539 539
540 540 <script>
541 541 $('.expand_commit').on('click',function(e){
542 542 var target_expand = $(this);
543 543 var cid = target_expand.data('commitId');
544 544
545 545 if (target_expand.hasClass('open')){
546 546 $('#c-'+cid).css({
547 547 'height': '1.5em',
548 548 'white-space': 'nowrap',
549 549 'text-overflow': 'ellipsis',
550 550 'overflow':'hidden'
551 551 });
552 552 target_expand.removeClass('open');
553 553 }
554 554 else {
555 555 $('#c-'+cid).css({
556 556 'height': 'auto',
557 557 'white-space': 'pre-line',
558 558 'text-overflow': 'initial',
559 559 'overflow':'visible'
560 560 });
561 561 target_expand.addClass('open');
562 562 }
563 563 });
564 564 </script>
565 565
566 566 % endif
567 567
568 568 % else:
569 569 <%include file="/compare/compare_commits.mako" />
570 570 % endif
571 571
572 572 <div class="cs_files">
573 573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
574 574 ${cbdiffs.render_diffset_menu()}
575 575 ${cbdiffs.render_diffset(
576 576 c.diffset, use_comments=True,
577 577 collapse_when_files_over=30,
578 578 disable_new_comments=not c.allowed_to_comment,
579 579 deleted_files_comments=c.deleted_files_comments)}
580 580 </div>
581 581 % else:
582 582 ## skipping commits we need to clear the view for missing commits
583 583 <div style="clear:both;"></div>
584 584 % endif
585 585
586 586 </div>
587 587 </div>
588 588
589 589 ## template for inline comment form
590 590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
591 591
592 592 ## render general comments
593 593
594 594 <div id="comment-tr-show">
595 595 <div class="comment">
596 596 % if general_outdated_comm_count_ver:
597 597 <div class="meta">
598 598 % if general_outdated_comm_count_ver == 1:
599 599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
600 600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
601 601 % else:
602 602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
603 603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
604 604 % endif
605 605 </div>
606 606 % endif
607 607 </div>
608 608 </div>
609 609
610 610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
611 611
612 612 % if not c.pull_request.is_closed():
613 613 ## merge status, and merge action
614 614 <div class="pull-request-merge">
615 615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
616 616 </div>
617 617
618 618 ## main comment form and it status
619 619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
620 620 pull_request_id=c.pull_request.pull_request_id),
621 621 c.pull_request_review_status,
622 622 is_pull_request=True, change_status=c.allowed_to_change_status)}
623 623 %endif
624 624
625 625 <script type="text/javascript">
626 626 if (location.hash) {
627 627 var result = splitDelimitedHash(location.hash);
628 628 var line = $('html').find(result.loc);
629 629 // show hidden comments if we use location.hash
630 630 if (line.hasClass('comment-general')) {
631 631 $(line).show();
632 632 } else if (line.hasClass('comment-inline')) {
633 633 $(line).show();
634 634 var $cb = $(line).closest('.cb');
635 635 $cb.removeClass('cb-collapsed')
636 636 }
637 637 if (line.length > 0){
638 638 offsetScroll(line, 70);
639 639 }
640 640 }
641 641
642 642 versionController = new VersionController();
643 643 versionController.init();
644 644
645 645 reviewersController = new ReviewersController();
646 646
647 647 $(function(){
648 648
649 649 // custom code mirror
650 650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
651 651
652 652 var PRDetails = {
653 653 editButton: $('#open_edit_pullrequest'),
654 654 closeButton: $('#close_edit_pullrequest'),
655 655 deleteButton: $('#delete_pullrequest'),
656 656 viewFields: $('#pr-desc, #pr-title'),
657 657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
658 658
659 659 init: function() {
660 660 var that = this;
661 661 this.editButton.on('click', function(e) { that.edit(); });
662 662 this.closeButton.on('click', function(e) { that.view(); });
663 663 },
664 664
665 665 edit: function(event) {
666 666 this.viewFields.hide();
667 667 this.editButton.hide();
668 668 this.deleteButton.hide();
669 669 this.closeButton.show();
670 670 this.editFields.show();
671 671 codeMirrorInstance.refresh();
672 672 },
673 673
674 674 view: function(event) {
675 675 this.editButton.show();
676 676 this.deleteButton.show();
677 677 this.editFields.hide();
678 678 this.closeButton.hide();
679 679 this.viewFields.show();
680 680 }
681 681 };
682 682
683 683 var ReviewersPanel = {
684 684 editButton: $('#open_edit_reviewers'),
685 685 closeButton: $('#close_edit_reviewers'),
686 686 addButton: $('#add_reviewer'),
687 687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
688 688
689 689 init: function() {
690 690 var self = this;
691 691 this.editButton.on('click', function(e) { self.edit(); });
692 692 this.closeButton.on('click', function(e) { self.close(); });
693 693 },
694 694
695 695 edit: function(event) {
696 696 this.editButton.hide();
697 697 this.closeButton.show();
698 698 this.addButton.show();
699 699 this.removeButtons.css('visibility', 'visible');
700 700 // review rules
701 701 reviewersController.loadReviewRules(
702 702 ${c.pull_request.reviewer_data_json | n});
703 703 },
704 704
705 705 close: function(event) {
706 706 this.editButton.show();
707 707 this.closeButton.hide();
708 708 this.addButton.hide();
709 709 this.removeButtons.css('visibility', 'hidden');
710 710 // hide review rules
711 711 reviewersController.hideReviewRules()
712 712 }
713 713 };
714 714
715 715 PRDetails.init();
716 716 ReviewersPanel.init();
717 717
718 718 showOutdated = function(self){
719 719 $('.comment-inline.comment-outdated').show();
720 720 $('.filediff-outdated').show();
721 721 $('.showOutdatedComments').hide();
722 722 $('.hideOutdatedComments').show();
723 723 };
724 724
725 725 hideOutdated = function(self){
726 726 $('.comment-inline.comment-outdated').hide();
727 727 $('.filediff-outdated').hide();
728 728 $('.hideOutdatedComments').hide();
729 729 $('.showOutdatedComments').show();
730 730 };
731 731
732 732 refreshMergeChecks = function(){
733 733 var loadUrl = "${h.url.current(merge_checks=1)}";
734 734 $('.pull-request-merge').css('opacity', 0.3);
735 735 $('.action-buttons-extra').css('opacity', 0.3);
736 736
737 737 $('.pull-request-merge').load(
738 738 loadUrl, function() {
739 739 $('.pull-request-merge').css('opacity', 1);
740 740
741 741 $('.action-buttons-extra').css('opacity', 1);
742 742 injectCloseAction();
743 743 }
744 744 );
745 745 };
746 746
747 747 injectCloseAction = function() {
748 748 var closeAction = $('#close-pull-request-action').html();
749 749 var $actionButtons = $('.action-buttons-extra');
750 750 // clear the action before
751 751 $actionButtons.html("");
752 752 $actionButtons.html(closeAction);
753 753 };
754 754
755 755 closePullRequest = function (status) {
756 756 // inject closing flag
757 757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
758 758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
759 759 $(generalCommentForm.submitForm).submit();
760 760 };
761 761
762 762 $('#show-outdated-comments').on('click', function(e){
763 763 var button = $(this);
764 764 var outdated = $('.comment-outdated');
765 765
766 766 if (button.html() === "(Show)") {
767 767 button.html("(Hide)");
768 768 outdated.show();
769 769 } else {
770 770 button.html("(Show)");
771 771 outdated.hide();
772 772 }
773 773 });
774 774
775 775 $('.show-inline-comments').on('change', function(e){
776 776 var show = 'none';
777 777 var target = e.currentTarget;
778 778 if(target.checked){
779 779 show = ''
780 780 }
781 781 var boxid = $(target).attr('id_for');
782 782 var comments = $('#{0} .inline-comments'.format(boxid));
783 783 var fn_display = function(idx){
784 784 $(this).css('display', show);
785 785 };
786 786 $(comments).each(fn_display);
787 787 var btns = $('#{0} .inline-comments-button'.format(boxid));
788 788 $(btns).each(fn_display);
789 789 });
790 790
791 791 $('#merge_pull_request_form').submit(function() {
792 792 if (!$('#merge_pull_request').attr('disabled')) {
793 793 $('#merge_pull_request').attr('disabled', 'disabled');
794 794 }
795 795 return true;
796 796 });
797 797
798 798 $('#edit_pull_request').on('click', function(e){
799 799 var title = $('#pr-title-input').val();
800 800 var description = codeMirrorInstance.getValue();
801 801 editPullRequest(
802 802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
803 803 title, description);
804 804 });
805 805
806 806 $('#update_pull_request').on('click', function(e){
807 807 $(this).attr('disabled', 'disabled');
808 808 $(this).addClass('disabled');
809 809 $(this).html(_gettext('Saving...'));
810 810 reviewersController.updateReviewers(
811 811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
812 812 });
813 813
814 814 $('#update_commits').on('click', function(e){
815 815 var isDisabled = !$(e.currentTarget).attr('disabled');
816 816 $(e.currentTarget).attr('disabled', 'disabled');
817 817 $(e.currentTarget).addClass('disabled');
818 818 $(e.currentTarget).removeClass('btn-primary');
819 819 $(e.currentTarget).text(_gettext('Updating...'));
820 820 if(isDisabled){
821 821 updateCommits(
822 822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
823 823 }
824 824 });
825 825 // fixing issue with caches on firefox
826 826 $('#update_commits').removeAttr("disabled");
827 827
828 828 $('.show-inline-comments').on('click', function(e){
829 829 var boxid = $(this).attr('data-comment-id');
830 830 var button = $(this);
831 831
832 832 if(button.hasClass("comments-visible")) {
833 833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 834 $(this).hide();
835 835 });
836 836 button.removeClass("comments-visible");
837 837 } else {
838 838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
839 839 $(this).show();
840 840 });
841 841 button.addClass("comments-visible");
842 842 }
843 843 });
844 844
845 845 // register submit callback on commentForm form to track TODOs
846 846 window.commentFormGlobalSubmitSuccessCallback = function(){
847 847 refreshMergeChecks();
848 848 };
849 849 // initial injection
850 850 injectCloseAction();
851 851
852 852 ReviewerAutoComplete('#user');
853 853
854 854 })
855 855 </script>
856 856
857 857 </div>
858 858 </div>
859 859
860 860 </%def>
@@ -1,211 +1,211 b''
1 1 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
2 2 <span class="branchtag tag">
3 3 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
4 4 <i class="icon-branch"></i>${_ungettext(
5 5 '%(num)s Branch','%(num)s Branches', len(branches)) % {'num': len(branches)}}</a>
6 6 </span>
7 7
8 8 %if closed_branches:
9 9 <span class="branchtag tag">
10 10 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
11 11 <i class="icon-branch"></i>${_ungettext(
12 12 '%(num)s Closed Branch', '%(num)s Closed Branches', len(closed_branches)) % {'num': len(closed_branches)}}</a>
13 13 </span>
14 14 %endif
15 15
16 16 <span class="tagtag tag">
17 17 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
18 18 <i class="icon-tag"></i>${_ungettext(
19 19 '%(num)s Tag', '%(num)s Tags', len(tags)) % {'num': len(tags)}}</a>
20 20 </span>
21 21
22 22 %if bookmarks:
23 23 <span class="booktag tag">
24 24 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
25 25 <i class="icon-bookmark"></i>${_ungettext(
26 26 '%(num)s Bookmark', '%(num)s Bookmarks', len(bookmarks)) % {'num': len(bookmarks)}}</a>
27 27 </span>
28 28 %endif
29 29 </%def>
30 30
31 31 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
32 32 <% summary = lambda n:{False:'summary-short'}.get(n) %>
33 33
34 34 <div id="summary-menu-stats" class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <div class="breadcrumbs files_location">
37 37 <h4>
38 38 ${breadcrumbs_links}
39 39 </h4>
40 40 </div>
41 41 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
42 42 ${_('Show More')}
43 43 </div>
44 44 </div>
45 45
46 46 <div class="fieldset">
47 47 %if h.is_svn_without_proxy(c.rhodecode_db_repo):
48 48 <div class="left-label disabled">
49 49 ${_('Read-only url')}:
50 50 </div>
51 51 <div class="right-content disabled">
52 52 <input type="text" class="input-monospace" id="clone_url" disabled value="${c.clone_repo_url}"/>
53 53 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
54 54
55 55 <input type="text" class="input-monospace" id="clone_url_id" disabled value="${c.clone_repo_url_id}" style="display: none;"/>
56 56 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
57 57
58 58 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
59 59 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
60 60
61 61 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
62 62 </div>
63 63 %else:
64 64 <div class="left-label">
65 65 ${_('Clone url')}:
66 66 </div>
67 67 <div class="right-content">
68 68 <input type="text" class="input-monospace" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
69 69 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
70 70
71 71 <input type="text" class="input-monospace" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}" style="display: none;"/>
72 72 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
73 73
74 74 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
75 75 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
76 76 </div>
77 77 %endif
78 78 </div>
79 79
80 80 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
81 81 <div class="left-label">
82 82 ${_('Description')}:
83 83 </div>
84 84 <div class="right-content">
85 85 %if c.visual.stylify_metatags:
86 86 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.escaped_stylize(c.rhodecode_db_repo.description))}</div>
87 87 %else:
88 88 <div class="input ${summary(c.show_stats)} desc">${h.urlify_text(h.html_escape(c.rhodecode_db_repo.description))}</div>
89 89 %endif
90 90 </div>
91 91 </div>
92 92
93 93 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
94 94 <div class="left-label">
95 95 ${_('Information')}:
96 96 </div>
97 97 <div class="right-content">
98 98
99 99 <div class="repo-size">
100 100 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
101 101
102 102 ## commits
103 103 % if commit_rev == -1:
104 104 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}},
105 105 % else:
106 106 <a href="${h.route_path('repo_changelog', repo_name=c.repo_name)}">
107 107 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>,
108 108 % endif
109 109
110 110 ## forks
111 111 <a title="${_('Number of Repository Forks')}" href="${h.url('repo_forks_home', repo_name=c.repo_name)}">
112 112 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>,
113 113
114 114 ## repo size
115 115 % if commit_rev == -1:
116 116 <span class="stats-bullet">0 B</span>
117 117 % else:
118 118 <span class="stats-bullet" id="repo_size_container">
119 119 ${_('Calculating Repository Size...')}
120 120 </span>
121 121 % endif
122 122 </div>
123 123
124 124 <div class="commit-info">
125 125 <div class="tags">
126 126 % if c.rhodecode_repo:
127 127 ${refs_counters(
128 128 c.rhodecode_repo.branches,
129 129 c.rhodecode_repo.branches_closed,
130 130 c.rhodecode_repo.tags,
131 131 c.rhodecode_repo.bookmarks)}
132 132 % else:
133 133 ## missing requirements can make c.rhodecode_repo None
134 134 ${refs_counters([], [], [], [])}
135 135 % endif
136 136 </div>
137 137 </div>
138 138
139 139 </div>
140 140 </div>
141 141
142 142 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
143 143 <div class="left-label">
144 144 ${_('Statistics')}:
145 145 </div>
146 146 <div class="right-content">
147 147 <div class="input ${summary(c.show_stats)} statistics">
148 148 % if c.show_stats:
149 149 <div id="lang_stats" class="enabled">
150 150 ${_('Calculating Code Statistics...')}
151 151 </div>
152 152 % else:
153 153 <span class="disabled">
154 154 ${_('Statistics are disabled for this repository')}
155 155 </span>
156 156 % if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
157 , ${h.link_to(_('enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'))}
157 , ${h.link_to(_('enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_statistics'))}
158 158 % endif
159 159 % endif
160 160 </div>
161 161
162 162 </div>
163 163 </div>
164 164
165 165 % if show_downloads:
166 166 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
167 167 <div class="left-label">
168 168 ${_('Downloads')}:
169 169 </div>
170 170 <div class="right-content">
171 171 <div class="input ${summary(c.show_stats)} downloads">
172 172 % if c.rhodecode_repo and len(c.rhodecode_repo.revisions) == 0:
173 173 <span class="disabled">
174 174 ${_('There are no downloads yet')}
175 175 </span>
176 176 % elif not c.enable_downloads:
177 177 <span class="disabled">
178 178 ${_('Downloads are disabled for this repository')}
179 179 </span>
180 180 % if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
181 , ${h.link_to(_('enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'))}
181 , ${h.link_to(_('enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
182 182 % endif
183 183 % else:
184 184 <span class="enabled">
185 185 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
186 186 <i class="icon-archive"></i> tip.zip
187 187 ## replaced by some JS on select
188 188 </a>
189 189 </span>
190 190 ${h.hidden('download_options')}
191 191 % endif
192 192 </div>
193 193 </div>
194 194 </div>
195 195 % endif
196 196
197 197 </div><!--end summary-detail-->
198 198 </%def>
199 199
200 200 <%def name="summary_stats(gravatar_function)">
201 201 <div class="sidebar-right">
202 202 <div class="summary-detail-header">
203 203 <h4 class="item">
204 204 ${_('Owner')}
205 205 </h4>
206 206 </div>
207 207 <div class="sidebar-right-content">
208 208 ${gravatar_function(c.rhodecode_db_repo.user.email, 16)}
209 209 </div>
210 210 </div><!--end sidebar-right-->
211 211 </%def>
General Comments 0
You need to be logged in to leave comments. Login now