##// END OF EJS Templates
files: use a common function to handle url-by-refs, and fix landing refs for SVN....
marcink -
r4373:702c378c stable
parent child Browse files
Show More
@@ -1,807 +1,808 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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, IssueTrackerSettingsModel
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 228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
229 229 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
230 230
231 231 def _handle_missing_requirements(self, error):
232 232 log.error(
233 233 'Requirements are missing for repository %s: %s',
234 234 self.db_repo_name, safe_unicode(error))
235 235
236 236 def _get_local_tmpl_context(self, include_app_defaults=True):
237 237 _ = self.request.translate
238 238 c = super(RepoAppView, self)._get_local_tmpl_context(
239 239 include_app_defaults=include_app_defaults)
240 240
241 241 # register common vars for this type of view
242 242 c.rhodecode_db_repo = self.db_repo
243 243 c.repo_name = self.db_repo_name
244 244 c.repository_pull_requests = self.db_repo_pull_requests
245 245 c.repository_artifacts = self.db_repo_artifacts
246 246 c.repository_is_user_following = ScmModel().is_following_repo(
247 247 self.db_repo_name, self._rhodecode_user.user_id)
248 248 self.path_filter = PathFilter(None)
249 249
250 250 c.repository_requirements_missing = {}
251 251 try:
252 252 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
253 253 # NOTE(marcink):
254 254 # comparison to None since if it's an object __bool__ is expensive to
255 255 # calculate
256 256 if self.rhodecode_vcs_repo is not None:
257 257 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
258 258 c.auth_user.username)
259 259 self.path_filter = PathFilter(path_perms)
260 260 except RepositoryRequirementError as e:
261 261 c.repository_requirements_missing = {'error': str(e)}
262 262 self._handle_missing_requirements(e)
263 263 self.rhodecode_vcs_repo = None
264 264
265 265 c.path_filter = self.path_filter # used by atom_feed_entry.mako
266 266
267 267 if self.rhodecode_vcs_repo is None:
268 268 # unable to fetch this repo as vcs instance, report back to user
269 269 h.flash(_(
270 270 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
271 271 "Please check if it exist, or is not damaged.") %
272 272 {'repo_name': c.repo_name},
273 273 category='error', ignore_duplicate=True)
274 274 if c.repository_requirements_missing:
275 275 route = self.request.matched_route.name
276 276 if route.startswith(('edit_repo', 'repo_summary')):
277 277 # allow summary and edit repo on missing requirements
278 278 return c
279 279
280 280 raise HTTPFound(
281 281 h.route_path('repo_summary', repo_name=self.db_repo_name))
282 282
283 283 else: # redirect if we don't show missing requirements
284 284 raise HTTPFound(h.route_path('home'))
285 285
286 286 c.has_origin_repo_read_perm = False
287 287 if self.db_repo.fork:
288 288 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
289 289 'repository.write', 'repository.read', 'repository.admin')(
290 290 self.db_repo.fork.repo_name, 'summary fork link')
291 291
292 292 return c
293 293
294 294 def _get_f_path_unchecked(self, matchdict, default=None):
295 295 """
296 296 Should only be used by redirects, everything else should call _get_f_path
297 297 """
298 298 f_path = matchdict.get('f_path')
299 299 if f_path:
300 300 # fix for multiple initial slashes that causes errors for GIT
301 301 return f_path.lstrip('/')
302 302
303 303 return default
304 304
305 305 def _get_f_path(self, matchdict, default=None):
306 306 f_path_match = self._get_f_path_unchecked(matchdict, default)
307 307 return self.path_filter.assert_path_permissions(f_path_match)
308 308
309 309 def _get_general_setting(self, target_repo, settings_key, default=False):
310 310 settings_model = VcsSettingsModel(repo=target_repo)
311 311 settings = settings_model.get_general_settings()
312 312 return settings.get(settings_key, default)
313 313
314 314 def _get_repo_setting(self, target_repo, settings_key, default=False):
315 315 settings_model = VcsSettingsModel(repo=target_repo)
316 316 settings = settings_model.get_repo_settings_inherited()
317 317 return settings.get(settings_key, default)
318 318
319 319 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
320 320 log.debug('Looking for README file at path %s', path)
321 321 if commit_id:
322 322 landing_commit_id = commit_id
323 323 else:
324 324 landing_commit = db_repo.get_landing_commit()
325 325 if isinstance(landing_commit, EmptyCommit):
326 326 return None, None
327 327 landing_commit_id = landing_commit.raw_id
328 328
329 329 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
330 330 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
331 331 start = time.time()
332 332
333 333 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
334 334 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
335 335 readme_data = None
336 336 readme_filename = None
337 337
338 338 commit = db_repo.get_commit(_commit_id)
339 339 log.debug("Searching for a README file at commit %s.", _commit_id)
340 340 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
341 341
342 342 if readme_node:
343 343 log.debug('Found README node: %s', readme_node)
344 344 relative_urls = {
345 345 'raw': h.route_path(
346 346 'repo_file_raw', repo_name=_repo_name,
347 347 commit_id=commit.raw_id, f_path=readme_node.path),
348 348 'standard': h.route_path(
349 349 'repo_files', repo_name=_repo_name,
350 350 commit_id=commit.raw_id, f_path=readme_node.path),
351 351 }
352 352 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
353 353 readme_filename = readme_node.unicode_path
354 354
355 355 return readme_data, readme_filename
356 356
357 357 readme_data, readme_filename = generate_repo_readme(
358 358 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
359 359 compute_time = time.time() - start
360 360 log.debug('Repo README for path %s generated and computed in %.4fs',
361 361 path, compute_time)
362 362 return readme_data, readme_filename
363 363
364 364 def _render_readme_or_none(self, commit, readme_node, relative_urls):
365 365 log.debug('Found README file `%s` rendering...', readme_node.path)
366 366 renderer = MarkupRenderer()
367 367 try:
368 368 html_source = renderer.render(
369 369 readme_node.content, filename=readme_node.path)
370 370 if relative_urls:
371 371 return relative_links(html_source, relative_urls)
372 372 return html_source
373 373 except Exception:
374 374 log.exception(
375 375 "Exception while trying to render the README")
376 376
377 377 def get_recache_flag(self):
378 378 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
379 379 flag_val = self.request.GET.get(flag_name)
380 380 if str2bool(flag_val):
381 381 return True
382 382 return False
383 383
384 384
385 385 class PathFilter(object):
386 386
387 387 # Expects and instance of BasePathPermissionChecker or None
388 388 def __init__(self, permission_checker):
389 389 self.permission_checker = permission_checker
390 390
391 391 def assert_path_permissions(self, path):
392 392 if self.path_access_allowed(path):
393 393 return path
394 394 raise HTTPForbidden()
395 395
396 396 def path_access_allowed(self, path):
397 397 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
398 398 if self.permission_checker:
399 399 has_access = path and self.permission_checker.has_access(path)
400 400 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
401 401 return has_access
402 402
403 403 log.debug('ACL permissions checker not enabled, skipping...')
404 404 return True
405 405
406 406 def filter_patchset(self, patchset):
407 407 if not self.permission_checker or not patchset:
408 408 return patchset, False
409 409 had_filtered = False
410 410 filtered_patchset = []
411 411 for patch in patchset:
412 412 filename = patch.get('filename', None)
413 413 if not filename or self.permission_checker.has_access(filename):
414 414 filtered_patchset.append(patch)
415 415 else:
416 416 had_filtered = True
417 417 if had_filtered:
418 418 if isinstance(patchset, diffs.LimitedDiffContainer):
419 419 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
420 420 return filtered_patchset, True
421 421 else:
422 422 return patchset, False
423 423
424 424 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
425 425 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
426 426 result = diffset.render_patchset(
427 427 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
428 428 result.has_hidden_changes = has_hidden_changes
429 429 return result
430 430
431 431 def get_raw_patch(self, diff_processor):
432 432 if self.permission_checker is None:
433 433 return diff_processor.as_raw()
434 434 elif self.permission_checker.has_full_access:
435 435 return diff_processor.as_raw()
436 436 else:
437 437 return '# Repository has user-specific filters, raw patch generation is disabled.'
438 438
439 439 @property
440 440 def is_enabled(self):
441 441 return self.permission_checker is not None
442 442
443 443
444 444 class RepoGroupAppView(BaseAppView):
445 445 def __init__(self, context, request):
446 446 super(RepoGroupAppView, self).__init__(context, request)
447 447 self.db_repo_group = request.db_repo_group
448 448 self.db_repo_group_name = self.db_repo_group.group_name
449 449
450 450 def _get_local_tmpl_context(self, include_app_defaults=True):
451 451 _ = self.request.translate
452 452 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
453 453 include_app_defaults=include_app_defaults)
454 454 c.repo_group = self.db_repo_group
455 455 return c
456 456
457 457 def _revoke_perms_on_yourself(self, form_result):
458 458 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
459 459 form_result['perm_updates'])
460 460 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
461 461 form_result['perm_additions'])
462 462 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
463 463 form_result['perm_deletions'])
464 464 admin_perm = 'group.admin'
465 465 if _updates and _updates[0][1] != admin_perm or \
466 466 _additions and _additions[0][1] != admin_perm or \
467 467 _deletions and _deletions[0][1] != admin_perm:
468 468 return True
469 469 return False
470 470
471 471
472 472 class UserGroupAppView(BaseAppView):
473 473 def __init__(self, context, request):
474 474 super(UserGroupAppView, self).__init__(context, request)
475 475 self.db_user_group = request.db_user_group
476 476 self.db_user_group_name = self.db_user_group.users_group_name
477 477
478 478
479 479 class UserAppView(BaseAppView):
480 480 def __init__(self, context, request):
481 481 super(UserAppView, self).__init__(context, request)
482 482 self.db_user = request.db_user
483 483 self.db_user_id = self.db_user.user_id
484 484
485 485 _ = self.request.translate
486 486 if not request.db_user_supports_default:
487 487 if self.db_user.username == User.DEFAULT_USER:
488 488 h.flash(_("Editing user `{}` is disabled.".format(
489 489 User.DEFAULT_USER)), category='warning')
490 490 raise HTTPFound(h.route_path('users'))
491 491
492 492
493 493 class DataGridAppView(object):
494 494 """
495 495 Common class to have re-usable grid rendering components
496 496 """
497 497
498 498 def _extract_ordering(self, request, column_map=None):
499 499 column_map = column_map or {}
500 500 column_index = safe_int(request.GET.get('order[0][column]'))
501 501 order_dir = request.GET.get(
502 502 'order[0][dir]', 'desc')
503 503 order_by = request.GET.get(
504 504 'columns[%s][data][sort]' % column_index, 'name_raw')
505 505
506 506 # translate datatable to DB columns
507 507 order_by = column_map.get(order_by) or order_by
508 508
509 509 search_q = request.GET.get('search[value]')
510 510 return search_q, order_by, order_dir
511 511
512 512 def _extract_chunk(self, request):
513 513 start = safe_int(request.GET.get('start'), 0)
514 514 length = safe_int(request.GET.get('length'), 25)
515 515 draw = safe_int(request.GET.get('draw'))
516 516 return draw, start, length
517 517
518 518 def _get_order_col(self, order_by, model):
519 519 if isinstance(order_by, compat.string_types):
520 520 try:
521 521 return operator.attrgetter(order_by)(model)
522 522 except AttributeError:
523 523 return None
524 524 else:
525 525 return order_by
526 526
527 527
528 528 class BaseReferencesView(RepoAppView):
529 529 """
530 530 Base for reference view for branches, tags and bookmarks.
531 531 """
532 532 def load_default_context(self):
533 533 c = self._get_local_tmpl_context()
534
535
536 534 return c
537 535
538 536 def load_refs_context(self, ref_items, partials_template):
539 537 _render = self.request.get_partial_renderer(partials_template)
540 538 pre_load = ["author", "date", "message", "parents"]
541 539
542 540 is_svn = h.is_svn(self.rhodecode_vcs_repo)
543 541 is_hg = h.is_hg(self.rhodecode_vcs_repo)
544 542
545 543 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
546 544
547 545 closed_refs = {}
548 546 if is_hg:
549 547 closed_refs = self.rhodecode_vcs_repo.branches_closed
550 548
551 549 data = []
552 550 for ref_name, commit_id in ref_items:
553 551 commit = self.rhodecode_vcs_repo.get_commit(
554 552 commit_id=commit_id, pre_load=pre_load)
555 553 closed = ref_name in closed_refs
556 554
557 555 # TODO: johbo: Unify generation of reference links
558 556 use_commit_id = '/' in ref_name or is_svn
559 557
560 558 if use_commit_id:
561 559 files_url = h.route_path(
562 560 'repo_files',
563 561 repo_name=self.db_repo_name,
564 562 f_path=ref_name if is_svn else '',
565 commit_id=commit_id)
563 commit_id=commit_id,
564 _query=dict(at=ref_name)
565 )
566 566
567 567 else:
568 568 files_url = h.route_path(
569 569 'repo_files',
570 570 repo_name=self.db_repo_name,
571 571 f_path=ref_name if is_svn else '',
572 572 commit_id=ref_name,
573 _query=dict(at=ref_name))
573 _query=dict(at=ref_name)
574 )
574 575
575 576 data.append({
576 577 "name": _render('name', ref_name, files_url, closed),
577 578 "name_raw": ref_name,
578 579 "date": _render('date', commit.date),
579 580 "date_raw": datetime_to_time(commit.date),
580 581 "author": _render('author', commit.author),
581 582 "commit": _render(
582 583 'commit', commit.message, commit.raw_id, commit.idx),
583 584 "commit_raw": commit.idx,
584 585 "compare": _render(
585 586 'compare', format_ref_id(ref_name, commit.raw_id)),
586 587 })
587 588
588 589 return data
589 590
590 591
591 592 class RepoRoutePredicate(object):
592 593 def __init__(self, val, config):
593 594 self.val = val
594 595
595 596 def text(self):
596 597 return 'repo_route = %s' % self.val
597 598
598 599 phash = text
599 600
600 601 def __call__(self, info, request):
601 602 if hasattr(request, 'vcs_call'):
602 603 # skip vcs calls
603 604 return
604 605
605 606 repo_name = info['match']['repo_name']
606 607 repo_model = repo.RepoModel()
607 608
608 609 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
609 610
610 611 def redirect_if_creating(route_info, db_repo):
611 612 skip_views = ['edit_repo_advanced_delete']
612 613 route = route_info['route']
613 614 # we should skip delete view so we can actually "remove" repositories
614 615 # if they get stuck in creating state.
615 616 if route.name in skip_views:
616 617 return
617 618
618 619 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
619 620 repo_creating_url = request.route_path(
620 621 'repo_creating', repo_name=db_repo.repo_name)
621 622 raise HTTPFound(repo_creating_url)
622 623
623 624 if by_name_match:
624 625 # register this as request object we can re-use later
625 626 request.db_repo = by_name_match
626 627 redirect_if_creating(info, by_name_match)
627 628 return True
628 629
629 630 by_id_match = repo_model.get_repo_by_id(repo_name)
630 631 if by_id_match:
631 632 request.db_repo = by_id_match
632 633 redirect_if_creating(info, by_id_match)
633 634 return True
634 635
635 636 return False
636 637
637 638
638 639 class RepoForbidArchivedRoutePredicate(object):
639 640 def __init__(self, val, config):
640 641 self.val = val
641 642
642 643 def text(self):
643 644 return 'repo_forbid_archived = %s' % self.val
644 645
645 646 phash = text
646 647
647 648 def __call__(self, info, request):
648 649 _ = request.translate
649 650 rhodecode_db_repo = request.db_repo
650 651
651 652 log.debug(
652 653 '%s checking if archived flag for repo for %s',
653 654 self.__class__.__name__, rhodecode_db_repo.repo_name)
654 655
655 656 if rhodecode_db_repo.archived:
656 657 log.warning('Current view is not supported for archived repo:%s',
657 658 rhodecode_db_repo.repo_name)
658 659
659 660 h.flash(
660 661 h.literal(_('Action not supported for archived repository.')),
661 662 category='warning')
662 663 summary_url = request.route_path(
663 664 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
664 665 raise HTTPFound(summary_url)
665 666 return True
666 667
667 668
668 669 class RepoTypeRoutePredicate(object):
669 670 def __init__(self, val, config):
670 671 self.val = val or ['hg', 'git', 'svn']
671 672
672 673 def text(self):
673 674 return 'repo_accepted_type = %s' % self.val
674 675
675 676 phash = text
676 677
677 678 def __call__(self, info, request):
678 679 if hasattr(request, 'vcs_call'):
679 680 # skip vcs calls
680 681 return
681 682
682 683 rhodecode_db_repo = request.db_repo
683 684
684 685 log.debug(
685 686 '%s checking repo type for %s in %s',
686 687 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
687 688
688 689 if rhodecode_db_repo.repo_type in self.val:
689 690 return True
690 691 else:
691 692 log.warning('Current view is not supported for repo type:%s',
692 693 rhodecode_db_repo.repo_type)
693 694 return False
694 695
695 696
696 697 class RepoGroupRoutePredicate(object):
697 698 def __init__(self, val, config):
698 699 self.val = val
699 700
700 701 def text(self):
701 702 return 'repo_group_route = %s' % self.val
702 703
703 704 phash = text
704 705
705 706 def __call__(self, info, request):
706 707 if hasattr(request, 'vcs_call'):
707 708 # skip vcs calls
708 709 return
709 710
710 711 repo_group_name = info['match']['repo_group_name']
711 712 repo_group_model = repo_group.RepoGroupModel()
712 713 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
713 714
714 715 if by_name_match:
715 716 # register this as request object we can re-use later
716 717 request.db_repo_group = by_name_match
717 718 return True
718 719
719 720 return False
720 721
721 722
722 723 class UserGroupRoutePredicate(object):
723 724 def __init__(self, val, config):
724 725 self.val = val
725 726
726 727 def text(self):
727 728 return 'user_group_route = %s' % self.val
728 729
729 730 phash = text
730 731
731 732 def __call__(self, info, request):
732 733 if hasattr(request, 'vcs_call'):
733 734 # skip vcs calls
734 735 return
735 736
736 737 user_group_id = info['match']['user_group_id']
737 738 user_group_model = user_group.UserGroup()
738 739 by_id_match = user_group_model.get(user_group_id, cache=False)
739 740
740 741 if by_id_match:
741 742 # register this as request object we can re-use later
742 743 request.db_user_group = by_id_match
743 744 return True
744 745
745 746 return False
746 747
747 748
748 749 class UserRoutePredicateBase(object):
749 750 supports_default = None
750 751
751 752 def __init__(self, val, config):
752 753 self.val = val
753 754
754 755 def text(self):
755 756 raise NotImplementedError()
756 757
757 758 def __call__(self, info, request):
758 759 if hasattr(request, 'vcs_call'):
759 760 # skip vcs calls
760 761 return
761 762
762 763 user_id = info['match']['user_id']
763 764 user_model = user.User()
764 765 by_id_match = user_model.get(user_id, cache=False)
765 766
766 767 if by_id_match:
767 768 # register this as request object we can re-use later
768 769 request.db_user = by_id_match
769 770 request.db_user_supports_default = self.supports_default
770 771 return True
771 772
772 773 return False
773 774
774 775
775 776 class UserRoutePredicate(UserRoutePredicateBase):
776 777 supports_default = False
777 778
778 779 def text(self):
779 780 return 'user_route = %s' % self.val
780 781
781 782 phash = text
782 783
783 784
784 785 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
785 786 supports_default = True
786 787
787 788 def text(self):
788 789 return 'user_with_default_route = %s' % self.val
789 790
790 791 phash = text
791 792
792 793
793 794 def includeme(config):
794 795 config.add_route_predicate(
795 796 'repo_route', RepoRoutePredicate)
796 797 config.add_route_predicate(
797 798 'repo_accepted_types', RepoTypeRoutePredicate)
798 799 config.add_route_predicate(
799 800 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
800 801 config.add_route_predicate(
801 802 'repo_group_route', RepoGroupRoutePredicate)
802 803 config.add_route_predicate(
803 804 'user_group_route', UserGroupRoutePredicate)
804 805 config.add_route_predicate(
805 806 'user_route_with_default', UserRouteWithDefaultPredicate)
806 807 config.add_route_predicate(
807 808 'user_route', UserRoutePredicate)
@@ -1,1614 +1,1618 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_ref_name
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError) as e:
185 185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if not repo.is_empty():
241 241 commit = repo.get_commit(commit_id=commit_id)
242 242 if commit:
243 243 branch_name = commit.branch
244 244 sha_commit_id = commit.raw_id
245 245
246 246 return branch_name, sha_commit_id, is_head
247 247
248 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249 249
250 250 repo_id = self.db_repo.repo_id
251 251 force_recache = self.get_recache_flag()
252 252
253 253 cache_seconds = safe_int(
254 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 255 cache_on = not force_recache and cache_seconds > 0
256 256 log.debug(
257 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 258 'with caching: %s[TTL: %ss]' % (
259 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 260
261 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 263
264 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
265 265 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
266 266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 267 ver, _repo_id, _commit_id, _f_path)
268 268
269 269 c.full_load = _full_load
270 270 return render(
271 271 'rhodecode:templates/files/files_browser_tree.mako',
272 272 self._get_template_context(c), self.request, _at_rev)
273 273
274 274 return compute_file_tree(
275 275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
276 276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277 277
278 278 def _get_archive_spec(self, fname):
279 279 log.debug('Detecting archive spec for: `%s`', fname)
280 280
281 281 fileformat = None
282 282 ext = None
283 283 content_type = None
284 284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285 285
286 286 if fname.endswith(extension):
287 287 fileformat = a_type
288 288 log.debug('archive is of type: %s', fileformat)
289 289 ext = extension
290 290 break
291 291
292 292 if not fileformat:
293 293 raise ValueError()
294 294
295 295 # left over part of whole fname is the commit
296 296 commit_id = fname[:-len(ext)]
297 297
298 298 return commit_id, ext, fileformat, content_type
299 299
300 300 def create_pure_path(self, *parts):
301 301 # Split paths and sanitize them, removing any ../ etc
302 302 sanitized_path = [
303 303 x for x in pathlib2.PurePath(*parts).parts
304 304 if x not in ['.', '..']]
305 305
306 306 pure_path = pathlib2.PurePath(*sanitized_path)
307 307 return pure_path
308 308
309 309 def _is_lf_enabled(self, target_repo):
310 310 lf_enabled = False
311 311
312 312 lf_key_for_vcs_map = {
313 313 'hg': 'extensions_largefiles',
314 314 'git': 'vcs_git_lfs_enabled'
315 315 }
316 316
317 317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318 318
319 319 if lf_key_for_vcs:
320 320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321 321
322 322 return lf_enabled
323 323
324 324 @LoginRequired()
325 325 @HasRepoPermissionAnyDecorator(
326 326 'repository.read', 'repository.write', 'repository.admin')
327 327 @view_config(
328 328 route_name='repo_archivefile', request_method='GET',
329 329 renderer=None)
330 330 def repo_archivefile(self):
331 331 # archive cache config
332 332 from rhodecode import CONFIG
333 333 _ = self.request.translate
334 334 self.load_default_context()
335 335 default_at_path = '/'
336 336 fname = self.request.matchdict['fname']
337 337 subrepos = self.request.GET.get('subrepos') == 'true'
338 338 at_path = self.request.GET.get('at_path') or default_at_path
339 339
340 340 if not self.db_repo.enable_downloads:
341 341 return Response(_('Downloads disabled'))
342 342
343 343 try:
344 344 commit_id, ext, fileformat, content_type = \
345 345 self._get_archive_spec(fname)
346 346 except ValueError:
347 347 return Response(_('Unknown archive type for: `{}`').format(
348 348 h.escape(fname)))
349 349
350 350 try:
351 351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 352 except CommitDoesNotExistError:
353 353 return Response(_('Unknown commit_id {}').format(
354 354 h.escape(commit_id)))
355 355 except EmptyRepositoryError:
356 356 return Response(_('Empty repository'))
357 357
358 358 try:
359 359 at_path = commit.get_node(at_path).path or default_at_path
360 360 except Exception:
361 361 return Response(_('No node at path {} for this repository').format(at_path))
362 362
363 363 path_sha = sha1(at_path)[:8]
364 364
365 365 # original backward compat name of archive
366 366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 367 short_sha = safe_str(commit.short_id)
368 368
369 369 if at_path == default_at_path:
370 370 archive_name = '{}-{}{}{}'.format(
371 371 clean_name,
372 372 '-sub' if subrepos else '',
373 373 short_sha,
374 374 ext)
375 375 # custom path and new name
376 376 else:
377 377 archive_name = '{}-{}{}-{}{}'.format(
378 378 clean_name,
379 379 '-sub' if subrepos else '',
380 380 short_sha,
381 381 path_sha,
382 382 ext)
383 383
384 384 use_cached_archive = False
385 385 archive_cache_enabled = CONFIG.get(
386 386 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 387 cached_archive_path = None
388 388
389 389 if archive_cache_enabled:
390 390 # check if we it's ok to write
391 391 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 392 os.makedirs(CONFIG['archive_cache_dir'])
393 393 cached_archive_path = os.path.join(
394 394 CONFIG['archive_cache_dir'], archive_name)
395 395 if os.path.isfile(cached_archive_path):
396 396 log.debug('Found cached archive in %s', cached_archive_path)
397 397 fd, archive = None, cached_archive_path
398 398 use_cached_archive = True
399 399 else:
400 400 log.debug('Archive %s is not yet cached', archive_name)
401 401
402 402 if not use_cached_archive:
403 403 # generate new archive
404 404 fd, archive = tempfile.mkstemp()
405 405 log.debug('Creating new temp archive in %s', archive)
406 406 try:
407 407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 408 archive_at_path=at_path)
409 409 except ImproperArchiveTypeError:
410 410 return _('Unknown archive type')
411 411 if archive_cache_enabled:
412 412 # if we generated the archive and we have cache enabled
413 413 # let's use this for future
414 414 log.debug('Storing new archive in %s', cached_archive_path)
415 415 shutil.move(archive, cached_archive_path)
416 416 archive = cached_archive_path
417 417
418 418 # store download action
419 419 audit_logger.store_web(
420 420 'repo.archive.download', action_data={
421 421 'user_agent': self.request.user_agent,
422 422 'archive_name': archive_name,
423 423 'archive_spec': fname,
424 424 'archive_cached': use_cached_archive},
425 425 user=self._rhodecode_user,
426 426 repo=self.db_repo,
427 427 commit=True
428 428 )
429 429
430 430 def get_chunked_archive(archive_path):
431 431 with open(archive_path, 'rb') as stream:
432 432 while True:
433 433 data = stream.read(16 * 1024)
434 434 if not data:
435 435 if fd: # fd means we used temporary file
436 436 os.close(fd)
437 437 if not archive_cache_enabled:
438 438 log.debug('Destroying temp archive %s', archive_path)
439 439 os.remove(archive_path)
440 440 break
441 441 yield data
442 442
443 443 response = Response(app_iter=get_chunked_archive(archive))
444 444 response.content_disposition = str(
445 445 'attachment; filename=%s' % archive_name)
446 446 response.content_type = str(content_type)
447 447
448 448 return response
449 449
450 450 def _get_file_node(self, commit_id, f_path):
451 451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 453 try:
454 454 node = commit.get_node(f_path)
455 455 if node.is_dir():
456 456 raise NodeError('%s path is a %s not a file'
457 457 % (node, type(node)))
458 458 except NodeDoesNotExistError:
459 459 commit = EmptyCommit(
460 460 commit_id=commit_id,
461 461 idx=commit.idx,
462 462 repo=commit.repository,
463 463 alias=commit.repository.alias,
464 464 message=commit.message,
465 465 author=commit.author,
466 466 date=commit.date)
467 467 node = FileNode(f_path, '', commit=commit)
468 468 else:
469 469 commit = EmptyCommit(
470 470 repo=self.rhodecode_vcs_repo,
471 471 alias=self.rhodecode_vcs_repo.alias)
472 472 node = FileNode(f_path, '', commit=commit)
473 473 return node
474 474
475 475 @LoginRequired()
476 476 @HasRepoPermissionAnyDecorator(
477 477 'repository.read', 'repository.write', 'repository.admin')
478 478 @view_config(
479 479 route_name='repo_files_diff', request_method='GET',
480 480 renderer=None)
481 481 def repo_files_diff(self):
482 482 c = self.load_default_context()
483 483 f_path = self._get_f_path(self.request.matchdict)
484 484 diff1 = self.request.GET.get('diff1', '')
485 485 diff2 = self.request.GET.get('diff2', '')
486 486
487 487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488 488
489 489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 490 line_context = self.request.GET.get('context', 3)
491 491
492 492 if not any((diff1, diff2)):
493 493 h.flash(
494 494 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 495 category='error')
496 496 raise HTTPBadRequest()
497 497
498 498 c.action = self.request.GET.get('diff')
499 499 if c.action not in ['download', 'raw']:
500 500 compare_url = h.route_path(
501 501 'repo_compare',
502 502 repo_name=self.db_repo_name,
503 503 source_ref_type='rev',
504 504 source_ref=diff1,
505 505 target_repo=self.db_repo_name,
506 506 target_ref_type='rev',
507 507 target_ref=diff2,
508 508 _query=dict(f_path=f_path))
509 509 # redirect to new view if we render diff
510 510 raise HTTPFound(compare_url)
511 511
512 512 try:
513 513 node1 = self._get_file_node(diff1, path1)
514 514 node2 = self._get_file_node(diff2, f_path)
515 515 except (RepositoryError, NodeError):
516 516 log.exception("Exception while trying to get node from repository")
517 517 raise HTTPFound(
518 518 h.route_path('repo_files', repo_name=self.db_repo_name,
519 519 commit_id='tip', f_path=f_path))
520 520
521 521 if all(isinstance(node.commit, EmptyCommit)
522 522 for node in (node1, node2)):
523 523 raise HTTPNotFound()
524 524
525 525 c.commit_1 = node1.commit
526 526 c.commit_2 = node2.commit
527 527
528 528 if c.action == 'download':
529 529 _diff = diffs.get_gitdiff(node1, node2,
530 530 ignore_whitespace=ignore_whitespace,
531 531 context=line_context)
532 532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 533
534 534 response = Response(self.path_filter.get_raw_patch(diff))
535 535 response.content_type = 'text/plain'
536 536 response.content_disposition = (
537 537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 538 )
539 539 charset = self._get_default_encoding(c)
540 540 if charset:
541 541 response.charset = charset
542 542 return response
543 543
544 544 elif c.action == 'raw':
545 545 _diff = diffs.get_gitdiff(node1, node2,
546 546 ignore_whitespace=ignore_whitespace,
547 547 context=line_context)
548 548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549 549
550 550 response = Response(self.path_filter.get_raw_patch(diff))
551 551 response.content_type = 'text/plain'
552 552 charset = self._get_default_encoding(c)
553 553 if charset:
554 554 response.charset = charset
555 555 return response
556 556
557 557 # in case we ever end up here
558 558 raise HTTPNotFound()
559 559
560 560 @LoginRequired()
561 561 @HasRepoPermissionAnyDecorator(
562 562 'repository.read', 'repository.write', 'repository.admin')
563 563 @view_config(
564 564 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 565 renderer=None)
566 566 def repo_files_diff_2way_redirect(self):
567 567 """
568 568 Kept only to make OLD links work
569 569 """
570 570 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 571 diff1 = self.request.GET.get('diff1', '')
572 572 diff2 = self.request.GET.get('diff2', '')
573 573
574 574 if not any((diff1, diff2)):
575 575 h.flash(
576 576 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 577 category='error')
578 578 raise HTTPBadRequest()
579 579
580 580 compare_url = h.route_path(
581 581 'repo_compare',
582 582 repo_name=self.db_repo_name,
583 583 source_ref_type='rev',
584 584 source_ref=diff1,
585 585 target_ref_type='rev',
586 586 target_ref=diff2,
587 587 _query=dict(f_path=f_path, diffmode='sideside',
588 588 target_repo=self.db_repo_name,))
589 589 raise HTTPFound(compare_url)
590 590
591 591 @LoginRequired()
592 592 @view_config(
593 593 route_name='repo_files:default_commit', request_method='GET',
594 594 renderer=None)
595 595 def repo_files_default(self):
596 596 c = self.load_default_context()
597
598 landing_url = h.route_path(
599 'repo_files', repo_name=c.repo_name,
600 commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='',
601 _query={'at': c.rhodecode_db_repo.landing_ref_name})
597 ref_name = c.rhodecode_db_repo.landing_ref_name
598 landing_url = h.repo_files_by_ref_url(
599 c.rhodecode_db_repo.repo_name,
600 c.rhodecode_db_repo.repo_type,
601 f_path='',
602 ref_name=ref_name,
603 commit_id='tip',
604 query=dict(at=ref_name)
605 )
602 606
603 607 raise HTTPFound(landing_url)
604 608
605 609 @LoginRequired()
606 610 @HasRepoPermissionAnyDecorator(
607 611 'repository.read', 'repository.write', 'repository.admin')
608 612 @view_config(
609 613 route_name='repo_files', request_method='GET',
610 614 renderer=None)
611 615 @view_config(
612 616 route_name='repo_files:default_path', request_method='GET',
613 617 renderer=None)
614 618 @view_config(
615 619 route_name='repo_files:rendered', request_method='GET',
616 620 renderer=None)
617 621 @view_config(
618 622 route_name='repo_files:annotated', request_method='GET',
619 623 renderer=None)
620 624 def repo_files(self):
621 625 c = self.load_default_context()
622 626
623 627 view_name = getattr(self.request.matched_route, 'name', None)
624 628
625 629 c.annotate = view_name == 'repo_files:annotated'
626 630 # default is false, but .rst/.md files later are auto rendered, we can
627 631 # overwrite auto rendering by setting this GET flag
628 632 c.renderer = view_name == 'repo_files:rendered' or \
629 633 not self.request.GET.get('no-render', False)
630 634
631 635 commit_id, f_path = self._get_commit_and_path()
632 636
633 637 c.commit = self._get_commit_or_redirect(commit_id)
634 638 c.branch = self.request.GET.get('branch', None)
635 639 c.f_path = f_path
636 640 at_rev = self.request.GET.get('at')
637 641
638 642 # prev link
639 643 try:
640 644 prev_commit = c.commit.prev(c.branch)
641 645 c.prev_commit = prev_commit
642 646 c.url_prev = h.route_path(
643 647 'repo_files', repo_name=self.db_repo_name,
644 648 commit_id=prev_commit.raw_id, f_path=f_path)
645 649 if c.branch:
646 650 c.url_prev += '?branch=%s' % c.branch
647 651 except (CommitDoesNotExistError, VCSError):
648 652 c.url_prev = '#'
649 653 c.prev_commit = EmptyCommit()
650 654
651 655 # next link
652 656 try:
653 657 next_commit = c.commit.next(c.branch)
654 658 c.next_commit = next_commit
655 659 c.url_next = h.route_path(
656 660 'repo_files', repo_name=self.db_repo_name,
657 661 commit_id=next_commit.raw_id, f_path=f_path)
658 662 if c.branch:
659 663 c.url_next += '?branch=%s' % c.branch
660 664 except (CommitDoesNotExistError, VCSError):
661 665 c.url_next = '#'
662 666 c.next_commit = EmptyCommit()
663 667
664 668 # files or dirs
665 669 try:
666 670 c.file = c.commit.get_node(f_path)
667 671 c.file_author = True
668 672 c.file_tree = ''
669 673
670 674 # load file content
671 675 if c.file.is_file():
672 676 c.lf_node = {}
673 677
674 678 has_lf_enabled = self._is_lf_enabled(self.db_repo)
675 679 if has_lf_enabled:
676 680 c.lf_node = c.file.get_largefile_node()
677 681
678 682 c.file_source_page = 'true'
679 683 c.file_last_commit = c.file.last_commit
680 684
681 685 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
682 686
683 687 if not (c.file_size_too_big or c.file.is_binary):
684 688 if c.annotate: # annotation has precedence over renderer
685 689 c.annotated_lines = filenode_as_annotated_lines_tokens(
686 690 c.file
687 691 )
688 692 else:
689 693 c.renderer = (
690 694 c.renderer and h.renderer_from_filename(c.file.path)
691 695 )
692 696 if not c.renderer:
693 697 c.lines = filenode_as_lines_tokens(c.file)
694 698
695 699 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
696 700 commit_id, self.rhodecode_vcs_repo)
697 701 c.on_branch_head = is_head
698 702
699 703 branch = c.commit.branch if (
700 704 c.commit.branch and '/' not in c.commit.branch) else None
701 705 c.branch_or_raw_id = branch or c.commit.raw_id
702 706 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
703 707
704 708 author = c.file_last_commit.author
705 709 c.authors = [[
706 710 h.email(author),
707 711 h.person(author, 'username_or_name_or_email'),
708 712 1
709 713 ]]
710 714
711 715 else: # load tree content at path
712 716 c.file_source_page = 'false'
713 717 c.authors = []
714 718 # this loads a simple tree without metadata to speed things up
715 719 # later via ajax we call repo_nodetree_full and fetch whole
716 720 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
717 721
718 722 c.readme_data, c.readme_file = \
719 723 self._get_readme_data(self.db_repo, c.visual.default_renderer,
720 724 c.commit.raw_id, f_path)
721 725
722 726 except RepositoryError as e:
723 727 h.flash(safe_str(h.escape(e)), category='error')
724 728 raise HTTPNotFound()
725 729
726 730 if self.request.environ.get('HTTP_X_PJAX'):
727 731 html = render('rhodecode:templates/files/files_pjax.mako',
728 732 self._get_template_context(c), self.request)
729 733 else:
730 734 html = render('rhodecode:templates/files/files.mako',
731 735 self._get_template_context(c), self.request)
732 736 return Response(html)
733 737
734 738 @HasRepoPermissionAnyDecorator(
735 739 'repository.read', 'repository.write', 'repository.admin')
736 740 @view_config(
737 741 route_name='repo_files:annotated_previous', request_method='GET',
738 742 renderer=None)
739 743 def repo_files_annotated_previous(self):
740 744 self.load_default_context()
741 745
742 746 commit_id, f_path = self._get_commit_and_path()
743 747 commit = self._get_commit_or_redirect(commit_id)
744 748 prev_commit_id = commit.raw_id
745 749 line_anchor = self.request.GET.get('line_anchor')
746 750 is_file = False
747 751 try:
748 752 _file = commit.get_node(f_path)
749 753 is_file = _file.is_file()
750 754 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
751 755 pass
752 756
753 757 if is_file:
754 758 history = commit.get_path_history(f_path)
755 759 prev_commit_id = history[1].raw_id \
756 760 if len(history) > 1 else prev_commit_id
757 761 prev_url = h.route_path(
758 762 'repo_files:annotated', repo_name=self.db_repo_name,
759 763 commit_id=prev_commit_id, f_path=f_path,
760 764 _anchor='L{}'.format(line_anchor))
761 765
762 766 raise HTTPFound(prev_url)
763 767
764 768 @LoginRequired()
765 769 @HasRepoPermissionAnyDecorator(
766 770 'repository.read', 'repository.write', 'repository.admin')
767 771 @view_config(
768 772 route_name='repo_nodetree_full', request_method='GET',
769 773 renderer=None, xhr=True)
770 774 @view_config(
771 775 route_name='repo_nodetree_full:default_path', request_method='GET',
772 776 renderer=None, xhr=True)
773 777 def repo_nodetree_full(self):
774 778 """
775 779 Returns rendered html of file tree that contains commit date,
776 780 author, commit_id for the specified combination of
777 781 repo, commit_id and file path
778 782 """
779 783 c = self.load_default_context()
780 784
781 785 commit_id, f_path = self._get_commit_and_path()
782 786 commit = self._get_commit_or_redirect(commit_id)
783 787 try:
784 788 dir_node = commit.get_node(f_path)
785 789 except RepositoryError as e:
786 790 return Response('error: {}'.format(h.escape(safe_str(e))))
787 791
788 792 if dir_node.is_file():
789 793 return Response('')
790 794
791 795 c.file = dir_node
792 796 c.commit = commit
793 797 at_rev = self.request.GET.get('at')
794 798
795 799 html = self._get_tree_at_commit(
796 800 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
797 801
798 802 return Response(html)
799 803
800 804 def _get_attachement_headers(self, f_path):
801 805 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
802 806 safe_path = f_name.replace('"', '\\"')
803 807 encoded_path = urllib.quote(f_name)
804 808
805 809 return "attachment; " \
806 810 "filename=\"{}\"; " \
807 811 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
808 812
809 813 @LoginRequired()
810 814 @HasRepoPermissionAnyDecorator(
811 815 'repository.read', 'repository.write', 'repository.admin')
812 816 @view_config(
813 817 route_name='repo_file_raw', request_method='GET',
814 818 renderer=None)
815 819 def repo_file_raw(self):
816 820 """
817 821 Action for show as raw, some mimetypes are "rendered",
818 822 those include images, icons.
819 823 """
820 824 c = self.load_default_context()
821 825
822 826 commit_id, f_path = self._get_commit_and_path()
823 827 commit = self._get_commit_or_redirect(commit_id)
824 828 file_node = self._get_filenode_or_redirect(commit, f_path)
825 829
826 830 raw_mimetype_mapping = {
827 831 # map original mimetype to a mimetype used for "show as raw"
828 832 # you can also provide a content-disposition to override the
829 833 # default "attachment" disposition.
830 834 # orig_type: (new_type, new_dispo)
831 835
832 836 # show images inline:
833 837 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
834 838 # for example render an SVG with javascript inside or even render
835 839 # HTML.
836 840 'image/x-icon': ('image/x-icon', 'inline'),
837 841 'image/png': ('image/png', 'inline'),
838 842 'image/gif': ('image/gif', 'inline'),
839 843 'image/jpeg': ('image/jpeg', 'inline'),
840 844 'application/pdf': ('application/pdf', 'inline'),
841 845 }
842 846
843 847 mimetype = file_node.mimetype
844 848 try:
845 849 mimetype, disposition = raw_mimetype_mapping[mimetype]
846 850 except KeyError:
847 851 # we don't know anything special about this, handle it safely
848 852 if file_node.is_binary:
849 853 # do same as download raw for binary files
850 854 mimetype, disposition = 'application/octet-stream', 'attachment'
851 855 else:
852 856 # do not just use the original mimetype, but force text/plain,
853 857 # otherwise it would serve text/html and that might be unsafe.
854 858 # Note: underlying vcs library fakes text/plain mimetype if the
855 859 # mimetype can not be determined and it thinks it is not
856 860 # binary.This might lead to erroneous text display in some
857 861 # cases, but helps in other cases, like with text files
858 862 # without extension.
859 863 mimetype, disposition = 'text/plain', 'inline'
860 864
861 865 if disposition == 'attachment':
862 866 disposition = self._get_attachement_headers(f_path)
863 867
864 868 stream_content = file_node.stream_bytes()
865 869
866 870 response = Response(app_iter=stream_content)
867 871 response.content_disposition = disposition
868 872 response.content_type = mimetype
869 873
870 874 charset = self._get_default_encoding(c)
871 875 if charset:
872 876 response.charset = charset
873 877
874 878 return response
875 879
876 880 @LoginRequired()
877 881 @HasRepoPermissionAnyDecorator(
878 882 'repository.read', 'repository.write', 'repository.admin')
879 883 @view_config(
880 884 route_name='repo_file_download', request_method='GET',
881 885 renderer=None)
882 886 @view_config(
883 887 route_name='repo_file_download:legacy', request_method='GET',
884 888 renderer=None)
885 889 def repo_file_download(self):
886 890 c = self.load_default_context()
887 891
888 892 commit_id, f_path = self._get_commit_and_path()
889 893 commit = self._get_commit_or_redirect(commit_id)
890 894 file_node = self._get_filenode_or_redirect(commit, f_path)
891 895
892 896 if self.request.GET.get('lf'):
893 897 # only if lf get flag is passed, we download this file
894 898 # as LFS/Largefile
895 899 lf_node = file_node.get_largefile_node()
896 900 if lf_node:
897 901 # overwrite our pointer with the REAL large-file
898 902 file_node = lf_node
899 903
900 904 disposition = self._get_attachement_headers(f_path)
901 905
902 906 stream_content = file_node.stream_bytes()
903 907
904 908 response = Response(app_iter=stream_content)
905 909 response.content_disposition = disposition
906 910 response.content_type = file_node.mimetype
907 911
908 912 charset = self._get_default_encoding(c)
909 913 if charset:
910 914 response.charset = charset
911 915
912 916 return response
913 917
914 918 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
915 919
916 920 cache_seconds = safe_int(
917 921 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
918 922 cache_on = cache_seconds > 0
919 923 log.debug(
920 924 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
921 925 'with caching: %s[TTL: %ss]' % (
922 926 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
923 927
924 928 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
925 929 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
926 930
927 931 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
928 932 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
929 933 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
930 934 _repo_id, commit_id, f_path)
931 935 try:
932 936 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
933 937 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
934 938 log.exception(safe_str(e))
935 939 h.flash(safe_str(h.escape(e)), category='error')
936 940 raise HTTPFound(h.route_path(
937 941 'repo_files', repo_name=self.db_repo_name,
938 942 commit_id='tip', f_path='/'))
939 943
940 944 return _d + _f
941 945
942 946 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
943 947 commit_id, f_path)
944 948 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
945 949
946 950 @LoginRequired()
947 951 @HasRepoPermissionAnyDecorator(
948 952 'repository.read', 'repository.write', 'repository.admin')
949 953 @view_config(
950 954 route_name='repo_files_nodelist', request_method='GET',
951 955 renderer='json_ext', xhr=True)
952 956 def repo_nodelist(self):
953 957 self.load_default_context()
954 958
955 959 commit_id, f_path = self._get_commit_and_path()
956 960 commit = self._get_commit_or_redirect(commit_id)
957 961
958 962 metadata = self._get_nodelist_at_commit(
959 963 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
960 964 return {'nodes': metadata}
961 965
962 966 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
963 967 items = []
964 968 for name, commit_id in branches_or_tags.items():
965 969 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
966 970 items.append((sym_ref, name, ref_type))
967 971 return items
968 972
969 973 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
970 974 return commit_id
971 975
972 976 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
973 977 return commit_id
974 978
975 979 # NOTE(dan): old code we used in "diff" mode compare
976 980 new_f_path = vcspath.join(name, f_path)
977 981 return u'%s@%s' % (new_f_path, commit_id)
978 982
979 983 def _get_node_history(self, commit_obj, f_path, commits=None):
980 984 """
981 985 get commit history for given node
982 986
983 987 :param commit_obj: commit to calculate history
984 988 :param f_path: path for node to calculate history for
985 989 :param commits: if passed don't calculate history and take
986 990 commits defined in this list
987 991 """
988 992 _ = self.request.translate
989 993
990 994 # calculate history based on tip
991 995 tip = self.rhodecode_vcs_repo.get_commit()
992 996 if commits is None:
993 997 pre_load = ["author", "branch"]
994 998 try:
995 999 commits = tip.get_path_history(f_path, pre_load=pre_load)
996 1000 except (NodeDoesNotExistError, CommitError):
997 1001 # this node is not present at tip!
998 1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
999 1003
1000 1004 history = []
1001 1005 commits_group = ([], _("Changesets"))
1002 1006 for commit in commits:
1003 1007 branch = ' (%s)' % commit.branch if commit.branch else ''
1004 1008 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1005 1009 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1006 1010 history.append(commits_group)
1007 1011
1008 1012 symbolic_reference = self._symbolic_reference
1009 1013
1010 1014 if self.rhodecode_vcs_repo.alias == 'svn':
1011 1015 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1012 1016 f_path, self.rhodecode_vcs_repo)
1013 1017 if adjusted_f_path != f_path:
1014 1018 log.debug(
1015 1019 'Recognized svn tag or branch in file "%s", using svn '
1016 1020 'specific symbolic references', f_path)
1017 1021 f_path = adjusted_f_path
1018 1022 symbolic_reference = self._symbolic_reference_svn
1019 1023
1020 1024 branches = self._create_references(
1021 1025 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1022 1026 branches_group = (branches, _("Branches"))
1023 1027
1024 1028 tags = self._create_references(
1025 1029 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1026 1030 tags_group = (tags, _("Tags"))
1027 1031
1028 1032 history.append(branches_group)
1029 1033 history.append(tags_group)
1030 1034
1031 1035 return history, commits
1032 1036
1033 1037 @LoginRequired()
1034 1038 @HasRepoPermissionAnyDecorator(
1035 1039 'repository.read', 'repository.write', 'repository.admin')
1036 1040 @view_config(
1037 1041 route_name='repo_file_history', request_method='GET',
1038 1042 renderer='json_ext')
1039 1043 def repo_file_history(self):
1040 1044 self.load_default_context()
1041 1045
1042 1046 commit_id, f_path = self._get_commit_and_path()
1043 1047 commit = self._get_commit_or_redirect(commit_id)
1044 1048 file_node = self._get_filenode_or_redirect(commit, f_path)
1045 1049
1046 1050 if file_node.is_file():
1047 1051 file_history, _hist = self._get_node_history(commit, f_path)
1048 1052
1049 1053 res = []
1050 1054 for section_items, section in file_history:
1051 1055 items = []
1052 1056 for obj_id, obj_text, obj_type in section_items:
1053 1057 at_rev = ''
1054 1058 if obj_type in ['branch', 'bookmark', 'tag']:
1055 1059 at_rev = obj_text
1056 1060 entry = {
1057 1061 'id': obj_id,
1058 1062 'text': obj_text,
1059 1063 'type': obj_type,
1060 1064 'at_rev': at_rev
1061 1065 }
1062 1066
1063 1067 items.append(entry)
1064 1068
1065 1069 res.append({
1066 1070 'text': section,
1067 1071 'children': items
1068 1072 })
1069 1073
1070 1074 data = {
1071 1075 'more': False,
1072 1076 'results': res
1073 1077 }
1074 1078 return data
1075 1079
1076 1080 log.warning('Cannot fetch history for directory')
1077 1081 raise HTTPBadRequest()
1078 1082
1079 1083 @LoginRequired()
1080 1084 @HasRepoPermissionAnyDecorator(
1081 1085 'repository.read', 'repository.write', 'repository.admin')
1082 1086 @view_config(
1083 1087 route_name='repo_file_authors', request_method='GET',
1084 1088 renderer='rhodecode:templates/files/file_authors_box.mako')
1085 1089 def repo_file_authors(self):
1086 1090 c = self.load_default_context()
1087 1091
1088 1092 commit_id, f_path = self._get_commit_and_path()
1089 1093 commit = self._get_commit_or_redirect(commit_id)
1090 1094 file_node = self._get_filenode_or_redirect(commit, f_path)
1091 1095
1092 1096 if not file_node.is_file():
1093 1097 raise HTTPBadRequest()
1094 1098
1095 1099 c.file_last_commit = file_node.last_commit
1096 1100 if self.request.GET.get('annotate') == '1':
1097 1101 # use _hist from annotation if annotation mode is on
1098 1102 commit_ids = set(x[1] for x in file_node.annotate)
1099 1103 _hist = (
1100 1104 self.rhodecode_vcs_repo.get_commit(commit_id)
1101 1105 for commit_id in commit_ids)
1102 1106 else:
1103 1107 _f_history, _hist = self._get_node_history(commit, f_path)
1104 1108 c.file_author = False
1105 1109
1106 1110 unique = collections.OrderedDict()
1107 1111 for commit in _hist:
1108 1112 author = commit.author
1109 1113 if author not in unique:
1110 1114 unique[commit.author] = [
1111 1115 h.email(author),
1112 1116 h.person(author, 'username_or_name_or_email'),
1113 1117 1 # counter
1114 1118 ]
1115 1119
1116 1120 else:
1117 1121 # increase counter
1118 1122 unique[commit.author][2] += 1
1119 1123
1120 1124 c.authors = [val for val in unique.values()]
1121 1125
1122 1126 return self._get_template_context(c)
1123 1127
1124 1128 @LoginRequired()
1125 1129 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1126 1130 @view_config(
1127 1131 route_name='repo_files_check_head', request_method='POST',
1128 1132 renderer='json_ext', xhr=True)
1129 1133 def repo_files_check_head(self):
1130 1134 self.load_default_context()
1131 1135
1132 1136 commit_id, f_path = self._get_commit_and_path()
1133 1137 _branch_name, _sha_commit_id, is_head = \
1134 1138 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1135 1139
1136 1140 new_path = self.request.POST.get('path')
1137 1141 operation = self.request.POST.get('operation')
1138 1142 path_exist = ''
1139 1143
1140 1144 if new_path and operation in ['create', 'upload']:
1141 1145 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1142 1146 try:
1143 1147 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1144 1148 # NOTE(dan): construct whole path without leading /
1145 1149 file_node = commit_obj.get_node(new_f_path)
1146 1150 if file_node is not None:
1147 1151 path_exist = new_f_path
1148 1152 except EmptyRepositoryError:
1149 1153 pass
1150 1154 except Exception:
1151 1155 pass
1152 1156
1153 1157 return {
1154 1158 'branch': _branch_name,
1155 1159 'sha': _sha_commit_id,
1156 1160 'is_head': is_head,
1157 1161 'path_exists': path_exist
1158 1162 }
1159 1163
1160 1164 @LoginRequired()
1161 1165 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1162 1166 @view_config(
1163 1167 route_name='repo_files_remove_file', request_method='GET',
1164 1168 renderer='rhodecode:templates/files/files_delete.mako')
1165 1169 def repo_files_remove_file(self):
1166 1170 _ = self.request.translate
1167 1171 c = self.load_default_context()
1168 1172 commit_id, f_path = self._get_commit_and_path()
1169 1173
1170 1174 self._ensure_not_locked()
1171 1175 _branch_name, _sha_commit_id, is_head = \
1172 1176 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1173 1177
1174 1178 self.forbid_non_head(is_head, f_path)
1175 1179 self.check_branch_permission(_branch_name)
1176 1180
1177 1181 c.commit = self._get_commit_or_redirect(commit_id)
1178 1182 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1179 1183
1180 1184 c.default_message = _(
1181 1185 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1182 1186 c.f_path = f_path
1183 1187
1184 1188 return self._get_template_context(c)
1185 1189
1186 1190 @LoginRequired()
1187 1191 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1188 1192 @CSRFRequired()
1189 1193 @view_config(
1190 1194 route_name='repo_files_delete_file', request_method='POST',
1191 1195 renderer=None)
1192 1196 def repo_files_delete_file(self):
1193 1197 _ = self.request.translate
1194 1198
1195 1199 c = self.load_default_context()
1196 1200 commit_id, f_path = self._get_commit_and_path()
1197 1201
1198 1202 self._ensure_not_locked()
1199 1203 _branch_name, _sha_commit_id, is_head = \
1200 1204 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1201 1205
1202 1206 self.forbid_non_head(is_head, f_path)
1203 1207 self.check_branch_permission(_branch_name)
1204 1208
1205 1209 c.commit = self._get_commit_or_redirect(commit_id)
1206 1210 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1207 1211
1208 1212 c.default_message = _(
1209 1213 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1210 1214 c.f_path = f_path
1211 1215 node_path = f_path
1212 1216 author = self._rhodecode_db_user.full_contact
1213 1217 message = self.request.POST.get('message') or c.default_message
1214 1218 try:
1215 1219 nodes = {
1216 1220 node_path: {
1217 1221 'content': ''
1218 1222 }
1219 1223 }
1220 1224 ScmModel().delete_nodes(
1221 1225 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1222 1226 message=message,
1223 1227 nodes=nodes,
1224 1228 parent_commit=c.commit,
1225 1229 author=author,
1226 1230 )
1227 1231
1228 1232 h.flash(
1229 1233 _('Successfully deleted file `{}`').format(
1230 1234 h.escape(f_path)), category='success')
1231 1235 except Exception:
1232 1236 log.exception('Error during commit operation')
1233 1237 h.flash(_('Error occurred during commit'), category='error')
1234 1238 raise HTTPFound(
1235 1239 h.route_path('repo_commit', repo_name=self.db_repo_name,
1236 1240 commit_id='tip'))
1237 1241
1238 1242 @LoginRequired()
1239 1243 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1240 1244 @view_config(
1241 1245 route_name='repo_files_edit_file', request_method='GET',
1242 1246 renderer='rhodecode:templates/files/files_edit.mako')
1243 1247 def repo_files_edit_file(self):
1244 1248 _ = self.request.translate
1245 1249 c = self.load_default_context()
1246 1250 commit_id, f_path = self._get_commit_and_path()
1247 1251
1248 1252 self._ensure_not_locked()
1249 1253 _branch_name, _sha_commit_id, is_head = \
1250 1254 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1251 1255
1252 1256 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1253 1257 self.check_branch_permission(_branch_name, commit_id=commit_id)
1254 1258
1255 1259 c.commit = self._get_commit_or_redirect(commit_id)
1256 1260 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1257 1261
1258 1262 if c.file.is_binary:
1259 1263 files_url = h.route_path(
1260 1264 'repo_files',
1261 1265 repo_name=self.db_repo_name,
1262 1266 commit_id=c.commit.raw_id, f_path=f_path)
1263 1267 raise HTTPFound(files_url)
1264 1268
1265 1269 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1266 1270 c.f_path = f_path
1267 1271
1268 1272 return self._get_template_context(c)
1269 1273
1270 1274 @LoginRequired()
1271 1275 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1272 1276 @CSRFRequired()
1273 1277 @view_config(
1274 1278 route_name='repo_files_update_file', request_method='POST',
1275 1279 renderer=None)
1276 1280 def repo_files_update_file(self):
1277 1281 _ = self.request.translate
1278 1282 c = self.load_default_context()
1279 1283 commit_id, f_path = self._get_commit_and_path()
1280 1284
1281 1285 self._ensure_not_locked()
1282 1286
1283 1287 c.commit = self._get_commit_or_redirect(commit_id)
1284 1288 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1285 1289
1286 1290 if c.file.is_binary:
1287 1291 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1288 1292 commit_id=c.commit.raw_id, f_path=f_path))
1289 1293
1290 1294 _branch_name, _sha_commit_id, is_head = \
1291 1295 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1292 1296
1293 1297 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1294 1298 self.check_branch_permission(_branch_name, commit_id=commit_id)
1295 1299
1296 1300 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1297 1301 c.f_path = f_path
1298 1302
1299 1303 old_content = c.file.content
1300 1304 sl = old_content.splitlines(1)
1301 1305 first_line = sl[0] if sl else ''
1302 1306
1303 1307 r_post = self.request.POST
1304 1308 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1305 1309 line_ending_mode = detect_mode(first_line, 0)
1306 1310 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1307 1311
1308 1312 message = r_post.get('message') or c.default_message
1309 1313 org_node_path = c.file.unicode_path
1310 1314 filename = r_post['filename']
1311 1315
1312 1316 root_path = c.file.dir_path
1313 1317 pure_path = self.create_pure_path(root_path, filename)
1314 1318 node_path = safe_unicode(bytes(pure_path))
1315 1319
1316 1320 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1317 1321 commit_id=commit_id)
1318 1322 if content == old_content and node_path == org_node_path:
1319 1323 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1320 1324 category='warning')
1321 1325 raise HTTPFound(default_redirect_url)
1322 1326
1323 1327 try:
1324 1328 mapping = {
1325 1329 org_node_path: {
1326 1330 'org_filename': org_node_path,
1327 1331 'filename': node_path,
1328 1332 'content': content,
1329 1333 'lexer': '',
1330 1334 'op': 'mod',
1331 1335 'mode': c.file.mode
1332 1336 }
1333 1337 }
1334 1338
1335 1339 commit = ScmModel().update_nodes(
1336 1340 user=self._rhodecode_db_user.user_id,
1337 1341 repo=self.db_repo,
1338 1342 message=message,
1339 1343 nodes=mapping,
1340 1344 parent_commit=c.commit,
1341 1345 )
1342 1346
1343 1347 h.flash(_('Successfully committed changes to file `{}`').format(
1344 1348 h.escape(f_path)), category='success')
1345 1349 default_redirect_url = h.route_path(
1346 1350 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1347 1351
1348 1352 except Exception:
1349 1353 log.exception('Error occurred during commit')
1350 1354 h.flash(_('Error occurred during commit'), category='error')
1351 1355
1352 1356 raise HTTPFound(default_redirect_url)
1353 1357
1354 1358 @LoginRequired()
1355 1359 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1356 1360 @view_config(
1357 1361 route_name='repo_files_add_file', request_method='GET',
1358 1362 renderer='rhodecode:templates/files/files_add.mako')
1359 1363 @view_config(
1360 1364 route_name='repo_files_upload_file', request_method='GET',
1361 1365 renderer='rhodecode:templates/files/files_upload.mako')
1362 1366 def repo_files_add_file(self):
1363 1367 _ = self.request.translate
1364 1368 c = self.load_default_context()
1365 1369 commit_id, f_path = self._get_commit_and_path()
1366 1370
1367 1371 self._ensure_not_locked()
1368 1372
1369 1373 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1370 1374 if c.commit is None:
1371 1375 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1372 1376
1373 1377 if self.rhodecode_vcs_repo.is_empty():
1374 1378 # for empty repository we cannot check for current branch, we rely on
1375 1379 # c.commit.branch instead
1376 1380 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1377 1381 else:
1378 1382 _branch_name, _sha_commit_id, is_head = \
1379 1383 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1380 1384
1381 1385 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1382 1386 self.check_branch_permission(_branch_name, commit_id=commit_id)
1383 1387
1384 1388 c.default_message = (_('Added file via RhodeCode Enterprise'))
1385 1389 c.f_path = f_path.lstrip('/') # ensure not relative path
1386 1390
1387 1391 return self._get_template_context(c)
1388 1392
1389 1393 @LoginRequired()
1390 1394 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1391 1395 @CSRFRequired()
1392 1396 @view_config(
1393 1397 route_name='repo_files_create_file', request_method='POST',
1394 1398 renderer=None)
1395 1399 def repo_files_create_file(self):
1396 1400 _ = self.request.translate
1397 1401 c = self.load_default_context()
1398 1402 commit_id, f_path = self._get_commit_and_path()
1399 1403
1400 1404 self._ensure_not_locked()
1401 1405
1402 1406 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1403 1407 if c.commit is None:
1404 1408 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1405 1409
1406 1410 # calculate redirect URL
1407 1411 if self.rhodecode_vcs_repo.is_empty():
1408 1412 default_redirect_url = h.route_path(
1409 1413 'repo_summary', repo_name=self.db_repo_name)
1410 1414 else:
1411 1415 default_redirect_url = h.route_path(
1412 1416 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1413 1417
1414 1418 if self.rhodecode_vcs_repo.is_empty():
1415 1419 # for empty repository we cannot check for current branch, we rely on
1416 1420 # c.commit.branch instead
1417 1421 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1418 1422 else:
1419 1423 _branch_name, _sha_commit_id, is_head = \
1420 1424 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1421 1425
1422 1426 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1423 1427 self.check_branch_permission(_branch_name, commit_id=commit_id)
1424 1428
1425 1429 c.default_message = (_('Added file via RhodeCode Enterprise'))
1426 1430 c.f_path = f_path
1427 1431
1428 1432 r_post = self.request.POST
1429 1433 message = r_post.get('message') or c.default_message
1430 1434 filename = r_post.get('filename')
1431 1435 unix_mode = 0
1432 1436 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1433 1437
1434 1438 if not filename:
1435 1439 # If there's no commit, redirect to repo summary
1436 1440 if type(c.commit) is EmptyCommit:
1437 1441 redirect_url = h.route_path(
1438 1442 'repo_summary', repo_name=self.db_repo_name)
1439 1443 else:
1440 1444 redirect_url = default_redirect_url
1441 1445 h.flash(_('No filename specified'), category='warning')
1442 1446 raise HTTPFound(redirect_url)
1443 1447
1444 1448 root_path = f_path
1445 1449 pure_path = self.create_pure_path(root_path, filename)
1446 1450 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1447 1451
1448 1452 author = self._rhodecode_db_user.full_contact
1449 1453 nodes = {
1450 1454 node_path: {
1451 1455 'content': content
1452 1456 }
1453 1457 }
1454 1458
1455 1459 try:
1456 1460
1457 1461 commit = ScmModel().create_nodes(
1458 1462 user=self._rhodecode_db_user.user_id,
1459 1463 repo=self.db_repo,
1460 1464 message=message,
1461 1465 nodes=nodes,
1462 1466 parent_commit=c.commit,
1463 1467 author=author,
1464 1468 )
1465 1469
1466 1470 h.flash(_('Successfully committed new file `{}`').format(
1467 1471 h.escape(node_path)), category='success')
1468 1472
1469 1473 default_redirect_url = h.route_path(
1470 1474 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1471 1475
1472 1476 except NonRelativePathError:
1473 1477 log.exception('Non Relative path found')
1474 1478 h.flash(_('The location specified must be a relative path and must not '
1475 1479 'contain .. in the path'), category='warning')
1476 1480 raise HTTPFound(default_redirect_url)
1477 1481 except (NodeError, NodeAlreadyExistsError) as e:
1478 1482 h.flash(_(h.escape(e)), category='error')
1479 1483 except Exception:
1480 1484 log.exception('Error occurred during commit')
1481 1485 h.flash(_('Error occurred during commit'), category='error')
1482 1486
1483 1487 raise HTTPFound(default_redirect_url)
1484 1488
1485 1489 @LoginRequired()
1486 1490 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1487 1491 @CSRFRequired()
1488 1492 @view_config(
1489 1493 route_name='repo_files_upload_file', request_method='POST',
1490 1494 renderer='json_ext')
1491 1495 def repo_files_upload_file(self):
1492 1496 _ = self.request.translate
1493 1497 c = self.load_default_context()
1494 1498 commit_id, f_path = self._get_commit_and_path()
1495 1499
1496 1500 self._ensure_not_locked()
1497 1501
1498 1502 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1499 1503 if c.commit is None:
1500 1504 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1501 1505
1502 1506 # calculate redirect URL
1503 1507 if self.rhodecode_vcs_repo.is_empty():
1504 1508 default_redirect_url = h.route_path(
1505 1509 'repo_summary', repo_name=self.db_repo_name)
1506 1510 else:
1507 1511 default_redirect_url = h.route_path(
1508 1512 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1509 1513
1510 1514 if self.rhodecode_vcs_repo.is_empty():
1511 1515 # for empty repository we cannot check for current branch, we rely on
1512 1516 # c.commit.branch instead
1513 1517 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1514 1518 else:
1515 1519 _branch_name, _sha_commit_id, is_head = \
1516 1520 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1517 1521
1518 1522 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1519 1523 if error:
1520 1524 return {
1521 1525 'error': error,
1522 1526 'redirect_url': default_redirect_url
1523 1527 }
1524 1528 error = self.check_branch_permission(_branch_name, json_mode=True)
1525 1529 if error:
1526 1530 return {
1527 1531 'error': error,
1528 1532 'redirect_url': default_redirect_url
1529 1533 }
1530 1534
1531 1535 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1532 1536 c.f_path = f_path
1533 1537
1534 1538 r_post = self.request.POST
1535 1539
1536 1540 message = c.default_message
1537 1541 user_message = r_post.getall('message')
1538 1542 if isinstance(user_message, list) and user_message:
1539 1543 # we take the first from duplicated results if it's not empty
1540 1544 message = user_message[0] if user_message[0] else message
1541 1545
1542 1546 nodes = {}
1543 1547
1544 1548 for file_obj in r_post.getall('files_upload') or []:
1545 1549 content = file_obj.file
1546 1550 filename = file_obj.filename
1547 1551
1548 1552 root_path = f_path
1549 1553 pure_path = self.create_pure_path(root_path, filename)
1550 1554 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1551 1555
1552 1556 nodes[node_path] = {
1553 1557 'content': content
1554 1558 }
1555 1559
1556 1560 if not nodes:
1557 1561 error = 'missing files'
1558 1562 return {
1559 1563 'error': error,
1560 1564 'redirect_url': default_redirect_url
1561 1565 }
1562 1566
1563 1567 author = self._rhodecode_db_user.full_contact
1564 1568
1565 1569 try:
1566 1570 commit = ScmModel().create_nodes(
1567 1571 user=self._rhodecode_db_user.user_id,
1568 1572 repo=self.db_repo,
1569 1573 message=message,
1570 1574 nodes=nodes,
1571 1575 parent_commit=c.commit,
1572 1576 author=author,
1573 1577 )
1574 1578 if len(nodes) == 1:
1575 1579 flash_message = _('Successfully committed {} new files').format(len(nodes))
1576 1580 else:
1577 1581 flash_message = _('Successfully committed 1 new file')
1578 1582
1579 1583 h.flash(flash_message, category='success')
1580 1584
1581 1585 default_redirect_url = h.route_path(
1582 1586 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1583 1587
1584 1588 except NonRelativePathError:
1585 1589 log.exception('Non Relative path found')
1586 1590 error = _('The location specified must be a relative path and must not '
1587 1591 'contain .. in the path')
1588 1592 h.flash(error, category='warning')
1589 1593
1590 1594 return {
1591 1595 'error': error,
1592 1596 'redirect_url': default_redirect_url
1593 1597 }
1594 1598 except (NodeError, NodeAlreadyExistsError) as e:
1595 1599 error = h.escape(e)
1596 1600 h.flash(error, category='error')
1597 1601
1598 1602 return {
1599 1603 'error': error,
1600 1604 'redirect_url': default_redirect_url
1601 1605 }
1602 1606 except Exception:
1603 1607 log.exception('Error occurred during commit')
1604 1608 error = _('Error occurred during commit')
1605 1609 h.flash(error, category='error')
1606 1610 return {
1607 1611 'error': error,
1608 1612 'redirect_url': default_redirect_url
1609 1613 }
1610 1614
1611 1615 return {
1612 1616 'error': None,
1613 1617 'redirect_url': default_redirect_url
1614 1618 }
@@ -1,1974 +1,2029 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers2.html import literal, HTML, escape
57 57 from webhelpers2.html._autolink import _auto_link_urls
58 58 from webhelpers2.html.tools import (
59 59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 60
61 61 from webhelpers2.text import (
62 62 chop_at, collapse, convert_accented_entities,
63 63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 65 from webhelpers2.date import time_ago_in_words
66 66
67 67 from webhelpers2.html.tags import (
68 68 _input, NotGiven, _make_safe_id_component as safeid,
69 69 form as insecure_form,
70 70 auto_discovery_link, checkbox, end_form, file,
71 71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 73 ul, radio, Options)
74 74
75 75 from webhelpers2.number import format_byte_size
76 76
77 77 from rhodecode.lib.action_parser import action_parser
78 78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 79 from rhodecode.lib.ext_json import json
80 80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 81 from rhodecode.lib.utils2 import (
82 82 str2bool, safe_unicode, safe_str,
83 83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 91 from rhodecode.model.db import Permission, User, Repository
92 92 from rhodecode.model.repo_group import RepoGroupModel
93 93 from rhodecode.model.settings import IssueTrackerSettingsModel
94 94
95 95
96 96 log = logging.getLogger(__name__)
97 97
98 98
99 99 DEFAULT_USER = User.DEFAULT_USER
100 100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101 101
102 102
103 103 def asset(path, ver=None, **kwargs):
104 104 """
105 105 Helper to generate a static asset file path for rhodecode assets
106 106
107 107 eg. h.asset('images/image.png', ver='3923')
108 108
109 109 :param path: path of asset
110 110 :param ver: optional version query param to append as ?ver=
111 111 """
112 112 request = get_current_request()
113 113 query = {}
114 114 query.update(kwargs)
115 115 if ver:
116 116 query = {'ver': ver}
117 117 return request.static_path(
118 118 'rhodecode:public/{}'.format(path), _query=query)
119 119
120 120
121 121 default_html_escape_table = {
122 122 ord('&'): u'&amp;',
123 123 ord('<'): u'&lt;',
124 124 ord('>'): u'&gt;',
125 125 ord('"'): u'&quot;',
126 126 ord("'"): u'&#39;',
127 127 }
128 128
129 129
130 130 def html_escape(text, html_escape_table=default_html_escape_table):
131 131 """Produce entities within text."""
132 132 return text.translate(html_escape_table)
133 133
134 134
135 135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 136 """
137 137 Truncate string ``s`` at the first occurrence of ``sub``.
138 138
139 139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 140 """
141 141 suffix_if_chopped = suffix_if_chopped or ''
142 142 pos = s.find(sub)
143 143 if pos == -1:
144 144 return s
145 145
146 146 if inclusive:
147 147 pos += len(sub)
148 148
149 149 chopped = s[:pos]
150 150 left = s[pos:].strip()
151 151
152 152 if left and suffix_if_chopped:
153 153 chopped += suffix_if_chopped
154 154
155 155 return chopped
156 156
157 157
158 158 def shorter(text, size=20, prefix=False):
159 159 postfix = '...'
160 160 if len(text) > size:
161 161 if prefix:
162 162 # shorten in front
163 163 return postfix + text[-(size - len(postfix)):]
164 164 else:
165 165 return text[:size - len(postfix)] + postfix
166 166 return text
167 167
168 168
169 169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 170 """
171 171 Reset button
172 172 """
173 173 return _input(type, name, value, id, attrs)
174 174
175 175
176 176 def select(name, selected_values, options, id=NotGiven, **attrs):
177 177
178 178 if isinstance(options, (list, tuple)):
179 179 options_iter = options
180 180 # Handle old value,label lists ... where value also can be value,label lists
181 181 options = Options()
182 182 for opt in options_iter:
183 183 if isinstance(opt, tuple) and len(opt) == 2:
184 184 value, label = opt
185 185 elif isinstance(opt, basestring):
186 186 value = label = opt
187 187 else:
188 188 raise ValueError('invalid select option type %r' % type(opt))
189 189
190 190 if isinstance(value, (list, tuple)):
191 191 option_group = options.add_optgroup(label)
192 192 for opt2 in value:
193 193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 194 group_value, group_label = opt2
195 195 elif isinstance(opt2, basestring):
196 196 group_value = group_label = opt2
197 197 else:
198 198 raise ValueError('invalid select option type %r' % type(opt2))
199 199
200 200 option_group.add_option(group_label, group_value)
201 201 else:
202 202 options.add_option(label, value)
203 203
204 204 return raw_select(name, selected_values, options, id=id, **attrs)
205 205
206 206
207 207 def branding(name, length=40):
208 208 return truncate(name, length, indicator="")
209 209
210 210
211 211 def FID(raw_id, path):
212 212 """
213 213 Creates a unique ID for filenode based on it's hash of path and commit
214 214 it's safe to use in urls
215 215
216 216 :param raw_id:
217 217 :param path:
218 218 """
219 219
220 220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221 221
222 222
223 223 class _GetError(object):
224 224 """Get error from form_errors, and represent it as span wrapped error
225 225 message
226 226
227 227 :param field_name: field to fetch errors for
228 228 :param form_errors: form errors dict
229 229 """
230 230
231 231 def __call__(self, field_name, form_errors):
232 232 tmpl = """<span class="error_msg">%s</span>"""
233 233 if form_errors and field_name in form_errors:
234 234 return literal(tmpl % form_errors.get(field_name))
235 235
236 236
237 237 get_error = _GetError()
238 238
239 239
240 240 class _ToolTip(object):
241 241
242 242 def __call__(self, tooltip_title, trim_at=50):
243 243 """
244 244 Special function just to wrap our text into nice formatted
245 245 autowrapped text
246 246
247 247 :param tooltip_title:
248 248 """
249 249 tooltip_title = escape(tooltip_title)
250 250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 251 return tooltip_title
252 252
253 253
254 254 tooltip = _ToolTip()
255 255
256 256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
257 257
258 258
259 def files_breadcrumbs(repo_name, commit_id, file_path, landing_ref_name=None, at_ref=None,
260 limit_items=False, linkify_last_item=False, hide_last_item=False, copy_path_icon=True):
259 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
260 limit_items=False, linkify_last_item=False, hide_last_item=False,
261 copy_path_icon=True):
261 262 if isinstance(file_path, str):
262 263 file_path = safe_unicode(file_path)
263 264
264 265 if at_ref:
265 266 route_qry = {'at': at_ref}
266 default_commit_id = at_ref or landing_ref_name or commit_id
267 default_landing_ref = at_ref or landing_ref_name or commit_id
267 268 else:
268 269 route_qry = None
269 default_commit_id = commit_id
270 default_landing_ref = commit_id
270 271
271 # first segment is a `..` link to repo files
272 # first segment is a `HOME` link to repo files root location
272 273 root_name = literal(u'<i class="icon-home"></i>')
274
273 275 url_segments = [
274 276 link_to(
275 277 root_name,
276 route_path(
277 'repo_files',
278 repo_name=repo_name,
279 commit_id=default_commit_id,
280 f_path='',
281 _query=route_qry),
278 repo_files_by_ref_url(
279 repo_name,
280 repo_type,
281 f_path=None, # None here is a special case for SVN repos,
282 # that won't prefix with a ref
283 ref_name=default_landing_ref,
284 commit_id=commit_id,
285 query=route_qry
286 )
282 287 )]
283 288
284 289 path_segments = file_path.split('/')
285 290 last_cnt = len(path_segments) - 1
286 291 for cnt, segment in enumerate(path_segments):
287 292 if not segment:
288 293 continue
289 294 segment_html = escape(segment)
290 295
291 296 last_item = cnt == last_cnt
292 297
293 298 if last_item and hide_last_item:
294 299 # iterate over and hide last element
295 300 continue
296 301
297 302 if last_item and linkify_last_item is False:
298 303 # plain version
299 304 url_segments.append(segment_html)
300 305 else:
301 306 url_segments.append(
302 307 link_to(
303 308 segment_html,
304 route_path(
305 'repo_files',
306 repo_name=repo_name,
307 commit_id=default_commit_id,
309 repo_files_by_ref_url(
310 repo_name,
311 repo_type,
308 312 f_path='/'.join(path_segments[:cnt + 1]),
309 _query=route_qry),
313 ref_name=default_landing_ref,
314 commit_id=commit_id,
315 query=route_qry
316 ),
310 317 ))
311 318
312 319 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
313 320 if limit_items and len(limited_url_segments) < len(url_segments):
314 321 url_segments = limited_url_segments
315 322
316 323 full_path = file_path
317 324 if copy_path_icon:
318 325 icon = files_icon.format(escape(full_path))
319 326 else:
320 327 icon = ''
321 328
322 329 if file_path == '':
323 330 return root_name
324 331 else:
325 332 return literal(' / '.join(url_segments) + icon)
326 333
327 334
328 335 def files_url_data(request):
329 336 matchdict = request.matchdict
330 337
331 338 if 'f_path' not in matchdict:
332 339 matchdict['f_path'] = ''
333 340
334 341 if 'commit_id' not in matchdict:
335 342 matchdict['commit_id'] = 'tip'
336 343
337 344 return json.dumps(matchdict)
338 345
339 346
347 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
348 _is_svn = is_svn(db_repo_type)
349 final_f_path = f_path
350
351 if _is_svn:
352 """
353 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
354 actually commit_id followed by the ref_name. This should be done only in case
355 This is a initial landing url, without additional paths.
356
357 like: /1000/tags/1.0.0/?at=tags/1.0.0
358 """
359
360 if ref_name and ref_name != 'tip':
361 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
362 # for SVN we only do this magic prefix if it's root, .eg landing revision
363 # of files link. If we are in the tree we don't need this since we traverse the url
364 # that has everything stored
365 if f_path in ['', '/']:
366 final_f_path = '/'.join([ref_name, f_path])
367
368 # SVN always needs a commit_id explicitly, without a named REF
369 default_commit_id = commit_id
370 else:
371 """
372 For git and mercurial we construct a new URL using the names instead of commit_id
373 like: /master/some_path?at=master
374 """
375 # We currently do not support branches with slashes
376 if '/' in ref_name:
377 default_commit_id = commit_id
378 else:
379 default_commit_id = ref_name
380
381 # sometimes we pass f_path as None, to indicate explicit no prefix,
382 # we translate it to string to not have None
383 final_f_path = final_f_path or ''
384
385 files_url = route_path(
386 'repo_files',
387 repo_name=db_repo_name,
388 commit_id=default_commit_id,
389 f_path=final_f_path,
390 _query=query
391 )
392 return files_url
393
394
340 395 def code_highlight(code, lexer, formatter, use_hl_filter=False):
341 396 """
342 397 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
343 398
344 399 If ``outfile`` is given and a valid file object (an object
345 400 with a ``write`` method), the result will be written to it, otherwise
346 401 it is returned as a string.
347 402 """
348 403 if use_hl_filter:
349 404 # add HL filter
350 405 from rhodecode.lib.index import search_utils
351 406 lexer.add_filter(search_utils.ElasticSearchHLFilter())
352 407 return pygments.format(pygments.lex(code, lexer), formatter)
353 408
354 409
355 410 class CodeHtmlFormatter(HtmlFormatter):
356 411 """
357 412 My code Html Formatter for source codes
358 413 """
359 414
360 415 def wrap(self, source, outfile):
361 416 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
362 417
363 418 def _wrap_code(self, source):
364 419 for cnt, it in enumerate(source):
365 420 i, t = it
366 421 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
367 422 yield i, t
368 423
369 424 def _wrap_tablelinenos(self, inner):
370 425 dummyoutfile = StringIO.StringIO()
371 426 lncount = 0
372 427 for t, line in inner:
373 428 if t:
374 429 lncount += 1
375 430 dummyoutfile.write(line)
376 431
377 432 fl = self.linenostart
378 433 mw = len(str(lncount + fl - 1))
379 434 sp = self.linenospecial
380 435 st = self.linenostep
381 436 la = self.lineanchors
382 437 aln = self.anchorlinenos
383 438 nocls = self.noclasses
384 439 if sp:
385 440 lines = []
386 441
387 442 for i in range(fl, fl + lncount):
388 443 if i % st == 0:
389 444 if i % sp == 0:
390 445 if aln:
391 446 lines.append('<a href="#%s%d" class="special">%*d</a>' %
392 447 (la, i, mw, i))
393 448 else:
394 449 lines.append('<span class="special">%*d</span>' % (mw, i))
395 450 else:
396 451 if aln:
397 452 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
398 453 else:
399 454 lines.append('%*d' % (mw, i))
400 455 else:
401 456 lines.append('')
402 457 ls = '\n'.join(lines)
403 458 else:
404 459 lines = []
405 460 for i in range(fl, fl + lncount):
406 461 if i % st == 0:
407 462 if aln:
408 463 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
409 464 else:
410 465 lines.append('%*d' % (mw, i))
411 466 else:
412 467 lines.append('')
413 468 ls = '\n'.join(lines)
414 469
415 470 # in case you wonder about the seemingly redundant <div> here: since the
416 471 # content in the other cell also is wrapped in a div, some browsers in
417 472 # some configurations seem to mess up the formatting...
418 473 if nocls:
419 474 yield 0, ('<table class="%stable">' % self.cssclass +
420 475 '<tr><td><div class="linenodiv" '
421 476 'style="background-color: #f0f0f0; padding-right: 10px">'
422 477 '<pre style="line-height: 125%">' +
423 478 ls + '</pre></div></td><td id="hlcode" class="code">')
424 479 else:
425 480 yield 0, ('<table class="%stable">' % self.cssclass +
426 481 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
427 482 ls + '</pre></div></td><td id="hlcode" class="code">')
428 483 yield 0, dummyoutfile.getvalue()
429 484 yield 0, '</td></tr></table>'
430 485
431 486
432 487 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
433 488 def __init__(self, **kw):
434 489 # only show these line numbers if set
435 490 self.only_lines = kw.pop('only_line_numbers', [])
436 491 self.query_terms = kw.pop('query_terms', [])
437 492 self.max_lines = kw.pop('max_lines', 5)
438 493 self.line_context = kw.pop('line_context', 3)
439 494 self.url = kw.pop('url', None)
440 495
441 496 super(CodeHtmlFormatter, self).__init__(**kw)
442 497
443 498 def _wrap_code(self, source):
444 499 for cnt, it in enumerate(source):
445 500 i, t = it
446 501 t = '<pre>%s</pre>' % t
447 502 yield i, t
448 503
449 504 def _wrap_tablelinenos(self, inner):
450 505 yield 0, '<table class="code-highlight %stable">' % self.cssclass
451 506
452 507 last_shown_line_number = 0
453 508 current_line_number = 1
454 509
455 510 for t, line in inner:
456 511 if not t:
457 512 yield t, line
458 513 continue
459 514
460 515 if current_line_number in self.only_lines:
461 516 if last_shown_line_number + 1 != current_line_number:
462 517 yield 0, '<tr>'
463 518 yield 0, '<td class="line">...</td>'
464 519 yield 0, '<td id="hlcode" class="code"></td>'
465 520 yield 0, '</tr>'
466 521
467 522 yield 0, '<tr>'
468 523 if self.url:
469 524 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
470 525 self.url, current_line_number, current_line_number)
471 526 else:
472 527 yield 0, '<td class="line"><a href="">%i</a></td>' % (
473 528 current_line_number)
474 529 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
475 530 yield 0, '</tr>'
476 531
477 532 last_shown_line_number = current_line_number
478 533
479 534 current_line_number += 1
480 535
481 536 yield 0, '</table>'
482 537
483 538
484 539 def hsv_to_rgb(h, s, v):
485 540 """ Convert hsv color values to rgb """
486 541
487 542 if s == 0.0:
488 543 return v, v, v
489 544 i = int(h * 6.0) # XXX assume int() truncates!
490 545 f = (h * 6.0) - i
491 546 p = v * (1.0 - s)
492 547 q = v * (1.0 - s * f)
493 548 t = v * (1.0 - s * (1.0 - f))
494 549 i = i % 6
495 550 if i == 0:
496 551 return v, t, p
497 552 if i == 1:
498 553 return q, v, p
499 554 if i == 2:
500 555 return p, v, t
501 556 if i == 3:
502 557 return p, q, v
503 558 if i == 4:
504 559 return t, p, v
505 560 if i == 5:
506 561 return v, p, q
507 562
508 563
509 564 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
510 565 """
511 566 Generator for getting n of evenly distributed colors using
512 567 hsv color and golden ratio. It always return same order of colors
513 568
514 569 :param n: number of colors to generate
515 570 :param saturation: saturation of returned colors
516 571 :param lightness: lightness of returned colors
517 572 :returns: RGB tuple
518 573 """
519 574
520 575 golden_ratio = 0.618033988749895
521 576 h = 0.22717784590367374
522 577
523 578 for _ in xrange(n):
524 579 h += golden_ratio
525 580 h %= 1
526 581 HSV_tuple = [h, saturation, lightness]
527 582 RGB_tuple = hsv_to_rgb(*HSV_tuple)
528 583 yield map(lambda x: str(int(x * 256)), RGB_tuple)
529 584
530 585
531 586 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
532 587 """
533 588 Returns a function which when called with an argument returns a unique
534 589 color for that argument, eg.
535 590
536 591 :param n: number of colors to generate
537 592 :param saturation: saturation of returned colors
538 593 :param lightness: lightness of returned colors
539 594 :returns: css RGB string
540 595
541 596 >>> color_hash = color_hasher()
542 597 >>> color_hash('hello')
543 598 'rgb(34, 12, 59)'
544 599 >>> color_hash('hello')
545 600 'rgb(34, 12, 59)'
546 601 >>> color_hash('other')
547 602 'rgb(90, 224, 159)'
548 603 """
549 604
550 605 color_dict = {}
551 606 cgenerator = unique_color_generator(
552 607 saturation=saturation, lightness=lightness)
553 608
554 609 def get_color_string(thing):
555 610 if thing in color_dict:
556 611 col = color_dict[thing]
557 612 else:
558 613 col = color_dict[thing] = cgenerator.next()
559 614 return "rgb(%s)" % (', '.join(col))
560 615
561 616 return get_color_string
562 617
563 618
564 619 def get_lexer_safe(mimetype=None, filepath=None):
565 620 """
566 621 Tries to return a relevant pygments lexer using mimetype/filepath name,
567 622 defaulting to plain text if none could be found
568 623 """
569 624 lexer = None
570 625 try:
571 626 if mimetype:
572 627 lexer = get_lexer_for_mimetype(mimetype)
573 628 if not lexer:
574 629 lexer = get_lexer_for_filename(filepath)
575 630 except pygments.util.ClassNotFound:
576 631 pass
577 632
578 633 if not lexer:
579 634 lexer = get_lexer_by_name('text')
580 635
581 636 return lexer
582 637
583 638
584 639 def get_lexer_for_filenode(filenode):
585 640 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
586 641 return lexer
587 642
588 643
589 644 def pygmentize(filenode, **kwargs):
590 645 """
591 646 pygmentize function using pygments
592 647
593 648 :param filenode:
594 649 """
595 650 lexer = get_lexer_for_filenode(filenode)
596 651 return literal(code_highlight(filenode.content, lexer,
597 652 CodeHtmlFormatter(**kwargs)))
598 653
599 654
600 655 def is_following_repo(repo_name, user_id):
601 656 from rhodecode.model.scm import ScmModel
602 657 return ScmModel().is_following_repo(repo_name, user_id)
603 658
604 659
605 660 class _Message(object):
606 661 """A message returned by ``Flash.pop_messages()``.
607 662
608 663 Converting the message to a string returns the message text. Instances
609 664 also have the following attributes:
610 665
611 666 * ``message``: the message text.
612 667 * ``category``: the category specified when the message was created.
613 668 """
614 669
615 670 def __init__(self, category, message, sub_data=None):
616 671 self.category = category
617 672 self.message = message
618 673 self.sub_data = sub_data or {}
619 674
620 675 def __str__(self):
621 676 return self.message
622 677
623 678 __unicode__ = __str__
624 679
625 680 def __html__(self):
626 681 return escape(safe_unicode(self.message))
627 682
628 683
629 684 class Flash(object):
630 685 # List of allowed categories. If None, allow any category.
631 686 categories = ["warning", "notice", "error", "success"]
632 687
633 688 # Default category if none is specified.
634 689 default_category = "notice"
635 690
636 691 def __init__(self, session_key="flash", categories=None,
637 692 default_category=None):
638 693 """
639 694 Instantiate a ``Flash`` object.
640 695
641 696 ``session_key`` is the key to save the messages under in the user's
642 697 session.
643 698
644 699 ``categories`` is an optional list which overrides the default list
645 700 of categories.
646 701
647 702 ``default_category`` overrides the default category used for messages
648 703 when none is specified.
649 704 """
650 705 self.session_key = session_key
651 706 if categories is not None:
652 707 self.categories = categories
653 708 if default_category is not None:
654 709 self.default_category = default_category
655 710 if self.categories and self.default_category not in self.categories:
656 711 raise ValueError(
657 712 "unrecognized default category %r" % (self.default_category,))
658 713
659 714 def pop_messages(self, session=None, request=None):
660 715 """
661 716 Return all accumulated messages and delete them from the session.
662 717
663 718 The return value is a list of ``Message`` objects.
664 719 """
665 720 messages = []
666 721
667 722 if not session:
668 723 if not request:
669 724 request = get_current_request()
670 725 session = request.session
671 726
672 727 # Pop the 'old' pylons flash messages. They are tuples of the form
673 728 # (category, message)
674 729 for cat, msg in session.pop(self.session_key, []):
675 730 messages.append(_Message(cat, msg))
676 731
677 732 # Pop the 'new' pyramid flash messages for each category as list
678 733 # of strings.
679 734 for cat in self.categories:
680 735 for msg in session.pop_flash(queue=cat):
681 736 sub_data = {}
682 737 if hasattr(msg, 'rsplit'):
683 738 flash_data = msg.rsplit('|DELIM|', 1)
684 739 org_message = flash_data[0]
685 740 if len(flash_data) > 1:
686 741 sub_data = json.loads(flash_data[1])
687 742 else:
688 743 org_message = msg
689 744
690 745 messages.append(_Message(cat, org_message, sub_data=sub_data))
691 746
692 747 # Map messages from the default queue to the 'notice' category.
693 748 for msg in session.pop_flash():
694 749 messages.append(_Message('notice', msg))
695 750
696 751 session.save()
697 752 return messages
698 753
699 754 def json_alerts(self, session=None, request=None):
700 755 payloads = []
701 756 messages = flash.pop_messages(session=session, request=request) or []
702 757 for message in messages:
703 758 payloads.append({
704 759 'message': {
705 760 'message': u'{}'.format(message.message),
706 761 'level': message.category,
707 762 'force': True,
708 763 'subdata': message.sub_data
709 764 }
710 765 })
711 766 return json.dumps(payloads)
712 767
713 768 def __call__(self, message, category=None, ignore_duplicate=True,
714 769 session=None, request=None):
715 770
716 771 if not session:
717 772 if not request:
718 773 request = get_current_request()
719 774 session = request.session
720 775
721 776 session.flash(
722 777 message, queue=category, allow_duplicate=not ignore_duplicate)
723 778
724 779
725 780 flash = Flash()
726 781
727 782 #==============================================================================
728 783 # SCM FILTERS available via h.
729 784 #==============================================================================
730 785 from rhodecode.lib.vcs.utils import author_name, author_email
731 786 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
732 787 from rhodecode.model.db import User, ChangesetStatus
733 788
734 789 capitalize = lambda x: x.capitalize()
735 790 email = author_email
736 791 short_id = lambda x: x[:12]
737 792 hide_credentials = lambda x: ''.join(credentials_filter(x))
738 793
739 794
740 795 import pytz
741 796 import tzlocal
742 797 local_timezone = tzlocal.get_localzone()
743 798
744 799
745 800 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
746 801 title = value or format_date(datetime_iso)
747 802 tzinfo = '+00:00'
748 803
749 804 # detect if we have a timezone info, otherwise, add it
750 805 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
751 806 force_timezone = os.environ.get('RC_TIMEZONE', '')
752 807 if force_timezone:
753 808 force_timezone = pytz.timezone(force_timezone)
754 809 timezone = force_timezone or local_timezone
755 810 offset = timezone.localize(datetime_iso).strftime('%z')
756 811 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
757 812
758 813 return literal(
759 814 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
760 815 cls='tooltip' if tooltip else '',
761 816 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
762 817 title=title, dt=datetime_iso, tzinfo=tzinfo
763 818 ))
764 819
765 820
766 821 def _shorten_commit_id(commit_id, commit_len=None):
767 822 if commit_len is None:
768 823 request = get_current_request()
769 824 commit_len = request.call_context.visual.show_sha_length
770 825 return commit_id[:commit_len]
771 826
772 827
773 828 def show_id(commit, show_idx=None, commit_len=None):
774 829 """
775 830 Configurable function that shows ID
776 831 by default it's r123:fffeeefffeee
777 832
778 833 :param commit: commit instance
779 834 """
780 835 if show_idx is None:
781 836 request = get_current_request()
782 837 show_idx = request.call_context.visual.show_revision_number
783 838
784 839 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
785 840 if show_idx:
786 841 return 'r%s:%s' % (commit.idx, raw_id)
787 842 else:
788 843 return '%s' % (raw_id, )
789 844
790 845
791 846 def format_date(date):
792 847 """
793 848 use a standardized formatting for dates used in RhodeCode
794 849
795 850 :param date: date/datetime object
796 851 :return: formatted date
797 852 """
798 853
799 854 if date:
800 855 _fmt = "%a, %d %b %Y %H:%M:%S"
801 856 return safe_unicode(date.strftime(_fmt))
802 857
803 858 return u""
804 859
805 860
806 861 class _RepoChecker(object):
807 862
808 863 def __init__(self, backend_alias):
809 864 self._backend_alias = backend_alias
810 865
811 866 def __call__(self, repository):
812 867 if hasattr(repository, 'alias'):
813 868 _type = repository.alias
814 869 elif hasattr(repository, 'repo_type'):
815 870 _type = repository.repo_type
816 871 else:
817 872 _type = repository
818 873 return _type == self._backend_alias
819 874
820 875
821 876 is_git = _RepoChecker('git')
822 877 is_hg = _RepoChecker('hg')
823 878 is_svn = _RepoChecker('svn')
824 879
825 880
826 881 def get_repo_type_by_name(repo_name):
827 882 repo = Repository.get_by_repo_name(repo_name)
828 883 if repo:
829 884 return repo.repo_type
830 885
831 886
832 887 def is_svn_without_proxy(repository):
833 888 if is_svn(repository):
834 889 from rhodecode.model.settings import VcsSettingsModel
835 890 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
836 891 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
837 892 return False
838 893
839 894
840 895 def discover_user(author):
841 896 """
842 897 Tries to discover RhodeCode User based on the author string. Author string
843 898 is typically `FirstName LastName <email@address.com>`
844 899 """
845 900
846 901 # if author is already an instance use it for extraction
847 902 if isinstance(author, User):
848 903 return author
849 904
850 905 # Valid email in the attribute passed, see if they're in the system
851 906 _email = author_email(author)
852 907 if _email != '':
853 908 user = User.get_by_email(_email, case_insensitive=True, cache=True)
854 909 if user is not None:
855 910 return user
856 911
857 912 # Maybe it's a username, we try to extract it and fetch by username ?
858 913 _author = author_name(author)
859 914 user = User.get_by_username(_author, case_insensitive=True, cache=True)
860 915 if user is not None:
861 916 return user
862 917
863 918 return None
864 919
865 920
866 921 def email_or_none(author):
867 922 # extract email from the commit string
868 923 _email = author_email(author)
869 924
870 925 # If we have an email, use it, otherwise
871 926 # see if it contains a username we can get an email from
872 927 if _email != '':
873 928 return _email
874 929 else:
875 930 user = User.get_by_username(
876 931 author_name(author), case_insensitive=True, cache=True)
877 932
878 933 if user is not None:
879 934 return user.email
880 935
881 936 # No valid email, not a valid user in the system, none!
882 937 return None
883 938
884 939
885 940 def link_to_user(author, length=0, **kwargs):
886 941 user = discover_user(author)
887 942 # user can be None, but if we have it already it means we can re-use it
888 943 # in the person() function, so we save 1 intensive-query
889 944 if user:
890 945 author = user
891 946
892 947 display_person = person(author, 'username_or_name_or_email')
893 948 if length:
894 949 display_person = shorter(display_person, length)
895 950
896 951 if user:
897 952 return link_to(
898 953 escape(display_person),
899 954 route_path('user_profile', username=user.username),
900 955 **kwargs)
901 956 else:
902 957 return escape(display_person)
903 958
904 959
905 960 def link_to_group(users_group_name, **kwargs):
906 961 return link_to(
907 962 escape(users_group_name),
908 963 route_path('user_group_profile', user_group_name=users_group_name),
909 964 **kwargs)
910 965
911 966
912 967 def person(author, show_attr="username_and_name"):
913 968 user = discover_user(author)
914 969 if user:
915 970 return getattr(user, show_attr)
916 971 else:
917 972 _author = author_name(author)
918 973 _email = email(author)
919 974 return _author or _email
920 975
921 976
922 977 def author_string(email):
923 978 if email:
924 979 user = User.get_by_email(email, case_insensitive=True, cache=True)
925 980 if user:
926 981 if user.first_name or user.last_name:
927 982 return '%s %s &lt;%s&gt;' % (
928 983 user.first_name, user.last_name, email)
929 984 else:
930 985 return email
931 986 else:
932 987 return email
933 988 else:
934 989 return None
935 990
936 991
937 992 def person_by_id(id_, show_attr="username_and_name"):
938 993 # attr to return from fetched user
939 994 person_getter = lambda usr: getattr(usr, show_attr)
940 995
941 996 #maybe it's an ID ?
942 997 if str(id_).isdigit() or isinstance(id_, int):
943 998 id_ = int(id_)
944 999 user = User.get(id_)
945 1000 if user is not None:
946 1001 return person_getter(user)
947 1002 return id_
948 1003
949 1004
950 1005 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
951 1006 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
952 1007 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
953 1008
954 1009
955 1010 tags_paterns = OrderedDict((
956 1011 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
957 1012 '<div class="metatag" tag="lang">\\2</div>')),
958 1013
959 1014 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
960 1015 '<div class="metatag" tag="see">see: \\1 </div>')),
961 1016
962 1017 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
963 1018 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
964 1019
965 1020 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
966 1021 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
967 1022
968 1023 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
969 1024 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
970 1025
971 1026 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
972 1027 '<div class="metatag" tag="state \\1">\\1</div>')),
973 1028
974 1029 # label in grey
975 1030 ('label', (re.compile(r'\[([a-z]+)\]'),
976 1031 '<div class="metatag" tag="label">\\1</div>')),
977 1032
978 1033 # generic catch all in grey
979 1034 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
980 1035 '<div class="metatag" tag="generic">\\1</div>')),
981 1036 ))
982 1037
983 1038
984 1039 def extract_metatags(value):
985 1040 """
986 1041 Extract supported meta-tags from given text value
987 1042 """
988 1043 tags = []
989 1044 if not value:
990 1045 return tags, ''
991 1046
992 1047 for key, val in tags_paterns.items():
993 1048 pat, replace_html = val
994 1049 tags.extend([(key, x.group()) for x in pat.finditer(value)])
995 1050 value = pat.sub('', value)
996 1051
997 1052 return tags, value
998 1053
999 1054
1000 1055 def style_metatag(tag_type, value):
1001 1056 """
1002 1057 converts tags from value into html equivalent
1003 1058 """
1004 1059 if not value:
1005 1060 return ''
1006 1061
1007 1062 html_value = value
1008 1063 tag_data = tags_paterns.get(tag_type)
1009 1064 if tag_data:
1010 1065 pat, replace_html = tag_data
1011 1066 # convert to plain `unicode` instead of a markup tag to be used in
1012 1067 # regex expressions. safe_unicode doesn't work here
1013 1068 html_value = pat.sub(replace_html, unicode(value))
1014 1069
1015 1070 return html_value
1016 1071
1017 1072
1018 1073 def bool2icon(value, show_at_false=True):
1019 1074 """
1020 1075 Returns boolean value of a given value, represented as html element with
1021 1076 classes that will represent icons
1022 1077
1023 1078 :param value: given value to convert to html node
1024 1079 """
1025 1080
1026 1081 if value: # does bool conversion
1027 1082 return HTML.tag('i', class_="icon-true", title='True')
1028 1083 else: # not true as bool
1029 1084 if show_at_false:
1030 1085 return HTML.tag('i', class_="icon-false", title='False')
1031 1086 return HTML.tag('i')
1032 1087
1033 1088 #==============================================================================
1034 1089 # PERMS
1035 1090 #==============================================================================
1036 1091 from rhodecode.lib.auth import (
1037 1092 HasPermissionAny, HasPermissionAll,
1038 1093 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1039 1094 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1040 1095 csrf_token_key, AuthUser)
1041 1096
1042 1097
1043 1098 #==============================================================================
1044 1099 # GRAVATAR URL
1045 1100 #==============================================================================
1046 1101 class InitialsGravatar(object):
1047 1102 def __init__(self, email_address, first_name, last_name, size=30,
1048 1103 background=None, text_color='#fff'):
1049 1104 self.size = size
1050 1105 self.first_name = first_name
1051 1106 self.last_name = last_name
1052 1107 self.email_address = email_address
1053 1108 self.background = background or self.str2color(email_address)
1054 1109 self.text_color = text_color
1055 1110
1056 1111 def get_color_bank(self):
1057 1112 """
1058 1113 returns a predefined list of colors that gravatars can use.
1059 1114 Those are randomized distinct colors that guarantee readability and
1060 1115 uniqueness.
1061 1116
1062 1117 generated with: http://phrogz.net/css/distinct-colors.html
1063 1118 """
1064 1119 return [
1065 1120 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1066 1121 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1067 1122 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1068 1123 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1069 1124 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1070 1125 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1071 1126 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1072 1127 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1073 1128 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1074 1129 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1075 1130 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1076 1131 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1077 1132 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1078 1133 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1079 1134 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1080 1135 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1081 1136 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1082 1137 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1083 1138 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1084 1139 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1085 1140 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1086 1141 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1087 1142 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1088 1143 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1089 1144 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1090 1145 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1091 1146 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1092 1147 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1093 1148 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1094 1149 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1095 1150 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1096 1151 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1097 1152 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1098 1153 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1099 1154 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1100 1155 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1101 1156 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1102 1157 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1103 1158 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1104 1159 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1105 1160 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1106 1161 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1107 1162 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1108 1163 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1109 1164 '#4f8c46', '#368dd9', '#5c0073'
1110 1165 ]
1111 1166
1112 1167 def rgb_to_hex_color(self, rgb_tuple):
1113 1168 """
1114 1169 Converts an rgb_tuple passed to an hex color.
1115 1170
1116 1171 :param rgb_tuple: tuple with 3 ints represents rgb color space
1117 1172 """
1118 1173 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1119 1174
1120 1175 def email_to_int_list(self, email_str):
1121 1176 """
1122 1177 Get every byte of the hex digest value of email and turn it to integer.
1123 1178 It's going to be always between 0-255
1124 1179 """
1125 1180 digest = md5_safe(email_str.lower())
1126 1181 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1127 1182
1128 1183 def pick_color_bank_index(self, email_str, color_bank):
1129 1184 return self.email_to_int_list(email_str)[0] % len(color_bank)
1130 1185
1131 1186 def str2color(self, email_str):
1132 1187 """
1133 1188 Tries to map in a stable algorithm an email to color
1134 1189
1135 1190 :param email_str:
1136 1191 """
1137 1192 color_bank = self.get_color_bank()
1138 1193 # pick position (module it's length so we always find it in the
1139 1194 # bank even if it's smaller than 256 values
1140 1195 pos = self.pick_color_bank_index(email_str, color_bank)
1141 1196 return color_bank[pos]
1142 1197
1143 1198 def normalize_email(self, email_address):
1144 1199 import unicodedata
1145 1200 # default host used to fill in the fake/missing email
1146 1201 default_host = u'localhost'
1147 1202
1148 1203 if not email_address:
1149 1204 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1150 1205
1151 1206 email_address = safe_unicode(email_address)
1152 1207
1153 1208 if u'@' not in email_address:
1154 1209 email_address = u'%s@%s' % (email_address, default_host)
1155 1210
1156 1211 if email_address.endswith(u'@'):
1157 1212 email_address = u'%s%s' % (email_address, default_host)
1158 1213
1159 1214 email_address = unicodedata.normalize('NFKD', email_address)\
1160 1215 .encode('ascii', 'ignore')
1161 1216 return email_address
1162 1217
1163 1218 def get_initials(self):
1164 1219 """
1165 1220 Returns 2 letter initials calculated based on the input.
1166 1221 The algorithm picks first given email address, and takes first letter
1167 1222 of part before @, and then the first letter of server name. In case
1168 1223 the part before @ is in a format of `somestring.somestring2` it replaces
1169 1224 the server letter with first letter of somestring2
1170 1225
1171 1226 In case function was initialized with both first and lastname, this
1172 1227 overrides the extraction from email by first letter of the first and
1173 1228 last name. We add special logic to that functionality, In case Full name
1174 1229 is compound, like Guido Von Rossum, we use last part of the last name
1175 1230 (Von Rossum) picking `R`.
1176 1231
1177 1232 Function also normalizes the non-ascii characters to they ascii
1178 1233 representation, eg Δ„ => A
1179 1234 """
1180 1235 import unicodedata
1181 1236 # replace non-ascii to ascii
1182 1237 first_name = unicodedata.normalize(
1183 1238 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1184 1239 last_name = unicodedata.normalize(
1185 1240 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1186 1241
1187 1242 # do NFKD encoding, and also make sure email has proper format
1188 1243 email_address = self.normalize_email(self.email_address)
1189 1244
1190 1245 # first push the email initials
1191 1246 prefix, server = email_address.split('@', 1)
1192 1247
1193 1248 # check if prefix is maybe a 'first_name.last_name' syntax
1194 1249 _dot_split = prefix.rsplit('.', 1)
1195 1250 if len(_dot_split) == 2 and _dot_split[1]:
1196 1251 initials = [_dot_split[0][0], _dot_split[1][0]]
1197 1252 else:
1198 1253 initials = [prefix[0], server[0]]
1199 1254
1200 1255 # then try to replace either first_name or last_name
1201 1256 fn_letter = (first_name or " ")[0].strip()
1202 1257 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1203 1258
1204 1259 if fn_letter:
1205 1260 initials[0] = fn_letter
1206 1261
1207 1262 if ln_letter:
1208 1263 initials[1] = ln_letter
1209 1264
1210 1265 return ''.join(initials).upper()
1211 1266
1212 1267 def get_img_data_by_type(self, font_family, img_type):
1213 1268 default_user = """
1214 1269 <svg xmlns="http://www.w3.org/2000/svg"
1215 1270 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1216 1271 viewBox="-15 -10 439.165 429.164"
1217 1272
1218 1273 xml:space="preserve"
1219 1274 style="background:{background};" >
1220 1275
1221 1276 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1222 1277 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1223 1278 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1224 1279 168.596,153.916,216.671,
1225 1280 204.583,216.671z" fill="{text_color}"/>
1226 1281 <path d="M407.164,374.717L360.88,
1227 1282 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1228 1283 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1229 1284 15.366-44.203,23.488-69.076,23.488c-24.877,
1230 1285 0-48.762-8.122-69.078-23.488
1231 1286 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1232 1287 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1233 1288 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1234 1289 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1235 1290 19.402-10.527 C409.699,390.129,
1236 1291 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1237 1292 </svg>""".format(
1238 1293 size=self.size,
1239 1294 background='#979797', # @grey4
1240 1295 text_color=self.text_color,
1241 1296 font_family=font_family)
1242 1297
1243 1298 return {
1244 1299 "default_user": default_user
1245 1300 }[img_type]
1246 1301
1247 1302 def get_img_data(self, svg_type=None):
1248 1303 """
1249 1304 generates the svg metadata for image
1250 1305 """
1251 1306 fonts = [
1252 1307 '-apple-system',
1253 1308 'BlinkMacSystemFont',
1254 1309 'Segoe UI',
1255 1310 'Roboto',
1256 1311 'Oxygen-Sans',
1257 1312 'Ubuntu',
1258 1313 'Cantarell',
1259 1314 'Helvetica Neue',
1260 1315 'sans-serif'
1261 1316 ]
1262 1317 font_family = ','.join(fonts)
1263 1318 if svg_type:
1264 1319 return self.get_img_data_by_type(font_family, svg_type)
1265 1320
1266 1321 initials = self.get_initials()
1267 1322 img_data = """
1268 1323 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1269 1324 width="{size}" height="{size}"
1270 1325 style="width: 100%; height: 100%; background-color: {background}"
1271 1326 viewBox="0 0 {size} {size}">
1272 1327 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1273 1328 pointer-events="auto" fill="{text_color}"
1274 1329 font-family="{font_family}"
1275 1330 style="font-weight: 400; font-size: {f_size}px;">{text}
1276 1331 </text>
1277 1332 </svg>""".format(
1278 1333 size=self.size,
1279 1334 f_size=self.size/2.05, # scale the text inside the box nicely
1280 1335 background=self.background,
1281 1336 text_color=self.text_color,
1282 1337 text=initials.upper(),
1283 1338 font_family=font_family)
1284 1339
1285 1340 return img_data
1286 1341
1287 1342 def generate_svg(self, svg_type=None):
1288 1343 img_data = self.get_img_data(svg_type)
1289 1344 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1290 1345
1291 1346
1292 1347 def initials_gravatar(email_address, first_name, last_name, size=30):
1293 1348 svg_type = None
1294 1349 if email_address == User.DEFAULT_USER_EMAIL:
1295 1350 svg_type = 'default_user'
1296 1351 klass = InitialsGravatar(email_address, first_name, last_name, size)
1297 1352 return klass.generate_svg(svg_type=svg_type)
1298 1353
1299 1354
1300 1355 def gravatar_url(email_address, size=30, request=None):
1301 1356 request = get_current_request()
1302 1357 _use_gravatar = request.call_context.visual.use_gravatar
1303 1358 _gravatar_url = request.call_context.visual.gravatar_url
1304 1359
1305 1360 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1306 1361
1307 1362 email_address = email_address or User.DEFAULT_USER_EMAIL
1308 1363 if isinstance(email_address, unicode):
1309 1364 # hashlib crashes on unicode items
1310 1365 email_address = safe_str(email_address)
1311 1366
1312 1367 # empty email or default user
1313 1368 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1314 1369 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1315 1370
1316 1371 if _use_gravatar:
1317 1372 # TODO: Disuse pyramid thread locals. Think about another solution to
1318 1373 # get the host and schema here.
1319 1374 request = get_current_request()
1320 1375 tmpl = safe_str(_gravatar_url)
1321 1376 tmpl = tmpl.replace('{email}', email_address)\
1322 1377 .replace('{md5email}', md5_safe(email_address.lower())) \
1323 1378 .replace('{netloc}', request.host)\
1324 1379 .replace('{scheme}', request.scheme)\
1325 1380 .replace('{size}', safe_str(size))
1326 1381 return tmpl
1327 1382 else:
1328 1383 return initials_gravatar(email_address, '', '', size=size)
1329 1384
1330 1385
1331 1386 def breadcrumb_repo_link(repo):
1332 1387 """
1333 1388 Makes a breadcrumbs path link to repo
1334 1389
1335 1390 ex::
1336 1391 group >> subgroup >> repo
1337 1392
1338 1393 :param repo: a Repository instance
1339 1394 """
1340 1395
1341 1396 path = [
1342 1397 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1343 1398 title='last change:{}'.format(format_date(group.last_commit_change)))
1344 1399 for group in repo.groups_with_parents
1345 1400 ] + [
1346 1401 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1347 1402 title='last change:{}'.format(format_date(repo.last_commit_change)))
1348 1403 ]
1349 1404
1350 1405 return literal(' &raquo; '.join(path))
1351 1406
1352 1407
1353 1408 def breadcrumb_repo_group_link(repo_group):
1354 1409 """
1355 1410 Makes a breadcrumbs path link to repo
1356 1411
1357 1412 ex::
1358 1413 group >> subgroup
1359 1414
1360 1415 :param repo_group: a Repository Group instance
1361 1416 """
1362 1417
1363 1418 path = [
1364 1419 link_to(group.name,
1365 1420 route_path('repo_group_home', repo_group_name=group.group_name),
1366 1421 title='last change:{}'.format(format_date(group.last_commit_change)))
1367 1422 for group in repo_group.parents
1368 1423 ] + [
1369 1424 link_to(repo_group.name,
1370 1425 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1371 1426 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1372 1427 ]
1373 1428
1374 1429 return literal(' &raquo; '.join(path))
1375 1430
1376 1431
1377 1432 def format_byte_size_binary(file_size):
1378 1433 """
1379 1434 Formats file/folder sizes to standard.
1380 1435 """
1381 1436 if file_size is None:
1382 1437 file_size = 0
1383 1438
1384 1439 formatted_size = format_byte_size(file_size, binary=True)
1385 1440 return formatted_size
1386 1441
1387 1442
1388 1443 def urlify_text(text_, safe=True, **href_attrs):
1389 1444 """
1390 1445 Extract urls from text and make html links out of them
1391 1446 """
1392 1447
1393 1448 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1394 1449 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1395 1450
1396 1451 def url_func(match_obj):
1397 1452 url_full = match_obj.groups()[0]
1398 1453 a_options = dict(href_attrs)
1399 1454 a_options['href'] = url_full
1400 1455 a_text = url_full
1401 1456 return HTML.tag("a", a_text, **a_options)
1402 1457
1403 1458 _new_text = url_pat.sub(url_func, text_)
1404 1459
1405 1460 if safe:
1406 1461 return literal(_new_text)
1407 1462 return _new_text
1408 1463
1409 1464
1410 1465 def urlify_commits(text_, repo_name):
1411 1466 """
1412 1467 Extract commit ids from text and make link from them
1413 1468
1414 1469 :param text_:
1415 1470 :param repo_name: repo name to build the URL with
1416 1471 """
1417 1472
1418 1473 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1419 1474
1420 1475 def url_func(match_obj):
1421 1476 commit_id = match_obj.groups()[1]
1422 1477 pref = match_obj.groups()[0]
1423 1478 suf = match_obj.groups()[2]
1424 1479
1425 1480 tmpl = (
1426 1481 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1427 1482 '%(commit_id)s</a>%(suf)s'
1428 1483 )
1429 1484 return tmpl % {
1430 1485 'pref': pref,
1431 1486 'cls': 'revision-link',
1432 1487 'url': route_url(
1433 1488 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1434 1489 'commit_id': commit_id,
1435 1490 'suf': suf,
1436 1491 'hovercard_alt': 'Commit: {}'.format(commit_id),
1437 1492 'hovercard_url': route_url(
1438 1493 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1439 1494 }
1440 1495
1441 1496 new_text = url_pat.sub(url_func, text_)
1442 1497
1443 1498 return new_text
1444 1499
1445 1500
1446 1501 def _process_url_func(match_obj, repo_name, uid, entry,
1447 1502 return_raw_data=False, link_format='html'):
1448 1503 pref = ''
1449 1504 if match_obj.group().startswith(' '):
1450 1505 pref = ' '
1451 1506
1452 1507 issue_id = ''.join(match_obj.groups())
1453 1508
1454 1509 if link_format == 'html':
1455 1510 tmpl = (
1456 1511 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1457 1512 '%(issue-prefix)s%(id-repr)s'
1458 1513 '</a>')
1459 1514 elif link_format == 'html+hovercard':
1460 1515 tmpl = (
1461 1516 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1462 1517 '%(issue-prefix)s%(id-repr)s'
1463 1518 '</a>')
1464 1519 elif link_format in ['rst', 'rst+hovercard']:
1465 1520 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1466 1521 elif link_format in ['markdown', 'markdown+hovercard']:
1467 1522 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1468 1523 else:
1469 1524 raise ValueError('Bad link_format:{}'.format(link_format))
1470 1525
1471 1526 (repo_name_cleaned,
1472 1527 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1473 1528
1474 1529 # variables replacement
1475 1530 named_vars = {
1476 1531 'id': issue_id,
1477 1532 'repo': repo_name,
1478 1533 'repo_name': repo_name_cleaned,
1479 1534 'group_name': parent_group_name,
1480 1535 # set dummy keys so we always have them
1481 1536 'hostname': '',
1482 1537 'netloc': '',
1483 1538 'scheme': ''
1484 1539 }
1485 1540
1486 1541 request = get_current_request()
1487 1542 if request:
1488 1543 # exposes, hostname, netloc, scheme
1489 1544 host_data = get_host_info(request)
1490 1545 named_vars.update(host_data)
1491 1546
1492 1547 # named regex variables
1493 1548 named_vars.update(match_obj.groupdict())
1494 1549 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1495 1550 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1496 1551 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1497 1552
1498 1553 def quote_cleaner(input_str):
1499 1554 """Remove quotes as it's HTML"""
1500 1555 return input_str.replace('"', '')
1501 1556
1502 1557 data = {
1503 1558 'pref': pref,
1504 1559 'cls': quote_cleaner('issue-tracker-link'),
1505 1560 'url': quote_cleaner(_url),
1506 1561 'id-repr': issue_id,
1507 1562 'issue-prefix': entry['pref'],
1508 1563 'serv': entry['url'],
1509 1564 'title': bleach.clean(desc, strip=True),
1510 1565 'hovercard_url': hovercard_url
1511 1566 }
1512 1567
1513 1568 if return_raw_data:
1514 1569 return {
1515 1570 'id': issue_id,
1516 1571 'url': _url
1517 1572 }
1518 1573 return tmpl % data
1519 1574
1520 1575
1521 1576 def get_active_pattern_entries(repo_name):
1522 1577 repo = None
1523 1578 if repo_name:
1524 1579 # Retrieving repo_name to avoid invalid repo_name to explode on
1525 1580 # IssueTrackerSettingsModel but still passing invalid name further down
1526 1581 repo = Repository.get_by_repo_name(repo_name, cache=True)
1527 1582
1528 1583 settings_model = IssueTrackerSettingsModel(repo=repo)
1529 1584 active_entries = settings_model.get_settings(cache=True)
1530 1585 return active_entries
1531 1586
1532 1587
1533 1588 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1534 1589
1535 1590
1536 1591 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1537 1592
1538 1593 allowed_formats = ['html', 'rst', 'markdown',
1539 1594 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1540 1595 if link_format not in allowed_formats:
1541 1596 raise ValueError('Link format can be only one of:{} got {}'.format(
1542 1597 allowed_formats, link_format))
1543 1598
1544 1599 if active_entries is None:
1545 1600 log.debug('Fetch active patterns for repo: %s', repo_name)
1546 1601 active_entries = get_active_pattern_entries(repo_name)
1547 1602
1548 1603 issues_data = []
1549 1604 new_text = text_string
1550 1605
1551 1606 log.debug('Got %s entries to process', len(active_entries))
1552 1607 for uid, entry in active_entries.items():
1553 1608 log.debug('found issue tracker entry with uid %s', uid)
1554 1609
1555 1610 if not (entry['pat'] and entry['url']):
1556 1611 log.debug('skipping due to missing data')
1557 1612 continue
1558 1613
1559 1614 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1560 1615 uid, entry['pat'], entry['url'], entry['pref'])
1561 1616
1562 1617 if entry.get('pat_compiled'):
1563 1618 pattern = entry['pat_compiled']
1564 1619 else:
1565 1620 try:
1566 1621 pattern = re.compile(r'%s' % entry['pat'])
1567 1622 except re.error:
1568 1623 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1569 1624 continue
1570 1625
1571 1626 data_func = partial(
1572 1627 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1573 1628 return_raw_data=True)
1574 1629
1575 1630 for match_obj in pattern.finditer(text_string):
1576 1631 issues_data.append(data_func(match_obj))
1577 1632
1578 1633 url_func = partial(
1579 1634 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1580 1635 link_format=link_format)
1581 1636
1582 1637 new_text = pattern.sub(url_func, new_text)
1583 1638 log.debug('processed prefix:uid `%s`', uid)
1584 1639
1585 1640 # finally use global replace, eg !123 -> pr-link, those will not catch
1586 1641 # if already similar pattern exists
1587 1642 server_url = '${scheme}://${netloc}'
1588 1643 pr_entry = {
1589 1644 'pref': '!',
1590 1645 'url': server_url + '/_admin/pull-requests/${id}',
1591 1646 'desc': 'Pull Request !${id}',
1592 1647 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1593 1648 }
1594 1649 pr_url_func = partial(
1595 1650 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1596 1651 link_format=link_format+'+hovercard')
1597 1652 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1598 1653 log.debug('processed !pr pattern')
1599 1654
1600 1655 return new_text, issues_data
1601 1656
1602 1657
1603 1658 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1604 1659 """
1605 1660 Parses given text message and makes proper links.
1606 1661 issues are linked to given issue-server, and rest is a commit link
1607 1662 """
1608 1663
1609 1664 def escaper(_text):
1610 1665 return _text.replace('<', '&lt;').replace('>', '&gt;')
1611 1666
1612 1667 new_text = escaper(commit_text)
1613 1668
1614 1669 # extract http/https links and make them real urls
1615 1670 new_text = urlify_text(new_text, safe=False)
1616 1671
1617 1672 # urlify commits - extract commit ids and make link out of them, if we have
1618 1673 # the scope of repository present.
1619 1674 if repository:
1620 1675 new_text = urlify_commits(new_text, repository)
1621 1676
1622 1677 # process issue tracker patterns
1623 1678 new_text, issues = process_patterns(new_text, repository or '',
1624 1679 active_entries=active_pattern_entries)
1625 1680
1626 1681 return literal(new_text)
1627 1682
1628 1683
1629 1684 def render_binary(repo_name, file_obj):
1630 1685 """
1631 1686 Choose how to render a binary file
1632 1687 """
1633 1688
1634 1689 # unicode
1635 1690 filename = file_obj.name
1636 1691
1637 1692 # images
1638 1693 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1639 1694 if fnmatch.fnmatch(filename, pat=ext):
1640 1695 src = route_path(
1641 1696 'repo_file_raw', repo_name=repo_name,
1642 1697 commit_id=file_obj.commit.raw_id,
1643 1698 f_path=file_obj.path)
1644 1699
1645 1700 return literal(
1646 1701 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1647 1702
1648 1703
1649 1704 def renderer_from_filename(filename, exclude=None):
1650 1705 """
1651 1706 choose a renderer based on filename, this works only for text based files
1652 1707 """
1653 1708
1654 1709 # ipython
1655 1710 for ext in ['*.ipynb']:
1656 1711 if fnmatch.fnmatch(filename, pat=ext):
1657 1712 return 'jupyter'
1658 1713
1659 1714 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1660 1715 if is_markup:
1661 1716 return is_markup
1662 1717 return None
1663 1718
1664 1719
1665 1720 def render(source, renderer='rst', mentions=False, relative_urls=None,
1666 1721 repo_name=None, active_pattern_entries=None):
1667 1722
1668 1723 def maybe_convert_relative_links(html_source):
1669 1724 if relative_urls:
1670 1725 return relative_links(html_source, relative_urls)
1671 1726 return html_source
1672 1727
1673 1728 if renderer == 'plain':
1674 1729 return literal(
1675 1730 MarkupRenderer.plain(source, leading_newline=False))
1676 1731
1677 1732 elif renderer == 'rst':
1678 1733 if repo_name:
1679 1734 # process patterns on comments if we pass in repo name
1680 1735 source, issues = process_patterns(
1681 1736 source, repo_name, link_format='rst',
1682 1737 active_entries=active_pattern_entries)
1683 1738
1684 1739 return literal(
1685 1740 '<div class="rst-block">%s</div>' %
1686 1741 maybe_convert_relative_links(
1687 1742 MarkupRenderer.rst(source, mentions=mentions)))
1688 1743
1689 1744 elif renderer == 'markdown':
1690 1745 if repo_name:
1691 1746 # process patterns on comments if we pass in repo name
1692 1747 source, issues = process_patterns(
1693 1748 source, repo_name, link_format='markdown',
1694 1749 active_entries=active_pattern_entries)
1695 1750
1696 1751 return literal(
1697 1752 '<div class="markdown-block">%s</div>' %
1698 1753 maybe_convert_relative_links(
1699 1754 MarkupRenderer.markdown(source, flavored=True,
1700 1755 mentions=mentions)))
1701 1756
1702 1757 elif renderer == 'jupyter':
1703 1758 return literal(
1704 1759 '<div class="ipynb">%s</div>' %
1705 1760 maybe_convert_relative_links(
1706 1761 MarkupRenderer.jupyter(source)))
1707 1762
1708 1763 # None means just show the file-source
1709 1764 return None
1710 1765
1711 1766
1712 1767 def commit_status(repo, commit_id):
1713 1768 return ChangesetStatusModel().get_status(repo, commit_id)
1714 1769
1715 1770
1716 1771 def commit_status_lbl(commit_status):
1717 1772 return dict(ChangesetStatus.STATUSES).get(commit_status)
1718 1773
1719 1774
1720 1775 def commit_time(repo_name, commit_id):
1721 1776 repo = Repository.get_by_repo_name(repo_name)
1722 1777 commit = repo.get_commit(commit_id=commit_id)
1723 1778 return commit.date
1724 1779
1725 1780
1726 1781 def get_permission_name(key):
1727 1782 return dict(Permission.PERMS).get(key)
1728 1783
1729 1784
1730 1785 def journal_filter_help(request):
1731 1786 _ = request.translate
1732 1787 from rhodecode.lib.audit_logger import ACTIONS
1733 1788 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1734 1789
1735 1790 return _(
1736 1791 'Example filter terms:\n' +
1737 1792 ' repository:vcs\n' +
1738 1793 ' username:marcin\n' +
1739 1794 ' username:(NOT marcin)\n' +
1740 1795 ' action:*push*\n' +
1741 1796 ' ip:127.0.0.1\n' +
1742 1797 ' date:20120101\n' +
1743 1798 ' date:[20120101100000 TO 20120102]\n' +
1744 1799 '\n' +
1745 1800 'Actions: {actions}\n' +
1746 1801 '\n' +
1747 1802 'Generate wildcards using \'*\' character:\n' +
1748 1803 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1749 1804 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1750 1805 '\n' +
1751 1806 'Optional AND / OR operators in queries\n' +
1752 1807 ' "repository:vcs OR repository:test"\n' +
1753 1808 ' "username:test AND repository:test*"\n'
1754 1809 ).format(actions=actions)
1755 1810
1756 1811
1757 1812 def not_mapped_error(repo_name):
1758 1813 from rhodecode.translation import _
1759 1814 flash(_('%s repository is not mapped to db perhaps'
1760 1815 ' it was created or renamed from the filesystem'
1761 1816 ' please run the application again'
1762 1817 ' in order to rescan repositories') % repo_name, category='error')
1763 1818
1764 1819
1765 1820 def ip_range(ip_addr):
1766 1821 from rhodecode.model.db import UserIpMap
1767 1822 s, e = UserIpMap._get_ip_range(ip_addr)
1768 1823 return '%s - %s' % (s, e)
1769 1824
1770 1825
1771 1826 def form(url, method='post', needs_csrf_token=True, **attrs):
1772 1827 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1773 1828 if method.lower() != 'get' and needs_csrf_token:
1774 1829 raise Exception(
1775 1830 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1776 1831 'CSRF token. If the endpoint does not require such token you can ' +
1777 1832 'explicitly set the parameter needs_csrf_token to false.')
1778 1833
1779 1834 return insecure_form(url, method=method, **attrs)
1780 1835
1781 1836
1782 1837 def secure_form(form_url, method="POST", multipart=False, **attrs):
1783 1838 """Start a form tag that points the action to an url. This
1784 1839 form tag will also include the hidden field containing
1785 1840 the auth token.
1786 1841
1787 1842 The url options should be given either as a string, or as a
1788 1843 ``url()`` function. The method for the form defaults to POST.
1789 1844
1790 1845 Options:
1791 1846
1792 1847 ``multipart``
1793 1848 If set to True, the enctype is set to "multipart/form-data".
1794 1849 ``method``
1795 1850 The method to use when submitting the form, usually either
1796 1851 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1797 1852 hidden input with name _method is added to simulate the verb
1798 1853 over POST.
1799 1854
1800 1855 """
1801 1856
1802 1857 if 'request' in attrs:
1803 1858 session = attrs['request'].session
1804 1859 del attrs['request']
1805 1860 else:
1806 1861 raise ValueError(
1807 1862 'Calling this form requires request= to be passed as argument')
1808 1863
1809 1864 _form = insecure_form(form_url, method, multipart, **attrs)
1810 1865 token = literal(
1811 1866 '<input type="hidden" name="{}" value="{}">'.format(
1812 1867 csrf_token_key, get_csrf_token(session)))
1813 1868
1814 1869 return literal("%s\n%s" % (_form, token))
1815 1870
1816 1871
1817 1872 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1818 1873 select_html = select(name, selected, options, **attrs)
1819 1874
1820 1875 select2 = """
1821 1876 <script>
1822 1877 $(document).ready(function() {
1823 1878 $('#%s').select2({
1824 1879 containerCssClass: 'drop-menu %s',
1825 1880 dropdownCssClass: 'drop-menu-dropdown',
1826 1881 dropdownAutoWidth: true%s
1827 1882 });
1828 1883 });
1829 1884 </script>
1830 1885 """
1831 1886
1832 1887 filter_option = """,
1833 1888 minimumResultsForSearch: -1
1834 1889 """
1835 1890 input_id = attrs.get('id') or name
1836 1891 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1837 1892 filter_enabled = "" if enable_filter else filter_option
1838 1893 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1839 1894
1840 1895 return literal(select_html+select_script)
1841 1896
1842 1897
1843 1898 def get_visual_attr(tmpl_context_var, attr_name):
1844 1899 """
1845 1900 A safe way to get a variable from visual variable of template context
1846 1901
1847 1902 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1848 1903 :param attr_name: name of the attribute we fetch from the c.visual
1849 1904 """
1850 1905 visual = getattr(tmpl_context_var, 'visual', None)
1851 1906 if not visual:
1852 1907 return
1853 1908 else:
1854 1909 return getattr(visual, attr_name, None)
1855 1910
1856 1911
1857 1912 def get_last_path_part(file_node):
1858 1913 if not file_node.path:
1859 1914 return u'/'
1860 1915
1861 1916 path = safe_unicode(file_node.path.split('/')[-1])
1862 1917 return u'../' + path
1863 1918
1864 1919
1865 1920 def route_url(*args, **kwargs):
1866 1921 """
1867 1922 Wrapper around pyramids `route_url` (fully qualified url) function.
1868 1923 """
1869 1924 req = get_current_request()
1870 1925 return req.route_url(*args, **kwargs)
1871 1926
1872 1927
1873 1928 def route_path(*args, **kwargs):
1874 1929 """
1875 1930 Wrapper around pyramids `route_path` function.
1876 1931 """
1877 1932 req = get_current_request()
1878 1933 return req.route_path(*args, **kwargs)
1879 1934
1880 1935
1881 1936 def route_path_or_none(*args, **kwargs):
1882 1937 try:
1883 1938 return route_path(*args, **kwargs)
1884 1939 except KeyError:
1885 1940 return None
1886 1941
1887 1942
1888 1943 def current_route_path(request, **kw):
1889 1944 new_args = request.GET.mixed()
1890 1945 new_args.update(kw)
1891 1946 return request.current_route_path(_query=new_args)
1892 1947
1893 1948
1894 1949 def curl_api_example(method, args):
1895 1950 args_json = json.dumps(OrderedDict([
1896 1951 ('id', 1),
1897 1952 ('auth_token', 'SECRET'),
1898 1953 ('method', method),
1899 1954 ('args', args)
1900 1955 ]))
1901 1956
1902 1957 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1903 1958 api_url=route_url('apiv2'),
1904 1959 args_json=args_json
1905 1960 )
1906 1961
1907 1962
1908 1963 def api_call_example(method, args):
1909 1964 """
1910 1965 Generates an API call example via CURL
1911 1966 """
1912 1967 curl_call = curl_api_example(method, args)
1913 1968
1914 1969 return literal(
1915 1970 curl_call +
1916 1971 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1917 1972 "and needs to be of `api calls` role."
1918 1973 .format(token_url=route_url('my_account_auth_tokens')))
1919 1974
1920 1975
1921 1976 def notification_description(notification, request):
1922 1977 """
1923 1978 Generate notification human readable description based on notification type
1924 1979 """
1925 1980 from rhodecode.model.notification import NotificationModel
1926 1981 return NotificationModel().make_description(
1927 1982 notification, translate=request.translate)
1928 1983
1929 1984
1930 1985 def go_import_header(request, db_repo=None):
1931 1986 """
1932 1987 Creates a header for go-import functionality in Go Lang
1933 1988 """
1934 1989
1935 1990 if not db_repo:
1936 1991 return
1937 1992 if 'go-get' not in request.GET:
1938 1993 return
1939 1994
1940 1995 clone_url = db_repo.clone_url()
1941 1996 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1942 1997 # we have a repo and go-get flag,
1943 1998 return literal('<meta name="go-import" content="{} {} {}">'.format(
1944 1999 prefix, db_repo.repo_type, clone_url))
1945 2000
1946 2001
1947 2002 def reviewer_as_json(*args, **kwargs):
1948 2003 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1949 2004 return _reviewer_as_json(*args, **kwargs)
1950 2005
1951 2006
1952 2007 def get_repo_view_type(request):
1953 2008 route_name = request.matched_route.name
1954 2009 route_to_view_type = {
1955 2010 'repo_changelog': 'commits',
1956 2011 'repo_commits': 'commits',
1957 2012 'repo_files': 'files',
1958 2013 'repo_summary': 'summary',
1959 2014 'repo_commit': 'commit'
1960 2015 }
1961 2016
1962 2017 return route_to_view_type.get(route_name)
1963 2018
1964 2019
1965 2020 def is_active(menu_entry, selected):
1966 2021 """
1967 2022 Returns active class for selecting menus in templates
1968 2023 <li class=${h.is_active('settings', current_active)}></li>
1969 2024 """
1970 2025 if not isinstance(menu_entry, list):
1971 2026 menu_entry = [menu_entry]
1972 2027
1973 2028 if selected in menu_entry:
1974 2029 return "active"
@@ -1,81 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2020 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 logging
22 22 from dogpile.cache import register_backend
23 23
24 24 register_backend(
25 25 "dogpile.cache.rc.memory_lru", "rhodecode.lib.rc_cache.backends",
26 26 "LRUMemoryBackend")
27 27
28 28 register_backend(
29 29 "dogpile.cache.rc.file_namespace", "rhodecode.lib.rc_cache.backends",
30 30 "FileNamespaceBackend")
31 31
32 32 register_backend(
33 33 "dogpile.cache.rc.redis", "rhodecode.lib.rc_cache.backends",
34 34 "RedisPickleBackend")
35 35
36 36 register_backend(
37 37 "dogpile.cache.rc.redis_msgpack", "rhodecode.lib.rc_cache.backends",
38 38 "RedisMsgPackBackend")
39 39
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43 from . import region_meta
44 44 from .utils import (
45 45 get_default_cache_settings, backend_key_generator, get_or_create_region,
46 46 clear_cache_namespace, make_region, InvalidationContext,
47 47 FreshRegionCache, ActiveRegionCache)
48 48
49 49
50 FILE_TREE_CACHE_VER = 'v3'
50 FILE_TREE_CACHE_VER = 'v4'
51 51
52 52
53 53 def configure_dogpile_cache(settings):
54 54 cache_dir = settings.get('cache_dir')
55 55 if cache_dir:
56 56 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
57 57
58 58 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
59 59
60 60 # inspect available namespaces
61 61 avail_regions = set()
62 62 for key in rc_cache_data.keys():
63 63 namespace_name = key.split('.', 1)[0]
64 64 avail_regions.add(namespace_name)
65 65 log.debug('dogpile: found following cache regions: %s', avail_regions)
66 66
67 67 # register them into namespace
68 68 for region_name in avail_regions:
69 69 new_region = make_region(
70 70 name=region_name,
71 71 function_key_generator=None
72 72 )
73 73
74 74 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
75 75 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
76 76 log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
77 77 region_meta.dogpile_cache_regions[region_name] = new_region
78 78
79 79
80 80 def includeme(config):
81 81 configure_dogpile_cache(config.registry.settings)
@@ -1,1201 +1,1201 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 4 ## base64 filter e.g ${ example | base64 }
5 5 def base64(text):
6 6 import base64
7 7 from rhodecode.lib.helpers import safe_str
8 8 return base64.encodestring(safe_str(text))
9 9 %>
10 10
11 11 <%inherit file="root.mako"/>
12 12
13 13 <%include file="/ejs_templates/templates.html"/>
14 14
15 15 <div class="outerwrapper">
16 16 <!-- HEADER -->
17 17 <div class="header">
18 18 <div id="header-inner" class="wrapper">
19 19 <div id="logo">
20 20 <div class="logo-wrapper">
21 21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
22 22 </div>
23 23 % if c.rhodecode_name:
24 24 <div class="branding">
25 25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
26 26 </div>
27 27 % endif
28 28 </div>
29 29 <!-- MENU BAR NAV -->
30 30 ${self.menu_bar_nav()}
31 31 <!-- END MENU BAR NAV -->
32 32 </div>
33 33 </div>
34 34 ${self.menu_bar_subnav()}
35 35 <!-- END HEADER -->
36 36
37 37 <!-- CONTENT -->
38 38 <div id="content" class="wrapper">
39 39
40 40 <rhodecode-toast id="notifications"></rhodecode-toast>
41 41
42 42 <div class="main">
43 43 ${next.main()}
44 44 </div>
45 45 </div>
46 46 <!-- END CONTENT -->
47 47
48 48 </div>
49 49 <!-- FOOTER -->
50 50 <div id="footer">
51 51 <div id="footer-inner" class="title wrapper">
52 52 <div>
53 53 <p class="footer-link-right">
54 54 % if c.visual.show_version:
55 55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
56 56 % endif
57 57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
58 58 % if c.visual.rhodecode_support_url:
59 59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
60 60 % endif
61 61 </p>
62 62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
63 63 <p class="server-instance" style="display:${sid}">
64 64 ## display hidden instance ID if specially defined
65 65 % if c.rhodecode_instanceid:
66 66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
67 67 % endif
68 68 </p>
69 69 </div>
70 70 </div>
71 71 </div>
72 72
73 73 <!-- END FOOTER -->
74 74
75 75 ### MAKO DEFS ###
76 76
77 77 <%def name="menu_bar_subnav()">
78 78 </%def>
79 79
80 80 <%def name="breadcrumbs(class_='breadcrumbs')">
81 81 <div class="${class_}">
82 82 ${self.breadcrumbs_links()}
83 83 </div>
84 84 </%def>
85 85
86 86 <%def name="admin_menu(active=None)">
87 87
88 88 <div id="context-bar">
89 89 <div class="wrapper">
90 90 <div class="title">
91 91 <div class="title-content">
92 92 <div class="title-main">
93 93 % if c.is_super_admin:
94 94 ${_('Super-admin Panel')}
95 95 % else:
96 96 ${_('Delegated Admin Panel')}
97 97 % endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102 <ul id="context-pages" class="navigation horizontal-list">
103 103
104 104 ## super-admin case
105 105 % if c.is_super_admin:
106 106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
107 107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
108 108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
109 109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
110 110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
111 111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
112 112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
113 113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
114 114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
115 115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
116 116
117 117 ## delegated admin
118 118 % elif c.is_delegated_admin:
119 119 <%
120 120 repositories=c.auth_user.repositories_admin or c.can_create_repo
121 121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
122 122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
123 123 %>
124 124
125 125 %if repositories:
126 126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
127 127 %endif
128 128 %if repository_groups:
129 129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
130 130 %endif
131 131 %if user_groups:
132 132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
133 133 %endif
134 134 % endif
135 135 </ul>
136 136
137 137 </div>
138 138 <div class="clear"></div>
139 139 </div>
140 140 </%def>
141 141
142 142 <%def name="dt_info_panel(elements)">
143 143 <dl class="dl-horizontal">
144 144 %for dt, dd, title, show_items in elements:
145 145 <dt>${dt}:</dt>
146 146 <dd title="${h.tooltip(title)}">
147 147 %if callable(dd):
148 148 ## allow lazy evaluation of elements
149 149 ${dd()}
150 150 %else:
151 151 ${dd}
152 152 %endif
153 153 %if show_items:
154 154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
155 155 %endif
156 156 </dd>
157 157
158 158 %if show_items:
159 159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
160 160 %for item in show_items:
161 161 <dt></dt>
162 162 <dd>${item}</dd>
163 163 %endfor
164 164 </div>
165 165 %endif
166 166
167 167 %endfor
168 168 </dl>
169 169 </%def>
170 170
171 171 <%def name="tr_info_entry(element)">
172 172 <% key, val, title, show_items = element %>
173 173
174 174 <tr>
175 175 <td style="vertical-align: top">${key}</td>
176 176 <td title="${h.tooltip(title)}">
177 177 %if callable(val):
178 178 ## allow lazy evaluation of elements
179 179 ${val()}
180 180 %else:
181 181 ${val}
182 182 %endif
183 183 %if show_items:
184 184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
185 185 % for item in show_items:
186 186 <dt></dt>
187 187 <dd>${item}</dd>
188 188 % endfor
189 189 </div>
190 190 %endif
191 191 </td>
192 192 <td style="vertical-align: top">
193 193 %if show_items:
194 194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
195 195 %endif
196 196 </td>
197 197 </tr>
198 198
199 199 </%def>
200 200
201 201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
202 202 <%
203 203 if size > 16:
204 204 gravatar_class = ['gravatar','gravatar-large']
205 205 else:
206 206 gravatar_class = ['gravatar']
207 207
208 208 data_hovercard_url = ''
209 209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
210 210
211 211 if tooltip:
212 212 gravatar_class += ['tooltip-hovercard']
213 213 if extra_class:
214 214 gravatar_class += extra_class
215 215 if tooltip and user:
216 216 if user.username == h.DEFAULT_USER:
217 217 gravatar_class.pop(-1)
218 218 else:
219 219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
220 220 gravatar_class = ' '.join(gravatar_class)
221 221
222 222 %>
223 223 <%doc>
224 224 TODO: johbo: For now we serve double size images to make it smooth
225 225 for retina. This is how it worked until now. Should be replaced
226 226 with a better solution at some point.
227 227 </%doc>
228 228
229 229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
230 230 </%def>
231 231
232 232
233 233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
234 234 <%
235 235 email = h.email_or_none(contact)
236 236 rc_user = h.discover_user(contact)
237 237 %>
238 238
239 239 <div class="${_class}">
240 240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
241 241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
242 242 </div>
243 243 </%def>
244 244
245 245
246 246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
247 247 <%
248 248 if (size > 16):
249 249 gravatar_class = 'icon-user-group-alt'
250 250 else:
251 251 gravatar_class = 'icon-user-group-alt'
252 252
253 253 if tooltip:
254 254 gravatar_class += ' tooltip-hovercard'
255 255
256 256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
257 257 %>
258 258 <%doc>
259 259 TODO: johbo: For now we serve double size images to make it smooth
260 260 for retina. This is how it worked until now. Should be replaced
261 261 with a better solution at some point.
262 262 </%doc>
263 263
264 264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
265 265 </%def>
266 266
267 267 <%def name="repo_page_title(repo_instance)">
268 268 <div class="title-content repo-title">
269 269
270 270 <div class="title-main">
271 271 ## SVN/HG/GIT icons
272 272 %if h.is_hg(repo_instance):
273 273 <i class="icon-hg"></i>
274 274 %endif
275 275 %if h.is_git(repo_instance):
276 276 <i class="icon-git"></i>
277 277 %endif
278 278 %if h.is_svn(repo_instance):
279 279 <i class="icon-svn"></i>
280 280 %endif
281 281
282 282 ## public/private
283 283 %if repo_instance.private:
284 284 <i class="icon-repo-private"></i>
285 285 %else:
286 286 <i class="icon-repo-public"></i>
287 287 %endif
288 288
289 289 ## repo name with group name
290 290 ${h.breadcrumb_repo_link(repo_instance)}
291 291
292 292 ## Context Actions
293 293 <div class="pull-right">
294 294 %if c.rhodecode_user.username != h.DEFAULT_USER:
295 295 <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>
296 296
297 297 <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 '')}">
298 298 % if c.repository_is_user_following:
299 299 <i class="icon-eye-off"></i>${_('Unwatch')}
300 300 % else:
301 301 <i class="icon-eye"></i>${_('Watch')}
302 302 % endif
303 303
304 304 </a>
305 305 %else:
306 306 <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>
307 307 %endif
308 308 </div>
309 309
310 310 </div>
311 311
312 312 ## FORKED
313 313 %if repo_instance.fork:
314 314 <p class="discreet">
315 315 <i class="icon-code-fork"></i> ${_('Fork of')}
316 316 ${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))}
317 317 </p>
318 318 %endif
319 319
320 320 ## IMPORTED FROM REMOTE
321 321 %if repo_instance.clone_uri:
322 322 <p class="discreet">
323 323 <i class="icon-code-fork"></i> ${_('Clone from')}
324 324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
325 325 </p>
326 326 %endif
327 327
328 328 ## LOCKING STATUS
329 329 %if repo_instance.locked[0]:
330 330 <p class="locking_locked discreet">
331 331 <i class="icon-repo-lock"></i>
332 332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
333 333 </p>
334 334 %elif repo_instance.enable_locking:
335 335 <p class="locking_unlocked discreet">
336 336 <i class="icon-repo-unlock"></i>
337 337 ${_('Repository not locked. Pull repository to lock it.')}
338 338 </p>
339 339 %endif
340 340
341 341 </div>
342 342 </%def>
343 343
344 344 <%def name="repo_menu(active=None)">
345 345 <%
346 346 ## determine if we have "any" option available
347 347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
348 348 has_actions = can_lock
349 349
350 350 %>
351 351 % if c.rhodecode_db_repo.archived:
352 352 <div class="alert alert-warning text-center">
353 353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
354 354 </div>
355 355 % endif
356 356
357 357 <!--- REPO CONTEXT BAR -->
358 358 <div id="context-bar">
359 359 <div class="wrapper">
360 360
361 361 <div class="title">
362 362 ${self.repo_page_title(c.rhodecode_db_repo)}
363 363 </div>
364 364
365 365 <ul id="context-pages" class="navigation horizontal-list">
366 366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
367 367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='', _query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
369 369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
370 370
371 371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
372 372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
373 373 <li class="${h.is_active('showpullrequest', active)}">
374 374 <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)}">
375 375 <div class="menulabel">
376 376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
377 377 </div>
378 378 </a>
379 379 </li>
380 380 %endif
381 381
382 382 <li class="${h.is_active('artifacts', active)}">
383 383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
384 384 <div class="menulabel">
385 385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
386 386 </div>
387 387 </a>
388 388 </li>
389 389
390 390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
391 391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
392 392 %endif
393 393
394 394 <li class="${h.is_active('options', active)}">
395 395 % if has_actions:
396 396 <a class="menulink dropdown">
397 397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
398 398 </a>
399 399 <ul class="submenu">
400 400 %if can_lock:
401 401 %if c.rhodecode_db_repo.locked[0]:
402 402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
403 403 %else:
404 404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
405 405 %endif
406 406 %endif
407 407 </ul>
408 408 % endif
409 409 </li>
410 410
411 411 </ul>
412 412 </div>
413 413 <div class="clear"></div>
414 414 </div>
415 415
416 416 <!--- REPO END CONTEXT BAR -->
417 417
418 418 </%def>
419 419
420 420 <%def name="repo_group_page_title(repo_group_instance)">
421 421 <div class="title-content">
422 422 <div class="title-main">
423 423 ## Repository Group icon
424 424 <i class="icon-repo-group"></i>
425 425
426 426 ## repo name with group name
427 427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
428 428 </div>
429 429
430 430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
431 431 <div class="repo-group-desc discreet">
432 432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
433 433 </div>
434 434
435 435 </div>
436 436 </%def>
437 437
438 438
439 439 <%def name="repo_group_menu(active=None)">
440 440 <%
441 441 gr_name = c.repo_group.group_name if c.repo_group else None
442 442 # create repositories with write permission on group is set to true
443 443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
444 444
445 445 %>
446 446
447 447
448 448 <!--- REPO GROUP CONTEXT BAR -->
449 449 <div id="context-bar">
450 450 <div class="wrapper">
451 451 <div class="title">
452 452 ${self.repo_group_page_title(c.repo_group)}
453 453 </div>
454 454
455 455 <ul id="context-pages" class="navigation horizontal-list">
456 456 <li class="${h.is_active('home', active)}">
457 457 <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>
458 458 </li>
459 459 % if c.is_super_admin or group_admin:
460 460 <li class="${h.is_active('settings', active)}">
461 461 <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>
462 462 </li>
463 463 % endif
464 464
465 465 </ul>
466 466 </div>
467 467 <div class="clear"></div>
468 468 </div>
469 469
470 470 <!--- REPO GROUP CONTEXT BAR -->
471 471
472 472 </%def>
473 473
474 474
475 475 <%def name="usermenu(active=False)">
476 476 <%
477 477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
478 478
479 479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
480 480 # create repositories with write permission on group is set to true
481 481
482 482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
483 483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
484 484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
485 485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
486 486
487 487 can_create_repos = c.is_super_admin or c.can_create_repo
488 488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
489 489
490 490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
491 491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
492 492 %>
493 493
494 494 % if not_anonymous:
495 495 <%
496 496 default_target_group = dict()
497 497 if c.rhodecode_user.personal_repo_group:
498 498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
499 499 %>
500 500
501 501 ## create action
502 502 <li>
503 503 <a href="#create-actions" onclick="return false;" class="menulink childs">
504 504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
505 505 </a>
506 506
507 507 <div class="action-menu submenu">
508 508
509 509 <ol>
510 510 ## scope of within a repository
511 511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
512 512 <li class="submenu-title">${_('This Repository')}</li>
513 513 <li>
514 514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
515 515 </li>
516 516 % if can_fork:
517 517 <li>
518 518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
519 519 </li>
520 520 % endif
521 521 % endif
522 522
523 523 ## scope of within repository groups
524 524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
525 525 <li class="submenu-title">${_('This Repository Group')}</li>
526 526
527 527 % if can_create_repos_in_group:
528 528 <li>
529 529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
530 530 </li>
531 531 % endif
532 532
533 533 % if can_create_repo_groups_in_group:
534 534 <li>
535 535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
536 536 </li>
537 537 % endif
538 538 % endif
539 539
540 540 ## personal group
541 541 % if c.rhodecode_user.personal_repo_group:
542 542 <li class="submenu-title">Personal Group</li>
543 543
544 544 <li>
545 545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
546 546 </li>
547 547
548 548 <li>
549 549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
550 550 </li>
551 551 % endif
552 552
553 553 ## Global actions
554 554 <li class="submenu-title">RhodeCode</li>
555 555 % if can_create_repos:
556 556 <li>
557 557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
558 558 </li>
559 559 % endif
560 560
561 561 % if can_create_repo_groups:
562 562 <li>
563 563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
564 564 </li>
565 565 % endif
566 566
567 567 <li>
568 568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
569 569 </li>
570 570
571 571 </ol>
572 572
573 573 </div>
574 574 </li>
575 575
576 576 ## notifications
577 577 <li>
578 578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
579 579 ${c.unread_notifications}
580 580 </a>
581 581 </li>
582 582 % endif
583 583
584 584 ## USER MENU
585 585 <li id="quick_login_li" class="${'active' if active else ''}">
586 586 % if c.rhodecode_user.username == h.DEFAULT_USER:
587 587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
588 588 ${gravatar(c.rhodecode_user.email, 20)}
589 589 <span class="user">
590 590 <span>${_('Sign in')}</span>
591 591 </span>
592 592 </a>
593 593 % else:
594 594 ## logged in user
595 595 <a id="quick_login_link" class="menulink childs">
596 596 ${gravatar(c.rhodecode_user.email, 20)}
597 597 <span class="user">
598 598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
599 599 <div class="show_more"></div>
600 600 </span>
601 601 </a>
602 602 ## subnav with menu for logged in user
603 603 <div class="user-menu submenu">
604 604 <div id="quick_login">
605 605 %if c.rhodecode_user.username != h.DEFAULT_USER:
606 606 <div class="">
607 607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
608 608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
609 609 <div class="email">${c.rhodecode_user.email}</div>
610 610 </div>
611 611 <div class="">
612 612 <ol class="links">
613 613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
614 614 % if c.rhodecode_user.personal_repo_group:
615 615 <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>
616 616 % endif
617 617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
618 618
619 619 % if c.debug_style:
620 620 <li>
621 621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
622 622 <div class="menulabel">${_('[Style]')}</div>
623 623 </a>
624 624 </li>
625 625 % endif
626 626
627 627 ## bookmark-items
628 628 <li class="bookmark-items">
629 629 ${_('Bookmarks')}
630 630 <div class="pull-right">
631 631 <a href="${h.route_path('my_account_bookmarks')}">
632 632
633 633 <i class="icon-cog"></i>
634 634 </a>
635 635 </div>
636 636 </li>
637 637 % if not c.bookmark_items:
638 638 <li>
639 639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
640 640 </li>
641 641 % endif
642 642 % for item in c.bookmark_items:
643 643 <li>
644 644 % if item.repository:
645 645 <div>
646 646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
647 647 <code>${item.position}</code>
648 648 % if item.repository.repo_type == 'hg':
649 649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
650 650 % elif item.repository.repo_type == 'git':
651 651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
652 652 % elif item.repository.repo_type == 'svn':
653 653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
654 654 % endif
655 655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
656 656 </a>
657 657 </div>
658 658 % elif item.repository_group:
659 659 <div>
660 660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
661 661 <code>${item.position}</code>
662 662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
663 663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
664 664 </a>
665 665 </div>
666 666 % else:
667 667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
668 668 <code>${item.position}</code>
669 669 ${item.title}
670 670 </a>
671 671 % endif
672 672 </li>
673 673 % endfor
674 674
675 675 <li class="logout">
676 676 ${h.secure_form(h.route_path('logout'), request=request)}
677 677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
678 678 ${h.end_form()}
679 679 </li>
680 680 </ol>
681 681 </div>
682 682 %endif
683 683 </div>
684 684 </div>
685 685
686 686 % endif
687 687 </li>
688 688 </%def>
689 689
690 690 <%def name="menu_items(active=None)">
691 691 <%
692 692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
693 693 notice_display = 'none' if len(notice_messages) == 0 else ''
694 694 %>
695 695 <style>
696 696
697 697 </style>
698 698
699 699 <ul id="quick" class="main_nav navigation horizontal-list">
700 700 ## notice box for important system messages
701 701 <li style="display: ${notice_display}">
702 702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
703 703 <div class="menulabel-notice ${notice_level}" >
704 704 ${len(notice_messages)}
705 705 </div>
706 706 </a>
707 707 </li>
708 708 <div class="notice-messages-container" style="display: none">
709 709 <div class="notice-messages">
710 710 <table class="rctable">
711 711 % for notice in notice_messages:
712 712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
713 713 <td style="vertical-align: text-top; width: 20px">
714 714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
715 715 </td>
716 716 <td>
717 717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
718 718 ${notice['subject']}
719 719
720 720 <div id="notice-${notice['msg_id']}" style="display: none">
721 721 ${h.render(notice['body'], renderer='markdown')}
722 722 </div>
723 723 </td>
724 724 <td style="vertical-align: text-top; width: 35px;">
725 725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
726 726 <i class="icon-remove icon-filled-red"></i>
727 727 </a>
728 728 </td>
729 729 </tr>
730 730
731 731 % endfor
732 732 </table>
733 733 </div>
734 734 </div>
735 735 ## Main filter
736 736 <li>
737 737 <div class="menulabel main_filter_box">
738 738 <div class="main_filter_input_box">
739 739 <ul class="searchItems">
740 740
741 741 <li class="searchTag searchTagIcon">
742 742 <i class="icon-search"></i>
743 743 </li>
744 744
745 745 % if c.template_context['search_context']['repo_id']:
746 746 <li class="searchTag searchTagFilter searchTagHidable" >
747 747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
748 748 <span class="tag">
749 749 This repo
750 750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
751 751 </span>
752 752 ##</a>
753 753 </li>
754 754 % elif c.template_context['search_context']['repo_group_id']:
755 755 <li class="searchTag searchTagFilter searchTagHidable">
756 756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
757 757 <span class="tag">
758 758 This group
759 759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
760 760 </span>
761 761 ##</a>
762 762 </li>
763 763 % endif
764 764
765 765 <li class="searchTagInput">
766 766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
767 767 </li>
768 768 <li class="searchTag searchTagHelp">
769 769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
770 770 </li>
771 771 </ul>
772 772 </div>
773 773 </div>
774 774
775 775 <div id="main_filter_help" style="display: none">
776 776 - Use '/' key to quickly access this field.
777 777
778 778 - Enter a name of repository, or repository group for quick search.
779 779
780 780 - Prefix query to allow special search:
781 781
782 782 user:admin, to search for usernames, always global
783 783
784 784 user_group:devops, to search for user groups, always global
785 785
786 786 pr:303, to search for pull request number, title, or description, always global
787 787
788 788 commit:efced4, to search for commits, scoped to repositories or groups
789 789
790 790 file:models.py, to search for file paths, scoped to repositories or groups
791 791
792 792 % if c.template_context['search_context']['repo_id']:
793 793 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>
794 794 % elif c.template_context['search_context']['repo_group_id']:
795 795 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>
796 796 % else:
797 797 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
798 798 % endif
799 799 </div>
800 800 </li>
801 801
802 802 ## ROOT MENU
803 803 <li class="${h.is_active('home', active)}">
804 804 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
805 805 <div class="menulabel">${_('Home')}</div>
806 806 </a>
807 807 </li>
808 808
809 809 %if c.rhodecode_user.username != h.DEFAULT_USER:
810 810 <li class="${h.is_active('journal', active)}">
811 811 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
812 812 <div class="menulabel">${_('Journal')}</div>
813 813 </a>
814 814 </li>
815 815 %else:
816 816 <li class="${h.is_active('journal', active)}">
817 817 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
818 818 <div class="menulabel">${_('Public journal')}</div>
819 819 </a>
820 820 </li>
821 821 %endif
822 822
823 823 <li class="${h.is_active('gists', active)}">
824 824 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
825 825 <div class="menulabel">${_('Gists')}</div>
826 826 </a>
827 827 </li>
828 828
829 829 % if c.is_super_admin or c.is_delegated_admin:
830 830 <li class="${h.is_active('admin', active)}">
831 831 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
832 832 <div class="menulabel">${_('Admin')} </div>
833 833 </a>
834 834 </li>
835 835 % endif
836 836
837 837 ## render extra user menu
838 838 ${usermenu(active=(active=='my_account'))}
839 839
840 840 </ul>
841 841
842 842 <script type="text/javascript">
843 843 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
844 844
845 845 var formatRepoResult = function(result, container, query, escapeMarkup) {
846 846 return function(data, escapeMarkup) {
847 847 if (!data.repo_id){
848 848 return data.text; // optgroup text Repositories
849 849 }
850 850
851 851 var tmpl = '';
852 852 var repoType = data['repo_type'];
853 853 var repoName = data['text'];
854 854
855 855 if(data && data.type == 'repo'){
856 856 if(repoType === 'hg'){
857 857 tmpl += '<i class="icon-hg"></i> ';
858 858 }
859 859 else if(repoType === 'git'){
860 860 tmpl += '<i class="icon-git"></i> ';
861 861 }
862 862 else if(repoType === 'svn'){
863 863 tmpl += '<i class="icon-svn"></i> ';
864 864 }
865 865 if(data['private']){
866 866 tmpl += '<i class="icon-lock" ></i> ';
867 867 }
868 868 else if(visualShowPublicIcon){
869 869 tmpl += '<i class="icon-unlock-alt"></i> ';
870 870 }
871 871 }
872 872 tmpl += escapeMarkup(repoName);
873 873 return tmpl;
874 874
875 875 }(result, escapeMarkup);
876 876 };
877 877
878 878 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
879 879 return function(data, escapeMarkup) {
880 880 if (!data.repo_group_id){
881 881 return data.text; // optgroup text Repositories
882 882 }
883 883
884 884 var tmpl = '';
885 885 var repoGroupName = data['text'];
886 886
887 887 if(data){
888 888
889 889 tmpl += '<i class="icon-repo-group"></i> ';
890 890
891 891 }
892 892 tmpl += escapeMarkup(repoGroupName);
893 893 return tmpl;
894 894
895 895 }(result, escapeMarkup);
896 896 };
897 897
898 898 var escapeRegExChars = function (value) {
899 899 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
900 900 };
901 901
902 902 var getRepoIcon = function(repo_type) {
903 903 if (repo_type === 'hg') {
904 904 return '<i class="icon-hg"></i> ';
905 905 }
906 906 else if (repo_type === 'git') {
907 907 return '<i class="icon-git"></i> ';
908 908 }
909 909 else if (repo_type === 'svn') {
910 910 return '<i class="icon-svn"></i> ';
911 911 }
912 912 return ''
913 913 };
914 914
915 915 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
916 916
917 917 if (value.split(':').length === 2) {
918 918 value = value.split(':')[1]
919 919 }
920 920
921 921 var searchType = data['type'];
922 922 var searchSubType = data['subtype'];
923 923 var valueDisplay = data['value_display'];
924 924 var valueIcon = data['value_icon'];
925 925
926 926 var pattern = '(' + escapeRegExChars(value) + ')';
927 927
928 928 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
929 929
930 930 // highlight match
931 931 if (searchType != 'text') {
932 932 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
933 933 }
934 934
935 935 var icon = '';
936 936
937 937 if (searchType === 'hint') {
938 938 icon += '<i class="icon-repo-group"></i> ';
939 939 }
940 940 // full text search/hints
941 941 else if (searchType === 'search') {
942 942 if (valueIcon === undefined) {
943 943 icon += '<i class="icon-more"></i> ';
944 944 } else {
945 945 icon += valueIcon + ' ';
946 946 }
947 947
948 948 if (searchSubType !== undefined && searchSubType == 'repo') {
949 949 valueDisplay += '<div class="pull-right tag">repository</div>';
950 950 }
951 951 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
952 952 valueDisplay += '<div class="pull-right tag">repo group</div>';
953 953 }
954 954 }
955 955 // repository
956 956 else if (searchType === 'repo') {
957 957
958 958 var repoIcon = getRepoIcon(data['repo_type']);
959 959 icon += repoIcon;
960 960
961 961 if (data['private']) {
962 962 icon += '<i class="icon-lock" ></i> ';
963 963 }
964 964 else if (visualShowPublicIcon) {
965 965 icon += '<i class="icon-unlock-alt"></i> ';
966 966 }
967 967 }
968 968 // repository groups
969 969 else if (searchType === 'repo_group') {
970 970 icon += '<i class="icon-repo-group"></i> ';
971 971 }
972 972 // user group
973 973 else if (searchType === 'user_group') {
974 974 icon += '<i class="icon-group"></i> ';
975 975 }
976 976 // user
977 977 else if (searchType === 'user') {
978 978 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
979 979 }
980 980 // pull request
981 981 else if (searchType === 'pull_request') {
982 982 icon += '<i class="icon-merge"></i> ';
983 983 }
984 984 // commit
985 985 else if (searchType === 'commit') {
986 986 var repo_data = data['repo_data'];
987 987 var repoIcon = getRepoIcon(repo_data['repository_type']);
988 988 if (repoIcon) {
989 989 icon += repoIcon;
990 990 } else {
991 991 icon += '<i class="icon-tag"></i>';
992 992 }
993 993 }
994 994 // file
995 995 else if (searchType === 'file') {
996 996 var repo_data = data['repo_data'];
997 997 var repoIcon = getRepoIcon(repo_data['repository_type']);
998 998 if (repoIcon) {
999 999 icon += repoIcon;
1000 1000 } else {
1001 1001 icon += '<i class="icon-tag"></i>';
1002 1002 }
1003 1003 }
1004 1004 // generic text
1005 1005 else if (searchType === 'text') {
1006 1006 icon = '';
1007 1007 }
1008 1008
1009 1009 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1010 1010 return tmpl.format(icon, valueDisplay);
1011 1011 };
1012 1012
1013 1013 var handleSelect = function(element, suggestion) {
1014 1014 if (suggestion.type === "hint") {
1015 1015 // we skip action
1016 1016 $('#main_filter').focus();
1017 1017 }
1018 1018 else if (suggestion.type === "text") {
1019 1019 // we skip action
1020 1020 $('#main_filter').focus();
1021 1021
1022 1022 } else {
1023 1023 window.location = suggestion['url'];
1024 1024 }
1025 1025 };
1026 1026
1027 1027 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1028 1028 if (queryLowerCase.split(':').length === 2) {
1029 1029 queryLowerCase = queryLowerCase.split(':')[1]
1030 1030 }
1031 1031 if (suggestion.type === "text") {
1032 1032 // special case we don't want to "skip" display for
1033 1033 return true
1034 1034 }
1035 1035 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1036 1036 };
1037 1037
1038 1038 var cleanContext = {
1039 1039 repo_view_type: null,
1040 1040
1041 1041 repo_id: null,
1042 1042 repo_name: "",
1043 1043
1044 1044 repo_group_id: null,
1045 1045 repo_group_name: null
1046 1046 };
1047 1047 var removeGoToFilter = function () {
1048 1048 $('.searchTagHidable').hide();
1049 1049 $('#main_filter').autocomplete(
1050 1050 'setOptions', {params:{search_context: cleanContext}});
1051 1051 };
1052 1052
1053 1053 $('#main_filter').autocomplete({
1054 1054 serviceUrl: pyroutes.url('goto_switcher_data'),
1055 1055 params: {
1056 1056 "search_context": templateContext.search_context
1057 1057 },
1058 1058 minChars:2,
1059 1059 maxHeight:400,
1060 1060 deferRequestBy: 300, //miliseconds
1061 1061 tabDisabled: true,
1062 1062 autoSelectFirst: false,
1063 1063 containerClass: 'autocomplete-qfilter-suggestions',
1064 1064 formatResult: autocompleteMainFilterFormatResult,
1065 1065 lookupFilter: autocompleteMainFilterResult,
1066 1066 onSelect: function (element, suggestion) {
1067 1067 handleSelect(element, suggestion);
1068 1068 return false;
1069 1069 },
1070 1070 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1071 1071 if (jqXHR !== 'abort') {
1072 1072 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1073 1073 SwalNoAnimation.fire({
1074 1074 icon: 'error',
1075 1075 title: _gettext('Error during search operation'),
1076 1076 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1077 1077 }).then(function(result) {
1078 1078 window.location.reload();
1079 1079 })
1080 1080 }
1081 1081 },
1082 1082 onSearchStart: function (params) {
1083 1083 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1084 1084 },
1085 1085 onSearchComplete: function (query, suggestions) {
1086 1086 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1087 1087 },
1088 1088 });
1089 1089
1090 1090 showMainFilterBox = function () {
1091 1091 $('#main_filter_help').toggle();
1092 1092 };
1093 1093
1094 1094 $('#main_filter').on('keydown.autocomplete', function (e) {
1095 1095
1096 1096 var BACKSPACE = 8;
1097 1097 var el = $(e.currentTarget);
1098 1098 if(e.which === BACKSPACE){
1099 1099 var inputVal = el.val();
1100 1100 if (inputVal === ""){
1101 1101 removeGoToFilter()
1102 1102 }
1103 1103 }
1104 1104 });
1105 1105
1106 1106 var dismissNotice = function(noticeId) {
1107 1107
1108 1108 var url = pyroutes.url('user_notice_dismiss',
1109 1109 {"user_id": templateContext.rhodecode_user.user_id});
1110 1110
1111 1111 var postData = {
1112 1112 'csrf_token': CSRF_TOKEN,
1113 1113 'notice_id': noticeId,
1114 1114 };
1115 1115
1116 1116 var success = function(response) {
1117 1117 $('#notice-message-' + noticeId).remove();
1118 1118 return false;
1119 1119 };
1120 1120 var failure = function(data, textStatus, xhr) {
1121 1121 alert("error processing request: " + textStatus);
1122 1122 return false;
1123 1123 };
1124 1124 ajaxPOST(url, postData, success, failure);
1125 1125 }
1126 1126 </script>
1127 1127 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1128 1128 </%def>
1129 1129
1130 1130 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1131 1131 <div class="modal-dialog">
1132 1132 <div class="modal-content">
1133 1133 <div class="modal-header">
1134 1134 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1135 1135 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1136 1136 </div>
1137 1137 <div class="modal-body">
1138 1138 <div class="block-left">
1139 1139 <table class="keyboard-mappings">
1140 1140 <tbody>
1141 1141 <tr>
1142 1142 <th></th>
1143 1143 <th>${_('Site-wide shortcuts')}</th>
1144 1144 </tr>
1145 1145 <%
1146 1146 elems = [
1147 1147 ('/', 'Use quick search box'),
1148 1148 ('g h', 'Goto home page'),
1149 1149 ('g g', 'Goto my private gists page'),
1150 1150 ('g G', 'Goto my public gists page'),
1151 1151 ('g 0-9', 'Goto bookmarked items from 0-9'),
1152 1152 ('n r', 'New repository page'),
1153 1153 ('n g', 'New gist page'),
1154 1154 ]
1155 1155 %>
1156 1156 %for key, desc in elems:
1157 1157 <tr>
1158 1158 <td class="keys">
1159 1159 <span class="key tag">${key}</span>
1160 1160 </td>
1161 1161 <td>${desc}</td>
1162 1162 </tr>
1163 1163 %endfor
1164 1164 </tbody>
1165 1165 </table>
1166 1166 </div>
1167 1167 <div class="block-left">
1168 1168 <table class="keyboard-mappings">
1169 1169 <tbody>
1170 1170 <tr>
1171 1171 <th></th>
1172 1172 <th>${_('Repositories')}</th>
1173 1173 </tr>
1174 1174 <%
1175 1175 elems = [
1176 1176 ('g s', 'Goto summary page'),
1177 1177 ('g c', 'Goto changelog page'),
1178 1178 ('g f', 'Goto files page'),
1179 1179 ('g F', 'Goto files page with file search activated'),
1180 1180 ('g p', 'Goto pull requests page'),
1181 1181 ('g o', 'Goto repository settings'),
1182 1182 ('g O', 'Goto repository access permissions settings'),
1183 1183 ]
1184 1184 %>
1185 1185 %for key, desc in elems:
1186 1186 <tr>
1187 1187 <td class="keys">
1188 1188 <span class="key tag">${key}</span>
1189 1189 </td>
1190 1190 <td>${desc}</td>
1191 1191 </tr>
1192 1192 %endfor
1193 1193 </tbody>
1194 1194 </table>
1195 1195 </div>
1196 1196 </div>
1197 1197 <div class="modal-footer">
1198 1198 </div>
1199 1199 </div><!-- /.modal-content -->
1200 1200 </div><!-- /.modal-dialog -->
1201 1201 </div><!-- /.modal -->
@@ -1,116 +1,116 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Files Add').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()"></%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='files')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23
24 24 <div class="edit-file-title">
25 25 <span class="title-heading">${_('Add new file')} @ <code>${h.show_id(c.commit)}</code></span>
26 26 % if c.commit.branch:
27 27 <span class="tag branchtag">
28 28 <i class="icon-branch"></i> ${c.commit.branch}
29 29 </span>
30 30 % endif
31 31 </div>
32 32
33 33 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 34 <div class="edit-file-fieldset">
35 35 <div class="path-items">
36 36 <ul>
37 37 <li class="breadcrumb-path">
38 38 <div>
39 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.f_path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=False, linkify_last_item=True, copy_path_icon=False)} /
39 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.f_path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=False, linkify_last_item=True, copy_path_icon=False)} /
40 40 </div>
41 41 </li>
42 42 <li class="location-path">
43 43 <input class="file-name-input input-small" type="text" value="${request.GET.get('filename')}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
44 44 </li>
45 45 </ul>
46 46 </div>
47 47
48 48 </div>
49 49
50 50 <div class="table">
51 51 <div>
52 52 <div id="codeblock" class="codeblock">
53 53 <div class="editor-items">
54 54 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
55 55 ${_('Edit')}
56 56 </div>
57 57
58 58 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
59 59 ${_('Preview')}
60 60 </div>
61 61
62 62 <div class="pull-right">
63 63 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off'))], extra_classes=['last-item'])}
64 64 </div>
65 65 <div class="pull-right">
66 66 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))], enable_filter=True)}
67 67 </div>
68 68 </div>
69 69
70 70 <div id="editor_container">
71 71 <pre id="editor_pre"></pre>
72 72 <textarea id="editor" name="content" ></textarea>
73 73 <div id="editor_preview"></div>
74 74 </div>
75 75 </div>
76 76 </div>
77 77 </div>
78 78
79 79 <div class="edit-file-fieldset">
80 80 <div class="fieldset">
81 81 <div class="message">
82 82 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
83 83 </div>
84 84 </div>
85 85 <div class="pull-left">
86 86 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
87 87 </div>
88 88 </div>
89 89 ${h.end_form()}
90 90 </div>
91 91
92 92 <script type="text/javascript">
93 93
94 94 $(document).ready(function () {
95 95 var modes_select = $('#set_mode');
96 96 var filename_selector = '#filename';
97 97 fillCodeMirrorOptions(modes_select);
98 98
99 99 fileEditor = new FileEditor('#editor');
100 100
101 101 // on change of select field set mode
102 102 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
103 103
104 104 // on entering the new filename set mode, from given extension
105 105 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
106 106
107 107 $('#filename').focus();
108 108
109 109 var commit_id = "${c.commit.raw_id}";
110 110 var f_path = "${c.f_path}";
111 111
112 112 checkFileHead($('#eform'), commit_id, f_path, 'create')
113 113 });
114 114
115 115 </script>
116 116 </%def>
@@ -1,98 +1,98 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 at_ref = request.GET.get('at')
5 5 if at_ref:
6 6 query={'at': at_ref}
7 default_commit_id = at_ref or c.rhodecode_db_repo.landing_ref_name
7 default_landing_ref = at_ref or c.rhodecode_db_repo.landing_ref_name
8 8 else:
9 9 query=None
10 default_commit_id = c.commit.raw_id
10 default_landing_ref = c.commit.raw_id
11 11 %>
12 12 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
13 13 <table class="code-browser rctable repo_summary">
14 14 <thead>
15 15 <tr>
16 16 <th>${_('Name')}</th>
17 17 <th>${_('Size')}</th>
18 18 <th>${_('Modified')}</th>
19 19 <th>${_('Last Commit')}</th>
20 20 <th>${_('Author')}</th>
21 21 </tr>
22 22 </thead>
23 23
24 24 <tbody id="tbody">
25 25 <tr>
26 26 <td colspan="5">
27 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True)}
27 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True)}
28 28 </td>
29 29 </tr>
30 30
31 31 <% has_files = False %>
32 32 % for cnt,node in enumerate(c.file):
33 33 <% has_files = True %>
34 34 <tr class="parity${(cnt % 2)}">
35 35 <td class="td-componentname">
36 36 % if node.is_submodule():
37 37 <span class="submodule-dir">
38 38 % if node.url.startswith('http://') or node.url.startswith('https://'):
39 39 <a href="${node.url}">
40 40 <i class="icon-directory browser-dir"></i>${node.name}
41 41 </a>
42 42 % else:
43 43 <i class="icon-directory browser-dir"></i>${node.name}
44 44 % endif
45 45 </span>
46 46 % else:
47 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=default_commit_id,f_path=h.safe_unicode(node.path), _query=query)}">
47 <a href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path=h.safe_unicode(node.path), ref_name=default_landing_ref, commit_id=c.commit.raw_id, query=query)}">
48 48 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
49 49 </a>
50 50 % endif
51 51 </td>
52 52 %if node.is_file():
53 53 <td class="td-size" data-attr-name="size">
54 54 % if c.full_load:
55 55 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
56 56 % else:
57 57 ${_('Loading ...')}
58 58 % endif
59 59 </td>
60 60 <td class="td-time" data-attr-name="modified_at">
61 61 % if c.full_load:
62 62 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
63 63 % endif
64 64 </td>
65 65 <td class="td-hash" data-attr-name="commit_id">
66 66 % if c.full_load:
67 67 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
68 68 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
69 69 </div>
70 70 % endif
71 71 </td>
72 72 <td class="td-user" data-attr-name="author">
73 73 % if c.full_load:
74 74 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
75 75 % endif
76 76 </td>
77 77 %else:
78 78 <td></td>
79 79 <td></td>
80 80 <td></td>
81 81 <td></td>
82 82 %endif
83 83 </tr>
84 84 % endfor
85 85
86 86 % if not has_files:
87 87 <tr>
88 88 <td colspan="5">
89 89 ##empty-dir mostly SVN
90 90 &nbsp;
91 91 </td>
92 92 </tr>
93 93 % endif
94 94
95 95 </tbody>
96 96 <tbody id="tbody_filtered"></tbody>
97 97 </table>
98 98 </div>
@@ -1,92 +1,92 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Files Delete').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()"></%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='files')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23
24 24 <div class="edit-file-title">
25 25 <span class="title-heading">${_('Delete file')} @ <code>${h.show_id(c.commit)}</code></span>
26 26 % if c.commit.branch:
27 27 <span class="tag branchtag">
28 28 <i class="icon-branch"></i> ${c.commit.branch}
29 29 </span>
30 30 % endif
31 31 </div>
32 32
33 33 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 34 <div class="edit-file-fieldset">
35 35 <div class="path-items">
36 36 <li class="breadcrumb-path">
37 37 <div>
38 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
38 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
39 39 </div>
40 40 </li>
41 41 <li class="location-path">
42 42 <input type="hidden" value="${c.f_path}" name="root_path">
43 43 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" disabled="disabled">
44 44 </li>
45 45 </div>
46 46
47 47 </div>
48 48
49 49 <div id="codeblock" class="codeblock delete-file-preview">
50 50 <div class="code-body">
51 51 %if c.file.is_binary:
52 52 ${_('Binary file (%s)') % c.file.mimetype}
53 53 %else:
54 54 %if c.file.size < c.visual.cut_off_limit_file:
55 55 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
56 56 %else:
57 57 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
58 58 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
59 59 %endif
60 60 %endif
61 61 </div>
62 62 </div>
63 63
64 64 <div class="edit-file-fieldset">
65 65 <div class="fieldset">
66 66 <div class="message">
67 67 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
68 68 </div>
69 69 </div>
70 70 <div class="pull-left">
71 71 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-danger-action")}
72 72 </div>
73 73 </div>
74 74 ${h.end_form()}
75 75 </div>
76 76
77 77
78 78 <script type="text/javascript">
79 79
80 80 $(document).ready(function () {
81 81
82 82 fileEditor = new FileEditor('#editor');
83 83
84 84 var commit_id = "${c.commit.raw_id}";
85 85 var f_path = "${c.f_path}";
86 86
87 87 checkFileHead($('#eform'), commit_id, f_path, 'delete');
88 88 });
89 89
90 90 </script>
91 91
92 92 </%def>
@@ -1,128 +1,128 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Files Edit').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()"></%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='files')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23
24 24 <div class="edit-file-title">
25 25 <span class="title-heading">${_('Edit file')} @ <code>${h.show_id(c.commit)}</code></span>
26 26 % if c.commit.branch:
27 27 <span class="tag branchtag">
28 28 <i class="icon-branch"></i> ${c.commit.branch}
29 29 </span>
30 30 % endif
31 31 </div>
32 32
33 33 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 34 <div class="edit-file-fieldset">
35 35 <div class="path-items">
36 36 <ul>
37 37 <li class="breadcrumb-path">
38 38 <div>
39 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
39 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
40 40 </div>
41 41 </li>
42 42 <li class="location-path">
43 43 <input type="hidden" value="${c.f_path}" name="root_path">
44 44 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
45 45 </li>
46 46 </ul>
47 47 </div>
48 48
49 49 </div>
50 50
51 51 <div class="table">
52 52 <div>
53 53
54 54 <div id="codeblock" class="codeblock">
55 55 <div class="editor-items">
56 56 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
57 57 ${_('Edit')}
58 58 </div>
59 59
60 60 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
61 61 ${_('Preview')}
62 62 </div>
63 63
64 64 <div class="pull-right">
65 65 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off')),])}
66 66 </div>
67 67 <div class="pull-right">
68 68 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))],enable_filter=True)}
69 69 </div>
70 70 </div>
71 71
72 72 <div id="editor_container">
73 73 <pre id="editor_pre"></pre>
74 74 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
75 75 <div id="editor_preview" ></div>
76 76 </div>
77 77 </div>
78 78 </div>
79 79 </div>
80 80
81 81 <div class="edit-file-fieldset">
82 82 <div class="fieldset">
83 83 <div class="message">
84 84 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
85 85 </div>
86 86 </div>
87 87 <div class="pull-left">
88 88 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
89 89 </div>
90 90 </div>
91 91 ${h.end_form()}
92 92 </div>
93 93
94 94 <script type="text/javascript">
95 95
96 96 $(document).ready(function() {
97 97 var modes_select = $('#set_mode');
98 98 var filename_selector = '#filename';
99 99 fillCodeMirrorOptions(modes_select);
100 100
101 101 fileEditor = new FileEditor('#editor');
102 102
103 103 // try to detect the mode based on the file we edit
104 104 var detected_mode = detectCodeMirrorMode("${c.file.name}", "${c.file.mimetype}");
105 105
106 106 if (detected_mode) {
107 107 setCodeMirrorMode(fileEditor.cm, detected_mode);
108 108
109 109 var mimetype = $(modes_select).find("option[mode={0}]".format(detected_mode)).val();
110 110 $(modes_select).select2("val", mimetype).trigger('change');
111 111 }
112 112
113 113 // on change of select field set mode
114 114 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
115 115
116 116 // on entering the new filename set mode, from given extension
117 117 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
118 118
119 119 var commit_id = "${c.commit.raw_id}";
120 120 var f_path = "${c.f_path}";
121 121
122 122 checkFileHead($('#eform'), commit_id, f_path, 'edit')
123 123
124 124 });
125 125
126 126
127 127 </script>
128 128 </%def>
@@ -1,189 +1,189 b''
1 1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
2 2
3 3 <%
4 4 at_ref = request.GET.get('at')
5 5 if at_ref:
6 6 query={'at': at_ref}
7 7 default_commit_id = at_ref or c.rhodecode_db_repo.landing_ref_name
8 8 else:
9 9 query=None
10 10 default_commit_id = c.commit.raw_id
11 11 %>
12 12
13 13 <div id="codeblock" class="browserblock">
14 14 <div class="browser-header">
15 15 <div class="browser-nav">
16 16 <div class="pull-left">
17 17 ## loads the history for a file
18 18 ${h.hidden('file_refs_filter')}
19 19 </div>
20 20
21 21 <div class="pull-right">
22 22
23 23 ## Download
24 24 % if c.lf_node:
25 25 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
26 26 ${_('Download largefile')}
27 27 </a>
28 28 % else:
29 29 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
30 30 ${_('Download file')}
31 31 </a>
32 32 % endif
33 33
34 34 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
35 35 ## on branch head, can edit files
36 36 %if c.on_branch_head and c.branch_or_raw_id:
37 37 ## binary files are delete only
38 38 % if c.file.is_binary:
39 39 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
40 40 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query),class_="btn btn-danger")}
41 41 % else:
42 42 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
43 43 ${_('Edit on branch: ')}<code>${c.branch_name}</code>
44 44 </a>
45 45
46 46 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
47 47 ${_('Delete')}
48 48 </a>
49 49 % endif
50 50 ## not on head, forbid all
51 51 % else:
52 52 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
53 53 ${h.link_to(_('Delete'), '#Delete', class_="btn btn-default btn-danger disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
54 54 % endif
55 55 %endif
56 56
57 57 </div>
58 58 </div>
59 59 <div id="file_history_container"></div>
60 60
61 61 </div>
62 62 </div>
63 63
64 64 <div class="codeblock">
65 65 <div class=" codeblock-header">
66 66 <div class="file-filename">
67 67 <i class="icon-file"></i> ${c.file}
68 68 </div>
69 69
70 70 <div class="file-stats">
71 71
72 72 <div class="stats-info">
73 73 <span class="stats-first-item">
74 74 % if c.file_size_too_big:
75 75 0 ${(_('lines'))}
76 76 % else:
77 77 ${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}
78 78 % endif
79 79 </span>
80 80
81 81 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
82 82 % if c.lf_node:
83 83 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
84 84 % endif
85 85 <span>
86 86 | ${c.file.mimetype}
87 87 </span>
88 88
89 89 % if not c.file_size_too_big:
90 90 <span> |
91 91 ${h.get_lexer_for_filenode(c.file).__class__.__name__}
92 92 </span>
93 93 % endif
94 94
95 95 </div>
96 96 </div>
97 97 </div>
98 98
99 99 <div class="path clear-fix">
100 100 <div class="pull-left">
101 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'))}
101 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'))}
102 102 </div>
103 103
104 104 <div class="pull-right stats">
105 105 <a id="file_history_overview" href="#loadHistory">
106 106 ${_('History')}
107 107 </a>
108 108 |
109 109 %if c.annotate:
110 110 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
111 111 %else:
112 112 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
113 113 %endif
114 114 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
115 115 % if not c.file.is_binary:
116 116 |<a href="#copySource" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${c.file.content}">${_('Copy content')}</a>
117 117 |<a href="#copyPermaLink" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${h.route_url('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">${_('Copy permalink')}</a>
118 118 % endif
119 119
120 120 </div>
121 121 <div class="clear-fix"></div>
122 122 </div>
123 123
124 124 <div class="code-body clear-fix ">
125 125 %if c.file.is_binary:
126 126 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
127 127 % if rendered_binary:
128 128 <div class="text-center">
129 129 ${rendered_binary}
130 130 </div>
131 131 % else:
132 132 <div>
133 133 ${_('Binary file ({})').format(c.file.mimetype)}
134 134 </div>
135 135 % endif
136 136 %else:
137 137 % if c.file_size_too_big:
138 138 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
139 139 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
140 140 % else:
141 141 %if c.renderer and not c.annotate:
142 142 ## pick relative url based on renderer
143 143 <%
144 144 relative_urls = {
145 145 'raw': h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
146 146 'standard': h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
147 147 }
148 148 %>
149 149 ${h.render(c.file.content, renderer=c.renderer, relative_urls=relative_urls)}
150 150 %else:
151 151 <table class="cb codehilite">
152 152 %if c.annotate:
153 153 <% color_hasher = h.color_hasher() %>
154 154 %for annotation, lines in c.annotated_lines:
155 155 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
156 156 %endfor
157 157 %else:
158 158 %for line_num, tokens in enumerate(c.lines, 1):
159 159 ${sourceblock.render_line(line_num, tokens)}
160 160 %endfor
161 161 %endif
162 162 </table>
163 163 %endif
164 164 % endif
165 165 %endif
166 166 </div>
167 167
168 168 </div>
169 169
170 170 <script type="text/javascript">
171 171 % if request.GET.get('mark'):
172 172
173 173 $(function(){
174 174 $(".codehilite").mark(
175 175 "${request.GET.get('mark')}",
176 176 {
177 177 "className": 'match',
178 178 "accuracy": "complementary",
179 179 "ignorePunctuation": ":._(){}[]!'+=".split(""),
180 180 "each": function(el) {
181 181 // and also highlight lines !
182 182 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
183 183 }
184 184 }
185 185 );
186 186
187 187 });
188 188 % endif
189 189 </script>
@@ -1,165 +1,165 b''
1 1 <%namespace name="search" file="/search/search.mako"/>
2 2
3 3 <%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
4 4 % if has_matched_content:
5 5 ${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
6 6 % else:
7 7 ${_('No content matched')} <br/>
8 8 % endif
9 9
10 10 %if len(matching_lines) > shown_matching_lines:
11 11 <a href="${url}">
12 12 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
13 13 </a>
14 14 %endif
15 15 </%def>
16 16
17 17 <div class="search-results">
18 18 <% query_mark = c.searcher.query_to_mark(c.cur_query, 'content') %>
19 19
20 20 %for entry in c.formatted_results:
21 21
22 22 <%
23 23 file_content = entry['content_highlight'] or entry['content']
24 24 mimetype = entry.get('mimetype')
25 25 filepath = entry.get('path')
26 26 max_lines = h.safe_int(request.GET.get('max_lines', '10'))
27 27 line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
28 28
29 29 match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
30 30 terms = c.cur_query
31 31
32 32 if c.searcher.is_es_6:
33 33 # use empty terms so we default to markers usage
34 34 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
35 35 else:
36 36 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
37 37
38 38 shown_matching_lines = 0
39 39 lines_of_interest = set()
40 40 for line_number in matching_lines:
41 41 if len(lines_of_interest) < max_lines:
42 42 lines_of_interest |= set(range(
43 43 max(line_number - line_context, 0),
44 44 min(line_number + line_context, total_lines + 1)))
45 45 shown_matching_lines += 1
46 46 lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
47 47
48 48 html_formatter = h.SearchContentCodeHtmlFormatter(
49 49 linenos=True,
50 50 cssclass="code-highlight",
51 51 url=match_file_url,
52 52 query_terms=terms,
53 53 only_line_numbers=lines_of_interest
54 54 )
55 55
56 56 has_matched_content = len(lines_of_interest) >= 1
57 57
58 58 %>
59 59 ## search results are additionally filtered, and this check is just a safe gate
60 60 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
61 61 <div class="codeblock">
62 62 <h1>
63 63 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
64 64 ${search.repo_icon(repo_type)}
65 65 ${h.link_to(entry['repository'], h.route_path('repo_summary', repo_name=entry['repository']))}
66 66 </h1>
67 67
68 68 <div class="codeblock-header">
69 69
70 70 <div class="file-filename">
71 71 <i class="icon-file"></i> ${entry['f_path'].split('/')[-1]}
72 72 </div>
73 73
74 74 <div class="file-stats">
75 75 <div class="stats-info">
76 76 <span class="stats-first-item">
77 77 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
78 78 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
79 79 </span>
80 80 <span>
81 81 % if entry.get('size'):
82 82 | ${h.format_byte_size_binary(entry['size'])}
83 83 % endif
84 84 </span>
85 85 <span>
86 86 % if entry.get('mimetype'):
87 87 | ${entry.get('mimetype', "unknown mimetype")}
88 88 % endif
89 89 </span>
90 90 </div>
91 91 </div>
92 92 </div>
93 93
94 94 <div class="path clear-fix">
95 95 <div class="pull-left">
96 ${h.files_breadcrumbs(entry['repository'], entry.get('commit_id', 'tip'), entry['f_path'], linkify_last_item=True)}
96 ${h.files_breadcrumbs(entry['repository'], repo_type, entry.get('commit_id', 'tip'), entry['f_path'], linkify_last_item=True)}
97 97 </div>
98 98
99 99 <div class="pull-right stats">
100 100 ## <a id="file_history_overview_full" href="${h.route_path('repo_commits_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
101 101 ## ${_('Show Full History')}
102 102 ## </a>
103 103 ## | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
104 104 ## | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
105 105 <div class="search-tags">
106 106
107 107 <% repo_group = entry.get('repository_group')%>
108 108 ## hiden if in repo group view
109 109 % if repo_group and not c.repo_group_name:
110 110 <span class="tag tag8">
111 111 ${search.repo_group_icon()}
112 112 <a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
113 113 </span>
114 114 % endif
115 115 ## hiden if in repo view
116 116 % if not c.repo_name:
117 117 <span class="tag tag8">
118 118 ${search.repo_icon(repo_type)}
119 119 <a href="${h.route_path('search_repo', repo_name=entry.get('repo_name'), _query={'q': c.cur_query})}">${_('Narrow to this repository')}</a>
120 120 </span>
121 121 % endif
122 122 </div>
123 123
124 124 </div>
125 125 <div class="clear-fix"></div>
126 126 </div>
127 127
128 128
129 129 <div class="code-body search-code-body clear-fix">
130 130 ${highlight_text_file(
131 131 has_matched_content=has_matched_content,
132 132 file_content=file_content,
133 133 lexer=lexer,
134 134 html_formatter=html_formatter,
135 135 matching_lines=matching_lines,
136 136 shown_matching_lines=shown_matching_lines,
137 137 url=match_file_url,
138 138 use_hl_filter=c.searcher.is_es_6
139 139 )}
140 140 </div>
141 141
142 142 </div>
143 143 % endif
144 144 %endfor
145 145 </div>
146 146 %if c.cur_query and c.formatted_results:
147 147 <div class="pagination-wh pagination-left" >
148 148 ${c.formatted_results.render()}
149 149 </div>
150 150 %endif
151 151
152 152 %if c.cur_query:
153 153 <script type="text/javascript">
154 154 $(function(){
155 155 $(".search-code-body").mark(
156 156 "${query_mark}",
157 157 {
158 158 "className": 'match',
159 159 "accuracy": "complementary",
160 160 "ignorePunctuation": ":._(){}[]!'+=".split("")
161 161 }
162 162 );
163 163 })
164 164 </script>
165 165 %endif
General Comments 0
You need to be logged in to leave comments. Login now