##// END OF EJS Templates
menu: expose artifacts count into menu
marcink -
r3984:9bf54585 default
parent child Browse files
Show More
@@ -1,800 +1,802 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 import time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid import compat
26 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 27
28 28 from rhodecode.lib import helpers as h, diffs, rc_cache
29 29 from rhodecode.lib.utils2 import (
30 30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
32 32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
34 34 from rhodecode.model import repo
35 35 from rhodecode.model import repo_group
36 36 from rhodecode.model import user_group
37 37 from rhodecode.model import user
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.scm import ScmModel
40 40 from rhodecode.model.settings import VcsSettingsModel
41 41 from rhodecode.model.repo import ReadmeFinder
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 ADMIN_PREFIX = '/_admin'
47 47 STATIC_FILE_PREFIX = '/_static'
48 48
49 49 URL_NAME_REQUIREMENTS = {
50 50 # group name can have a slash in them, but they must not end with a slash
51 51 'group_name': r'.*?[^/]',
52 52 'repo_group_name': r'.*?[^/]',
53 53 # repo names can have a slash in them, but they must not end with a slash
54 54 'repo_name': r'.*?[^/]',
55 55 # file path eats up everything at the end
56 56 'f_path': r'.*',
57 57 # reference types
58 58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
59 59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
60 60 }
61 61
62 62
63 63 def add_route_with_slash(config,name, pattern, **kw):
64 64 config.add_route(name, pattern, **kw)
65 65 if not pattern.endswith('/'):
66 66 config.add_route(name + '_slash', pattern + '/', **kw)
67 67
68 68
69 69 def add_route_requirements(route_path, requirements=None):
70 70 """
71 71 Adds regex requirements to pyramid routes using a mapping dict
72 72 e.g::
73 73 add_route_requirements('{repo_name}/settings')
74 74 """
75 75 requirements = requirements or URL_NAME_REQUIREMENTS
76 76 for key, regex in requirements.items():
77 77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
78 78 return route_path
79 79
80 80
81 81 def get_format_ref_id(repo):
82 82 """Returns a `repo` specific reference formatter function"""
83 83 if h.is_svn(repo):
84 84 return _format_ref_id_svn
85 85 else:
86 86 return _format_ref_id
87 87
88 88
89 89 def _format_ref_id(name, raw_id):
90 90 """Default formatting of a given reference `name`"""
91 91 return name
92 92
93 93
94 94 def _format_ref_id_svn(name, raw_id):
95 95 """Special way of formatting a reference for Subversion including path"""
96 96 return '%s@%s' % (name, raw_id)
97 97
98 98
99 99 class TemplateArgs(StrictAttributeDict):
100 100 pass
101 101
102 102
103 103 class BaseAppView(object):
104 104
105 105 def __init__(self, context, request):
106 106 self.request = request
107 107 self.context = context
108 108 self.session = request.session
109 109 if not hasattr(request, 'user'):
110 110 # NOTE(marcink): edge case, we ended up in matched route
111 111 # but probably of web-app context, e.g API CALL/VCS CALL
112 112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
113 113 log.warning('Unable to process request `%s` in this scope', request)
114 114 raise HTTPBadRequest()
115 115
116 116 self._rhodecode_user = request.user # auth user
117 117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
118 118 self._maybe_needs_password_change(
119 119 request.matched_route.name, self._rhodecode_db_user)
120 120
121 121 def _maybe_needs_password_change(self, view_name, user_obj):
122 122 log.debug('Checking if user %s needs password change on view %s',
123 123 user_obj, view_name)
124 124 skip_user_views = [
125 125 'logout', 'login',
126 126 'my_account_password', 'my_account_password_update'
127 127 ]
128 128
129 129 if not user_obj:
130 130 return
131 131
132 132 if user_obj.username == User.DEFAULT_USER:
133 133 return
134 134
135 135 now = time.time()
136 136 should_change = user_obj.user_data.get('force_password_change')
137 137 change_after = safe_int(should_change) or 0
138 138 if should_change and now > change_after:
139 139 log.debug('User %s requires password change', user_obj)
140 140 h.flash('You are required to change your password', 'warning',
141 141 ignore_duplicate=True)
142 142
143 143 if view_name not in skip_user_views:
144 144 raise HTTPFound(
145 145 self.request.route_path('my_account_password'))
146 146
147 147 def _log_creation_exception(self, e, repo_name):
148 148 _ = self.request.translate
149 149 reason = None
150 150 if len(e.args) == 2:
151 151 reason = e.args[1]
152 152
153 153 if reason == 'INVALID_CERTIFICATE':
154 154 log.exception(
155 155 'Exception creating a repository: invalid certificate')
156 156 msg = (_('Error creating repository %s: invalid certificate')
157 157 % repo_name)
158 158 else:
159 159 log.exception("Exception creating a repository")
160 160 msg = (_('Error creating repository %s')
161 161 % repo_name)
162 162 return msg
163 163
164 164 def _get_local_tmpl_context(self, include_app_defaults=True):
165 165 c = TemplateArgs()
166 166 c.auth_user = self.request.user
167 167 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
168 168 c.rhodecode_user = self.request.user
169 169
170 170 if include_app_defaults:
171 171 from rhodecode.lib.base import attach_context_attributes
172 172 attach_context_attributes(c, self.request, self.request.user.user_id)
173 173
174 174 c.is_super_admin = c.auth_user.is_admin
175 175
176 176 c.can_create_repo = c.is_super_admin
177 177 c.can_create_repo_group = c.is_super_admin
178 178 c.can_create_user_group = c.is_super_admin
179 179
180 180 c.is_delegated_admin = False
181 181
182 182 if not c.auth_user.is_default and not c.is_super_admin:
183 183 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
184 184 user=self.request.user)
185 185 repositories = c.auth_user.repositories_admin or c.can_create_repo
186 186
187 187 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
188 188 user=self.request.user)
189 189 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
190 190
191 191 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
192 192 user=self.request.user)
193 193 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
194 194 # delegated admin can create, or manage some objects
195 195 c.is_delegated_admin = repositories or repository_groups or user_groups
196 196 return c
197 197
198 198 def _get_template_context(self, tmpl_args, **kwargs):
199 199
200 200 local_tmpl_args = {
201 201 'defaults': {},
202 202 'errors': {},
203 203 'c': tmpl_args
204 204 }
205 205 local_tmpl_args.update(kwargs)
206 206 return local_tmpl_args
207 207
208 208 def load_default_context(self):
209 209 """
210 210 example:
211 211
212 212 def load_default_context(self):
213 213 c = self._get_local_tmpl_context()
214 214 c.custom_var = 'foobar'
215 215
216 216 return c
217 217 """
218 218 raise NotImplementedError('Needs implementation in view class')
219 219
220 220
221 221 class RepoAppView(BaseAppView):
222 222
223 223 def __init__(self, context, request):
224 224 super(RepoAppView, self).__init__(context, request)
225 225 self.db_repo = request.db_repo
226 226 self.db_repo_name = self.db_repo.repo_name
227 227 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
228 229
229 230 def _handle_missing_requirements(self, error):
230 231 log.error(
231 232 'Requirements are missing for repository %s: %s',
232 233 self.db_repo_name, safe_unicode(error))
233 234
234 235 def _get_local_tmpl_context(self, include_app_defaults=True):
235 236 _ = self.request.translate
236 237 c = super(RepoAppView, self)._get_local_tmpl_context(
237 238 include_app_defaults=include_app_defaults)
238 239
239 240 # register common vars for this type of view
240 241 c.rhodecode_db_repo = self.db_repo
241 242 c.repo_name = self.db_repo_name
242 243 c.repository_pull_requests = self.db_repo_pull_requests
244 c.repository_artifacts = self.db_repo_artifacts
243 245 c.repository_is_user_following = ScmModel().is_following_repo(
244 246 self.db_repo_name, self._rhodecode_user.user_id)
245 247 self.path_filter = PathFilter(None)
246 248
247 249 c.repository_requirements_missing = {}
248 250 try:
249 251 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
250 252 # NOTE(marcink):
251 253 # comparison to None since if it's an object __bool__ is expensive to
252 254 # calculate
253 255 if self.rhodecode_vcs_repo is not None:
254 256 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
255 257 c.auth_user.username)
256 258 self.path_filter = PathFilter(path_perms)
257 259 except RepositoryRequirementError as e:
258 260 c.repository_requirements_missing = {'error': str(e)}
259 261 self._handle_missing_requirements(e)
260 262 self.rhodecode_vcs_repo = None
261 263
262 264 c.path_filter = self.path_filter # used by atom_feed_entry.mako
263 265
264 266 if self.rhodecode_vcs_repo is None:
265 267 # unable to fetch this repo as vcs instance, report back to user
266 268 h.flash(_(
267 269 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
268 270 "Please check if it exist, or is not damaged.") %
269 271 {'repo_name': c.repo_name},
270 272 category='error', ignore_duplicate=True)
271 273 if c.repository_requirements_missing:
272 274 route = self.request.matched_route.name
273 275 if route.startswith(('edit_repo', 'repo_summary')):
274 276 # allow summary and edit repo on missing requirements
275 277 return c
276 278
277 279 raise HTTPFound(
278 280 h.route_path('repo_summary', repo_name=self.db_repo_name))
279 281
280 282 else: # redirect if we don't show missing requirements
281 283 raise HTTPFound(h.route_path('home'))
282 284
283 285 c.has_origin_repo_read_perm = False
284 286 if self.db_repo.fork:
285 287 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
286 288 'repository.write', 'repository.read', 'repository.admin')(
287 289 self.db_repo.fork.repo_name, 'summary fork link')
288 290
289 291 return c
290 292
291 293 def _get_f_path_unchecked(self, matchdict, default=None):
292 294 """
293 295 Should only be used by redirects, everything else should call _get_f_path
294 296 """
295 297 f_path = matchdict.get('f_path')
296 298 if f_path:
297 299 # fix for multiple initial slashes that causes errors for GIT
298 300 return f_path.lstrip('/')
299 301
300 302 return default
301 303
302 304 def _get_f_path(self, matchdict, default=None):
303 305 f_path_match = self._get_f_path_unchecked(matchdict, default)
304 306 return self.path_filter.assert_path_permissions(f_path_match)
305 307
306 308 def _get_general_setting(self, target_repo, settings_key, default=False):
307 309 settings_model = VcsSettingsModel(repo=target_repo)
308 310 settings = settings_model.get_general_settings()
309 311 return settings.get(settings_key, default)
310 312
311 313 def _get_repo_setting(self, target_repo, settings_key, default=False):
312 314 settings_model = VcsSettingsModel(repo=target_repo)
313 315 settings = settings_model.get_repo_settings_inherited()
314 316 return settings.get(settings_key, default)
315 317
316 318 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
317 319 log.debug('Looking for README file at path %s', path)
318 320 if commit_id:
319 321 landing_commit_id = commit_id
320 322 else:
321 323 landing_commit = db_repo.get_landing_commit()
322 324 if isinstance(landing_commit, EmptyCommit):
323 325 return None, None
324 326 landing_commit_id = landing_commit.raw_id
325 327
326 328 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
327 329 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
328 330 start = time.time()
329 331
330 332 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
331 333 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
332 334 readme_data = None
333 335 readme_filename = None
334 336
335 337 commit = db_repo.get_commit(_commit_id)
336 338 log.debug("Searching for a README file at commit %s.", _commit_id)
337 339 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
338 340
339 341 if readme_node:
340 342 log.debug('Found README node: %s', readme_node)
341 343 relative_urls = {
342 344 'raw': h.route_path(
343 345 'repo_file_raw', repo_name=_repo_name,
344 346 commit_id=commit.raw_id, f_path=readme_node.path),
345 347 'standard': h.route_path(
346 348 'repo_files', repo_name=_repo_name,
347 349 commit_id=commit.raw_id, f_path=readme_node.path),
348 350 }
349 351 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
350 352 readme_filename = readme_node.unicode_path
351 353
352 354 return readme_data, readme_filename
353 355
354 356 readme_data, readme_filename = generate_repo_readme(
355 357 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
356 358 compute_time = time.time() - start
357 359 log.debug('Repo README for path %s generated and computed in %.4fs',
358 360 path, compute_time)
359 361 return readme_data, readme_filename
360 362
361 363 def _render_readme_or_none(self, commit, readme_node, relative_urls):
362 364 log.debug('Found README file `%s` rendering...', readme_node.path)
363 365 renderer = MarkupRenderer()
364 366 try:
365 367 html_source = renderer.render(
366 368 readme_node.content, filename=readme_node.path)
367 369 if relative_urls:
368 370 return relative_links(html_source, relative_urls)
369 371 return html_source
370 372 except Exception:
371 373 log.exception(
372 374 "Exception while trying to render the README")
373 375
374 376 def get_recache_flag(self):
375 377 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
376 378 flag_val = self.request.GET.get(flag_name)
377 379 if str2bool(flag_val):
378 380 return True
379 381 return False
380 382
381 383
382 384 class PathFilter(object):
383 385
384 386 # Expects and instance of BasePathPermissionChecker or None
385 387 def __init__(self, permission_checker):
386 388 self.permission_checker = permission_checker
387 389
388 390 def assert_path_permissions(self, path):
389 391 if self.path_access_allowed(path):
390 392 return path
391 393 raise HTTPForbidden()
392 394
393 395 def path_access_allowed(self, path):
394 396 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
395 397 if self.permission_checker:
396 398 return path and self.permission_checker.has_access(path)
397 399 return True
398 400
399 401 def filter_patchset(self, patchset):
400 402 if not self.permission_checker or not patchset:
401 403 return patchset, False
402 404 had_filtered = False
403 405 filtered_patchset = []
404 406 for patch in patchset:
405 407 filename = patch.get('filename', None)
406 408 if not filename or self.permission_checker.has_access(filename):
407 409 filtered_patchset.append(patch)
408 410 else:
409 411 had_filtered = True
410 412 if had_filtered:
411 413 if isinstance(patchset, diffs.LimitedDiffContainer):
412 414 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
413 415 return filtered_patchset, True
414 416 else:
415 417 return patchset, False
416 418
417 419 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
418 420 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
419 421 result = diffset.render_patchset(
420 422 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
421 423 result.has_hidden_changes = has_hidden_changes
422 424 return result
423 425
424 426 def get_raw_patch(self, diff_processor):
425 427 if self.permission_checker is None:
426 428 return diff_processor.as_raw()
427 429 elif self.permission_checker.has_full_access:
428 430 return diff_processor.as_raw()
429 431 else:
430 432 return '# Repository has user-specific filters, raw patch generation is disabled.'
431 433
432 434 @property
433 435 def is_enabled(self):
434 436 return self.permission_checker is not None
435 437
436 438
437 439 class RepoGroupAppView(BaseAppView):
438 440 def __init__(self, context, request):
439 441 super(RepoGroupAppView, self).__init__(context, request)
440 442 self.db_repo_group = request.db_repo_group
441 443 self.db_repo_group_name = self.db_repo_group.group_name
442 444
443 445 def _get_local_tmpl_context(self, include_app_defaults=True):
444 446 _ = self.request.translate
445 447 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
446 448 include_app_defaults=include_app_defaults)
447 449 c.repo_group = self.db_repo_group
448 450 return c
449 451
450 452 def _revoke_perms_on_yourself(self, form_result):
451 453 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
452 454 form_result['perm_updates'])
453 455 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
454 456 form_result['perm_additions'])
455 457 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
456 458 form_result['perm_deletions'])
457 459 admin_perm = 'group.admin'
458 460 if _updates and _updates[0][1] != admin_perm or \
459 461 _additions and _additions[0][1] != admin_perm or \
460 462 _deletions and _deletions[0][1] != admin_perm:
461 463 return True
462 464 return False
463 465
464 466
465 467 class UserGroupAppView(BaseAppView):
466 468 def __init__(self, context, request):
467 469 super(UserGroupAppView, self).__init__(context, request)
468 470 self.db_user_group = request.db_user_group
469 471 self.db_user_group_name = self.db_user_group.users_group_name
470 472
471 473
472 474 class UserAppView(BaseAppView):
473 475 def __init__(self, context, request):
474 476 super(UserAppView, self).__init__(context, request)
475 477 self.db_user = request.db_user
476 478 self.db_user_id = self.db_user.user_id
477 479
478 480 _ = self.request.translate
479 481 if not request.db_user_supports_default:
480 482 if self.db_user.username == User.DEFAULT_USER:
481 483 h.flash(_("Editing user `{}` is disabled.".format(
482 484 User.DEFAULT_USER)), category='warning')
483 485 raise HTTPFound(h.route_path('users'))
484 486
485 487
486 488 class DataGridAppView(object):
487 489 """
488 490 Common class to have re-usable grid rendering components
489 491 """
490 492
491 493 def _extract_ordering(self, request, column_map=None):
492 494 column_map = column_map or {}
493 495 column_index = safe_int(request.GET.get('order[0][column]'))
494 496 order_dir = request.GET.get(
495 497 'order[0][dir]', 'desc')
496 498 order_by = request.GET.get(
497 499 'columns[%s][data][sort]' % column_index, 'name_raw')
498 500
499 501 # translate datatable to DB columns
500 502 order_by = column_map.get(order_by) or order_by
501 503
502 504 search_q = request.GET.get('search[value]')
503 505 return search_q, order_by, order_dir
504 506
505 507 def _extract_chunk(self, request):
506 508 start = safe_int(request.GET.get('start'), 0)
507 509 length = safe_int(request.GET.get('length'), 25)
508 510 draw = safe_int(request.GET.get('draw'))
509 511 return draw, start, length
510 512
511 513 def _get_order_col(self, order_by, model):
512 514 if isinstance(order_by, compat.string_types):
513 515 try:
514 516 return operator.attrgetter(order_by)(model)
515 517 except AttributeError:
516 518 return None
517 519 else:
518 520 return order_by
519 521
520 522
521 523 class BaseReferencesView(RepoAppView):
522 524 """
523 525 Base for reference view for branches, tags and bookmarks.
524 526 """
525 527 def load_default_context(self):
526 528 c = self._get_local_tmpl_context()
527 529
528 530
529 531 return c
530 532
531 533 def load_refs_context(self, ref_items, partials_template):
532 534 _render = self.request.get_partial_renderer(partials_template)
533 535 pre_load = ["author", "date", "message", "parents"]
534 536
535 537 is_svn = h.is_svn(self.rhodecode_vcs_repo)
536 538 is_hg = h.is_hg(self.rhodecode_vcs_repo)
537 539
538 540 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
539 541
540 542 closed_refs = {}
541 543 if is_hg:
542 544 closed_refs = self.rhodecode_vcs_repo.branches_closed
543 545
544 546 data = []
545 547 for ref_name, commit_id in ref_items:
546 548 commit = self.rhodecode_vcs_repo.get_commit(
547 549 commit_id=commit_id, pre_load=pre_load)
548 550 closed = ref_name in closed_refs
549 551
550 552 # TODO: johbo: Unify generation of reference links
551 553 use_commit_id = '/' in ref_name or is_svn
552 554
553 555 if use_commit_id:
554 556 files_url = h.route_path(
555 557 'repo_files',
556 558 repo_name=self.db_repo_name,
557 559 f_path=ref_name if is_svn else '',
558 560 commit_id=commit_id)
559 561
560 562 else:
561 563 files_url = h.route_path(
562 564 'repo_files',
563 565 repo_name=self.db_repo_name,
564 566 f_path=ref_name if is_svn else '',
565 567 commit_id=ref_name,
566 568 _query=dict(at=ref_name))
567 569
568 570 data.append({
569 571 "name": _render('name', ref_name, files_url, closed),
570 572 "name_raw": ref_name,
571 573 "date": _render('date', commit.date),
572 574 "date_raw": datetime_to_time(commit.date),
573 575 "author": _render('author', commit.author),
574 576 "commit": _render(
575 577 'commit', commit.message, commit.raw_id, commit.idx),
576 578 "commit_raw": commit.idx,
577 579 "compare": _render(
578 580 'compare', format_ref_id(ref_name, commit.raw_id)),
579 581 })
580 582
581 583 return data
582 584
583 585
584 586 class RepoRoutePredicate(object):
585 587 def __init__(self, val, config):
586 588 self.val = val
587 589
588 590 def text(self):
589 591 return 'repo_route = %s' % self.val
590 592
591 593 phash = text
592 594
593 595 def __call__(self, info, request):
594 596 if hasattr(request, 'vcs_call'):
595 597 # skip vcs calls
596 598 return
597 599
598 600 repo_name = info['match']['repo_name']
599 601 repo_model = repo.RepoModel()
600 602
601 603 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
602 604
603 605 def redirect_if_creating(route_info, db_repo):
604 606 skip_views = ['edit_repo_advanced_delete']
605 607 route = route_info['route']
606 608 # we should skip delete view so we can actually "remove" repositories
607 609 # if they get stuck in creating state.
608 610 if route.name in skip_views:
609 611 return
610 612
611 613 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
612 614 repo_creating_url = request.route_path(
613 615 'repo_creating', repo_name=db_repo.repo_name)
614 616 raise HTTPFound(repo_creating_url)
615 617
616 618 if by_name_match:
617 619 # register this as request object we can re-use later
618 620 request.db_repo = by_name_match
619 621 redirect_if_creating(info, by_name_match)
620 622 return True
621 623
622 624 by_id_match = repo_model.get_repo_by_id(repo_name)
623 625 if by_id_match:
624 626 request.db_repo = by_id_match
625 627 redirect_if_creating(info, by_id_match)
626 628 return True
627 629
628 630 return False
629 631
630 632
631 633 class RepoForbidArchivedRoutePredicate(object):
632 634 def __init__(self, val, config):
633 635 self.val = val
634 636
635 637 def text(self):
636 638 return 'repo_forbid_archived = %s' % self.val
637 639
638 640 phash = text
639 641
640 642 def __call__(self, info, request):
641 643 _ = request.translate
642 644 rhodecode_db_repo = request.db_repo
643 645
644 646 log.debug(
645 647 '%s checking if archived flag for repo for %s',
646 648 self.__class__.__name__, rhodecode_db_repo.repo_name)
647 649
648 650 if rhodecode_db_repo.archived:
649 651 log.warning('Current view is not supported for archived repo:%s',
650 652 rhodecode_db_repo.repo_name)
651 653
652 654 h.flash(
653 655 h.literal(_('Action not supported for archived repository.')),
654 656 category='warning')
655 657 summary_url = request.route_path(
656 658 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
657 659 raise HTTPFound(summary_url)
658 660 return True
659 661
660 662
661 663 class RepoTypeRoutePredicate(object):
662 664 def __init__(self, val, config):
663 665 self.val = val or ['hg', 'git', 'svn']
664 666
665 667 def text(self):
666 668 return 'repo_accepted_type = %s' % self.val
667 669
668 670 phash = text
669 671
670 672 def __call__(self, info, request):
671 673 if hasattr(request, 'vcs_call'):
672 674 # skip vcs calls
673 675 return
674 676
675 677 rhodecode_db_repo = request.db_repo
676 678
677 679 log.debug(
678 680 '%s checking repo type for %s in %s',
679 681 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
680 682
681 683 if rhodecode_db_repo.repo_type in self.val:
682 684 return True
683 685 else:
684 686 log.warning('Current view is not supported for repo type:%s',
685 687 rhodecode_db_repo.repo_type)
686 688 return False
687 689
688 690
689 691 class RepoGroupRoutePredicate(object):
690 692 def __init__(self, val, config):
691 693 self.val = val
692 694
693 695 def text(self):
694 696 return 'repo_group_route = %s' % self.val
695 697
696 698 phash = text
697 699
698 700 def __call__(self, info, request):
699 701 if hasattr(request, 'vcs_call'):
700 702 # skip vcs calls
701 703 return
702 704
703 705 repo_group_name = info['match']['repo_group_name']
704 706 repo_group_model = repo_group.RepoGroupModel()
705 707 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
706 708
707 709 if by_name_match:
708 710 # register this as request object we can re-use later
709 711 request.db_repo_group = by_name_match
710 712 return True
711 713
712 714 return False
713 715
714 716
715 717 class UserGroupRoutePredicate(object):
716 718 def __init__(self, val, config):
717 719 self.val = val
718 720
719 721 def text(self):
720 722 return 'user_group_route = %s' % self.val
721 723
722 724 phash = text
723 725
724 726 def __call__(self, info, request):
725 727 if hasattr(request, 'vcs_call'):
726 728 # skip vcs calls
727 729 return
728 730
729 731 user_group_id = info['match']['user_group_id']
730 732 user_group_model = user_group.UserGroup()
731 733 by_id_match = user_group_model.get(user_group_id, cache=False)
732 734
733 735 if by_id_match:
734 736 # register this as request object we can re-use later
735 737 request.db_user_group = by_id_match
736 738 return True
737 739
738 740 return False
739 741
740 742
741 743 class UserRoutePredicateBase(object):
742 744 supports_default = None
743 745
744 746 def __init__(self, val, config):
745 747 self.val = val
746 748
747 749 def text(self):
748 750 raise NotImplementedError()
749 751
750 752 def __call__(self, info, request):
751 753 if hasattr(request, 'vcs_call'):
752 754 # skip vcs calls
753 755 return
754 756
755 757 user_id = info['match']['user_id']
756 758 user_model = user.User()
757 759 by_id_match = user_model.get(user_id, cache=False)
758 760
759 761 if by_id_match:
760 762 # register this as request object we can re-use later
761 763 request.db_user = by_id_match
762 764 request.db_user_supports_default = self.supports_default
763 765 return True
764 766
765 767 return False
766 768
767 769
768 770 class UserRoutePredicate(UserRoutePredicateBase):
769 771 supports_default = False
770 772
771 773 def text(self):
772 774 return 'user_route = %s' % self.val
773 775
774 776 phash = text
775 777
776 778
777 779 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
778 780 supports_default = True
779 781
780 782 def text(self):
781 783 return 'user_with_default_route = %s' % self.val
782 784
783 785 phash = text
784 786
785 787
786 788 def includeme(config):
787 789 config.add_route_predicate(
788 790 'repo_route', RepoRoutePredicate)
789 791 config.add_route_predicate(
790 792 'repo_accepted_types', RepoTypeRoutePredicate)
791 793 config.add_route_predicate(
792 794 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
793 795 config.add_route_predicate(
794 796 'repo_group_route', RepoGroupRoutePredicate)
795 797 config.add_route_predicate(
796 798 'user_group_route', UserGroupRoutePredicate)
797 799 config.add_route_predicate(
798 800 'user_route_with_default', UserRouteWithDefaultPredicate)
799 801 config.add_route_predicate(
800 802 'user_route', UserRoutePredicate)
@@ -1,464 +1,465 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 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 import deform
22 22 import logging
23 23 import peppercorn
24 24 import webhelpers.paginate
25 25
26 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
27 27
28 28 from rhodecode.integrations import integration_type_registry
29 29 from rhodecode.apps._base import BaseAppView
30 30 from rhodecode.apps._base.navigation import navigation_list
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, CSRFRequired, HasPermissionAnyDecorator,
33 33 HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
34 34 from rhodecode.lib.utils2 import safe_int
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.integration import IntegrationModel
39 39 from rhodecode.model.validation_schema.schemas.integration_schema import (
40 40 make_integration_schema, IntegrationScopeType)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class IntegrationSettingsViewBase(BaseAppView):
46 46 """
47 47 Base Integration settings view used by both repo / global settings
48 48 """
49 49
50 50 def __init__(self, context, request):
51 51 super(IntegrationSettingsViewBase, self).__init__(context, request)
52 52 self._load_view_context()
53 53
54 54 def _load_view_context(self):
55 55 """
56 56 This avoids boilerplate for repo/global+list/edit+views/templates
57 57 by doing all possible contexts at the same time however it should
58 58 be split up into separate functions once more "contexts" exist
59 59 """
60 60
61 61 self.IntegrationType = None
62 62 self.repo = None
63 63 self.repo_group = None
64 64 self.integration = None
65 65 self.integrations = {}
66 66
67 67 request = self.request
68 68
69 69 if 'repo_name' in request.matchdict: # in repo settings context
70 70 repo_name = request.matchdict['repo_name']
71 71 self.repo = Repository.get_by_repo_name(repo_name)
72 72
73 73 if 'repo_group_name' in request.matchdict: # in group settings context
74 74 repo_group_name = request.matchdict['repo_group_name']
75 75 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
76 76
77 77 if 'integration' in request.matchdict: # integration type context
78 78 integration_type = request.matchdict['integration']
79 79 if integration_type not in integration_type_registry:
80 80 raise HTTPNotFound()
81 81
82 82 self.IntegrationType = integration_type_registry[integration_type]
83 83 if self.IntegrationType.is_dummy:
84 84 raise HTTPNotFound()
85 85
86 86 if 'integration_id' in request.matchdict: # single integration context
87 87 integration_id = request.matchdict['integration_id']
88 88 self.integration = Integration.get(integration_id)
89 89
90 90 # extra perms check just in case
91 91 if not self._has_perms_for_integration(self.integration):
92 92 raise HTTPForbidden()
93 93
94 94 self.settings = self.integration and self.integration.settings or {}
95 95 self.admin_view = not (self.repo or self.repo_group)
96 96
97 97 def _has_perms_for_integration(self, integration):
98 98 perms = self.request.user.permissions
99 99
100 100 if 'hg.admin' in perms['global']:
101 101 return True
102 102
103 103 if integration.repo:
104 104 return perms['repositories'].get(
105 105 integration.repo.repo_name) == 'repository.admin'
106 106
107 107 if integration.repo_group:
108 108 return perms['repositories_groups'].get(
109 109 integration.repo_group.group_name) == 'group.admin'
110 110
111 111 return False
112 112
113 113 def _get_local_tmpl_context(self, include_app_defaults=True):
114 114 _ = self.request.translate
115 115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
116 116 include_app_defaults=include_app_defaults)
117 117 c.active = 'integrations'
118 118
119 119 return c
120 120
121 121 def _form_schema(self):
122 122 schema = make_integration_schema(IntegrationType=self.IntegrationType,
123 123 settings=self.settings)
124 124
125 125 # returns a clone, important if mutating the schema later
126 126 return schema.bind(
127 127 permissions=self.request.user.permissions,
128 128 no_scope=not self.admin_view)
129 129
130 130 def _form_defaults(self):
131 131 _ = self.request.translate
132 132 defaults = {}
133 133
134 134 if self.integration:
135 135 defaults['settings'] = self.integration.settings or {}
136 136 defaults['options'] = {
137 137 'name': self.integration.name,
138 138 'enabled': self.integration.enabled,
139 139 'scope': {
140 140 'repo': self.integration.repo,
141 141 'repo_group': self.integration.repo_group,
142 142 'child_repos_only': self.integration.child_repos_only,
143 143 },
144 144 }
145 145 else:
146 146 if self.repo:
147 147 scope = _('{repo_name} repository').format(
148 148 repo_name=self.repo.repo_name)
149 149 elif self.repo_group:
150 150 scope = _('{repo_group_name} repo group').format(
151 151 repo_group_name=self.repo_group.group_name)
152 152 else:
153 153 scope = _('Global')
154 154
155 155 defaults['options'] = {
156 156 'enabled': True,
157 157 'name': _('{name} integration').format(
158 158 name=self.IntegrationType.display_name),
159 159 }
160 160 defaults['options']['scope'] = {
161 161 'repo': self.repo,
162 162 'repo_group': self.repo_group,
163 163 }
164 164
165 165 return defaults
166 166
167 167 def _delete_integration(self, integration):
168 168 _ = self.request.translate
169 169 Session().delete(integration)
170 170 Session().commit()
171 171 h.flash(
172 172 _('Integration {integration_name} deleted successfully.').format(
173 173 integration_name=integration.name),
174 174 category='success')
175 175
176 176 if self.repo:
177 177 redirect_to = self.request.route_path(
178 178 'repo_integrations_home', repo_name=self.repo.repo_name)
179 179 elif self.repo_group:
180 180 redirect_to = self.request.route_path(
181 181 'repo_group_integrations_home',
182 182 repo_group_name=self.repo_group.group_name)
183 183 else:
184 184 redirect_to = self.request.route_path('global_integrations_home')
185 185 raise HTTPFound(redirect_to)
186 186
187 187 def _integration_list(self):
188 188 """ List integrations """
189 189
190 190 c = self.load_default_context()
191 191 if self.repo:
192 192 scope = self.repo
193 193 elif self.repo_group:
194 194 scope = self.repo_group
195 195 else:
196 196 scope = 'all'
197 197
198 198 integrations = []
199 199
200 200 for IntType, integration in IntegrationModel().get_integrations(
201 201 scope=scope, IntegrationType=self.IntegrationType):
202 202
203 203 # extra permissions check *just in case*
204 204 if not self._has_perms_for_integration(integration):
205 205 continue
206 206
207 207 integrations.append((IntType, integration))
208 208
209 209 sort_arg = self.request.GET.get('sort', 'name:asc')
210 210 sort_dir = 'asc'
211 211 if ':' in sort_arg:
212 212 sort_field, sort_dir = sort_arg.split(':')
213 213 else:
214 214 sort_field = sort_arg, 'asc'
215 215
216 216 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
217 217
218 218 integrations.sort(
219 219 key=lambda x: getattr(x[1], sort_field),
220 220 reverse=(sort_dir == 'desc'))
221 221
222 222 page_url = webhelpers.paginate.PageURL(
223 223 self.request.path, self.request.GET)
224 224 page = safe_int(self.request.GET.get('page', 1), 1)
225 225
226 226 integrations = h.Page(
227 227 integrations, page=page, items_per_page=10, url=page_url)
228 228
229 229 c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc'
230 230
231 231 c.current_IntegrationType = self.IntegrationType
232 232 c.integrations_list = integrations
233 233 c.available_integrations = integration_type_registry
234 234
235 235 return self._get_template_context(c)
236 236
237 237 def _settings_get(self, defaults=None, form=None):
238 238 """
239 239 View that displays the integration settings as a form.
240 240 """
241 241 c = self.load_default_context()
242 242
243 243 defaults = defaults or self._form_defaults()
244 244 schema = self._form_schema()
245 245
246 246 if self.integration:
247 247 buttons = ('submit', 'delete')
248 248 else:
249 249 buttons = ('submit',)
250 250
251 251 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
252 252
253 253 c.form = form
254 254 c.current_IntegrationType = self.IntegrationType
255 255 c.integration = self.integration
256 256
257 257 return self._get_template_context(c)
258 258
259 259 def _settings_post(self):
260 260 """
261 261 View that validates and stores the integration settings.
262 262 """
263 263 _ = self.request.translate
264 264
265 265 controls = self.request.POST.items()
266 266 pstruct = peppercorn.parse(controls)
267 267
268 268 if self.integration and pstruct.get('delete'):
269 269 return self._delete_integration(self.integration)
270 270
271 271 schema = self._form_schema()
272 272
273 273 skip_settings_validation = False
274 274 if self.integration and 'enabled' not in pstruct.get('options', {}):
275 275 skip_settings_validation = True
276 276 schema['settings'].validator = None
277 277 for field in schema['settings'].children:
278 278 field.validator = None
279 279 field.missing = ''
280 280
281 281 if self.integration:
282 282 buttons = ('submit', 'delete')
283 283 else:
284 284 buttons = ('submit',)
285 285
286 286 form = deform.Form(schema, buttons=buttons)
287 287
288 288 if not self.admin_view:
289 289 # scope is read only field in these cases, and has to be added
290 290 options = pstruct.setdefault('options', {})
291 291 if 'scope' not in options:
292 292 options['scope'] = IntegrationScopeType().serialize(None, {
293 293 'repo': self.repo,
294 294 'repo_group': self.repo_group,
295 295 })
296 296
297 297 try:
298 298 valid_data = form.validate_pstruct(pstruct)
299 299 except deform.ValidationFailure as e:
300 300 h.flash(
301 301 _('Errors exist when saving integration settings. '
302 302 'Please check the form inputs.'),
303 303 category='error')
304 304 return self._settings_get(form=e)
305 305
306 306 if not self.integration:
307 307 self.integration = Integration()
308 308 self.integration.integration_type = self.IntegrationType.key
309 309 Session().add(self.integration)
310 310
311 311 scope = valid_data['options']['scope']
312 312
313 313 IntegrationModel().update_integration(self.integration,
314 314 name=valid_data['options']['name'],
315 315 enabled=valid_data['options']['enabled'],
316 316 settings=valid_data['settings'],
317 317 repo=scope['repo'],
318 318 repo_group=scope['repo_group'],
319 319 child_repos_only=scope['child_repos_only'],
320 320 )
321 321
322 322 self.integration.settings = valid_data['settings']
323 323 Session().commit()
324 324 # Display success message and redirect.
325 325 h.flash(
326 326 _('Integration {integration_name} updated successfully.').format(
327 327 integration_name=self.IntegrationType.display_name),
328 328 category='success')
329 329
330 330 # if integration scope changes, we must redirect to the right place
331 331 # keeping in mind if the original view was for /repo/ or /_admin/
332 332 admin_view = not (self.repo or self.repo_group)
333 333
334 334 if self.integration.repo and not admin_view:
335 335 redirect_to = self.request.route_path(
336 336 'repo_integrations_edit',
337 337 repo_name=self.integration.repo.repo_name,
338 338 integration=self.integration.integration_type,
339 339 integration_id=self.integration.integration_id)
340 340 elif self.integration.repo_group and not admin_view:
341 341 redirect_to = self.request.route_path(
342 342 'repo_group_integrations_edit',
343 343 repo_group_name=self.integration.repo_group.group_name,
344 344 integration=self.integration.integration_type,
345 345 integration_id=self.integration.integration_id)
346 346 else:
347 347 redirect_to = self.request.route_path(
348 348 'global_integrations_edit',
349 349 integration=self.integration.integration_type,
350 350 integration_id=self.integration.integration_id)
351 351
352 352 return HTTPFound(redirect_to)
353 353
354 354 def _new_integration(self):
355 355 c = self.load_default_context()
356 356 c.available_integrations = integration_type_registry
357 357 return self._get_template_context(c)
358 358
359 359 def load_default_context(self):
360 360 raise NotImplementedError()
361 361
362 362
363 363 class GlobalIntegrationsView(IntegrationSettingsViewBase):
364 364 def load_default_context(self):
365 365 c = self._get_local_tmpl_context()
366 366 c.repo = self.repo
367 367 c.repo_group = self.repo_group
368 368 c.navlist = navigation_list(self.request)
369 369
370 370 return c
371 371
372 372 @LoginRequired()
373 373 @HasPermissionAnyDecorator('hg.admin')
374 374 def integration_list(self):
375 375 return self._integration_list()
376 376
377 377 @LoginRequired()
378 378 @HasPermissionAnyDecorator('hg.admin')
379 379 def settings_get(self):
380 380 return self._settings_get()
381 381
382 382 @LoginRequired()
383 383 @HasPermissionAnyDecorator('hg.admin')
384 384 @CSRFRequired()
385 385 def settings_post(self):
386 386 return self._settings_post()
387 387
388 388 @LoginRequired()
389 389 @HasPermissionAnyDecorator('hg.admin')
390 390 def new_integration(self):
391 391 return self._new_integration()
392 392
393 393
394 394 class RepoIntegrationsView(IntegrationSettingsViewBase):
395 395 def load_default_context(self):
396 396 c = self._get_local_tmpl_context()
397 397
398 398 c.repo = self.repo
399 399 c.repo_group = self.repo_group
400 400
401 401 self.db_repo = self.repo
402 402 c.rhodecode_db_repo = self.repo
403 403 c.repo_name = self.db_repo.repo_name
404 404 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
405 c.repository_artifacts = ScmModel().get_artifacts(self.repo)
405 406 c.repository_is_user_following = ScmModel().is_following_repo(
406 407 c.repo_name, self._rhodecode_user.user_id)
407 408 c.has_origin_repo_read_perm = False
408 409 if self.db_repo.fork:
409 410 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
410 411 'repository.write', 'repository.read', 'repository.admin')(
411 412 self.db_repo.fork.repo_name, 'summary fork link')
412 413 return c
413 414
414 415 @LoginRequired()
415 416 @HasRepoPermissionAnyDecorator('repository.admin')
416 417 def integration_list(self):
417 418 return self._integration_list()
418 419
419 420 @LoginRequired()
420 421 @HasRepoPermissionAnyDecorator('repository.admin')
421 422 def settings_get(self):
422 423 return self._settings_get()
423 424
424 425 @LoginRequired()
425 426 @HasRepoPermissionAnyDecorator('repository.admin')
426 427 @CSRFRequired()
427 428 def settings_post(self):
428 429 return self._settings_post()
429 430
430 431 @LoginRequired()
431 432 @HasRepoPermissionAnyDecorator('repository.admin')
432 433 def new_integration(self):
433 434 return self._new_integration()
434 435
435 436
436 437 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
437 438 def load_default_context(self):
438 439 c = self._get_local_tmpl_context()
439 440 c.repo = self.repo
440 441 c.repo_group = self.repo_group
441 442 c.navlist = navigation_list(self.request)
442 443
443 444 return c
444 445
445 446 @LoginRequired()
446 447 @HasRepoGroupPermissionAnyDecorator('group.admin')
447 448 def integration_list(self):
448 449 return self._integration_list()
449 450
450 451 @LoginRequired()
451 452 @HasRepoGroupPermissionAnyDecorator('group.admin')
452 453 def settings_get(self):
453 454 return self._settings_get()
454 455
455 456 @LoginRequired()
456 457 @HasRepoGroupPermissionAnyDecorator('group.admin')
457 458 @CSRFRequired()
458 459 def settings_post(self):
459 460 return self._settings_post()
460 461
461 462 @LoginRequired()
462 463 @HasRepoGroupPermissionAnyDecorator('group.admin')
463 464 def new_integration(self):
464 465 return self._new_integration()
@@ -1,1014 +1,1021 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Scm model for RhodeCode
23 23 """
24 24
25 25 import os.path
26 26 import traceback
27 27 import logging
28 28 import cStringIO
29 29
30 30 from sqlalchemy import func
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 import rhodecode
34 34 from rhodecode.lib.vcs import get_backend
35 35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib import helpers as h, rc_cache
39 39 from rhodecode.lib.auth import (
40 40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
41 41 HasUserGroupPermissionAny)
42 42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
43 43 from rhodecode.lib import hooks_utils
44 44 from rhodecode.lib.utils import (
45 45 get_filesystem_repos, make_db_config)
46 46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
47 47 from rhodecode.lib.system_info import get_system_info
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.db import (
50 or_, false,
50 51 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 PullRequest)
52 PullRequest, FileStore)
52 53 from rhodecode.model.settings import VcsSettingsModel
53 54 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 55
55 56 log = logging.getLogger(__name__)
56 57
57 58
58 59 class UserTemp(object):
59 60 def __init__(self, user_id):
60 61 self.user_id = user_id
61 62
62 63 def __repr__(self):
63 64 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 65
65 66
66 67 class RepoTemp(object):
67 68 def __init__(self, repo_id):
68 69 self.repo_id = repo_id
69 70
70 71 def __repr__(self):
71 72 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 73
73 74
74 75 class SimpleCachedRepoList(object):
75 76 """
76 77 Lighter version of of iteration of repos without the scm initialisation,
77 78 and with cache usage
78 79 """
79 80 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 81 self.db_repo_list = db_repo_list
81 82 self.repos_path = repos_path
82 83 self.order_by = order_by
83 84 self.reversed = (order_by or '').startswith('-')
84 85 if not perm_set:
85 86 perm_set = ['repository.read', 'repository.write',
86 87 'repository.admin']
87 88 self.perm_set = perm_set
88 89
89 90 def __len__(self):
90 91 return len(self.db_repo_list)
91 92
92 93 def __repr__(self):
93 94 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
94 95
95 96 def __iter__(self):
96 97 for dbr in self.db_repo_list:
97 98 # check permission at this level
98 99 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 100 dbr.repo_name, 'SimpleCachedRepoList check')
100 101 if not has_perm:
101 102 continue
102 103
103 104 tmp_d = {
104 105 'name': dbr.repo_name,
105 106 'dbrepo': dbr.get_dict(),
106 107 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 108 }
108 109 yield tmp_d
109 110
110 111
111 112 class _PermCheckIterator(object):
112 113
113 114 def __init__(
114 115 self, obj_list, obj_attr, perm_set, perm_checker,
115 116 extra_kwargs=None):
116 117 """
117 118 Creates iterator from given list of objects, additionally
118 119 checking permission for them from perm_set var
119 120
120 121 :param obj_list: list of db objects
121 122 :param obj_attr: attribute of object to pass into perm_checker
122 123 :param perm_set: list of permissions to check
123 124 :param perm_checker: callable to check permissions against
124 125 """
125 126 self.obj_list = obj_list
126 127 self.obj_attr = obj_attr
127 128 self.perm_set = perm_set
128 129 self.perm_checker = perm_checker
129 130 self.extra_kwargs = extra_kwargs or {}
130 131
131 132 def __len__(self):
132 133 return len(self.obj_list)
133 134
134 135 def __repr__(self):
135 136 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136 137
137 138 def __iter__(self):
138 139 checker = self.perm_checker(*self.perm_set)
139 140 for db_obj in self.obj_list:
140 141 # check permission at this level
141 142 name = getattr(db_obj, self.obj_attr, None)
142 143 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
143 144 continue
144 145
145 146 yield db_obj
146 147
147 148
148 149 class RepoList(_PermCheckIterator):
149 150
150 151 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 152 if not perm_set:
152 153 perm_set = [
153 154 'repository.read', 'repository.write', 'repository.admin']
154 155
155 156 super(RepoList, self).__init__(
156 157 obj_list=db_repo_list,
157 158 obj_attr='repo_name', perm_set=perm_set,
158 159 perm_checker=HasRepoPermissionAny,
159 160 extra_kwargs=extra_kwargs)
160 161
161 162
162 163 class RepoGroupList(_PermCheckIterator):
163 164
164 165 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
165 166 if not perm_set:
166 167 perm_set = ['group.read', 'group.write', 'group.admin']
167 168
168 169 super(RepoGroupList, self).__init__(
169 170 obj_list=db_repo_group_list,
170 171 obj_attr='group_name', perm_set=perm_set,
171 172 perm_checker=HasRepoGroupPermissionAny,
172 173 extra_kwargs=extra_kwargs)
173 174
174 175
175 176 class UserGroupList(_PermCheckIterator):
176 177
177 178 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
178 179 if not perm_set:
179 180 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
180 181
181 182 super(UserGroupList, self).__init__(
182 183 obj_list=db_user_group_list,
183 184 obj_attr='users_group_name', perm_set=perm_set,
184 185 perm_checker=HasUserGroupPermissionAny,
185 186 extra_kwargs=extra_kwargs)
186 187
187 188
188 189 class ScmModel(BaseModel):
189 190 """
190 191 Generic Scm Model
191 192 """
192 193
193 194 @LazyProperty
194 195 def repos_path(self):
195 196 """
196 197 Gets the repositories root path from database
197 198 """
198 199
199 200 settings_model = VcsSettingsModel(sa=self.sa)
200 201 return settings_model.get_repos_location()
201 202
202 203 def repo_scan(self, repos_path=None):
203 204 """
204 205 Listing of repositories in given path. This path should not be a
205 206 repository itself. Return a dictionary of repository objects
206 207
207 208 :param repos_path: path to directory containing repositories
208 209 """
209 210
210 211 if repos_path is None:
211 212 repos_path = self.repos_path
212 213
213 214 log.info('scanning for repositories in %s', repos_path)
214 215
215 216 config = make_db_config()
216 217 config.set('extensions', 'largefiles', '')
217 218 repos = {}
218 219
219 220 for name, path in get_filesystem_repos(repos_path, recursive=True):
220 221 # name need to be decomposed and put back together using the /
221 222 # since this is internal storage separator for rhodecode
222 223 name = Repository.normalize_repo_name(name)
223 224
224 225 try:
225 226 if name in repos:
226 227 raise RepositoryError('Duplicate repository name %s '
227 228 'found in %s' % (name, path))
228 229 elif path[0] in rhodecode.BACKENDS:
229 230 backend = get_backend(path[0])
230 231 repos[name] = backend(path[1], config=config,
231 232 with_wire={"cache": False})
232 233 except OSError:
233 234 continue
234 235 log.debug('found %s paths with repositories', len(repos))
235 236 return repos
236 237
237 238 def get_repos(self, all_repos=None, sort_key=None):
238 239 """
239 240 Get all repositories from db and for each repo create it's
240 241 backend instance and fill that backed with information from database
241 242
242 243 :param all_repos: list of repository names as strings
243 244 give specific repositories list, good for filtering
244 245
245 246 :param sort_key: initial sorting of repositories
246 247 """
247 248 if all_repos is None:
248 249 all_repos = self.sa.query(Repository)\
249 250 .filter(Repository.group_id == None)\
250 251 .order_by(func.lower(Repository.repo_name)).all()
251 252 repo_iter = SimpleCachedRepoList(
252 253 all_repos, repos_path=self.repos_path, order_by=sort_key)
253 254 return repo_iter
254 255
255 256 def get_repo_groups(self, all_groups=None):
256 257 if all_groups is None:
257 258 all_groups = RepoGroup.query()\
258 259 .filter(RepoGroup.group_parent_id == None).all()
259 260 return [x for x in RepoGroupList(all_groups)]
260 261
261 262 def mark_for_invalidation(self, repo_name, delete=False):
262 263 """
263 264 Mark caches of this repo invalid in the database. `delete` flag
264 265 removes the cache entries
265 266
266 267 :param repo_name: the repo_name for which caches should be marked
267 268 invalid, or deleted
268 269 :param delete: delete the entry keys instead of setting bool
269 270 flag on them, and also purge caches used by the dogpile
270 271 """
271 272 repo = Repository.get_by_repo_name(repo_name)
272 273
273 274 if repo:
274 275 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
275 276 repo_id=repo.repo_id)
276 277 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
277 278
278 279 repo_id = repo.repo_id
279 280 config = repo._config
280 281 config.set('extensions', 'largefiles', '')
281 282 repo.update_commit_cache(config=config, cs_cache=None)
282 283 if delete:
283 284 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
284 285 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
285 286
286 287 def toggle_following_repo(self, follow_repo_id, user_id):
287 288
288 289 f = self.sa.query(UserFollowing)\
289 290 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
290 291 .filter(UserFollowing.user_id == user_id).scalar()
291 292
292 293 if f is not None:
293 294 try:
294 295 self.sa.delete(f)
295 296 return
296 297 except Exception:
297 298 log.error(traceback.format_exc())
298 299 raise
299 300
300 301 try:
301 302 f = UserFollowing()
302 303 f.user_id = user_id
303 304 f.follows_repo_id = follow_repo_id
304 305 self.sa.add(f)
305 306 except Exception:
306 307 log.error(traceback.format_exc())
307 308 raise
308 309
309 310 def toggle_following_user(self, follow_user_id, user_id):
310 311 f = self.sa.query(UserFollowing)\
311 312 .filter(UserFollowing.follows_user_id == follow_user_id)\
312 313 .filter(UserFollowing.user_id == user_id).scalar()
313 314
314 315 if f is not None:
315 316 try:
316 317 self.sa.delete(f)
317 318 return
318 319 except Exception:
319 320 log.error(traceback.format_exc())
320 321 raise
321 322
322 323 try:
323 324 f = UserFollowing()
324 325 f.user_id = user_id
325 326 f.follows_user_id = follow_user_id
326 327 self.sa.add(f)
327 328 except Exception:
328 329 log.error(traceback.format_exc())
329 330 raise
330 331
331 332 def is_following_repo(self, repo_name, user_id, cache=False):
332 333 r = self.sa.query(Repository)\
333 334 .filter(Repository.repo_name == repo_name).scalar()
334 335
335 336 f = self.sa.query(UserFollowing)\
336 337 .filter(UserFollowing.follows_repository == r)\
337 338 .filter(UserFollowing.user_id == user_id).scalar()
338 339
339 340 return f is not None
340 341
341 342 def is_following_user(self, username, user_id, cache=False):
342 343 u = User.get_by_username(username)
343 344
344 345 f = self.sa.query(UserFollowing)\
345 346 .filter(UserFollowing.follows_user == u)\
346 347 .filter(UserFollowing.user_id == user_id).scalar()
347 348
348 349 return f is not None
349 350
350 351 def get_followers(self, repo):
351 352 repo = self._get_repo(repo)
352 353
353 354 return self.sa.query(UserFollowing)\
354 355 .filter(UserFollowing.follows_repository == repo).count()
355 356
356 357 def get_forks(self, repo):
357 358 repo = self._get_repo(repo)
358 359 return self.sa.query(Repository)\
359 360 .filter(Repository.fork == repo).count()
360 361
361 362 def get_pull_requests(self, repo):
362 363 repo = self._get_repo(repo)
363 364 return self.sa.query(PullRequest)\
364 365 .filter(PullRequest.target_repo == repo)\
365 366 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
366 367
368 def get_artifacts(self, repo):
369 repo = self._get_repo(repo)
370 return self.sa.query(FileStore)\
371 .filter(FileStore.repo == repo)\
372 .filter(or_(FileStore.hidden == None, FileStore.hidden == false())).count()
373
367 374 def mark_as_fork(self, repo, fork, user):
368 375 repo = self._get_repo(repo)
369 376 fork = self._get_repo(fork)
370 377 if fork and repo.repo_id == fork.repo_id:
371 378 raise Exception("Cannot set repository as fork of itself")
372 379
373 380 if fork and repo.repo_type != fork.repo_type:
374 381 raise RepositoryError(
375 382 "Cannot set repository as fork of repository with other type")
376 383
377 384 repo.fork = fork
378 385 self.sa.add(repo)
379 386 return repo
380 387
381 388 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
382 389 dbrepo = self._get_repo(repo)
383 390 remote_uri = remote_uri or dbrepo.clone_uri
384 391 if not remote_uri:
385 392 raise Exception("This repository doesn't have a clone uri")
386 393
387 394 repo = dbrepo.scm_instance(cache=False)
388 395 repo.config.clear_section('hooks')
389 396
390 397 try:
391 398 # NOTE(marcink): add extra validation so we skip invalid urls
392 399 # this is due this tasks can be executed via scheduler without
393 400 # proper validation of remote_uri
394 401 if validate_uri:
395 402 config = make_db_config(clear_session=False)
396 403 url_validator(remote_uri, dbrepo.repo_type, config)
397 404 except InvalidCloneUrl:
398 405 raise
399 406
400 407 repo_name = dbrepo.repo_name
401 408 try:
402 409 # TODO: we need to make sure those operations call proper hooks !
403 410 repo.fetch(remote_uri)
404 411
405 412 self.mark_for_invalidation(repo_name)
406 413 except Exception:
407 414 log.error(traceback.format_exc())
408 415 raise
409 416
410 417 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
411 418 dbrepo = self._get_repo(repo)
412 419 remote_uri = remote_uri or dbrepo.push_uri
413 420 if not remote_uri:
414 421 raise Exception("This repository doesn't have a clone uri")
415 422
416 423 repo = dbrepo.scm_instance(cache=False)
417 424 repo.config.clear_section('hooks')
418 425
419 426 try:
420 427 # NOTE(marcink): add extra validation so we skip invalid urls
421 428 # this is due this tasks can be executed via scheduler without
422 429 # proper validation of remote_uri
423 430 if validate_uri:
424 431 config = make_db_config(clear_session=False)
425 432 url_validator(remote_uri, dbrepo.repo_type, config)
426 433 except InvalidCloneUrl:
427 434 raise
428 435
429 436 try:
430 437 repo.push(remote_uri)
431 438 except Exception:
432 439 log.error(traceback.format_exc())
433 440 raise
434 441
435 442 def commit_change(self, repo, repo_name, commit, user, author, message,
436 443 content, f_path):
437 444 """
438 445 Commits changes
439 446
440 447 :param repo: SCM instance
441 448
442 449 """
443 450 user = self._get_user(user)
444 451
445 452 # decoding here will force that we have proper encoded values
446 453 # in any other case this will throw exceptions and deny commit
447 454 content = safe_str(content)
448 455 path = safe_str(f_path)
449 456 # message and author needs to be unicode
450 457 # proper backend should then translate that into required type
451 458 message = safe_unicode(message)
452 459 author = safe_unicode(author)
453 460 imc = repo.in_memory_commit
454 461 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
455 462 try:
456 463 # TODO: handle pre-push action !
457 464 tip = imc.commit(
458 465 message=message, author=author, parents=[commit],
459 466 branch=commit.branch)
460 467 except Exception as e:
461 468 log.error(traceback.format_exc())
462 469 raise IMCCommitError(str(e))
463 470 finally:
464 471 # always clear caches, if commit fails we want fresh object also
465 472 self.mark_for_invalidation(repo_name)
466 473
467 474 # We trigger the post-push action
468 475 hooks_utils.trigger_post_push_hook(
469 476 username=user.username, action='push_local', hook_type='post_push',
470 477 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
471 478 return tip
472 479
473 480 def _sanitize_path(self, f_path):
474 481 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
475 482 raise NonRelativePathError('%s is not an relative path' % f_path)
476 483 if f_path:
477 484 f_path = os.path.normpath(f_path)
478 485 return f_path
479 486
480 487 def get_dirnode_metadata(self, request, commit, dir_node):
481 488 if not dir_node.is_dir():
482 489 return []
483 490
484 491 data = []
485 492 for node in dir_node:
486 493 if not node.is_file():
487 494 # we skip file-nodes
488 495 continue
489 496
490 497 last_commit = node.last_commit
491 498 last_commit_date = last_commit.date
492 499 data.append({
493 500 'name': node.name,
494 501 'size': h.format_byte_size_binary(node.size),
495 502 'modified_at': h.format_date(last_commit_date),
496 503 'modified_ts': last_commit_date.isoformat(),
497 504 'revision': last_commit.revision,
498 505 'short_id': last_commit.short_id,
499 506 'message': h.escape(last_commit.message),
500 507 'author': h.escape(last_commit.author),
501 508 'user_profile': h.gravatar_with_user(
502 509 request, last_commit.author),
503 510 })
504 511
505 512 return data
506 513
507 514 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
508 515 extended_info=False, content=False, max_file_bytes=None):
509 516 """
510 517 recursive walk in root dir and return a set of all path in that dir
511 518 based on repository walk function
512 519
513 520 :param repo_name: name of repository
514 521 :param commit_id: commit id for which to list nodes
515 522 :param root_path: root path to list
516 523 :param flat: return as a list, if False returns a dict with description
517 524 :param extended_info: show additional info such as md5, binary, size etc
518 525 :param content: add nodes content to the return data
519 526 :param max_file_bytes: will not return file contents over this limit
520 527
521 528 """
522 529 _files = list()
523 530 _dirs = list()
524 531 try:
525 532 _repo = self._get_repo(repo_name)
526 533 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
527 534 root_path = root_path.lstrip('/')
528 535 for __, dirs, files in commit.walk(root_path):
529 536
530 537 for f in files:
531 538 _content = None
532 539 _data = f_name = f.unicode_path
533 540
534 541 if not flat:
535 542 _data = {
536 543 "name": h.escape(f_name),
537 544 "type": "file",
538 545 }
539 546 if extended_info:
540 547 _data.update({
541 548 "md5": f.md5,
542 549 "binary": f.is_binary,
543 550 "size": f.size,
544 551 "extension": f.extension,
545 552 "mimetype": f.mimetype,
546 553 "lines": f.lines()[0]
547 554 })
548 555
549 556 if content:
550 557 over_size_limit = (max_file_bytes is not None
551 558 and f.size > max_file_bytes)
552 559 full_content = None
553 560 if not f.is_binary and not over_size_limit:
554 561 full_content = safe_str(f.content)
555 562
556 563 _data.update({
557 564 "content": full_content,
558 565 })
559 566 _files.append(_data)
560 567
561 568 for d in dirs:
562 569 _data = d_name = d.unicode_path
563 570 if not flat:
564 571 _data = {
565 572 "name": h.escape(d_name),
566 573 "type": "dir",
567 574 }
568 575 if extended_info:
569 576 _data.update({
570 577 "md5": None,
571 578 "binary": None,
572 579 "size": None,
573 580 "extension": None,
574 581 })
575 582 if content:
576 583 _data.update({
577 584 "content": None
578 585 })
579 586 _dirs.append(_data)
580 587 except RepositoryError:
581 588 log.exception("Exception in get_nodes")
582 589 raise
583 590
584 591 return _dirs, _files
585 592
586 593 def get_quick_filter_nodes(self, repo_name, commit_id, root_path='/'):
587 594 """
588 595 Generate files for quick filter in files view
589 596 """
590 597
591 598 _files = list()
592 599 _dirs = list()
593 600 try:
594 601 _repo = self._get_repo(repo_name)
595 602 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
596 603 root_path = root_path.lstrip('/')
597 604 for __, dirs, files in commit.walk(root_path):
598 605
599 606 for f in files:
600 607
601 608 _data = {
602 609 "name": h.escape(f.unicode_path),
603 610 "type": "file",
604 611 }
605 612
606 613 _files.append(_data)
607 614
608 615 for d in dirs:
609 616
610 617 _data = {
611 618 "name": h.escape(d.unicode_path),
612 619 "type": "dir",
613 620 }
614 621
615 622 _dirs.append(_data)
616 623 except RepositoryError:
617 624 log.exception("Exception in get_quick_filter_nodes")
618 625 raise
619 626
620 627 return _dirs, _files
621 628
622 629 def get_node(self, repo_name, commit_id, file_path,
623 630 extended_info=False, content=False, max_file_bytes=None, cache=True):
624 631 """
625 632 retrieve single node from commit
626 633 """
627 634 try:
628 635
629 636 _repo = self._get_repo(repo_name)
630 637 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
631 638
632 639 file_node = commit.get_node(file_path)
633 640 if file_node.is_dir():
634 641 raise RepositoryError('The given path is a directory')
635 642
636 643 _content = None
637 644 f_name = file_node.unicode_path
638 645
639 646 file_data = {
640 647 "name": h.escape(f_name),
641 648 "type": "file",
642 649 }
643 650
644 651 if extended_info:
645 652 file_data.update({
646 653 "extension": file_node.extension,
647 654 "mimetype": file_node.mimetype,
648 655 })
649 656
650 657 if cache:
651 658 md5 = file_node.md5
652 659 is_binary = file_node.is_binary
653 660 size = file_node.size
654 661 else:
655 662 is_binary, md5, size, _content = file_node.metadata_uncached()
656 663
657 664 file_data.update({
658 665 "md5": md5,
659 666 "binary": is_binary,
660 667 "size": size,
661 668 })
662 669
663 670 if content and cache:
664 671 # get content + cache
665 672 size = file_node.size
666 673 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
667 674 full_content = None
668 675 all_lines = 0
669 676 if not file_node.is_binary and not over_size_limit:
670 677 full_content = safe_unicode(file_node.content)
671 678 all_lines, empty_lines = file_node.count_lines(full_content)
672 679
673 680 file_data.update({
674 681 "content": full_content,
675 682 "lines": all_lines
676 683 })
677 684 elif content:
678 685 # get content *without* cache
679 686 if _content is None:
680 687 is_binary, md5, size, _content = file_node.metadata_uncached()
681 688
682 689 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
683 690 full_content = None
684 691 all_lines = 0
685 692 if not is_binary and not over_size_limit:
686 693 full_content = safe_unicode(_content)
687 694 all_lines, empty_lines = file_node.count_lines(full_content)
688 695
689 696 file_data.update({
690 697 "content": full_content,
691 698 "lines": all_lines
692 699 })
693 700
694 701 except RepositoryError:
695 702 log.exception("Exception in get_node")
696 703 raise
697 704
698 705 return file_data
699 706
700 707 def get_fts_data(self, repo_name, commit_id, root_path='/'):
701 708 """
702 709 Fetch node tree for usage in full text search
703 710 """
704 711
705 712 tree_info = list()
706 713
707 714 try:
708 715 _repo = self._get_repo(repo_name)
709 716 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
710 717 root_path = root_path.lstrip('/')
711 718 for __, dirs, files in commit.walk(root_path):
712 719
713 720 for f in files:
714 721 is_binary, md5, size, _content = f.metadata_uncached()
715 722 _data = {
716 723 "name": f.unicode_path,
717 724 "md5": md5,
718 725 "extension": f.extension,
719 726 "binary": is_binary,
720 727 "size": size
721 728 }
722 729
723 730 tree_info.append(_data)
724 731
725 732 except RepositoryError:
726 733 log.exception("Exception in get_nodes")
727 734 raise
728 735
729 736 return tree_info
730 737
731 738 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
732 739 author=None, trigger_push_hook=True):
733 740 """
734 741 Commits given multiple nodes into repo
735 742
736 743 :param user: RhodeCode User object or user_id, the commiter
737 744 :param repo: RhodeCode Repository object
738 745 :param message: commit message
739 746 :param nodes: mapping {filename:{'content':content},...}
740 747 :param parent_commit: parent commit, can be empty than it's
741 748 initial commit
742 749 :param author: author of commit, cna be different that commiter
743 750 only for git
744 751 :param trigger_push_hook: trigger push hooks
745 752
746 753 :returns: new commited commit
747 754 """
748 755
749 756 user = self._get_user(user)
750 757 scm_instance = repo.scm_instance(cache=False)
751 758
752 759 processed_nodes = []
753 760 for f_path in nodes:
754 761 f_path = self._sanitize_path(f_path)
755 762 content = nodes[f_path]['content']
756 763 f_path = safe_str(f_path)
757 764 # decoding here will force that we have proper encoded values
758 765 # in any other case this will throw exceptions and deny commit
759 766 if isinstance(content, (basestring,)):
760 767 content = safe_str(content)
761 768 elif isinstance(content, (file, cStringIO.OutputType,)):
762 769 content = content.read()
763 770 else:
764 771 raise Exception('Content is of unrecognized type %s' % (
765 772 type(content)
766 773 ))
767 774 processed_nodes.append((f_path, content))
768 775
769 776 message = safe_unicode(message)
770 777 commiter = user.full_contact
771 778 author = safe_unicode(author) if author else commiter
772 779
773 780 imc = scm_instance.in_memory_commit
774 781
775 782 if not parent_commit:
776 783 parent_commit = EmptyCommit(alias=scm_instance.alias)
777 784
778 785 if isinstance(parent_commit, EmptyCommit):
779 786 # EmptyCommit means we we're editing empty repository
780 787 parents = None
781 788 else:
782 789 parents = [parent_commit]
783 790 # add multiple nodes
784 791 for path, content in processed_nodes:
785 792 imc.add(FileNode(path, content=content))
786 793 # TODO: handle pre push scenario
787 794 tip = imc.commit(message=message,
788 795 author=author,
789 796 parents=parents,
790 797 branch=parent_commit.branch)
791 798
792 799 self.mark_for_invalidation(repo.repo_name)
793 800 if trigger_push_hook:
794 801 hooks_utils.trigger_post_push_hook(
795 802 username=user.username, action='push_local',
796 803 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
797 804 hook_type='post_push',
798 805 commit_ids=[tip.raw_id])
799 806 return tip
800 807
801 808 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
802 809 author=None, trigger_push_hook=True):
803 810 user = self._get_user(user)
804 811 scm_instance = repo.scm_instance(cache=False)
805 812
806 813 message = safe_unicode(message)
807 814 commiter = user.full_contact
808 815 author = safe_unicode(author) if author else commiter
809 816
810 817 imc = scm_instance.in_memory_commit
811 818
812 819 if not parent_commit:
813 820 parent_commit = EmptyCommit(alias=scm_instance.alias)
814 821
815 822 if isinstance(parent_commit, EmptyCommit):
816 823 # EmptyCommit means we we're editing empty repository
817 824 parents = None
818 825 else:
819 826 parents = [parent_commit]
820 827
821 828 # add multiple nodes
822 829 for _filename, data in nodes.items():
823 830 # new filename, can be renamed from the old one, also sanitaze
824 831 # the path for any hack around relative paths like ../../ etc.
825 832 filename = self._sanitize_path(data['filename'])
826 833 old_filename = self._sanitize_path(_filename)
827 834 content = data['content']
828 835 file_mode = data.get('mode')
829 836 filenode = FileNode(old_filename, content=content, mode=file_mode)
830 837 op = data['op']
831 838 if op == 'add':
832 839 imc.add(filenode)
833 840 elif op == 'del':
834 841 imc.remove(filenode)
835 842 elif op == 'mod':
836 843 if filename != old_filename:
837 844 # TODO: handle renames more efficient, needs vcs lib changes
838 845 imc.remove(filenode)
839 846 imc.add(FileNode(filename, content=content, mode=file_mode))
840 847 else:
841 848 imc.change(filenode)
842 849
843 850 try:
844 851 # TODO: handle pre push scenario commit changes
845 852 tip = imc.commit(message=message,
846 853 author=author,
847 854 parents=parents,
848 855 branch=parent_commit.branch)
849 856 except NodeNotChangedError:
850 857 raise
851 858 except Exception as e:
852 859 log.exception("Unexpected exception during call to imc.commit")
853 860 raise IMCCommitError(str(e))
854 861 finally:
855 862 # always clear caches, if commit fails we want fresh object also
856 863 self.mark_for_invalidation(repo.repo_name)
857 864
858 865 if trigger_push_hook:
859 866 hooks_utils.trigger_post_push_hook(
860 867 username=user.username, action='push_local', hook_type='post_push',
861 868 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
862 869 commit_ids=[tip.raw_id])
863 870
864 871 return tip
865 872
866 873 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
867 874 author=None, trigger_push_hook=True):
868 875 """
869 876 Deletes given multiple nodes into `repo`
870 877
871 878 :param user: RhodeCode User object or user_id, the committer
872 879 :param repo: RhodeCode Repository object
873 880 :param message: commit message
874 881 :param nodes: mapping {filename:{'content':content},...}
875 882 :param parent_commit: parent commit, can be empty than it's initial
876 883 commit
877 884 :param author: author of commit, cna be different that commiter only
878 885 for git
879 886 :param trigger_push_hook: trigger push hooks
880 887
881 888 :returns: new commit after deletion
882 889 """
883 890
884 891 user = self._get_user(user)
885 892 scm_instance = repo.scm_instance(cache=False)
886 893
887 894 processed_nodes = []
888 895 for f_path in nodes:
889 896 f_path = self._sanitize_path(f_path)
890 897 # content can be empty but for compatabilty it allows same dicts
891 898 # structure as add_nodes
892 899 content = nodes[f_path].get('content')
893 900 processed_nodes.append((f_path, content))
894 901
895 902 message = safe_unicode(message)
896 903 commiter = user.full_contact
897 904 author = safe_unicode(author) if author else commiter
898 905
899 906 imc = scm_instance.in_memory_commit
900 907
901 908 if not parent_commit:
902 909 parent_commit = EmptyCommit(alias=scm_instance.alias)
903 910
904 911 if isinstance(parent_commit, EmptyCommit):
905 912 # EmptyCommit means we we're editing empty repository
906 913 parents = None
907 914 else:
908 915 parents = [parent_commit]
909 916 # add multiple nodes
910 917 for path, content in processed_nodes:
911 918 imc.remove(FileNode(path, content=content))
912 919
913 920 # TODO: handle pre push scenario
914 921 tip = imc.commit(message=message,
915 922 author=author,
916 923 parents=parents,
917 924 branch=parent_commit.branch)
918 925
919 926 self.mark_for_invalidation(repo.repo_name)
920 927 if trigger_push_hook:
921 928 hooks_utils.trigger_post_push_hook(
922 929 username=user.username, action='push_local', hook_type='post_push',
923 930 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
924 931 commit_ids=[tip.raw_id])
925 932 return tip
926 933
927 934 def strip(self, repo, commit_id, branch):
928 935 scm_instance = repo.scm_instance(cache=False)
929 936 scm_instance.config.clear_section('hooks')
930 937 scm_instance.strip(commit_id, branch)
931 938 self.mark_for_invalidation(repo.repo_name)
932 939
933 940 def get_unread_journal(self):
934 941 return self.sa.query(UserLog).count()
935 942
936 943 @classmethod
937 944 def backend_landing_ref(cls, repo_type):
938 945 """
939 946 Return a default landing ref based on a repository type.
940 947 """
941 948
942 949 landing_ref = {
943 950 'hg': ('branch:default', 'default'),
944 951 'git': ('branch:master', 'master'),
945 952 'svn': ('rev:tip', 'latest tip'),
946 953 'default': ('rev:tip', 'latest tip'),
947 954 }
948 955
949 956 return landing_ref.get(repo_type) or landing_ref['default']
950 957
951 958 def get_repo_landing_revs(self, translator, repo=None):
952 959 """
953 960 Generates select option with tags branches and bookmarks (for hg only)
954 961 grouped by type
955 962
956 963 :param repo:
957 964 """
958 965 _ = translator
959 966 repo = self._get_repo(repo)
960 967
961 968 if repo:
962 969 repo_type = repo.repo_type
963 970 else:
964 971 repo_type = 'default'
965 972
966 973 default_landing_ref, landing_ref_lbl = self.backend_landing_ref(repo_type)
967 974
968 975 default_ref_options = [
969 976 [default_landing_ref, landing_ref_lbl]
970 977 ]
971 978 default_choices = [
972 979 default_landing_ref
973 980 ]
974 981
975 982 if not repo:
976 983 return default_choices, default_ref_options
977 984
978 985 repo = repo.scm_instance()
979 986
980 987 ref_options = [('rev:tip', 'latest tip')]
981 988 choices = ['rev:tip']
982 989
983 990 # branches
984 991 branch_group = [(u'branch:%s' % safe_unicode(b), safe_unicode(b)) for b in repo.branches]
985 992 if not branch_group:
986 993 # new repo, or without maybe a branch?
987 994 branch_group = default_ref_options
988 995
989 996 branches_group = (branch_group, _("Branches"))
990 997 ref_options.append(branches_group)
991 998 choices.extend([x[0] for x in branches_group[0]])
992 999
993 1000 # bookmarks for HG
994 1001 if repo.alias == 'hg':
995 1002 bookmarks_group = (
996 1003 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
997 1004 for b in repo.bookmarks],
998 1005 _("Bookmarks"))
999 1006 ref_options.append(bookmarks_group)
1000 1007 choices.extend([x[0] for x in bookmarks_group[0]])
1001 1008
1002 1009 # tags
1003 1010 tags_group = (
1004 1011 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
1005 1012 for t in repo.tags],
1006 1013 _("Tags"))
1007 1014 ref_options.append(tags_group)
1008 1015 choices.extend([x[0] for x in tags_group[0]])
1009 1016
1010 1017 return choices, ref_options
1011 1018
1012 1019 def get_server_info(self, environ=None):
1013 1020 server_info = get_system_info(environ)
1014 1021 return server_info
@@ -1,806 +1,816 b''
1 1 // navigation.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // TOP MAIN DARK NAVIGATION
6 6
7 7 .header .main_nav.horizontal-list {
8 8 float: right;
9 9 color: @grey4;
10 10 > li {
11 11 a {
12 12 color: @grey4;
13 13 }
14 14 }
15 15 }
16 16
17 17 // HEADER NAVIGATION
18 18
19 19 .horizontal-list {
20 20 display: block;
21 21 margin: 0;
22 22 padding: 0;
23 23 -webkit-padding-start: 0;
24 24 text-align: left;
25 25 font-size: @navigation-fontsize;
26 26 color: @grey6;
27 27 z-index:10;
28 28
29 29 li {
30 30 line-height: 1em;
31 31 list-style-type: none;
32 32 margin: 0 20px 0 0;
33 33
34 34 a {
35 35 padding: 0 .5em;
36 36
37 37 &.menu_link_notifications {
38 38 .pill(7px,@rcblue);
39 39 display: inline;
40 40 margin: 0 7px 0 .7em;
41 41 font-size: @basefontsize;
42 42 color: white;
43 43
44 44 &.empty {
45 45 background-color: @grey4;
46 46 }
47 47
48 48 &:hover {
49 49 background-color: @rcdarkblue;
50 50 }
51 51 }
52 52 }
53 53 .pill_container {
54 54 margin: 1.25em 0px 0px 0px;
55 55 float: right;
56 56 }
57 57
58 58 &#quick_login_li {
59 59 &:hover {
60 60 color: @grey5;
61 61 }
62 62
63 63 a.menu_link_notifications {
64 64 color: white;
65 65 }
66 66
67 67 .user {
68 68 padding-bottom: 10px;
69 69 }
70 70 }
71 71
72 72 &:before { content: none; }
73 73
74 74 &:last-child {
75 75 .menulabel {
76 76 padding-right: 0;
77 77 border-right: none;
78 78
79 79 .show_more {
80 80 padding-right: 0;
81 81 }
82 82 }
83 83
84 84 &> a {
85 85 border-bottom: none;
86 86 }
87 87 }
88 88
89 89 &.open {
90 90
91 91 a {
92 92 color: white;
93 93 }
94 94 }
95 95
96 96 &:focus {
97 97 outline: none;
98 98 }
99 99
100 100 ul li {
101 101 display: block;
102 102
103 103 &:last-child> a {
104 104 border-bottom: none;
105 105 }
106 106
107 107 ul li:last-child a {
108 108 /* we don't expect more then 3 levels of submenu and the third
109 109 level can have different html structure */
110 110 border-bottom: none;
111 111 }
112 112 }
113 113 }
114 114
115 115 > li {
116 116 float: left;
117 117 display: block;
118 118 padding: 0;
119 119
120 120 > a,
121 121 &.has_select2 a {
122 122 display: block;
123 123 padding: 10px 0;
124 124 }
125 125
126 126 .menulabel {
127 127 line-height: 1em;
128 128 // for this specifically we do not use a variable
129 129 }
130 130
131 .menulink-counter {
132 border: 1px solid @grey2;
133 border-radius: @border-radius;
134 background: @grey7;
135 display: inline-block;
136 padding: 0px 4px;
137 text-align: center;
138 font-size: 12px;
139 }
140
131 141 .pr_notifications {
132 142 padding-left: .5em;
133 143 }
134 144
135 145 .pr_notifications + .menulabel {
136 146 display:inline;
137 147 padding-left: 0;
138 148 }
139 149
140 150 &:hover,
141 151 &.open,
142 152 &.active {
143 153 a {
144 154 color: @rcblue;
145 155 }
146 156 }
147 157 }
148 158
149 159 pre {
150 160 margin: 0;
151 161 padding: 0;
152 162 }
153 163
154 164 .select2-container,
155 165 .menulink.childs {
156 166 position: relative;
157 167 }
158 168
159 169 .menulink {
160 170 &.disabled {
161 171 color: @grey3;
162 172 cursor: default;
163 173 opacity: 0.5;
164 174 }
165 175 }
166 176
167 177 #quick_login {
168 178
169 179 li a {
170 180 padding: .5em 0;
171 181 border-bottom: none;
172 182 color: @grey2;
173 183
174 184 &:hover { color: @grey1; }
175 185 }
176 186 }
177 187
178 188 #quick_login_link {
179 189 display: inline-block;
180 190
181 191 .gravatar {
182 192 border: 1px solid @grey5;
183 193 }
184 194
185 195 .gravatar-login {
186 196 height: 20px;
187 197 width: 20px;
188 198 margin: -8px 0;
189 199 padding: 0;
190 200 }
191 201
192 202 &:hover .user {
193 203 color: @grey6;
194 204 }
195 205 }
196 206 }
197 207 .header .horizontal-list {
198 208
199 209 li {
200 210
201 211 &#quick_login_li {
202 212 padding-left: .5em;
203 213 margin-right: 0px;
204 214
205 215 &:hover #quick_login_link {
206 216 color: inherit;
207 217 }
208 218
209 219 .menu_link_user {
210 220 padding: 0 2px;
211 221 }
212 222 }
213 223 list-style-type: none;
214 224 }
215 225
216 226 > li {
217 227
218 228 a {
219 229 padding: 18px 0 12px 0;
220 230 color: @nav-grey;
221 231
222 232 &.menu_link_notifications {
223 233 padding: 1px 8px;
224 234 }
225 235 }
226 236
227 237 &:hover,
228 238 &.open,
229 239 &.active {
230 240 .pill_container a {
231 241 // don't select text for the pill container, it has it' own
232 242 // hover behaviour
233 243 color: @nav-grey;
234 244 }
235 245 }
236 246
237 247 &:hover,
238 248 &.open,
239 249 &.active {
240 250 a {
241 251 color: @grey6;
242 252 }
243 253 }
244 254
245 255 .select2-dropdown-open a {
246 256 color: @grey6;
247 257 }
248 258
249 259 .repo-switcher {
250 260 padding-left: 0;
251 261
252 262 .menulabel {
253 263 padding-left: 0;
254 264 }
255 265 }
256 266 }
257 267
258 268 li ul li {
259 269 background-color:@grey2;
260 270
261 271 a {
262 272 padding: .5em 0;
263 273 border-bottom: @border-thickness solid @border-default-color;
264 274 color: @grey6;
265 275 }
266 276
267 277 &:last-child a, &.last a{
268 278 border-bottom: none;
269 279 }
270 280
271 281 &:hover {
272 282 background-color: @grey3;
273 283 }
274 284 }
275 285
276 286 .submenu {
277 287 margin-top: 5px;
278 288 }
279 289 }
280 290
281 291 // SUBMENUS
282 292 .navigation .submenu {
283 293 display: none;
284 294 }
285 295
286 296 .navigation li.open {
287 297 .submenu {
288 298 display: block;
289 299 }
290 300 }
291 301
292 302 .navigation li:last-child .submenu {
293 303 right: auto;
294 304 left: 0;
295 305 border: 1px solid @grey5;
296 306 background: @white;
297 307 box-shadow: @dropdown-shadow;
298 308 }
299 309
300 310 .submenu {
301 311 position: absolute;
302 312 top: 100%;
303 313 left: 0;
304 314 min-width: 180px;
305 315 margin: 2px 0 0;
306 316 padding: 0;
307 317 text-align: left;
308 318 font-family: @text-light;
309 319 border-radius: @border-radius;
310 320 z-index: 20;
311 321
312 322 li {
313 323 display: block;
314 324 margin: 0;
315 325 padding: 0 .5em;
316 326 line-height: 1em;
317 327 color: @grey3;
318 328 background-color: @white;
319 329 list-style-type: none;
320 330
321 331 a {
322 332 display: block;
323 333 width: 100%;
324 334 padding: .5em 0;
325 335 border-right: none;
326 336 border-bottom: @border-thickness solid white;
327 337 color: @grey3;
328 338 }
329 339
330 340 ul {
331 341 display: none;
332 342 position: absolute;
333 343 top: 0;
334 344 right: 100%;
335 345 padding: 0;
336 346 z-index: 30;
337 347 }
338 348 &:hover {
339 349 background-color: @grey7;
340 350 -webkit-transition: background .3s;
341 351 -moz-transition: background .3s;
342 352 -o-transition: background .3s;
343 353 transition: background .3s;
344 354
345 355 ul {
346 356 display: block;
347 357 }
348 358 }
349 359 }
350 360
351 361 }
352 362
353 363
354 364
355 365
356 366 // repo dropdown
357 367 .quick_repo_menu {
358 368 width: 15px;
359 369 text-align: center;
360 370 position: relative;
361 371 cursor: pointer;
362 372
363 373 div {
364 374 overflow: visible !important;
365 375 }
366 376
367 377 &.sorting {
368 378 cursor: auto;
369 379 }
370 380
371 381 &:hover {
372 382 .menu_items_container {
373 383 position: absolute;
374 384 display: block;
375 385 }
376 386 .menu_items {
377 387 display: block;
378 388 }
379 389 }
380 390
381 391 i {
382 392 margin: 0;
383 393 color: @grey4;
384 394 }
385 395
386 396 .menu_items_container {
387 397 position: absolute;
388 398 top: 0;
389 399 left: 100%;
390 400 margin: 0;
391 401 padding: 0;
392 402 list-style: none;
393 403 background-color: @grey6;
394 404 z-index: 999;
395 405 text-align: left;
396 406
397 407 a {
398 408 color: @grey2;
399 409 }
400 410
401 411 ul.menu_items {
402 412 margin: 0;
403 413 padding: 0;
404 414 }
405 415
406 416 li {
407 417 margin: 0;
408 418 padding: 0;
409 419 line-height: 1em;
410 420 list-style-type: none;
411 421
412 422 a {
413 423 display: block;
414 424 height: 16px;
415 425 padding: 8px; //must add up to td height (28px)
416 426 width: 120px; // set width
417 427
418 428 &:hover {
419 429 background-color: @grey5;
420 430 -webkit-transition: background .3s;
421 431 -moz-transition: background .3s;
422 432 -o-transition: background .3s;
423 433 transition: background .3s;
424 434 }
425 435 }
426 436 }
427 437 }
428 438 }
429 439
430 440
431 441 // new objects main action
432 442 .action-menu {
433 443 left: auto;
434 444 right: 0;
435 445 padding: 12px;
436 446 z-index: 999;
437 447 overflow: hidden;
438 448 background-color: #fff;
439 449 border: 1px solid @grey5;
440 450 color: @grey2;
441 451 box-shadow: @dropdown-shadow;
442 452
443 453 .submenu-title {
444 454 font-weight: bold;
445 455 }
446 456
447 457 .submenu-title:not(:first-of-type) {
448 458 padding-top: 10px;
449 459 }
450 460
451 461 &.submenu {
452 462 min-width: 200px;
453 463
454 464 ol {
455 465 padding:0;
456 466 }
457 467
458 468 li {
459 469 display: block;
460 470 margin: 0;
461 471 padding: .2em .5em;
462 472 line-height: 1em;
463 473
464 474 background-color: #fff;
465 475 list-style-type: none;
466 476
467 477 a {
468 478 padding: 4px;
469 479 color: @grey4 !important;
470 480 border-bottom: none;
471 481 }
472 482 }
473 483 li:not(.submenu-title) a:hover{
474 484 color: @grey2 !important;
475 485 }
476 486 }
477 487 }
478 488
479 489
480 490 // Header Repository Switcher
481 491 // Select2 Dropdown
482 492 #select2-drop.select2-drop.repo-switcher-dropdown {
483 493 width: auto !important;
484 494 margin-top: 5px;
485 495 padding: 1em 0;
486 496 text-align: left;
487 497 .border-radius-bottom(@border-radius);
488 498 border-color: transparent;
489 499 color: @grey6;
490 500 background-color: @grey2;
491 501
492 502 input {
493 503 min-width: 90%;
494 504 }
495 505
496 506 ul.select2-result-sub {
497 507
498 508 li {
499 509 line-height: 1em;
500 510
501 511 &:hover,
502 512 &.select2-highlighted {
503 513 background-color: @grey3;
504 514 }
505 515 }
506 516
507 517 &:before { content: none; }
508 518 }
509 519
510 520 ul.select2-results {
511 521 min-width: 200px;
512 522 margin: 0;
513 523 padding: 0;
514 524 list-style-type: none;
515 525 overflow-x: visible;
516 526 overflow-y: scroll;
517 527
518 528 li {
519 529 padding: 0 8px;
520 530 line-height: 1em;
521 531 color: @grey6;
522 532
523 533 &>.select2-result-label {
524 534 padding: 8px 0;
525 535 border-bottom: @border-thickness solid @grey3;
526 536 white-space: nowrap;
527 537 color: @grey5;
528 538 cursor: pointer;
529 539 }
530 540
531 541 &.select2-result-with-children {
532 542 margin: 0;
533 543 padding: 0;
534 544 }
535 545
536 546 &.select2-result-unselectable > .select2-result-label {
537 547 margin: 0 8px;
538 548 }
539 549
540 550 }
541 551 }
542 552
543 553 ul.select2-result-sub {
544 554 margin: 0;
545 555 padding: 0;
546 556
547 557 li {
548 558 display: block;
549 559 margin: 0;
550 560 border-right: none;
551 561 line-height: 1em;
552 562 font-family: @text-light;
553 563 color: @grey2;
554 564 list-style-type: none;
555 565
556 566 &:hover {
557 567 background-color: @grey3;
558 568 }
559 569 }
560 570 }
561 571 }
562 572
563 573
564 574 #context-bar {
565 575 display: block;
566 576 margin: 0 auto 20px 0;
567 577 padding: 0 @header-padding;
568 578 background-color: @grey7;
569 579 border-bottom: 1px solid @grey5;
570 580
571 581 .clear {
572 582 clear: both;
573 583 }
574 584 }
575 585
576 586 ul#context-pages {
577 587 li {
578 588 list-style-type: none;
579 589
580 590 a {
581 591 color: @grey2;
582 592
583 593 &:hover {
584 594 color: @grey1;
585 595 }
586 596 }
587 597
588 598 &.active {
589 599 // special case, non-variable color
590 600 border-bottom: 2px solid @rcblue;
591 601
592 602 a {
593 603 color: @rcblue;
594 604 }
595 605 }
596 606 }
597 607 }
598 608
599 609 // PAGINATION
600 610
601 611 .pagination {
602 612 border: @border-thickness solid @grey5;
603 613 color: @grey2;
604 614 box-shadow: @button-shadow;
605 615
606 616 .current {
607 617 color: @grey4;
608 618 }
609 619 }
610 620
611 621 .dataTables_processing {
612 622 text-align: center;
613 623 font-size: 1.1em;
614 624 position: relative;
615 625 top: 95px;
616 626 }
617 627
618 628 .dataTables_paginate, .pagination-wh {
619 629 text-align: left;
620 630 display: inline-block;
621 631 border-left: 1px solid @grey5;
622 632 float: none;
623 633 overflow: hidden;
624 634 box-shadow: @button-shadow;
625 635
626 636 .paginate_button, .pager_curpage,
627 637 .pager_link, .pg-previous, .pg-next, .pager_dotdot {
628 638 display: inline-block;
629 639 padding: @menupadding/4 @menupadding;
630 640 border: 1px solid @grey5;
631 641 border-left: 0;
632 642 color: @grey2;
633 643 cursor: pointer;
634 644 float: left;
635 645
636 646 &:hover {
637 647 color: @rcdarkblue;
638 648 }
639 649 }
640 650
641 651 .paginate_button.disabled,
642 652 .disabled {
643 653 color: @grey3;
644 654 cursor: default;
645 655 opacity: 0.5;
646 656 }
647 657
648 658 .paginate_button.current, .pager_curpage {
649 659 background: @rcblue;
650 660 border-color: @rcblue;
651 661 color: @white;
652 662 }
653 663
654 664 .ellipsis {
655 665 display: inline-block;
656 666 text-align: left;
657 667 padding: @menupadding/4 @menupadding;
658 668 border: 1px solid @grey5;
659 669 border-left: 0;
660 670 float: left;
661 671 }
662 672 }
663 673
664 674 // SIDEBAR
665 675
666 676 .sidebar {
667 677 .block-left;
668 678 clear: left;
669 679 max-width: @sidebar-width;
670 680 margin-right: @sidebarpadding;
671 681 padding-right: @sidebarpadding;
672 682 font-family: @text-regular;
673 683 color: @grey1;
674 684
675 685 .nav-pills {
676 686 margin: 0;
677 687 }
678 688
679 689 .nav {
680 690 list-style: none;
681 691 padding: 0;
682 692
683 693 li {
684 694 padding-bottom: @menupadding;
685 695 line-height: 1em;
686 696 color: @grey4;
687 697 list-style-type: none;
688 698
689 699 &.active a {
690 700 color: @grey2;
691 701 }
692 702
693 703 a {
694 704 color: @grey4;
695 705 }
696 706 }
697 707
698 708 }
699 709 }
700 710
701 711 .main_filter_help_box {
702 712 padding: 7px 7px;
703 713 display: inline-block;
704 714 vertical-align: top;
705 715 background: inherit;
706 716 position: absolute;
707 717 right: 0;
708 718 top: 9px;
709 719 }
710 720
711 721 .main_filter_input_box {
712 722 display: inline-block;
713 723
714 724 .searchItems {
715 725 display:flex;
716 726 background: @black;
717 727 padding: 0px;
718 728 border-radius: 3px;
719 729 border: 1px solid @black;
720 730
721 731 a {
722 732 border: none !important;
723 733 }
724 734 }
725 735
726 736 .searchTag {
727 737 line-height: 28px;
728 738 padding: 0 5px;
729 739
730 740 .tag {
731 741 color: @grey5;
732 742 border-color: @grey2;
733 743 background: @grey1;
734 744 }
735 745 }
736 746
737 747 .searchTagFilter {
738 748 background-color: @black !important;
739 749 margin-right: 0;
740 750 }
741 751
742 752 .searchTagHelp {
743 753 background-color: @grey1 !important;
744 754 margin: 0;
745 755 }
746 756 .searchTagHelp:hover {
747 757 background-color: @grey1 !important;
748 758 }
749 759 .searchTagInput {
750 760 background-color: @grey1 !important;
751 761 margin-right: 0;
752 762 }
753 763 }
754 764
755 765 .main_filter_box {
756 766 margin: 9px 0 0 0;
757 767 }
758 768
759 769 #main_filter_help {
760 770 background: @grey1;
761 771 border: 1px solid black;
762 772 position: absolute;
763 773 white-space: pre;
764 774 z-index: 9999;
765 775 color: @nav-grey;
766 776 padding: 0 10px;
767 777 }
768 778
769 779 input {
770 780
771 781 &.main_filter_input {
772 782 padding: 5px 10px;
773 783 min-width: 340px;
774 784 color: @grey7;
775 785 background: @black;
776 786 min-height: 18px;
777 787 border: 0;
778 788
779 789 &:active {
780 790 color: @grey2 !important;
781 791 background: white !important;
782 792 }
783 793 &:focus {
784 794 color: @grey2 !important;
785 795 background: white !important;
786 796 }
787 797 }
788 798 }
789 799
790 800
791 801
792 802 .main_filter_input::placeholder {
793 803 color: @nav-grey;
794 804 opacity: 1;
795 805 }
796 806
797 807 .notice-box {
798 808 display:block !important;
799 809 padding: 9px 0 !important;
800 810 }
801 811
802 812 .menulabel-notice {
803 813 border: 1px solid @color5;
804 814 padding:7px 10px;
805 815 color: @color5;
806 816 }
@@ -1,1061 +1,1063 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 % if c.rhodecode_name:
15 15 <div class="branding">
16 16 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
17 17 </div>
18 18 % endif
19 19 </div>
20 20 <!-- MENU BAR NAV -->
21 21 ${self.menu_bar_nav()}
22 22 <!-- END MENU BAR NAV -->
23 23 </div>
24 24 </div>
25 25 ${self.menu_bar_subnav()}
26 26 <!-- END HEADER -->
27 27
28 28 <!-- CONTENT -->
29 29 <div id="content" class="wrapper">
30 30
31 31 <rhodecode-toast id="notifications"></rhodecode-toast>
32 32
33 33 <div class="main">
34 34 ${next.main()}
35 35 </div>
36 36 </div>
37 37 <!-- END CONTENT -->
38 38
39 39 </div>
40 40 <!-- FOOTER -->
41 41 <div id="footer">
42 42 <div id="footer-inner" class="title wrapper">
43 43 <div>
44 44 <p class="footer-link-right">
45 45 % if c.visual.show_version:
46 46 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
47 47 % endif
48 48 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
49 49 % if c.visual.rhodecode_support_url:
50 50 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
51 51 % endif
52 52 </p>
53 53 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
54 54 <p class="server-instance" style="display:${sid}">
55 55 ## display hidden instance ID if specially defined
56 56 % if c.rhodecode_instanceid:
57 57 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
58 58 % endif
59 59 </p>
60 60 </div>
61 61 </div>
62 62 </div>
63 63
64 64 <!-- END FOOTER -->
65 65
66 66 ### MAKO DEFS ###
67 67
68 68 <%def name="menu_bar_subnav()">
69 69 </%def>
70 70
71 71 <%def name="breadcrumbs(class_='breadcrumbs')">
72 72 <div class="${class_}">
73 73 ${self.breadcrumbs_links()}
74 74 </div>
75 75 </%def>
76 76
77 77 <%def name="admin_menu(active=None)">
78 78 <%
79 79 def is_active(selected):
80 80 if selected == active:
81 81 return "active"
82 82 %>
83 83
84 84 <div id="context-bar">
85 85 <div class="wrapper">
86 86 <div class="title">
87 87 <div class="title-content">
88 88 <div class="title-main">
89 89 % if c.is_super_admin:
90 90 ${_('Super Admin Panel')}
91 91 % else:
92 92 ${_('Delegated Admin Panel')}
93 93 % endif
94 94 </div>
95 95 </div>
96 96 </div>
97 97
98 98 <ul id="context-pages" class="navigation horizontal-list">
99 99
100 100 ## super admin case
101 101 % if c.is_super_admin:
102 102 <li class="${is_active('audit_logs')}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
103 103 <li class="${is_active('repositories')}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
104 104 <li class="${is_active('repository_groups')}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
105 105 <li class="${is_active('users')}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
106 106 <li class="${is_active('user_groups')}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
107 107 <li class="${is_active('permissions')}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
108 108 <li class="${is_active('authentication')}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
109 109 <li class="${is_active('integrations')}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
110 110 <li class="${is_active('defaults')}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
111 111 <li class="${is_active('settings')}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
112 112
113 113 ## delegated admin
114 114 % elif c.is_delegated_admin:
115 115 <%
116 116 repositories=c.auth_user.repositories_admin or c.can_create_repo
117 117 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
118 118 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
119 119 %>
120 120
121 121 %if repositories:
122 122 <li class="${is_active('repositories')} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
123 123 %endif
124 124 %if repository_groups:
125 125 <li class="${is_active('repository_groups')} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
126 126 %endif
127 127 %if user_groups:
128 128 <li class="${is_active('user_groups')} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
129 129 %endif
130 130 % endif
131 131 </ul>
132 132
133 133 </div>
134 134 <div class="clear"></div>
135 135 </div>
136 136 </%def>
137 137
138 138 <%def name="dt_info_panel(elements)">
139 139 <dl class="dl-horizontal">
140 140 %for dt, dd, title, show_items in elements:
141 141 <dt>${dt}:</dt>
142 142 <dd title="${h.tooltip(title)}">
143 143 %if callable(dd):
144 144 ## allow lazy evaluation of elements
145 145 ${dd()}
146 146 %else:
147 147 ${dd}
148 148 %endif
149 149 %if show_items:
150 150 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
151 151 %endif
152 152 </dd>
153 153
154 154 %if show_items:
155 155 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
156 156 %for item in show_items:
157 157 <dt></dt>
158 158 <dd>${item}</dd>
159 159 %endfor
160 160 </div>
161 161 %endif
162 162
163 163 %endfor
164 164 </dl>
165 165 </%def>
166 166
167 167 <%def name="gravatar(email, size=16)">
168 168 <%
169 169 if (size > 16):
170 170 gravatar_class = 'gravatar gravatar-large'
171 171 else:
172 172 gravatar_class = 'gravatar'
173 173 %>
174 174 <%doc>
175 175 TODO: johbo: For now we serve double size images to make it smooth
176 176 for retina. This is how it worked until now. Should be replaced
177 177 with a better solution at some point.
178 178 </%doc>
179 179 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
180 180 </%def>
181 181
182 182
183 183 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
184 184 <% email = h.email_or_none(contact) %>
185 185 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
186 186 ${self.gravatar(email, size)}
187 187 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
188 188 </div>
189 189 </%def>
190 190
191 191
192 192 <%def name="repo_page_title(repo_instance)">
193 193 <div class="title-content repo-title">
194 194
195 195 <div class="title-main">
196 196 ## SVN/HG/GIT icons
197 197 %if h.is_hg(repo_instance):
198 198 <i class="icon-hg"></i>
199 199 %endif
200 200 %if h.is_git(repo_instance):
201 201 <i class="icon-git"></i>
202 202 %endif
203 203 %if h.is_svn(repo_instance):
204 204 <i class="icon-svn"></i>
205 205 %endif
206 206
207 207 ## public/private
208 208 %if repo_instance.private:
209 209 <i class="icon-repo-private"></i>
210 210 %else:
211 211 <i class="icon-repo-public"></i>
212 212 %endif
213 213
214 214 ## repo name with group name
215 215 ${h.breadcrumb_repo_link(repo_instance)}
216 216
217 217 ## Context Actions
218 218 <div class="pull-right">
219 219 %if c.rhodecode_user.username != h.DEFAULT_USER:
220 220 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
221 221
222 222 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
223 223 % if c.repository_is_user_following:
224 224 <i class="icon-eye-off"></i>${_('Unwatch')}
225 225 % else:
226 226 <i class="icon-eye"></i>${_('Watch')}
227 227 % endif
228 228
229 229 </a>
230 230 %else:
231 231 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
232 232 %endif
233 233 </div>
234 234
235 235 </div>
236 236
237 237 ## FORKED
238 238 %if repo_instance.fork:
239 239 <p class="discreet">
240 240 <i class="icon-code-fork"></i> ${_('Fork of')}
241 241 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
242 242 </p>
243 243 %endif
244 244
245 245 ## IMPORTED FROM REMOTE
246 246 %if repo_instance.clone_uri:
247 247 <p class="discreet">
248 248 <i class="icon-code-fork"></i> ${_('Clone from')}
249 249 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
250 250 </p>
251 251 %endif
252 252
253 253 ## LOCKING STATUS
254 254 %if repo_instance.locked[0]:
255 255 <p class="locking_locked discreet">
256 256 <i class="icon-repo-lock"></i>
257 257 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
258 258 </p>
259 259 %elif repo_instance.enable_locking:
260 260 <p class="locking_unlocked discreet">
261 261 <i class="icon-repo-unlock"></i>
262 262 ${_('Repository not locked. Pull repository to lock it.')}
263 263 </p>
264 264 %endif
265 265
266 266 </div>
267 267 </%def>
268 268
269 269 <%def name="repo_menu(active=None)">
270 270 <%
271 271 def is_active(selected):
272 272 if selected == active:
273 273 return "active"
274 274 ## determine if we have "any" option available
275 275 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
276 276 has_actions = can_lock
277 277
278 278 %>
279 279 % if c.rhodecode_db_repo.archived:
280 280 <div class="alert alert-warning text-center">
281 281 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
282 282 </div>
283 283 % endif
284 284
285 285 <!--- REPO CONTEXT BAR -->
286 286 <div id="context-bar">
287 287 <div class="wrapper">
288 288
289 289 <div class="title">
290 290 ${self.repo_page_title(c.rhodecode_db_repo)}
291 291 </div>
292 292
293 293 <ul id="context-pages" class="navigation horizontal-list">
294 294 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
295 295 <li class="${is_active('commits')}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
296 296 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
297 297 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
298 298
299 299 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
300 300 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
301 301 <li class="${is_active('showpullrequest')}">
302 302 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
303 303 <div class="menulabel">
304 %if c.repository_pull_requests == 1:
305 ${_('Pull Request')} ${c.repository_pull_requests}
306 %else:
307 ${_('Pull Requests')} ${c.repository_pull_requests}
308 %endif
304 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
309 305 </div>
310 306 </a>
311 307 </li>
312 308 %endif
313 309
314 <li class="${is_active('artifacts')}"><a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}"><div class="menulabel">${_('Artifacts')}</div></a></li>
310 <li class="${is_active('artifacts')}">
311 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
312 <div class="menulabel">
313 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
314 </div>
315 </a>
316 </li>
315 317
316 318 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
317 319 <li class="${is_active('settings')}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
318 320 %endif
319 321
320 322 <li class="${is_active('options')}">
321 323 % if has_actions:
322 324 <a class="menulink dropdown">
323 325 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
324 326 </a>
325 327 <ul class="submenu">
326 328 %if can_lock:
327 329 %if c.rhodecode_db_repo.locked[0]:
328 330 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
329 331 %else:
330 332 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
331 333 %endif
332 334 %endif
333 335 </ul>
334 336 % else:
335 337 <a class="menulink disabled">
336 338 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
337 339 </a>
338 340 % endif
339 341 </li>
340 342
341 343 </ul>
342 344 </div>
343 345 <div class="clear"></div>
344 346 </div>
345 347
346 348 <!--- REPO END CONTEXT BAR -->
347 349
348 350 </%def>
349 351
350 352 <%def name="repo_group_page_title(repo_group_instance)">
351 353 <div class="title-content">
352 354 <div class="title-main">
353 355 ## Repository Group icon
354 356 <i class="icon-repo-group"></i>
355 357
356 358 ## repo name with group name
357 359 ${h.breadcrumb_repo_group_link(repo_group_instance)}
358 360 </div>
359 361
360 362 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
361 363 <div class="repo-group-desc discreet">
362 364 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
363 365 </div>
364 366
365 367 </div>
366 368 </%def>
367 369
368 370
369 371 <%def name="repo_group_menu(active=None)">
370 372 <%
371 373 def is_active(selected):
372 374 if selected == active:
373 375 return "active"
374 376
375 377 gr_name = c.repo_group.group_name if c.repo_group else None
376 378 # create repositories with write permission on group is set to true
377 379 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
378 380
379 381 %>
380 382
381 383
382 384 <!--- REPO GROUP CONTEXT BAR -->
383 385 <div id="context-bar">
384 386 <div class="wrapper">
385 387 <div class="title">
386 388 ${self.repo_group_page_title(c.repo_group)}
387 389 </div>
388 390
389 391 <ul id="context-pages" class="navigation horizontal-list">
390 392 <li class="${is_active('home')}">
391 393 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
392 394 </li>
393 395 % if c.is_super_admin or group_admin:
394 396 <li class="${is_active('settings')}">
395 397 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
396 398 </li>
397 399 % endif
398 400
399 401 </ul>
400 402 </div>
401 403 <div class="clear"></div>
402 404 </div>
403 405
404 406 <!--- REPO GROUP CONTEXT BAR -->
405 407
406 408 </%def>
407 409
408 410
409 411 <%def name="usermenu(active=False)">
410 412 <%
411 413 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
412 414
413 415 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
414 416 # create repositories with write permission on group is set to true
415 417
416 418 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
417 419 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
418 420 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
419 421 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
420 422
421 423 can_create_repos = c.is_super_admin or c.can_create_repo
422 424 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
423 425
424 426 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
425 427 can_create_repo_groups_in_group = c.is_super_admin or group_admin
426 428 %>
427 429
428 430 % if not_anonymous:
429 431 <%
430 432 default_target_group = dict()
431 433 if c.rhodecode_user.personal_repo_group:
432 434 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
433 435 %>
434 436
435 437 ## create action
436 438 <li>
437 439 <a href="#create-actions" onclick="return false;" class="menulink childs">
438 440 <i class="icon-plus-circled"></i>
439 441 </a>
440 442
441 443 <div class="action-menu submenu">
442 444
443 445 <ol>
444 446 ## scope of within a repository
445 447 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
446 448 <li class="submenu-title">${_('This Repository')}</li>
447 449 <li>
448 450 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
449 451 </li>
450 452 % if can_fork:
451 453 <li>
452 454 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
453 455 </li>
454 456 % endif
455 457 % endif
456 458
457 459 ## scope of within repository groups
458 460 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
459 461 <li class="submenu-title">${_('This Repository Group')}</li>
460 462
461 463 % if can_create_repos_in_group:
462 464 <li>
463 465 <a href="${h.route_path('repo_new',_query=default_target_group)}">${_('New Repository')}</a>
464 466 </li>
465 467 % endif
466 468
467 469 % if can_create_repo_groups_in_group:
468 470 <li>
469 471 <a href="${h.route_path('repo_group_new',_query=default_target_group)}">${_(u'New Repository Group')}</a>
470 472 </li>
471 473 % endif
472 474 % endif
473 475
474 476 ## personal group
475 477 % if c.rhodecode_user.personal_repo_group:
476 478 <li class="submenu-title">Personal Group</li>
477 479
478 480 <li>
479 481 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
480 482 </li>
481 483
482 484 <li>
483 485 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
484 486 </li>
485 487 % endif
486 488
487 489 ## Global actions
488 490 <li class="submenu-title">RhodeCode</li>
489 491 % if can_create_repos:
490 492 <li>
491 493 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
492 494 </li>
493 495 % endif
494 496
495 497 % if can_create_repo_groups:
496 498 <li>
497 499 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
498 500 </li>
499 501 % endif
500 502
501 503 <li>
502 504 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
503 505 </li>
504 506
505 507 </ol>
506 508
507 509 </div>
508 510 </li>
509 511
510 512 ## notifications
511 513 <li>
512 514 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
513 515 ${c.unread_notifications}
514 516 </a>
515 517 </li>
516 518 % endif
517 519
518 520 ## USER MENU
519 521 <li id="quick_login_li" class="${'active' if active else ''}">
520 522 % if c.rhodecode_user.username == h.DEFAULT_USER:
521 523 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
522 524 ${gravatar(c.rhodecode_user.email, 20)}
523 525 <span class="user">
524 526 <span>${_('Sign in')}</span>
525 527 </span>
526 528 </a>
527 529 % else:
528 530 ## logged in user
529 531 <a id="quick_login_link" class="menulink childs">
530 532 ${gravatar(c.rhodecode_user.email, 20)}
531 533 <span class="user">
532 534 <span class="menu_link_user">${c.rhodecode_user.username}</span>
533 535 <div class="show_more"></div>
534 536 </span>
535 537 </a>
536 538 ## subnav with menu for logged in user
537 539 <div class="user-menu submenu">
538 540 <div id="quick_login">
539 541 %if c.rhodecode_user.username != h.DEFAULT_USER:
540 542 <div class="">
541 543 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
542 544 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
543 545 <div class="email">${c.rhodecode_user.email}</div>
544 546 </div>
545 547 <div class="">
546 548 <ol class="links">
547 549 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
548 550 % if c.rhodecode_user.personal_repo_group:
549 551 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
550 552 % endif
551 553 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
552 554
553 555 % if c.debug_style:
554 556 <li>
555 557 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
556 558 <div class="menulabel">${_('[Style]')}</div>
557 559 </a>
558 560 </li>
559 561 % endif
560 562
561 563 ## bookmark-items
562 564 <li class="bookmark-items">
563 565 ${_('Bookmarks')}
564 566 <div class="pull-right">
565 567 <a href="${h.route_path('my_account_bookmarks')}">
566 568
567 569 <i class="icon-cog"></i>
568 570 </a>
569 571 </div>
570 572 </li>
571 573 % if not c.bookmark_items:
572 574 <li>
573 575 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
574 576 </li>
575 577 % endif
576 578 % for item in c.bookmark_items:
577 579 <li>
578 580 % if item.repository:
579 581 <div>
580 582 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
581 583 <code>${item.position}</code>
582 584 % if item.repository.repo_type == 'hg':
583 585 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
584 586 % elif item.repository.repo_type == 'git':
585 587 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
586 588 % elif item.repository.repo_type == 'svn':
587 589 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
588 590 % endif
589 591 ${(item.title or h.shorter(item.repository.repo_name, 30))}
590 592 </a>
591 593 </div>
592 594 % elif item.repository_group:
593 595 <div>
594 596 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
595 597 <code>${item.position}</code>
596 598 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
597 599 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
598 600 </a>
599 601 </div>
600 602 % else:
601 603 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
602 604 <code>${item.position}</code>
603 605 ${item.title}
604 606 </a>
605 607 % endif
606 608 </li>
607 609 % endfor
608 610
609 611 <li class="logout">
610 612 ${h.secure_form(h.route_path('logout'), request=request)}
611 613 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
612 614 ${h.end_form()}
613 615 </li>
614 616 </ol>
615 617 </div>
616 618 %endif
617 619 </div>
618 620 </div>
619 621
620 622 % endif
621 623 </li>
622 624 </%def>
623 625
624 626 <%def name="menu_items(active=None)">
625 627 <%
626 628 def is_active(selected):
627 629 if selected == active:
628 630 return "active"
629 631 return ""
630 632 %>
631 633
632 634 <ul id="quick" class="main_nav navigation horizontal-list">
633 635 ## notice box for important system messages
634 636 <li style="display: none">
635 637 <a class="notice-box" href="#openNotice" onclick="return false">
636 638 <div class="menulabel-notice" >
637 639 0
638 640 </div>
639 641 </a>
640 642 </li>
641 643
642 644 ## Main filter
643 645 <li>
644 646 <div class="menulabel main_filter_box">
645 647 <div class="main_filter_input_box">
646 648 <ul class="searchItems">
647 649
648 650 % if c.template_context['search_context']['repo_id']:
649 651 <li class="searchTag searchTagFilter searchTagHidable" >
650 652 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
651 653 <span class="tag">
652 654 This repo
653 655 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
654 656 </span>
655 657 ##</a>
656 658 </li>
657 659 % elif c.template_context['search_context']['repo_group_id']:
658 660 <li class="searchTag searchTagFilter searchTagHidable">
659 661 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
660 662 <span class="tag">
661 663 This group
662 664 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
663 665 </span>
664 666 ##</a>
665 667 </li>
666 668 % endif
667 669
668 670 <li class="searchTagInput">
669 671 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
670 672 </li>
671 673 <li class="searchTag searchTagHelp">
672 674 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
673 675 </li>
674 676 </ul>
675 677 </div>
676 678 </div>
677 679
678 680 <div id="main_filter_help" style="display: none">
679 681 - Use '/' key to quickly access this field.
680 682
681 683 - Enter a name of repository, or repository group for quick search.
682 684
683 685 - Prefix query to allow special search:
684 686
685 687 user:admin, to search for usernames, always global
686 688
687 689 user_group:devops, to search for user groups, always global
688 690
689 691 commit:efced4, to search for commits, scoped to repositories or groups
690 692
691 693 file:models.py, to search for file paths, scoped to repositories or groups
692 694
693 695 % if c.template_context['search_context']['repo_id']:
694 696 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
695 697 % elif c.template_context['search_context']['repo_group_id']:
696 698 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
697 699 % else:
698 700 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
699 701 % endif
700 702 </div>
701 703 </li>
702 704
703 705 ## ROOT MENU
704 706 <li class="${is_active('home')}">
705 707 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
706 708 <div class="menulabel">${_('Home')}</div>
707 709 </a>
708 710 </li>
709 711
710 712 %if c.rhodecode_user.username != h.DEFAULT_USER:
711 713 <li class="${is_active('journal')}">
712 714 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
713 715 <div class="menulabel">${_('Journal')}</div>
714 716 </a>
715 717 </li>
716 718 %else:
717 719 <li class="${is_active('journal')}">
718 720 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
719 721 <div class="menulabel">${_('Public journal')}</div>
720 722 </a>
721 723 </li>
722 724 %endif
723 725
724 726 <li class="${is_active('gists')}">
725 727 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
726 728 <div class="menulabel">${_('Gists')}</div>
727 729 </a>
728 730 </li>
729 731
730 732 % if c.is_super_admin or c.is_delegated_admin:
731 733 <li class="${is_active('admin')}">
732 734 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
733 735 <div class="menulabel">${_('Admin')} </div>
734 736 </a>
735 737 </li>
736 738 % endif
737 739
738 740 ## render extra user menu
739 741 ${usermenu(active=(active=='my_account'))}
740 742
741 743 </ul>
742 744
743 745 <script type="text/javascript">
744 746 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
745 747
746 748 var formatRepoResult = function(result, container, query, escapeMarkup) {
747 749 return function(data, escapeMarkup) {
748 750 if (!data.repo_id){
749 751 return data.text; // optgroup text Repositories
750 752 }
751 753
752 754 var tmpl = '';
753 755 var repoType = data['repo_type'];
754 756 var repoName = data['text'];
755 757
756 758 if(data && data.type == 'repo'){
757 759 if(repoType === 'hg'){
758 760 tmpl += '<i class="icon-hg"></i> ';
759 761 }
760 762 else if(repoType === 'git'){
761 763 tmpl += '<i class="icon-git"></i> ';
762 764 }
763 765 else if(repoType === 'svn'){
764 766 tmpl += '<i class="icon-svn"></i> ';
765 767 }
766 768 if(data['private']){
767 769 tmpl += '<i class="icon-lock" ></i> ';
768 770 }
769 771 else if(visualShowPublicIcon){
770 772 tmpl += '<i class="icon-unlock-alt"></i> ';
771 773 }
772 774 }
773 775 tmpl += escapeMarkup(repoName);
774 776 return tmpl;
775 777
776 778 }(result, escapeMarkup);
777 779 };
778 780
779 781 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
780 782 return function(data, escapeMarkup) {
781 783 if (!data.repo_group_id){
782 784 return data.text; // optgroup text Repositories
783 785 }
784 786
785 787 var tmpl = '';
786 788 var repoGroupName = data['text'];
787 789
788 790 if(data){
789 791
790 792 tmpl += '<i class="icon-repo-group"></i> ';
791 793
792 794 }
793 795 tmpl += escapeMarkup(repoGroupName);
794 796 return tmpl;
795 797
796 798 }(result, escapeMarkup);
797 799 };
798 800
799 801 var escapeRegExChars = function (value) {
800 802 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
801 803 };
802 804
803 805 var getRepoIcon = function(repo_type) {
804 806 if (repo_type === 'hg') {
805 807 return '<i class="icon-hg"></i> ';
806 808 }
807 809 else if (repo_type === 'git') {
808 810 return '<i class="icon-git"></i> ';
809 811 }
810 812 else if (repo_type === 'svn') {
811 813 return '<i class="icon-svn"></i> ';
812 814 }
813 815 return ''
814 816 };
815 817
816 818 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
817 819
818 820 if (value.split(':').length === 2) {
819 821 value = value.split(':')[1]
820 822 }
821 823
822 824 var searchType = data['type'];
823 825 var searchSubType = data['subtype'];
824 826 var valueDisplay = data['value_display'];
825 827
826 828 var pattern = '(' + escapeRegExChars(value) + ')';
827 829
828 830 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
829 831
830 832 // highlight match
831 833 if (searchType != 'text') {
832 834 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
833 835 }
834 836
835 837 var icon = '';
836 838
837 839 if (searchType === 'hint') {
838 840 icon += '<i class="icon-repo-group"></i> ';
839 841 }
840 842 // full text search/hints
841 843 else if (searchType === 'search') {
842 844 icon += '<i class="icon-more"></i> ';
843 845 if (searchSubType !== undefined && searchSubType == 'repo') {
844 846 valueDisplay += '<div class="pull-right tag">repository</div>';
845 847 }
846 848 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
847 849 valueDisplay += '<div class="pull-right tag">repo group</div>';
848 850 }
849 851 }
850 852 // repository
851 853 else if (searchType === 'repo') {
852 854
853 855 var repoIcon = getRepoIcon(data['repo_type']);
854 856 icon += repoIcon;
855 857
856 858 if (data['private']) {
857 859 icon += '<i class="icon-lock" ></i> ';
858 860 }
859 861 else if (visualShowPublicIcon) {
860 862 icon += '<i class="icon-unlock-alt"></i> ';
861 863 }
862 864 }
863 865 // repository groups
864 866 else if (searchType === 'repo_group') {
865 867 icon += '<i class="icon-repo-group"></i> ';
866 868 }
867 869 // user group
868 870 else if (searchType === 'user_group') {
869 871 icon += '<i class="icon-group"></i> ';
870 872 }
871 873 // user
872 874 else if (searchType === 'user') {
873 875 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
874 876 }
875 877 // commit
876 878 else if (searchType === 'commit') {
877 879 var repo_data = data['repo_data'];
878 880 var repoIcon = getRepoIcon(repo_data['repository_type']);
879 881 if (repoIcon) {
880 882 icon += repoIcon;
881 883 } else {
882 884 icon += '<i class="icon-tag"></i>';
883 885 }
884 886 }
885 887 // file
886 888 else if (searchType === 'file') {
887 889 var repo_data = data['repo_data'];
888 890 var repoIcon = getRepoIcon(repo_data['repository_type']);
889 891 if (repoIcon) {
890 892 icon += repoIcon;
891 893 } else {
892 894 icon += '<i class="icon-tag"></i>';
893 895 }
894 896 }
895 897 // generic text
896 898 else if (searchType === 'text') {
897 899 icon = '';
898 900 }
899 901
900 902 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
901 903 return tmpl.format(icon, valueDisplay);
902 904 };
903 905
904 906 var handleSelect = function(element, suggestion) {
905 907 if (suggestion.type === "hint") {
906 908 // we skip action
907 909 $('#main_filter').focus();
908 910 }
909 911 else if (suggestion.type === "text") {
910 912 // we skip action
911 913 $('#main_filter').focus();
912 914
913 915 } else {
914 916 window.location = suggestion['url'];
915 917 }
916 918 };
917 919
918 920 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
919 921 if (queryLowerCase.split(':').length === 2) {
920 922 queryLowerCase = queryLowerCase.split(':')[1]
921 923 }
922 924 if (suggestion.type === "text") {
923 925 // special case we don't want to "skip" display for
924 926 return true
925 927 }
926 928 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
927 929 };
928 930
929 931 var cleanContext = {
930 932 repo_view_type: null,
931 933
932 934 repo_id: null,
933 935 repo_name: "",
934 936
935 937 repo_group_id: null,
936 938 repo_group_name: null
937 939 };
938 940 var removeGoToFilter = function () {
939 941 $('.searchTagHidable').hide();
940 942 $('#main_filter').autocomplete(
941 943 'setOptions', {params:{search_context: cleanContext}});
942 944 };
943 945
944 946 $('#main_filter').autocomplete({
945 947 serviceUrl: pyroutes.url('goto_switcher_data'),
946 948 params: {
947 949 "search_context": templateContext.search_context
948 950 },
949 951 minChars:2,
950 952 maxHeight:400,
951 953 deferRequestBy: 300, //miliseconds
952 954 tabDisabled: true,
953 955 autoSelectFirst: false,
954 956 containerClass: 'autocomplete-qfilter-suggestions',
955 957 formatResult: autocompleteMainFilterFormatResult,
956 958 lookupFilter: autocompleteMainFilterResult,
957 959 onSelect: function (element, suggestion) {
958 960 handleSelect(element, suggestion);
959 961 return false;
960 962 },
961 963 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
962 964 if (jqXHR !== 'abort') {
963 965 alert("Error during search.\nError code: {0}".format(textStatus));
964 966 window.location = '';
965 967 }
966 968 }
967 969 });
968 970
969 971 showMainFilterBox = function () {
970 972 $('#main_filter_help').toggle();
971 973 };
972 974
973 975 $('#main_filter').on('keydown.autocomplete', function (e) {
974 976
975 977 var BACKSPACE = 8;
976 978 var el = $(e.currentTarget);
977 979 if(e.which === BACKSPACE){
978 980 var inputVal = el.val();
979 981 if (inputVal === ""){
980 982 removeGoToFilter()
981 983 }
982 984 }
983 985 });
984 986
985 987 </script>
986 988 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
987 989 </%def>
988 990
989 991 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
990 992 <div class="modal-dialog">
991 993 <div class="modal-content">
992 994 <div class="modal-header">
993 995 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
994 996 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
995 997 </div>
996 998 <div class="modal-body">
997 999 <div class="block-left">
998 1000 <table class="keyboard-mappings">
999 1001 <tbody>
1000 1002 <tr>
1001 1003 <th></th>
1002 1004 <th>${_('Site-wide shortcuts')}</th>
1003 1005 </tr>
1004 1006 <%
1005 1007 elems = [
1006 1008 ('/', 'Use quick search box'),
1007 1009 ('g h', 'Goto home page'),
1008 1010 ('g g', 'Goto my private gists page'),
1009 1011 ('g G', 'Goto my public gists page'),
1010 1012 ('g 0-9', 'Goto bookmarked items from 0-9'),
1011 1013 ('n r', 'New repository page'),
1012 1014 ('n g', 'New gist page'),
1013 1015 ]
1014 1016 %>
1015 1017 %for key, desc in elems:
1016 1018 <tr>
1017 1019 <td class="keys">
1018 1020 <span class="key tag">${key}</span>
1019 1021 </td>
1020 1022 <td>${desc}</td>
1021 1023 </tr>
1022 1024 %endfor
1023 1025 </tbody>
1024 1026 </table>
1025 1027 </div>
1026 1028 <div class="block-left">
1027 1029 <table class="keyboard-mappings">
1028 1030 <tbody>
1029 1031 <tr>
1030 1032 <th></th>
1031 1033 <th>${_('Repositories')}</th>
1032 1034 </tr>
1033 1035 <%
1034 1036 elems = [
1035 1037 ('g s', 'Goto summary page'),
1036 1038 ('g c', 'Goto changelog page'),
1037 1039 ('g f', 'Goto files page'),
1038 1040 ('g F', 'Goto files page with file search activated'),
1039 1041 ('g p', 'Goto pull requests page'),
1040 1042 ('g o', 'Goto repository settings'),
1041 1043 ('g O', 'Goto repository permissions settings'),
1042 1044 ]
1043 1045 %>
1044 1046 %for key, desc in elems:
1045 1047 <tr>
1046 1048 <td class="keys">
1047 1049 <span class="key tag">${key}</span>
1048 1050 </td>
1049 1051 <td>${desc}</td>
1050 1052 </tr>
1051 1053 %endfor
1052 1054 </tbody>
1053 1055 </table>
1054 1056 </div>
1055 1057 </div>
1056 1058 <div class="modal-footer">
1057 1059 </div>
1058 1060 </div><!-- /.modal-content -->
1059 1061 </div><!-- /.modal-dialog -->
1060 1062 </div><!-- /.modal -->
1061 1063
General Comments 0
You need to be logged in to leave comments. Login now