##// END OF EJS Templates
files: ported repository files controllers to pyramid views.
marcink -
r1927:e6df2b71 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

This diff has been collapsed as it changes many lines, (1278 lines changed) Show them Hide them
@@ -0,0 +1,1278 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import itertools
22 import logging
23 import os
24 import shutil
25 import tempfile
26 import collections
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.view import view_config
30 from pyramid.renderers import render
31 from pyramid.response import Response
32
33 from rhodecode.apps._base import RepoAppView
34
35 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.exceptions import NonRelativePathError
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 from rhodecode.lib.utils2 import (
42 convert_line_endings, detect_mode, safe_str, str2bool)
43 from rhodecode.lib.auth import (
44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 from rhodecode.lib.vcs import path as vcspath
46 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 from rhodecode.lib.vcs.conf import settings
48 from rhodecode.lib.vcs.nodes import FileNode
49 from rhodecode.lib.vcs.exceptions import (
50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 NodeDoesNotExistError, CommitError, NodeError)
53
54 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.db import Repository
56
57 log = logging.getLogger(__name__)
58
59
60 class RepoFilesView(RepoAppView):
61
62 @staticmethod
63 def adjust_file_path_for_svn(f_path, repo):
64 """
65 Computes the relative path of `f_path`.
66
67 This is mainly based on prefix matching of the recognized tags and
68 branches in the underlying repository.
69 """
70 tags_and_branches = itertools.chain(
71 repo.branches.iterkeys(),
72 repo.tags.iterkeys())
73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74
75 for name in tags_and_branches:
76 if f_path.startswith('{}/'.format(name)):
77 f_path = vcspath.relpath(f_path, name)
78 break
79 return f_path
80
81 def load_default_context(self):
82 c = self._get_local_tmpl_context(include_app_defaults=True)
83
84 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
85 c.repo_info = self.db_repo
86 c.rhodecode_repo = self.rhodecode_vcs_repo
87
88 self._register_global_c(c)
89 return c
90
91 def _ensure_not_locked(self):
92 _ = self.request.translate
93
94 repo = self.db_repo
95 if repo.enable_locking and repo.locked[0]:
96 h.flash(_('This repository has been locked by %s on %s')
97 % (h.person_by_id(repo.locked[0]),
98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 'warning')
100 files_url = h.route_path(
101 'repo_files:default_path',
102 repo_name=self.db_repo_name, commit_id='tip')
103 raise HTTPFound(files_url)
104
105 def _get_commit_and_path(self):
106 default_commit_id = self.db_repo.landing_rev[1]
107 default_f_path = '/'
108
109 commit_id = self.request.matchdict.get(
110 'commit_id', default_commit_id)
111 f_path = self.request.matchdict.get('f_path', default_f_path)
112 return commit_id, f_path
113
114 def _get_default_encoding(self, c):
115 enc_list = getattr(c, 'default_encodings', [])
116 return enc_list[0] if enc_list else 'UTF-8'
117
118 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
119 """
120 This is a safe way to get commit. If an error occurs it redirects to
121 tip with proper message
122
123 :param commit_id: id of commit to fetch
124 :param redirect_after: toggle redirection
125 """
126 _ = self.request.translate
127
128 try:
129 return self.rhodecode_vcs_repo.get_commit(commit_id)
130 except EmptyRepositoryError:
131 if not redirect_after:
132 return None
133
134 _url = h.route_path(
135 'repo_files_add_file',
136 repo_name=self.db_repo_name, commit_id=0, f_path='',
137 _anchor='edit')
138
139 if h.HasRepoPermissionAny(
140 'repository.write', 'repository.admin')(self.db_repo_name):
141 add_new = h.link_to(
142 _('Click here to add a new file.'), _url, class_="alert-link")
143 else:
144 add_new = ""
145
146 h.flash(h.literal(
147 _('There are no files yet. %s') % add_new), category='warning')
148 raise HTTPFound(
149 h.route_path('repo_summary', repo_name=self.db_repo_name))
150
151 except (CommitDoesNotExistError, LookupError):
152 msg = _('No such commit exists for this repository')
153 h.flash(msg, category='error')
154 raise HTTPNotFound()
155 except RepositoryError as e:
156 h.flash(safe_str(h.escape(e)), category='error')
157 raise HTTPNotFound()
158
159 def _get_filenode_or_redirect(self, commit_obj, path):
160 """
161 Returns file_node, if error occurs or given path is directory,
162 it'll redirect to top level path
163 """
164 _ = self.request.translate
165
166 try:
167 file_node = commit_obj.get_node(path)
168 if file_node.is_dir():
169 raise RepositoryError('The given path is a directory')
170 except CommitDoesNotExistError:
171 log.exception('No such commit exists for this repository')
172 h.flash(_('No such commit exists for this repository'), category='error')
173 raise HTTPNotFound()
174 except RepositoryError as e:
175 log.warning('Repository error while fetching '
176 'filenode `%s`. Err:%s', path, e)
177 h.flash(safe_str(h.escape(e)), category='error')
178 raise HTTPNotFound()
179
180 return file_node
181
182 def _is_valid_head(self, commit_id, repo):
183 # check if commit is a branch identifier- basically we cannot
184 # create multiple heads via file editing
185 valid_heads = repo.branches.keys() + repo.branches.values()
186
187 if h.is_svn(repo) and not repo.is_empty():
188 # Note: Subversion only has one head, we add it here in case there
189 # is no branch matched.
190 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
191
192 # check if commit is a branch name or branch hash
193 return commit_id in valid_heads
194
195 def _get_tree_cache_manager(self, namespace_type):
196 _namespace = caches.get_repo_namespace_key(
197 namespace_type, self.db_repo_name)
198 return caches.get_cache_manager('repo_cache_long', _namespace)
199
200 def _get_tree_at_commit(
201 self, c, commit_id, f_path, full_load=False, force=False):
202 def _cached_tree():
203 log.debug('Generating cached file tree for %s, %s, %s',
204 self.db_repo_name, commit_id, f_path)
205
206 c.full_load = full_load
207 return render(
208 'rhodecode:templates/files/files_browser_tree.mako',
209 self._get_template_context(c), self.request)
210
211 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
212
213 cache_key = caches.compute_key_from_params(
214 self.db_repo_name, commit_id, f_path)
215
216 if force:
217 # we want to force recompute of caches
218 cache_manager.remove_value(cache_key)
219
220 return cache_manager.get(cache_key, createfunc=_cached_tree)
221
222 def _get_archive_spec(self, fname):
223 log.debug('Detecting archive spec for: `%s`', fname)
224
225 fileformat = None
226 ext = None
227 content_type = None
228 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
229 content_type, extension = ext_data
230
231 if fname.endswith(extension):
232 fileformat = a_type
233 log.debug('archive is of type: %s', fileformat)
234 ext = extension
235 break
236
237 if not fileformat:
238 raise ValueError()
239
240 # left over part of whole fname is the commit
241 commit_id = fname[:-len(ext)]
242
243 return commit_id, ext, fileformat, content_type
244
245 @LoginRequired()
246 @HasRepoPermissionAnyDecorator(
247 'repository.read', 'repository.write', 'repository.admin')
248 @view_config(
249 route_name='repo_archivefile', request_method='GET',
250 renderer=None)
251 def repo_archivefile(self):
252 # archive cache config
253 from rhodecode import CONFIG
254 _ = self.request.translate
255 self.load_default_context()
256
257 fname = self.request.matchdict['fname']
258 subrepos = self.request.GET.get('subrepos') == 'true'
259
260 if not self.db_repo.enable_downloads:
261 return Response(_('Downloads disabled'))
262
263 try:
264 commit_id, ext, fileformat, content_type = \
265 self._get_archive_spec(fname)
266 except ValueError:
267 return Response(_('Unknown archive type for: `{}`').format(fname))
268
269 try:
270 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
271 except CommitDoesNotExistError:
272 return Response(_('Unknown commit_id %s') % commit_id)
273 except EmptyRepositoryError:
274 return Response(_('Empty repository'))
275
276 archive_name = '%s-%s%s%s' % (
277 safe_str(self.db_repo_name.replace('/', '_')),
278 '-sub' if subrepos else '',
279 safe_str(commit.short_id), ext)
280
281 use_cached_archive = False
282 archive_cache_enabled = CONFIG.get(
283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284
285 if archive_cache_enabled:
286 # check if we it's ok to write
287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 os.makedirs(CONFIG['archive_cache_dir'])
289 cached_archive_path = os.path.join(
290 CONFIG['archive_cache_dir'], archive_name)
291 if os.path.isfile(cached_archive_path):
292 log.debug('Found cached archive in %s', cached_archive_path)
293 fd, archive = None, cached_archive_path
294 use_cached_archive = True
295 else:
296 log.debug('Archive %s is not yet cached', archive_name)
297
298 if not use_cached_archive:
299 # generate new archive
300 fd, archive = tempfile.mkstemp()
301 log.debug('Creating new temp archive in %s', archive)
302 try:
303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 except ImproperArchiveTypeError:
305 return _('Unknown archive type')
306 if archive_cache_enabled:
307 # if we generated the archive and we have cache enabled
308 # let's use this for future
309 log.debug('Storing new archive in %s', cached_archive_path)
310 shutil.move(archive, cached_archive_path)
311 archive = cached_archive_path
312
313 # store download action
314 audit_logger.store_web(
315 'repo.archive.download', action_data={
316 'user_agent': self.request.user_agent,
317 'archive_name': archive_name,
318 'archive_spec': fname,
319 'archive_cached': use_cached_archive},
320 user=self._rhodecode_user,
321 repo=self.db_repo,
322 commit=True
323 )
324
325 def get_chunked_archive(archive):
326 with open(archive, 'rb') as stream:
327 while True:
328 data = stream.read(16 * 1024)
329 if not data:
330 if fd: # fd means we used temporary file
331 os.close(fd)
332 if not archive_cache_enabled:
333 log.debug('Destroying temp archive %s', archive)
334 os.remove(archive)
335 break
336 yield data
337
338 response = Response(app_iter=get_chunked_archive(archive))
339 response.content_disposition = str(
340 'attachment; filename=%s' % archive_name)
341 response.content_type = str(content_type)
342
343 return response
344
345 def _get_file_node(self, commit_id, f_path):
346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 try:
349 node = commit.get_node(f_path)
350 if node.is_dir():
351 raise NodeError('%s path is a %s not a file'
352 % (node, type(node)))
353 except NodeDoesNotExistError:
354 commit = EmptyCommit(
355 commit_id=commit_id,
356 idx=commit.idx,
357 repo=commit.repository,
358 alias=commit.repository.alias,
359 message=commit.message,
360 author=commit.author,
361 date=commit.date)
362 node = FileNode(f_path, '', commit=commit)
363 else:
364 commit = EmptyCommit(
365 repo=self.rhodecode_vcs_repo,
366 alias=self.rhodecode_vcs_repo.alias)
367 node = FileNode(f_path, '', commit=commit)
368 return node
369
370 @LoginRequired()
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
373 @view_config(
374 route_name='repo_files_diff', request_method='GET',
375 renderer=None)
376 def repo_files_diff(self):
377 c = self.load_default_context()
378 diff1 = self.request.GET.get('diff1', '')
379 diff2 = self.request.GET.get('diff2', '')
380 f_path = self.request.matchdict['f_path']
381
382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383
384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 line_context = self.request.GET.get('context', 3)
386
387 if not any((diff1, diff2)):
388 h.flash(
389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 category='error')
391 raise HTTPBadRequest()
392
393 c.action = self.request.GET.get('diff')
394 if c.action not in ['download', 'raw']:
395 compare_url = h.url(
396 'compare_url', repo_name=self.db_repo_name,
397 source_ref_type='rev',
398 source_ref=diff1,
399 target_repo=self.db_repo_name,
400 target_ref_type='rev',
401 target_ref=diff2,
402 f_path=f_path)
403 # redirect to new view if we render diff
404 raise HTTPFound(compare_url)
405
406 try:
407 node1 = self._get_file_node(diff1, path1)
408 node2 = self._get_file_node(diff2, f_path)
409 except (RepositoryError, NodeError):
410 log.exception("Exception while trying to get node from repository")
411 raise HTTPFound(
412 h.route_path('repo_files', repo_name=self.db_repo_name,
413 commit_id='tip', f_path=f_path))
414
415 if all(isinstance(node.commit, EmptyCommit)
416 for node in (node1, node2)):
417 raise HTTPNotFound()
418
419 c.commit_1 = node1.commit
420 c.commit_2 = node2.commit
421
422 if c.action == 'download':
423 _diff = diffs.get_gitdiff(node1, node2,
424 ignore_whitespace=ignore_whitespace,
425 context=line_context)
426 diff = diffs.DiffProcessor(_diff, format='gitdiff')
427
428 response = Response(diff.as_raw())
429 response.content_type = 'text/plain'
430 response.content_disposition = (
431 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
432 )
433 charset = self._get_default_encoding(c)
434 if charset:
435 response.charset = charset
436 return response
437
438 elif c.action == 'raw':
439 _diff = diffs.get_gitdiff(node1, node2,
440 ignore_whitespace=ignore_whitespace,
441 context=line_context)
442 diff = diffs.DiffProcessor(_diff, format='gitdiff')
443
444 response = Response(diff.as_raw())
445 response.content_type = 'text/plain'
446 charset = self._get_default_encoding(c)
447 if charset:
448 response.charset = charset
449 return response
450
451 # in case we ever end up here
452 raise HTTPNotFound()
453
454 @LoginRequired()
455 @HasRepoPermissionAnyDecorator(
456 'repository.read', 'repository.write', 'repository.admin')
457 @view_config(
458 route_name='repo_files_diff_2way_redirect', request_method='GET',
459 renderer=None)
460 def repo_files_diff_2way_redirect(self):
461 """
462 Kept only to make OLD links work
463 """
464 diff1 = self.request.GET.get('diff1', '')
465 diff2 = self.request.GET.get('diff2', '')
466 f_path = self.request.matchdict['f_path']
467
468 if not any((diff1, diff2)):
469 h.flash(
470 'Need query parameter "diff1" or "diff2" to generate a diff.',
471 category='error')
472 raise HTTPBadRequest()
473
474 compare_url = h.url(
475 'compare_url', repo_name=self.db_repo_name,
476 source_ref_type='rev',
477 source_ref=diff1,
478 target_repo=self.db_repo_name,
479 target_ref_type='rev',
480 target_ref=diff2,
481 f_path=f_path,
482 diffmode='sideside')
483 raise HTTPFound(compare_url)
484
485 @LoginRequired()
486 @HasRepoPermissionAnyDecorator(
487 'repository.read', 'repository.write', 'repository.admin')
488 @view_config(
489 route_name='repo_files', request_method='GET',
490 renderer=None)
491 @view_config(
492 route_name='repo_files:default_path', request_method='GET',
493 renderer=None)
494 @view_config(
495 route_name='repo_files:default_commit', request_method='GET',
496 renderer=None)
497 @view_config(
498 route_name='repo_files:rendered', request_method='GET',
499 renderer=None)
500 @view_config(
501 route_name='repo_files:annotated', request_method='GET',
502 renderer=None)
503 def repo_files(self):
504 c = self.load_default_context()
505
506 view_name = getattr(self.request.matched_route, 'name', None)
507
508 c.annotate = view_name == 'repo_files:annotated'
509 # default is false, but .rst/.md files later are auto rendered, we can
510 # overwrite auto rendering by setting this GET flag
511 c.renderer = view_name == 'repo_files:rendered' or \
512 not self.request.GET.get('no-render', False)
513
514 # redirect to given commit_id from form if given
515 get_commit_id = self.request.GET.get('at_rev', None)
516 if get_commit_id:
517 self._get_commit_or_redirect(get_commit_id)
518
519 commit_id, f_path = self._get_commit_and_path()
520 c.commit = self._get_commit_or_redirect(commit_id)
521 c.branch = self.request.GET.get('branch', None)
522 c.f_path = f_path
523
524 # prev link
525 try:
526 prev_commit = c.commit.prev(c.branch)
527 c.prev_commit = prev_commit
528 c.url_prev = h.route_path(
529 'repo_files', repo_name=self.db_repo_name,
530 commit_id=prev_commit.raw_id, f_path=f_path)
531 if c.branch:
532 c.url_prev += '?branch=%s' % c.branch
533 except (CommitDoesNotExistError, VCSError):
534 c.url_prev = '#'
535 c.prev_commit = EmptyCommit()
536
537 # next link
538 try:
539 next_commit = c.commit.next(c.branch)
540 c.next_commit = next_commit
541 c.url_next = h.route_path(
542 'repo_files', repo_name=self.db_repo_name,
543 commit_id=next_commit.raw_id, f_path=f_path)
544 if c.branch:
545 c.url_next += '?branch=%s' % c.branch
546 except (CommitDoesNotExistError, VCSError):
547 c.url_next = '#'
548 c.next_commit = EmptyCommit()
549
550 # files or dirs
551 try:
552 c.file = c.commit.get_node(f_path)
553 c.file_author = True
554 c.file_tree = ''
555
556 # load file content
557 if c.file.is_file():
558 c.lf_node = c.file.get_largefile_node()
559
560 c.file_source_page = 'true'
561 c.file_last_commit = c.file.last_commit
562 if c.file.size < c.visual.cut_off_limit_diff:
563 if c.annotate: # annotation has precedence over renderer
564 c.annotated_lines = filenode_as_annotated_lines_tokens(
565 c.file
566 )
567 else:
568 c.renderer = (
569 c.renderer and h.renderer_from_filename(c.file.path)
570 )
571 if not c.renderer:
572 c.lines = filenode_as_lines_tokens(c.file)
573
574 c.on_branch_head = self._is_valid_head(
575 commit_id, self.rhodecode_vcs_repo)
576
577 branch = c.commit.branch if (
578 c.commit.branch and '/' not in c.commit.branch) else None
579 c.branch_or_raw_id = branch or c.commit.raw_id
580 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
581
582 author = c.file_last_commit.author
583 c.authors = [[
584 h.email(author),
585 h.person(author, 'username_or_name_or_email'),
586 1
587 ]]
588
589 else: # load tree content at path
590 c.file_source_page = 'false'
591 c.authors = []
592 # this loads a simple tree without metadata to speed things up
593 # later via ajax we call repo_nodetree_full and fetch whole
594 c.file_tree = self._get_tree_at_commit(
595 c, c.commit.raw_id, f_path)
596
597 except RepositoryError as e:
598 h.flash(safe_str(h.escape(e)), category='error')
599 raise HTTPNotFound()
600
601 if self.request.environ.get('HTTP_X_PJAX'):
602 html = render('rhodecode:templates/files/files_pjax.mako',
603 self._get_template_context(c), self.request)
604 else:
605 html = render('rhodecode:templates/files/files.mako',
606 self._get_template_context(c), self.request)
607 return Response(html)
608
609 @HasRepoPermissionAnyDecorator(
610 'repository.read', 'repository.write', 'repository.admin')
611 @view_config(
612 route_name='repo_files:annotated_previous', request_method='GET',
613 renderer=None)
614 def repo_files_annotated_previous(self):
615 self.load_default_context()
616
617 commit_id, f_path = self._get_commit_and_path()
618 commit = self._get_commit_or_redirect(commit_id)
619 prev_commit_id = commit.raw_id
620 line_anchor = self.request.GET.get('line_anchor')
621 is_file = False
622 try:
623 _file = commit.get_node(f_path)
624 is_file = _file.is_file()
625 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
626 pass
627
628 if is_file:
629 history = commit.get_file_history(f_path)
630 prev_commit_id = history[1].raw_id \
631 if len(history) > 1 else prev_commit_id
632 prev_url = h.route_path(
633 'repo_files:annotated', repo_name=self.db_repo_name,
634 commit_id=prev_commit_id, f_path=f_path,
635 _anchor='L{}'.format(line_anchor))
636
637 raise HTTPFound(prev_url)
638
639 @LoginRequired()
640 @HasRepoPermissionAnyDecorator(
641 'repository.read', 'repository.write', 'repository.admin')
642 @view_config(
643 route_name='repo_nodetree_full', request_method='GET',
644 renderer=None, xhr=True)
645 @view_config(
646 route_name='repo_nodetree_full:default_path', request_method='GET',
647 renderer=None, xhr=True)
648 def repo_nodetree_full(self):
649 """
650 Returns rendered html of file tree that contains commit date,
651 author, commit_id for the specified combination of
652 repo, commit_id and file path
653 """
654 c = self.load_default_context()
655
656 commit_id, f_path = self._get_commit_and_path()
657 commit = self._get_commit_or_redirect(commit_id)
658 try:
659 dir_node = commit.get_node(f_path)
660 except RepositoryError as e:
661 return Response('error: {}'.format(safe_str(e)))
662
663 if dir_node.is_file():
664 return Response('')
665
666 c.file = dir_node
667 c.commit = commit
668
669 # using force=True here, make a little trick. We flush the cache and
670 # compute it using the same key as without previous full_load, so now
671 # the fully loaded tree is now returned instead of partial,
672 # and we store this in caches
673 html = self._get_tree_at_commit(
674 c, commit.raw_id, dir_node.path, full_load=True, force=True)
675
676 return Response(html)
677
678 def _get_attachement_disposition(self, f_path):
679 return 'attachment; filename=%s' % \
680 safe_str(f_path.split(Repository.NAME_SEP)[-1])
681
682 @LoginRequired()
683 @HasRepoPermissionAnyDecorator(
684 'repository.read', 'repository.write', 'repository.admin')
685 @view_config(
686 route_name='repo_file_raw', request_method='GET',
687 renderer=None)
688 def repo_file_raw(self):
689 """
690 Action for show as raw, some mimetypes are "rendered",
691 those include images, icons.
692 """
693 c = self.load_default_context()
694
695 commit_id, f_path = self._get_commit_and_path()
696 commit = self._get_commit_or_redirect(commit_id)
697 file_node = self._get_filenode_or_redirect(commit, f_path)
698
699 raw_mimetype_mapping = {
700 # map original mimetype to a mimetype used for "show as raw"
701 # you can also provide a content-disposition to override the
702 # default "attachment" disposition.
703 # orig_type: (new_type, new_dispo)
704
705 # show images inline:
706 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
707 # for example render an SVG with javascript inside or even render
708 # HTML.
709 'image/x-icon': ('image/x-icon', 'inline'),
710 'image/png': ('image/png', 'inline'),
711 'image/gif': ('image/gif', 'inline'),
712 'image/jpeg': ('image/jpeg', 'inline'),
713 'application/pdf': ('application/pdf', 'inline'),
714 }
715
716 mimetype = file_node.mimetype
717 try:
718 mimetype, disposition = raw_mimetype_mapping[mimetype]
719 except KeyError:
720 # we don't know anything special about this, handle it safely
721 if file_node.is_binary:
722 # do same as download raw for binary files
723 mimetype, disposition = 'application/octet-stream', 'attachment'
724 else:
725 # do not just use the original mimetype, but force text/plain,
726 # otherwise it would serve text/html and that might be unsafe.
727 # Note: underlying vcs library fakes text/plain mimetype if the
728 # mimetype can not be determined and it thinks it is not
729 # binary.This might lead to erroneous text display in some
730 # cases, but helps in other cases, like with text files
731 # without extension.
732 mimetype, disposition = 'text/plain', 'inline'
733
734 if disposition == 'attachment':
735 disposition = self._get_attachement_disposition(f_path)
736
737 def stream_node():
738 yield file_node.raw_bytes
739
740 response = Response(app_iter=stream_node())
741 response.content_disposition = disposition
742 response.content_type = mimetype
743
744 charset = self._get_default_encoding(c)
745 if charset:
746 response.charset = charset
747
748 return response
749
750 @LoginRequired()
751 @HasRepoPermissionAnyDecorator(
752 'repository.read', 'repository.write', 'repository.admin')
753 @view_config(
754 route_name='repo_file_download', request_method='GET',
755 renderer=None)
756 @view_config(
757 route_name='repo_file_download:legacy', request_method='GET',
758 renderer=None)
759 def repo_file_download(self):
760 c = self.load_default_context()
761
762 commit_id, f_path = self._get_commit_and_path()
763 commit = self._get_commit_or_redirect(commit_id)
764 file_node = self._get_filenode_or_redirect(commit, f_path)
765
766 if self.request.GET.get('lf'):
767 # only if lf get flag is passed, we download this file
768 # as LFS/Largefile
769 lf_node = file_node.get_largefile_node()
770 if lf_node:
771 # overwrite our pointer with the REAL large-file
772 file_node = lf_node
773
774 disposition = self._get_attachement_disposition(f_path)
775
776 def stream_node():
777 yield file_node.raw_bytes
778
779 response = Response(app_iter=stream_node())
780 response.content_disposition = disposition
781 response.content_type = file_node.mimetype
782
783 charset = self._get_default_encoding(c)
784 if charset:
785 response.charset = charset
786
787 return response
788
789 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
790 def _cached_nodes():
791 log.debug('Generating cached nodelist for %s, %s, %s',
792 repo_name, commit_id, f_path)
793 _d, _f = ScmModel().get_nodes(
794 repo_name, commit_id, f_path, flat=False)
795 return _d + _f
796
797 cache_manager = self._get_tree_cache_manager(caches.FILE_SEARCH_TREE_META)
798
799 cache_key = caches.compute_key_from_params(
800 repo_name, commit_id, f_path)
801 return cache_manager.get(cache_key, createfunc=_cached_nodes)
802
803 @LoginRequired()
804 @HasRepoPermissionAnyDecorator(
805 'repository.read', 'repository.write', 'repository.admin')
806 @view_config(
807 route_name='repo_files_nodelist', request_method='GET',
808 renderer='json_ext', xhr=True)
809 def repo_nodelist(self):
810 self.load_default_context()
811
812 commit_id, f_path = self._get_commit_and_path()
813 commit = self._get_commit_or_redirect(commit_id)
814
815 metadata = self._get_nodelist_at_commit(
816 self.db_repo_name, commit.raw_id, f_path)
817 return {'nodes': metadata}
818
819 def _create_references(
820 self, branches_or_tags, symbolic_reference, f_path):
821 items = []
822 for name, commit_id in branches_or_tags.items():
823 sym_ref = symbolic_reference(commit_id, name, f_path)
824 items.append((sym_ref, name))
825 return items
826
827 def _symbolic_reference(self, commit_id, name, f_path):
828 return commit_id
829
830 def _symbolic_reference_svn(self, commit_id, name, f_path):
831 new_f_path = vcspath.join(name, f_path)
832 return u'%s@%s' % (new_f_path, commit_id)
833
834 def _get_node_history(self, commit_obj, f_path, commits=None):
835 """
836 get commit history for given node
837
838 :param commit_obj: commit to calculate history
839 :param f_path: path for node to calculate history for
840 :param commits: if passed don't calculate history and take
841 commits defined in this list
842 """
843 _ = self.request.translate
844
845 # calculate history based on tip
846 tip = self.rhodecode_vcs_repo.get_commit()
847 if commits is None:
848 pre_load = ["author", "branch"]
849 try:
850 commits = tip.get_file_history(f_path, pre_load=pre_load)
851 except (NodeDoesNotExistError, CommitError):
852 # this node is not present at tip!
853 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
854
855 history = []
856 commits_group = ([], _("Changesets"))
857 for commit in commits:
858 branch = ' (%s)' % commit.branch if commit.branch else ''
859 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
860 commits_group[0].append((commit.raw_id, n_desc,))
861 history.append(commits_group)
862
863 symbolic_reference = self._symbolic_reference
864
865 if self.rhodecode_vcs_repo.alias == 'svn':
866 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
867 f_path, self.rhodecode_vcs_repo)
868 if adjusted_f_path != f_path:
869 log.debug(
870 'Recognized svn tag or branch in file "%s", using svn '
871 'specific symbolic references', f_path)
872 f_path = adjusted_f_path
873 symbolic_reference = self._symbolic_reference_svn
874
875 branches = self._create_references(
876 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
877 branches_group = (branches, _("Branches"))
878
879 tags = self._create_references(
880 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
881 tags_group = (tags, _("Tags"))
882
883 history.append(branches_group)
884 history.append(tags_group)
885
886 return history, commits
887
888 @LoginRequired()
889 @HasRepoPermissionAnyDecorator(
890 'repository.read', 'repository.write', 'repository.admin')
891 @view_config(
892 route_name='repo_file_history', request_method='GET',
893 renderer='json_ext')
894 def repo_file_history(self):
895 self.load_default_context()
896
897 commit_id, f_path = self._get_commit_and_path()
898 commit = self._get_commit_or_redirect(commit_id)
899 file_node = self._get_filenode_or_redirect(commit, f_path)
900
901 if file_node.is_file():
902 file_history, _hist = self._get_node_history(commit, f_path)
903
904 res = []
905 for obj in file_history:
906 res.append({
907 'text': obj[1],
908 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
909 })
910
911 data = {
912 'more': False,
913 'results': res
914 }
915 return data
916
917 log.warning('Cannot fetch history for directory')
918 raise HTTPBadRequest()
919
920 @LoginRequired()
921 @HasRepoPermissionAnyDecorator(
922 'repository.read', 'repository.write', 'repository.admin')
923 @view_config(
924 route_name='repo_file_authors', request_method='GET',
925 renderer='rhodecode:templates/files/file_authors_box.mako')
926 def repo_file_authors(self):
927 c = self.load_default_context()
928
929 commit_id, f_path = self._get_commit_and_path()
930 commit = self._get_commit_or_redirect(commit_id)
931 file_node = self._get_filenode_or_redirect(commit, f_path)
932
933 if not file_node.is_file():
934 raise HTTPBadRequest()
935
936 c.file_last_commit = file_node.last_commit
937 if self.request.GET.get('annotate') == '1':
938 # use _hist from annotation if annotation mode is on
939 commit_ids = set(x[1] for x in file_node.annotate)
940 _hist = (
941 self.rhodecode_vcs_repo.get_commit(commit_id)
942 for commit_id in commit_ids)
943 else:
944 _f_history, _hist = self._get_node_history(commit, f_path)
945 c.file_author = False
946
947 unique = collections.OrderedDict()
948 for commit in _hist:
949 author = commit.author
950 if author not in unique:
951 unique[commit.author] = [
952 h.email(author),
953 h.person(author, 'username_or_name_or_email'),
954 1 # counter
955 ]
956
957 else:
958 # increase counter
959 unique[commit.author][2] += 1
960
961 c.authors = [val for val in unique.values()]
962
963 return self._get_template_context(c)
964
965 @LoginRequired()
966 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
967 @view_config(
968 route_name='repo_files_remove_file', request_method='GET',
969 renderer='rhodecode:templates/files/files_delete.mako')
970 def repo_files_remove_file(self):
971 _ = self.request.translate
972 c = self.load_default_context()
973 commit_id, f_path = self._get_commit_and_path()
974
975 self._ensure_not_locked()
976
977 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
978 h.flash(_('You can only delete files with commit '
979 'being a valid branch '), category='warning')
980 raise HTTPFound(
981 h.route_path('repo_files',
982 repo_name=self.db_repo_name, commit_id='tip',
983 f_path=f_path))
984
985 c.commit = self._get_commit_or_redirect(commit_id)
986 c.file = self._get_filenode_or_redirect(c.commit, f_path)
987
988 c.default_message = _(
989 'Deleted file {} via RhodeCode Enterprise').format(f_path)
990 c.f_path = f_path
991
992 return self._get_template_context(c)
993
994 @LoginRequired()
995 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
996 @CSRFRequired()
997 @view_config(
998 route_name='repo_files_delete_file', request_method='POST',
999 renderer=None)
1000 def repo_files_delete_file(self):
1001 _ = self.request.translate
1002
1003 c = self.load_default_context()
1004 commit_id, f_path = self._get_commit_and_path()
1005
1006 self._ensure_not_locked()
1007
1008 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1009 h.flash(_('You can only delete files with commit '
1010 'being a valid branch '), category='warning')
1011 raise HTTPFound(
1012 h.route_path('repo_files',
1013 repo_name=self.db_repo_name, commit_id='tip',
1014 f_path=f_path))
1015
1016 c.commit = self._get_commit_or_redirect(commit_id)
1017 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1018
1019 c.default_message = _(
1020 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1021 c.f_path = f_path
1022 node_path = f_path
1023 author = self._rhodecode_db_user.full_contact
1024 message = self.request.POST.get('message') or c.default_message
1025 try:
1026 nodes = {
1027 node_path: {
1028 'content': ''
1029 }
1030 }
1031 ScmModel().delete_nodes(
1032 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1033 message=message,
1034 nodes=nodes,
1035 parent_commit=c.commit,
1036 author=author,
1037 )
1038
1039 h.flash(
1040 _('Successfully deleted file `{}`').format(
1041 h.escape(f_path)), category='success')
1042 except Exception:
1043 log.exception('Error during commit operation')
1044 h.flash(_('Error occurred during commit'), category='error')
1045 raise HTTPFound(
1046 h.route_path('changeset_home', repo_name=self.db_repo_name,
1047 revision='tip'))
1048
1049 @LoginRequired()
1050 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1051 @view_config(
1052 route_name='repo_files_edit_file', request_method='GET',
1053 renderer='rhodecode:templates/files/files_edit.mako')
1054 def repo_files_edit_file(self):
1055 _ = self.request.translate
1056 c = self.load_default_context()
1057 commit_id, f_path = self._get_commit_and_path()
1058
1059 self._ensure_not_locked()
1060
1061 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1062 h.flash(_('You can only edit files with commit '
1063 'being a valid branch '), category='warning')
1064 raise HTTPFound(
1065 h.route_path('repo_files',
1066 repo_name=self.db_repo_name, commit_id='tip',
1067 f_path=f_path))
1068
1069 c.commit = self._get_commit_or_redirect(commit_id)
1070 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1071
1072 if c.file.is_binary:
1073 files_url = h.route_path(
1074 'repo_files',
1075 repo_name=self.db_repo_name,
1076 commit_id=c.commit.raw_id, f_path=f_path)
1077 raise HTTPFound(files_url)
1078
1079 c.default_message = _(
1080 'Edited file {} via RhodeCode Enterprise').format(f_path)
1081 c.f_path = f_path
1082
1083 return self._get_template_context(c)
1084
1085 @LoginRequired()
1086 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1087 @CSRFRequired()
1088 @view_config(
1089 route_name='repo_files_update_file', request_method='POST',
1090 renderer=None)
1091 def repo_files_update_file(self):
1092 _ = self.request.translate
1093 c = self.load_default_context()
1094 commit_id, f_path = self._get_commit_and_path()
1095
1096 self._ensure_not_locked()
1097
1098 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1099 h.flash(_('You can only edit files with commit '
1100 'being a valid branch '), category='warning')
1101 raise HTTPFound(
1102 h.route_path('repo_files',
1103 repo_name=self.db_repo_name, commit_id='tip',
1104 f_path=f_path))
1105
1106 c.commit = self._get_commit_or_redirect(commit_id)
1107 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1108
1109 if c.file.is_binary:
1110 raise HTTPFound(
1111 h.route_path('repo_files',
1112 repo_name=self.db_repo_name,
1113 commit_id=c.commit.raw_id,
1114 f_path=f_path))
1115
1116 c.default_message = _(
1117 'Edited file {} via RhodeCode Enterprise').format(f_path)
1118 c.f_path = f_path
1119 old_content = c.file.content
1120 sl = old_content.splitlines(1)
1121 first_line = sl[0] if sl else ''
1122
1123 r_post = self.request.POST
1124 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1125 mode = detect_mode(first_line, 0)
1126 content = convert_line_endings(r_post.get('content', ''), mode)
1127
1128 message = r_post.get('message') or c.default_message
1129 org_f_path = c.file.unicode_path
1130 filename = r_post['filename']
1131 org_filename = c.file.name
1132
1133 if content == old_content and filename == org_filename:
1134 h.flash(_('No changes'), category='warning')
1135 raise HTTPFound(
1136 h.route_path('changeset_home', repo_name=self.db_repo_name,
1137 revision='tip'))
1138 try:
1139 mapping = {
1140 org_f_path: {
1141 'org_filename': org_f_path,
1142 'filename': os.path.join(c.file.dir_path, filename),
1143 'content': content,
1144 'lexer': '',
1145 'op': 'mod',
1146 }
1147 }
1148
1149 ScmModel().update_nodes(
1150 user=self._rhodecode_db_user.user_id,
1151 repo=self.db_repo,
1152 message=message,
1153 nodes=mapping,
1154 parent_commit=c.commit,
1155 )
1156
1157 h.flash(
1158 _('Successfully committed changes to file `{}`').format(
1159 h.escape(f_path)), category='success')
1160 except Exception:
1161 log.exception('Error occurred during commit')
1162 h.flash(_('Error occurred during commit'), category='error')
1163 raise HTTPFound(
1164 h.route_path('changeset_home', repo_name=self.db_repo_name,
1165 revision='tip'))
1166
1167 @LoginRequired()
1168 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1169 @view_config(
1170 route_name='repo_files_add_file', request_method='GET',
1171 renderer='rhodecode:templates/files/files_add.mako')
1172 def repo_files_add_file(self):
1173 _ = self.request.translate
1174 c = self.load_default_context()
1175 commit_id, f_path = self._get_commit_and_path()
1176
1177 self._ensure_not_locked()
1178
1179 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1180 if c.commit is None:
1181 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1182 c.default_message = (_('Added file via RhodeCode Enterprise'))
1183 c.f_path = f_path
1184
1185 return self._get_template_context(c)
1186
1187 @LoginRequired()
1188 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1189 @CSRFRequired()
1190 @view_config(
1191 route_name='repo_files_create_file', request_method='POST',
1192 renderer=None)
1193 def repo_files_create_file(self):
1194 _ = self.request.translate
1195 c = self.load_default_context()
1196 commit_id, f_path = self._get_commit_and_path()
1197
1198 self._ensure_not_locked()
1199
1200 r_post = self.request.POST
1201
1202 c.commit = self._get_commit_or_redirect(
1203 commit_id, redirect_after=False)
1204 if c.commit is None:
1205 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1206 c.default_message = (_('Added file via RhodeCode Enterprise'))
1207 c.f_path = f_path
1208 unix_mode = 0
1209 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1210
1211 message = r_post.get('message') or c.default_message
1212 filename = r_post.get('filename')
1213 location = r_post.get('location', '') # dir location
1214 file_obj = r_post.get('upload_file', None)
1215
1216 if file_obj is not None and hasattr(file_obj, 'filename'):
1217 filename = r_post.get('filename_upload')
1218 content = file_obj.file
1219
1220 if hasattr(content, 'file'):
1221 # non posix systems store real file under file attr
1222 content = content.file
1223
1224 default_redirect_url = h.route_path(
1225 'changeset_home', repo_name=self.db_repo_name, revision='tip')
1226
1227 # If there's no commit, redirect to repo summary
1228 if type(c.commit) is EmptyCommit:
1229 redirect_url = h.route_path(
1230 'repo_summary', repo_name=self.db_repo_name)
1231 else:
1232 redirect_url = default_redirect_url
1233
1234 if not filename:
1235 h.flash(_('No filename'), category='warning')
1236 raise HTTPFound(redirect_url)
1237
1238 # extract the location from filename,
1239 # allows using foo/bar.txt syntax to create subdirectories
1240 subdir_loc = filename.rsplit('/', 1)
1241 if len(subdir_loc) == 2:
1242 location = os.path.join(location, subdir_loc[0])
1243
1244 # strip all crap out of file, just leave the basename
1245 filename = os.path.basename(filename)
1246 node_path = os.path.join(location, filename)
1247 author = self._rhodecode_db_user.full_contact
1248
1249 try:
1250 nodes = {
1251 node_path: {
1252 'content': content
1253 }
1254 }
1255 ScmModel().create_nodes(
1256 user=self._rhodecode_db_user.user_id,
1257 repo=self.db_repo,
1258 message=message,
1259 nodes=nodes,
1260 parent_commit=c.commit,
1261 author=author,
1262 )
1263
1264 h.flash(
1265 _('Successfully committed new file `{}`').format(
1266 h.escape(node_path)), category='success')
1267 except NonRelativePathError:
1268 h.flash(_(
1269 'The location specified must be a relative path and must not '
1270 'contain .. in the path'), category='warning')
1271 raise HTTPFound(default_redirect_url)
1272 except (NodeError, NodeAlreadyExistsError) as e:
1273 h.flash(_(h.escape(e)), category='error')
1274 except Exception:
1275 log.exception('Error occurred during commit')
1276 h.flash(_('Error occurred during commit'), category='error')
1277
1278 raise HTTPFound(default_redirect_url)
@@ -1,376 +1,385 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 29 from rhodecode.model import repo
30 30 from rhodecode.model import repo_group
31 31 from rhodecode.model.db import User
32 32 from rhodecode.model.scm import ScmModel
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 ADMIN_PREFIX = '/_admin'
38 38 STATIC_FILE_PREFIX = '/_static'
39 39
40 40
41 41 def add_route_with_slash(config,name, pattern, **kw):
42 42 config.add_route(name, pattern, **kw)
43 43 if not pattern.endswith('/'):
44 44 config.add_route(name + '_slash', pattern + '/', **kw)
45 45
46 46
47 47 def get_format_ref_id(repo):
48 48 """Returns a `repo` specific reference formatter function"""
49 49 if h.is_svn(repo):
50 50 return _format_ref_id_svn
51 51 else:
52 52 return _format_ref_id
53 53
54 54
55 55 def _format_ref_id(name, raw_id):
56 56 """Default formatting of a given reference `name`"""
57 57 return name
58 58
59 59
60 60 def _format_ref_id_svn(name, raw_id):
61 61 """Special way of formatting a reference for Subversion including path"""
62 62 return '%s@%s' % (name, raw_id)
63 63
64 64
65 65 class TemplateArgs(StrictAttributeDict):
66 66 pass
67 67
68 68
69 69 class BaseAppView(object):
70 70
71 71 def __init__(self, context, request):
72 72 self.request = request
73 73 self.context = context
74 74 self.session = request.session
75 75 self._rhodecode_user = request.user # auth user
76 76 self._rhodecode_db_user = self._rhodecode_user.get_instance()
77 77 self._maybe_needs_password_change(
78 78 request.matched_route.name, self._rhodecode_db_user)
79 79
80 80 def _maybe_needs_password_change(self, view_name, user_obj):
81 81 log.debug('Checking if user %s needs password change on view %s',
82 82 user_obj, view_name)
83 83 skip_user_views = [
84 84 'logout', 'login',
85 85 'my_account_password', 'my_account_password_update'
86 86 ]
87 87
88 88 if not user_obj:
89 89 return
90 90
91 91 if user_obj.username == User.DEFAULT_USER:
92 92 return
93 93
94 94 now = time.time()
95 95 should_change = user_obj.user_data.get('force_password_change')
96 96 change_after = safe_int(should_change) or 0
97 97 if should_change and now > change_after:
98 98 log.debug('User %s requires password change', user_obj)
99 99 h.flash('You are required to change your password', 'warning',
100 100 ignore_duplicate=True)
101 101
102 102 if view_name not in skip_user_views:
103 103 raise HTTPFound(
104 104 self.request.route_path('my_account_password'))
105 105
106 106 def _get_local_tmpl_context(self, include_app_defaults=False):
107 107 c = TemplateArgs()
108 108 c.auth_user = self.request.user
109 109 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
110 110 c.rhodecode_user = self.request.user
111 111
112 112 if include_app_defaults:
113 113 # NOTE(marcink): after full pyramid migration include_app_defaults
114 114 # should be turned on by default
115 115 from rhodecode.lib.base import attach_context_attributes
116 116 attach_context_attributes(c, self.request, self.request.user.user_id)
117 117
118 118 return c
119 119
120 120 def _register_global_c(self, tmpl_args):
121 121 """
122 122 Registers attributes to pylons global `c`
123 123 """
124 124
125 125 # TODO(marcink): remove once pyramid migration is finished
126 126 from pylons import tmpl_context as c
127 127 try:
128 128 for k, v in tmpl_args.items():
129 129 setattr(c, k, v)
130 130 except TypeError:
131 131 log.exception('Failed to register pylons C')
132 132 pass
133 133
134 134 def _get_template_context(self, tmpl_args):
135 135 self._register_global_c(tmpl_args)
136 136
137 137 local_tmpl_args = {
138 138 'defaults': {},
139 139 'errors': {},
140 140 # register a fake 'c' to be used in templates instead of global
141 141 # pylons c, after migration to pyramid we should rename it to 'c'
142 142 # make sure we replace usage of _c in templates too
143 143 '_c': tmpl_args
144 144 }
145 145 local_tmpl_args.update(tmpl_args)
146 146 return local_tmpl_args
147 147
148 148 def load_default_context(self):
149 149 """
150 150 example:
151 151
152 152 def load_default_context(self):
153 153 c = self._get_local_tmpl_context()
154 154 c.custom_var = 'foobar'
155 155 self._register_global_c(c)
156 156 return c
157 157 """
158 158 raise NotImplementedError('Needs implementation in view class')
159 159
160 160
161 161 class RepoAppView(BaseAppView):
162 162
163 163 def __init__(self, context, request):
164 164 super(RepoAppView, self).__init__(context, request)
165 165 self.db_repo = request.db_repo
166 166 self.db_repo_name = self.db_repo.repo_name
167 167 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
168 168
169 169 def _handle_missing_requirements(self, error):
170 170 log.error(
171 171 'Requirements are missing for repository %s: %s',
172 172 self.db_repo_name, error.message)
173 173
174 174 def _get_local_tmpl_context(self, include_app_defaults=False):
175 175 c = super(RepoAppView, self)._get_local_tmpl_context(
176 176 include_app_defaults=include_app_defaults)
177 177
178 178 # register common vars for this type of view
179 179 c.rhodecode_db_repo = self.db_repo
180 180 c.repo_name = self.db_repo_name
181 181 c.repository_pull_requests = self.db_repo_pull_requests
182 182
183 183 c.repository_requirements_missing = False
184 184 try:
185 185 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
186 186 except RepositoryRequirementError as e:
187 187 c.repository_requirements_missing = True
188 188 self._handle_missing_requirements(e)
189 189
190 190 return c
191 191
192 192
193 193 class DataGridAppView(object):
194 194 """
195 195 Common class to have re-usable grid rendering components
196 196 """
197 197
198 198 def _extract_ordering(self, request, column_map=None):
199 199 column_map = column_map or {}
200 200 column_index = safe_int(request.GET.get('order[0][column]'))
201 201 order_dir = request.GET.get(
202 202 'order[0][dir]', 'desc')
203 203 order_by = request.GET.get(
204 204 'columns[%s][data][sort]' % column_index, 'name_raw')
205 205
206 206 # translate datatable to DB columns
207 207 order_by = column_map.get(order_by) or order_by
208 208
209 209 search_q = request.GET.get('search[value]')
210 210 return search_q, order_by, order_dir
211 211
212 212 def _extract_chunk(self, request):
213 213 start = safe_int(request.GET.get('start'), 0)
214 214 length = safe_int(request.GET.get('length'), 25)
215 215 draw = safe_int(request.GET.get('draw'))
216 216 return draw, start, length
217 217
218 218
219 219 class BaseReferencesView(RepoAppView):
220 220 """
221 221 Base for reference view for branches, tags and bookmarks.
222 222 """
223 223 def load_default_context(self):
224 224 c = self._get_local_tmpl_context()
225 225
226 226 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
227 227 c.repo_info = self.db_repo
228 228
229 229 self._register_global_c(c)
230 230 return c
231 231
232 232 def load_refs_context(self, ref_items, partials_template):
233 233 _render = self.request.get_partial_renderer(partials_template)
234 234 pre_load = ["author", "date", "message"]
235 235
236 236 is_svn = h.is_svn(self.rhodecode_vcs_repo)
237 237 is_hg = h.is_hg(self.rhodecode_vcs_repo)
238 238
239 239 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
240 240
241 241 closed_refs = {}
242 242 if is_hg:
243 243 closed_refs = self.rhodecode_vcs_repo.branches_closed
244 244
245 245 data = []
246 246 for ref_name, commit_id in ref_items:
247 247 commit = self.rhodecode_vcs_repo.get_commit(
248 248 commit_id=commit_id, pre_load=pre_load)
249 249 closed = ref_name in closed_refs
250 250
251 251 # TODO: johbo: Unify generation of reference links
252 252 use_commit_id = '/' in ref_name or is_svn
253 files_url = h.url(
254 'files_home',
255 repo_name=self.db_repo_name,
256 f_path=ref_name if is_svn else '',
257 revision=commit_id if use_commit_id else ref_name,
258 at=ref_name)
253
254 if use_commit_id:
255 files_url = h.route_path(
256 'repo_files',
257 repo_name=self.db_repo_name,
258 f_path=ref_name if is_svn else '',
259 commit_id=commit_id)
260
261 else:
262 files_url = h.route_path(
263 'repo_files',
264 repo_name=self.db_repo_name,
265 f_path=ref_name if is_svn else '',
266 commit_id=ref_name,
267 _query=dict(at=ref_name))
259 268
260 269 data.append({
261 270 "name": _render('name', ref_name, files_url, closed),
262 271 "name_raw": ref_name,
263 272 "date": _render('date', commit.date),
264 273 "date_raw": datetime_to_time(commit.date),
265 274 "author": _render('author', commit.author),
266 275 "commit": _render(
267 276 'commit', commit.message, commit.raw_id, commit.idx),
268 277 "commit_raw": commit.idx,
269 278 "compare": _render(
270 279 'compare', format_ref_id(ref_name, commit.raw_id)),
271 280 })
272 281
273 282 return data
274 283
275 284
276 285 class RepoRoutePredicate(object):
277 286 def __init__(self, val, config):
278 287 self.val = val
279 288
280 289 def text(self):
281 290 return 'repo_route = %s' % self.val
282 291
283 292 phash = text
284 293
285 294 def __call__(self, info, request):
286 295
287 296 if hasattr(request, 'vcs_call'):
288 297 # skip vcs calls
289 298 return
290 299
291 300 repo_name = info['match']['repo_name']
292 301 repo_model = repo.RepoModel()
293 302 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
294 303
295 304 if by_name_match:
296 305 # register this as request object we can re-use later
297 306 request.db_repo = by_name_match
298 307 return True
299 308
300 309 by_id_match = repo_model.get_repo_by_id(repo_name)
301 310 if by_id_match:
302 311 request.db_repo = by_id_match
303 312 return True
304 313
305 314 return False
306 315
307 316
308 317 class RepoTypeRoutePredicate(object):
309 318 def __init__(self, val, config):
310 319 self.val = val or ['hg', 'git', 'svn']
311 320
312 321 def text(self):
313 322 return 'repo_accepted_type = %s' % self.val
314 323
315 324 phash = text
316 325
317 326 def __call__(self, info, request):
318 327 if hasattr(request, 'vcs_call'):
319 328 # skip vcs calls
320 329 return
321 330
322 331 rhodecode_db_repo = request.db_repo
323 332
324 333 log.debug(
325 334 '%s checking repo type for %s in %s',
326 335 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
327 336
328 337 if rhodecode_db_repo.repo_type in self.val:
329 338 return True
330 339 else:
331 340 log.warning('Current view is not supported for repo type:%s',
332 341 rhodecode_db_repo.repo_type)
333 342 #
334 343 # h.flash(h.literal(
335 344 # _('Action not supported for %s.' % rhodecode_repo.alias)),
336 345 # category='warning')
337 346 # return redirect(
338 347 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
339 348
340 349 return False
341 350
342 351
343 352 class RepoGroupRoutePredicate(object):
344 353 def __init__(self, val, config):
345 354 self.val = val
346 355
347 356 def text(self):
348 357 return 'repo_group_route = %s' % self.val
349 358
350 359 phash = text
351 360
352 361 def __call__(self, info, request):
353 362 if hasattr(request, 'vcs_call'):
354 363 # skip vcs calls
355 364 return
356 365
357 366 repo_group_name = info['match']['repo_group_name']
358 367 repo_group_model = repo_group.RepoGroupModel()
359 368 by_name_match = repo_group_model.get_by_group_name(
360 369 repo_group_name, cache=True)
361 370
362 371 if by_name_match:
363 372 # register this as request object we can re-use later
364 373 request.db_repo_group = by_name_match
365 374 return True
366 375
367 376 return False
368 377
369 378
370 379 def includeme(config):
371 380 config.add_route_predicate(
372 381 'repo_route', RepoRoutePredicate)
373 382 config.add_route_predicate(
374 383 'repo_accepted_types', RepoTypeRoutePredicate)
375 384 config.add_route_predicate(
376 385 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,177 +1,266 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # Summary
26 26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 27 # all pattern
28 28 config.add_route(
29 29 name='repo_summary_explicit',
30 30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 31 config.add_route(
32 32 name='repo_summary_commits',
33 33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34 34
35 35 # repo commits
36 36 config.add_route(
37 37 name='repo_commit',
38 38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39 39
40 # repo files
41 config.add_route(
42 name='repo_archivefile',
43 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
44
45 config.add_route(
46 name='repo_files_diff',
47 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
48 config.add_route( # legacy route to make old links work
49 name='repo_files_diff_2way_redirect',
50 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
51
52 config.add_route(
53 name='repo_files',
54 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
55 config.add_route(
56 name='repo_files:default_path',
57 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
58 config.add_route(
59 name='repo_files:default_commit',
60 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
61
62 config.add_route(
63 name='repo_files:rendered',
64 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
65
66 config.add_route(
67 name='repo_files:annotated',
68 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
69 config.add_route(
70 name='repo_files:annotated_previous',
71 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
72
73 config.add_route(
74 name='repo_nodetree_full',
75 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
76 config.add_route(
77 name='repo_nodetree_full:default_path',
78 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
79
80 config.add_route(
81 name='repo_files_nodelist',
82 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
83
84 config.add_route(
85 name='repo_file_raw',
86 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
87
88 config.add_route(
89 name='repo_file_download',
90 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
91 config.add_route( # backward compat to keep old links working
92 name='repo_file_download:legacy',
93 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
94 repo_route=True)
95
96 config.add_route(
97 name='repo_file_history',
98 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
99
100 config.add_route(
101 name='repo_file_authors',
102 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
103
104 config.add_route(
105 name='repo_files_remove_file',
106 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
107 repo_route=True)
108 config.add_route(
109 name='repo_files_delete_file',
110 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
111 repo_route=True)
112 config.add_route(
113 name='repo_files_edit_file',
114 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
115 repo_route=True)
116 config.add_route(
117 name='repo_files_update_file',
118 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
119 repo_route=True)
120 config.add_route(
121 name='repo_files_add_file',
122 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
123 repo_route=True)
124 config.add_route(
125 name='repo_files_create_file',
126 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
127 repo_route=True)
128
40 129 # refs data
41 130 config.add_route(
42 131 name='repo_refs_data',
43 132 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
44 133
45 134 config.add_route(
46 135 name='repo_refs_changelog_data',
47 136 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
48 137
49 138 config.add_route(
50 139 name='repo_stats',
51 140 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
52 141
53 142 # Tags
54 143 config.add_route(
55 144 name='tags_home',
56 145 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
57 146
58 147 # Branches
59 148 config.add_route(
60 149 name='branches_home',
61 150 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
62 151
63 152 config.add_route(
64 153 name='bookmarks_home',
65 154 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
66 155
67 156 # Pull Requests
68 157 config.add_route(
69 158 name='pullrequest_show',
70 159 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
71 160 repo_route=True)
72 161
73 162 config.add_route(
74 163 name='pullrequest_show_all',
75 164 pattern='/{repo_name:.*?[^/]}/pull-request',
76 165 repo_route=True, repo_accepted_types=['hg', 'git'])
77 166
78 167 config.add_route(
79 168 name='pullrequest_show_all_data',
80 169 pattern='/{repo_name:.*?[^/]}/pull-request-data',
81 170 repo_route=True, repo_accepted_types=['hg', 'git'])
82 171
83 172 # commits aka changesets
84 173 # TODO(dan): handle default landing revision ?
85 174 config.add_route(
86 175 name='changeset_home',
87 176 pattern='/{repo_name:.*?[^/]}/changeset/{revision}',
88 177 repo_route=True)
89 178 config.add_route(
90 179 name='changeset_children',
91 180 pattern='/{repo_name:.*?[^/]}/changeset_children/{revision}',
92 181 repo_route=True)
93 182 config.add_route(
94 183 name='changeset_parents',
95 184 pattern='/{repo_name:.*?[^/]}/changeset_parents/{revision}',
96 185 repo_route=True)
97 186
98 187 # Settings
99 188 config.add_route(
100 189 name='edit_repo',
101 190 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
102 191
103 192 # Settings advanced
104 193 config.add_route(
105 194 name='edit_repo_advanced',
106 195 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
107 196 config.add_route(
108 197 name='edit_repo_advanced_delete',
109 198 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
110 199 config.add_route(
111 200 name='edit_repo_advanced_locking',
112 201 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
113 202 config.add_route(
114 203 name='edit_repo_advanced_journal',
115 204 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
116 205 config.add_route(
117 206 name='edit_repo_advanced_fork',
118 207 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
119 208
120 209 # Caches
121 210 config.add_route(
122 211 name='edit_repo_caches',
123 212 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
124 213
125 214 # Permissions
126 215 config.add_route(
127 216 name='edit_repo_perms',
128 217 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
129 218
130 219 # Repo Review Rules
131 220 config.add_route(
132 221 name='repo_reviewers',
133 222 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
134 223
135 224 config.add_route(
136 225 name='repo_default_reviewers_data',
137 226 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
138 227
139 228 # Maintenance
140 229 config.add_route(
141 230 name='repo_maintenance',
142 231 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
143 232
144 233 config.add_route(
145 234 name='repo_maintenance_execute',
146 235 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
147 236
148 237 # Strip
149 238 config.add_route(
150 239 name='strip',
151 240 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
152 241
153 242 config.add_route(
154 243 name='strip_check',
155 244 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
156 245
157 246 config.add_route(
158 247 name='strip_execute',
159 248 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
160 249
161 250 # ATOM/RSS Feed
162 251 config.add_route(
163 252 name='rss_feed_home',
164 253 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
165 254
166 255 config.add_route(
167 256 name='atom_feed_home',
168 257 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
169 258
170 259 # NOTE(marcink): needs to be at the end for catch-all
171 260 add_route_with_slash(
172 261 config,
173 262 name='repo_summary',
174 263 pattern='/{repo_name:.*?[^/]}', repo_route=True)
175 264
176 265 # Scan module for configuration decorators.
177 266 config.scan()
This diff has been collapsed as it changes many lines, (959 lines changed) Show them Hide them
@@ -1,987 +1,1062 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 from rhodecode.controllers.files import FilesController
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.vcs import nodes
31 31
32 32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.tests import (
34 url, assert_session_flash, assert_not_in_session_flash)
33 from rhodecode.tests import assert_session_flash
35 34 from rhodecode.tests.fixture import Fixture
36 35
37 36 fixture = Fixture()
38 37
39 38 NODE_HISTORY = {
40 39 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
41 40 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
42 41 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
43 42 }
44 43
45 44
45 def route_path(name, params=None, **kwargs):
46 import urllib
47
48 base_url = {
49 'repo_archivefile': '/{repo_name}/archive/{fname}',
50 'repo_files_diff': '/{repo_name}/diff/{f_path}',
51 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
52 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
53 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
54 'repo_files:default_commit': '/{repo_name}/files',
55 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
56 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
57 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
58 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
59 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
60 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
61 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
62 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
63 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
64 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
65 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
66 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
67 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
68 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
69 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
70 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
71 }[name].format(**kwargs)
72
73 if params:
74 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
75 return base_url
76
77
78 def assert_files_in_response(response, files, params):
79 template = (
80 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
81 _assert_items_in_response(response, files, template, params)
82
83
84 def assert_dirs_in_response(response, dirs, params):
85 template = (
86 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
87 _assert_items_in_response(response, dirs, template, params)
88
89
90 def _assert_items_in_response(response, items, template, params):
91 for item in items:
92 item_params = {'name': item}
93 item_params.update(params)
94 response.mustcontain(template % item_params)
95
96
97 def assert_timeago_in_response(response, items, params):
98 for item in items:
99 response.mustcontain(h.age_component(params['date']))
100
46 101
47 102 @pytest.mark.usefixtures("app")
48 class TestFilesController:
103 class TestFilesViews(object):
49 104
50 def test_index(self, backend):
51 response = self.app.get(url(
52 controller='files', action='index',
53 repo_name=backend.repo_name, revision='tip', f_path='/'))
105 def test_show_files(self, backend):
106 response = self.app.get(
107 route_path('repo_files',
108 repo_name=backend.repo_name,
109 commit_id='tip', f_path='/'))
54 110 commit = backend.repo.get_commit()
55 111
56 112 params = {
57 113 'repo_name': backend.repo_name,
58 114 'commit_id': commit.raw_id,
59 115 'date': commit.date
60 116 }
61 117 assert_dirs_in_response(response, ['docs', 'vcs'], params)
62 118 files = [
63 119 '.gitignore',
64 120 '.hgignore',
65 121 '.hgtags',
66 122 # TODO: missing in Git
67 123 # '.travis.yml',
68 124 'MANIFEST.in',
69 125 'README.rst',
70 126 # TODO: File is missing in svn repository
71 127 # 'run_test_and_report.sh',
72 128 'setup.cfg',
73 129 'setup.py',
74 130 'test_and_report.sh',
75 131 'tox.ini',
76 132 ]
77 133 assert_files_in_response(response, files, params)
78 134 assert_timeago_in_response(response, files, params)
79 135
80 def test_index_links_submodules_with_absolute_url(self, backend_hg):
136 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
81 137 repo = backend_hg['subrepos']
82 response = self.app.get(url(
83 controller='files', action='index',
84 repo_name=repo.repo_name, revision='tip', f_path='/'))
138 response = self.app.get(
139 route_path('repo_files',
140 repo_name=repo.repo_name,
141 commit_id='tip', f_path='/'))
85 142 assert_response = response.assert_response()
86 143 assert_response.contains_one_link(
87 144 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
88 145
89 def test_index_links_submodules_with_absolute_url_subpaths(
146 def test_show_files_links_submodules_with_absolute_url_subpaths(
90 147 self, backend_hg):
91 148 repo = backend_hg['subrepos']
92 response = self.app.get(url(
93 controller='files', action='index',
94 repo_name=repo.repo_name, revision='tip', f_path='/'))
149 response = self.app.get(
150 route_path('repo_files',
151 repo_name=repo.repo_name,
152 commit_id='tip', f_path='/'))
95 153 assert_response = response.assert_response()
96 154 assert_response.contains_one_link(
97 155 'subpaths-path @ 000000000000',
98 156 'http://sub-base.example.com/subpaths-path')
99 157
100 158 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
101 159 def test_files_menu(self, backend):
102 160 new_branch = "temp_branch_name"
103 161 commits = [
104 162 {'message': 'a'},
105 163 {'message': 'b', 'branch': new_branch}
106 164 ]
107 165 backend.create_repo(commits)
108 166
109 167 backend.repo.landing_rev = "branch:%s" % new_branch
110 168
111 # get response based on tip and not new revision
112 response = self.app.get(url(
113 controller='files', action='index',
114 repo_name=backend.repo_name, revision='tip', f_path='/'),
115 status=200)
169 # get response based on tip and not new commit
170 response = self.app.get(
171 route_path('repo_files',
172 repo_name=backend.repo_name,
173 commit_id='tip', f_path='/'))
116 174
117 # make sure Files menu url is not tip but new revision
175 # make sure Files menu url is not tip but new commit
118 176 landing_rev = backend.repo.landing_rev[1]
119 files_url = url('files_home', repo_name=backend.repo_name,
120 revision=landing_rev)
177 files_url = route_path('repo_files:default_path',
178 repo_name=backend.repo_name,
179 commit_id=landing_rev)
121 180
122 181 assert landing_rev != 'tip'
123 response.mustcontain('<li class="active"><a class="menulink" href="%s">' % files_url)
182 response.mustcontain(
183 '<li class="active"><a class="menulink" href="%s">' % files_url)
124 184
125 def test_index_commit(self, backend):
185 def test_show_files_commit(self, backend):
126 186 commit = backend.repo.get_commit(commit_idx=32)
127 187
128 response = self.app.get(url(
129 controller='files', action='index',
130 repo_name=backend.repo_name,
131 revision=commit.raw_id,
132 f_path='/')
133 )
188 response = self.app.get(
189 route_path('repo_files',
190 repo_name=backend.repo_name,
191 commit_id=commit.raw_id, f_path='/'))
134 192
135 193 dirs = ['docs', 'tests']
136 194 files = ['README.rst']
137 195 params = {
138 196 'repo_name': backend.repo_name,
139 197 'commit_id': commit.raw_id,
140 198 }
141 199 assert_dirs_in_response(response, dirs, params)
142 200 assert_files_in_response(response, files, params)
143 201
144 def test_index_different_branch(self, backend):
202 def test_show_files_different_branch(self, backend):
145 203 branches = dict(
146 204 hg=(150, ['git']),
147 205 # TODO: Git test repository does not contain other branches
148 206 git=(633, ['master']),
149 207 # TODO: Branch support in Subversion
150 208 svn=(150, [])
151 209 )
152 210 idx, branches = branches[backend.alias]
153 211 commit = backend.repo.get_commit(commit_idx=idx)
154 response = self.app.get(url(
155 controller='files', action='index',
156 repo_name=backend.repo_name,
157 revision=commit.raw_id,
158 f_path='/'))
212 response = self.app.get(
213 route_path('repo_files',
214 repo_name=backend.repo_name,
215 commit_id=commit.raw_id, f_path='/'))
216
159 217 assert_response = response.assert_response()
160 218 for branch in branches:
161 219 assert_response.element_contains('.tags .branchtag', branch)
162 220
163 def test_index_paging(self, backend):
221 def test_show_files_paging(self, backend):
164 222 repo = backend.repo
165 223 indexes = [73, 92, 109, 1, 0]
166 224 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
167 225 for rev in indexes]
168 226
169 227 for idx in idx_map:
170 response = self.app.get(url(
171 controller='files', action='index',
172 repo_name=backend.repo_name,
173 revision=idx[1],
174 f_path='/'))
228 response = self.app.get(
229 route_path('repo_files',
230 repo_name=backend.repo_name,
231 commit_id=idx[1], f_path='/'))
175 232
176 233 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
177 234
178 235 def test_file_source(self, backend):
179 236 commit = backend.repo.get_commit(commit_idx=167)
180 response = self.app.get(url(
181 controller='files', action='index',
182 repo_name=backend.repo_name,
183 revision=commit.raw_id,
184 f_path='vcs/nodes.py'))
237 response = self.app.get(
238 route_path('repo_files',
239 repo_name=backend.repo_name,
240 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
185 241
186 242 msgbox = """<div class="commit right-content">%s</div>"""
187 243 response.mustcontain(msgbox % (commit.message, ))
188 244
189 245 assert_response = response.assert_response()
190 246 if commit.branch:
191 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
247 assert_response.element_contains(
248 '.tags.tags-main .branchtag', commit.branch)
192 249 if commit.tags:
193 250 for tag in commit.tags:
194 251 assert_response.element_contains('.tags.tags-main .tagtag', tag)
195 252
196 def test_file_source_history(self, backend):
197 response = self.app.get(
198 url(
199 controller='files', action='history',
200 repo_name=backend.repo_name,
201 revision='tip',
202 f_path='vcs/nodes.py'),
203 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
204 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
205
206 def test_file_source_history_svn(self, backend_svn):
207 simple_repo = backend_svn['svn-simple-layout']
253 def test_file_source_annotated(self, backend):
208 254 response = self.app.get(
209 url(
210 controller='files', action='history',
211 repo_name=simple_repo.repo_name,
212 revision='tip',
213 f_path='trunk/example.py'),
214 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
215
216 expected_data = json.loads(
217 fixture.load_resource('svn_node_history_branches.json'))
218 assert expected_data == response.json
219
220 def test_file_annotation_history(self, backend):
221 response = self.app.get(
222 url(
223 controller='files', action='history',
224 repo_name=backend.repo_name,
225 revision='tip',
226 f_path='vcs/nodes.py',
227 annotate=True),
228 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
229 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
230
231 def test_file_annotation(self, backend):
232 response = self.app.get(url(
233 controller='files', action='index',
234 repo_name=backend.repo_name, revision='tip', f_path='vcs/nodes.py',
235 annotate=True))
236
237 expected_revisions = {
255 route_path('repo_files:annotated',
256 repo_name=backend.repo_name,
257 commit_id='tip', f_path='vcs/nodes.py'))
258 expected_commits = {
238 259 'hg': 'r356',
239 260 'git': 'r345',
240 261 'svn': 'r208',
241 262 }
242 response.mustcontain(expected_revisions[backend.alias])
263 response.mustcontain(expected_commits[backend.alias])
243 264
244 def test_file_authors(self, backend):
245 response = self.app.get(url(
246 controller='files', action='authors',
247 repo_name=backend.repo_name,
248 revision='tip',
249 f_path='vcs/nodes.py',
250 annotate=True))
265 def test_file_source_authors(self, backend):
266 response = self.app.get(
267 route_path('repo_file_authors',
268 repo_name=backend.repo_name,
269 commit_id='tip', f_path='vcs/nodes.py'))
270 expected_authors = {
271 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
272 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
273 'svn': ('marcin', 'lukasz'),
274 }
251 275
276 for author in expected_authors[backend.alias]:
277 response.mustcontain(author)
278
279 def test_file_source_authors_with_annotation(self, backend):
280 response = self.app.get(
281 route_path('repo_file_authors',
282 repo_name=backend.repo_name,
283 commit_id='tip', f_path='vcs/nodes.py',
284 params=dict(annotate=1)))
252 285 expected_authors = {
253 286 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
254 287 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
255 288 'svn': ('marcin', 'lukasz'),
256 289 }
257 290
258 291 for author in expected_authors[backend.alias]:
259 292 response.mustcontain(author)
260 293
294 def test_file_source_history(self, backend, xhr_header):
295 response = self.app.get(
296 route_path('repo_file_history',
297 repo_name=backend.repo_name,
298 commit_id='tip', f_path='vcs/nodes.py'),
299 extra_environ=xhr_header)
300 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
301
302 def test_file_source_history_svn(self, backend_svn, xhr_header):
303 simple_repo = backend_svn['svn-simple-layout']
304 response = self.app.get(
305 route_path('repo_file_history',
306 repo_name=simple_repo.repo_name,
307 commit_id='tip', f_path='trunk/example.py'),
308 extra_environ=xhr_header)
309
310 expected_data = json.loads(
311 fixture.load_resource('svn_node_history_branches.json'))
312 assert expected_data == response.json
313
314 def test_file_source_history_with_annotation(self, backend, xhr_header):
315 response = self.app.get(
316 route_path('repo_file_history',
317 repo_name=backend.repo_name,
318 commit_id='tip', f_path='vcs/nodes.py',
319 params=dict(annotate=1)),
320
321 extra_environ=xhr_header)
322 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
323
261 324 def test_tree_search_top_level(self, backend, xhr_header):
262 325 commit = backend.repo.get_commit(commit_idx=173)
263 326 response = self.app.get(
264 url('files_nodelist_home', repo_name=backend.repo_name,
265 revision=commit.raw_id, f_path='/'),
327 route_path('repo_files_nodelist',
328 repo_name=backend.repo_name,
329 commit_id=commit.raw_id, f_path='/'),
266 330 extra_environ=xhr_header)
267 331 assert 'nodes' in response.json
268 332 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
269 333
334 def test_tree_search_missing_xhr(self, backend):
335 self.app.get(
336 route_path('repo_files_nodelist',
337 repo_name=backend.repo_name,
338 commit_id='tip', f_path='/'),
339 status=404)
340
270 341 def test_tree_search_at_path(self, backend, xhr_header):
271 342 commit = backend.repo.get_commit(commit_idx=173)
272 343 response = self.app.get(
273 url('files_nodelist_home', repo_name=backend.repo_name,
274 revision=commit.raw_id, f_path='/docs'),
344 route_path('repo_files_nodelist',
345 repo_name=backend.repo_name,
346 commit_id=commit.raw_id, f_path='/docs'),
275 347 extra_environ=xhr_header)
276 348 assert 'nodes' in response.json
277 349 nodes = response.json['nodes']
278 350 assert {'name': 'docs/api', 'type': 'dir'} in nodes
279 351 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
280 352
281 def test_tree_search_at_path_missing_xhr(self, backend):
282 self.app.get(
283 url('files_nodelist_home', repo_name=backend.repo_name,
284 revision='tip', f_path=''), status=400)
285
286 def test_tree_view_list(self, backend, xhr_header):
287 commit = backend.repo.get_commit(commit_idx=173)
288 response = self.app.get(
289 url('files_nodelist_home', repo_name=backend.repo_name,
290 f_path='/', revision=commit.raw_id),
291 extra_environ=xhr_header,
292 )
293 response.mustcontain("vcs/web/simplevcs/views/repository.py")
294
295 def test_tree_view_list_at_path(self, backend, xhr_header):
353 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
296 354 commit = backend.repo.get_commit(commit_idx=173)
297 355 response = self.app.get(
298 url('files_nodelist_home', repo_name=backend.repo_name,
299 f_path='/docs', revision=commit.raw_id),
300 extra_environ=xhr_header,
301 )
302 response.mustcontain("docs/index.rst")
356 route_path('repo_files_nodelist',
357 repo_name=backend.repo_name,
358 commit_id=commit.raw_id, f_path='/docs/api'),
359 extra_environ=xhr_header)
360 assert 'nodes' in response.json
361 nodes = response.json['nodes']
362 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
303 363
304 def test_tree_view_list_missing_xhr(self, backend):
364 def test_tree_search_at_path_missing_xhr(self, backend):
305 365 self.app.get(
306 url('files_nodelist_home', repo_name=backend.repo_name,
307 f_path='/', revision='tip'), status=400)
366 route_path('repo_files_nodelist',
367 repo_name=backend.repo_name,
368 commit_id='tip', f_path='/docs'),
369 status=404)
308 370
309 def test_nodetree_full_success(self, backend, xhr_header):
371 def test_nodetree(self, backend, xhr_header):
310 372 commit = backend.repo.get_commit(commit_idx=173)
311 373 response = self.app.get(
312 url('files_nodetree_full', repo_name=backend.repo_name,
313 f_path='/', commit_id=commit.raw_id),
374 route_path('repo_nodetree_full',
375 repo_name=backend.repo_name,
376 commit_id=commit.raw_id, f_path='/'),
314 377 extra_environ=xhr_header)
315 378
316 379 assert_response = response.assert_response()
317 380
318 381 for attr in ['data-commit-id', 'data-date', 'data-author']:
319 382 elements = assert_response.get_elements('[{}]'.format(attr))
320 383 assert len(elements) > 1
321 384
322 385 for element in elements:
323 386 assert element.get(attr)
324 387
325 def test_nodetree_full_if_file(self, backend, xhr_header):
388 def test_nodetree_if_file(self, backend, xhr_header):
326 389 commit = backend.repo.get_commit(commit_idx=173)
327 390 response = self.app.get(
328 url('files_nodetree_full', repo_name=backend.repo_name,
329 f_path='README.rst', commit_id=commit.raw_id),
391 route_path('repo_nodetree_full',
392 repo_name=backend.repo_name,
393 commit_id=commit.raw_id, f_path='README.rst'),
330 394 extra_environ=xhr_header)
331 395 assert response.body == ''
332 396
333 def test_tree_metadata_list_missing_xhr(self, backend):
334 self.app.get(
335 url('files_nodetree_full', repo_name=backend.repo_name,
336 f_path='/', commit_id='tip'), status=400)
397 def test_nodetree_wrong_path(self, backend, xhr_header):
398 commit = backend.repo.get_commit(commit_idx=173)
399 response = self.app.get(
400 route_path('repo_nodetree_full',
401 repo_name=backend.repo_name,
402 commit_id=commit.raw_id, f_path='/dont-exist'),
403 extra_environ=xhr_header)
337 404
338 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
339 self, app, backend_stub, autologin_regular_user, user_regular,
340 user_util):
341 repo = backend_stub.create_repo()
342 user_util.grant_user_permission_to_repo(
343 repo, user_regular, 'repository.write')
344 response = self.app.get(url(
345 controller='files', action='index',
346 repo_name=repo.repo_name, revision='tip', f_path='/'))
347 assert_session_flash(
348 response,
349 'There are no files yet. <a class="alert-link" '
350 'href="/%s/add/0/#edit">Click here to add a new file.</a>'
351 % (repo.repo_name))
405 err = 'error: There is no file nor ' \
406 'directory at the given path'
407 assert err in response.body
352 408
353 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
354 self, backend_stub, user_util):
355 repo = backend_stub.create_repo()
356 repo_file_url = url(
357 'files_add_home',
358 repo_name=repo.repo_name,
359 revision=0, f_path='', anchor='edit')
360 response = self.app.get(url(
361 controller='files', action='index',
362 repo_name=repo.repo_name, revision='tip', f_path='/'))
363 assert_not_in_session_flash(response, repo_file_url)
409 def test_nodetree_missing_xhr(self, backend):
410 self.app.get(
411 route_path('repo_nodetree_full',
412 repo_name=backend.repo_name,
413 commit_id='tip', f_path='/'),
414 status=404)
364 415
365 416
366 # TODO: johbo: Think about a better place for these tests. Either controller
367 # specific unit tests or we move down the whole logic further towards the vcs
368 # layer
369 class TestAdjustFilePathForSvn(object):
370 """SVN specific adjustments of node history in FileController."""
417 @pytest.mark.usefixtures("app", "autologin_user")
418 class TestRawFileHandling(object):
419
420 def test_download_file(self, backend):
421 commit = backend.repo.get_commit(commit_idx=173)
422 response = self.app.get(
423 route_path('repo_file_download',
424 repo_name=backend.repo_name,
425 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
426
427 assert response.content_disposition == "attachment; filename=nodes.py"
428 assert response.content_type == "text/x-python"
429
430 def test_download_file_wrong_cs(self, backend):
431 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
432
433 response = self.app.get(
434 route_path('repo_file_download',
435 repo_name=backend.repo_name,
436 commit_id=raw_id, f_path='vcs/nodes.svg'),
437 status=404)
371 438
372 def test_returns_path_relative_to_matched_reference(self):
373 repo = self._repo(branches=['trunk'])
374 self.assert_file_adjustment('trunk/file', 'file', repo)
439 msg = """No such commit exists for this repository"""
440 response.mustcontain(msg)
441
442 def test_download_file_wrong_f_path(self, backend):
443 commit = backend.repo.get_commit(commit_idx=173)
444 f_path = 'vcs/ERRORnodes.py'
375 445
376 def test_does_not_modify_file_if_no_reference_matches(self):
377 repo = self._repo(branches=['trunk'])
378 self.assert_file_adjustment('notes/file', 'notes/file', repo)
446 response = self.app.get(
447 route_path('repo_file_download',
448 repo_name=backend.repo_name,
449 commit_id=commit.raw_id, f_path=f_path),
450 status=404)
451
452 msg = (
453 "There is no file nor directory at the given path: "
454 "`%s` at commit %s" % (f_path, commit.short_id))
455 response.mustcontain(msg)
456
457 def test_file_raw(self, backend):
458 commit = backend.repo.get_commit(commit_idx=173)
459 response = self.app.get(
460 route_path('repo_file_raw',
461 repo_name=backend.repo_name,
462 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
379 463
380 def test_does_not_adjust_partial_directory_names(self):
381 repo = self._repo(branches=['trun'])
382 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
464 assert response.content_type == "text/plain"
465
466 def test_file_raw_binary(self, backend):
467 commit = backend.repo.get_commit()
468 response = self.app.get(
469 route_path('repo_file_raw',
470 repo_name=backend.repo_name,
471 commit_id=commit.raw_id,
472 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
473
474 assert response.content_disposition == 'inline'
383 475
384 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
385 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
386 self.assert_file_adjustment('trunk/new/file', 'file', repo)
476 def test_raw_file_wrong_cs(self, backend):
477 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
478
479 response = self.app.get(
480 route_path('repo_file_raw',
481 repo_name=backend.repo_name,
482 commit_id=raw_id, f_path='vcs/nodes.svg'),
483 status=404)
484
485 msg = """No such commit exists for this repository"""
486 response.mustcontain(msg)
387 487
388 def assert_file_adjustment(self, f_path, expected, repo):
389 controller = FilesController()
390 result = controller._adjust_file_path_for_svn(f_path, repo)
391 assert result == expected
488 def test_raw_wrong_f_path(self, backend):
489 commit = backend.repo.get_commit(commit_idx=173)
490 f_path = 'vcs/ERRORnodes.py'
491 response = self.app.get(
492 route_path('repo_file_raw',
493 repo_name=backend.repo_name,
494 commit_id=commit.raw_id, f_path=f_path),
495 status=404)
392 496
393 def _repo(self, branches=None):
394 repo = mock.Mock()
395 repo.branches = OrderedDict((name, '0') for name in branches or [])
396 repo.tags = {}
397 return repo
497 msg = (
498 "There is no file nor directory at the given path: "
499 "`%s` at commit %s" % (f_path, commit.short_id))
500 response.mustcontain(msg)
501
502 def test_raw_svg_should_not_be_rendered(self, backend):
503 backend.create_repo()
504 backend.ensure_file("xss.svg")
505 response = self.app.get(
506 route_path('repo_file_raw',
507 repo_name=backend.repo_name,
508 commit_id='tip', f_path='xss.svg'),)
509 # If the content type is image/svg+xml then it allows to render HTML
510 # and malicious SVG.
511 assert response.content_type == "text/plain"
398 512
399 513
400 514 @pytest.mark.usefixtures("app")
401 515 class TestRepositoryArchival(object):
402 516
403 517 def test_archival(self, backend):
404 518 backend.enable_downloads()
405 519 commit = backend.repo.get_commit(commit_idx=173)
406 520 for archive, info in settings.ARCHIVE_SPECS.items():
407 521 mime_type, arch_ext = info
408 522 short = commit.short_id + arch_ext
409 523 fname = commit.raw_id + arch_ext
410 524 filename = '%s-%s' % (backend.repo_name, short)
411 response = self.app.get(url(controller='files',
412 action='archivefile',
413 repo_name=backend.repo_name,
414 fname=fname))
525 response = self.app.get(
526 route_path('repo_archivefile',
527 repo_name=backend.repo_name,
528 fname=fname))
415 529
416 530 assert response.status == '200 OK'
417 531 headers = [
418 ('Pragma', 'no-cache'),
419 ('Cache-Control', 'no-cache'),
420 532 ('Content-Disposition', 'attachment; filename=%s' % filename),
421 533 ('Content-Type', '%s' % mime_type),
422 534 ]
423 if 'Set-Cookie' in response.response.headers:
424 del response.response.headers['Set-Cookie']
425 assert response.response.headers.items() == headers
535
536 for header in headers:
537 assert header in response.headers.items()
426 538
427 def test_archival_wrong_ext(self, backend):
539 @pytest.mark.parametrize('arch_ext',[
540 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
541 def test_archival_wrong_ext(self, backend, arch_ext):
428 542 backend.enable_downloads()
429 543 commit = backend.repo.get_commit(commit_idx=173)
430 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
431 fname = commit.raw_id + arch_ext
432 544
433 response = self.app.get(url(controller='files',
434 action='archivefile',
435 repo_name=backend.repo_name,
436 fname=fname))
437 response.mustcontain('Unknown archive type')
438
439 def test_archival_wrong_commit_id(self, backend):
440 backend.enable_downloads()
441 for commit_id in ['00x000000', 'tar', 'wrong', '@##$@$42413232',
442 '232dffcd']:
443 fname = '%s.zip' % commit_id
444
445 response = self.app.get(url(controller='files',
446 action='archivefile',
447 repo_name=backend.repo_name,
448 fname=fname))
449 response.mustcontain('Unknown revision')
450
545 fname = commit.raw_id + '.' + arch_ext
451 546
452 @pytest.mark.usefixtures("app", "autologin_user")
453 class TestRawFileHandling(object):
454
455 def test_raw_file_ok(self, backend):
456 commit = backend.repo.get_commit(commit_idx=173)
457 response = self.app.get(url(controller='files', action='rawfile',
458 repo_name=backend.repo_name,
459 revision=commit.raw_id,
460 f_path='vcs/nodes.py'))
461
462 assert response.content_disposition == "attachment; filename=nodes.py"
463 assert response.content_type == "text/x-python"
464
465 def test_raw_file_wrong_cs(self, backend):
466 commit_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
467 f_path = 'vcs/nodes.py'
468
469 response = self.app.get(url(controller='files', action='rawfile',
470 repo_name=backend.repo_name,
471 revision=commit_id,
472 f_path=f_path), status=404)
473
474 msg = """No such commit exists for this repository"""
475 response.mustcontain(msg)
547 response = self.app.get(
548 route_path('repo_archivefile',
549 repo_name=backend.repo_name,
550 fname=fname))
551 response.mustcontain(
552 'Unknown archive type for: `{}`'.format(fname))
476 553
477 def test_raw_file_wrong_f_path(self, backend):
478 commit = backend.repo.get_commit(commit_idx=173)
479 f_path = 'vcs/ERRORnodes.py'
480 response = self.app.get(url(controller='files', action='rawfile',
481 repo_name=backend.repo_name,
482 revision=commit.raw_id,
483 f_path=f_path), status=404)
484
485 msg = (
486 "There is no file nor directory at the given path: "
487 "`%s` at commit %s" % (f_path, commit.short_id))
488 response.mustcontain(msg)
489
490 def test_raw_ok(self, backend):
491 commit = backend.repo.get_commit(commit_idx=173)
492 response = self.app.get(url(controller='files', action='raw',
493 repo_name=backend.repo_name,
494 revision=commit.raw_id,
495 f_path='vcs/nodes.py'))
496
497 assert response.content_type == "text/plain"
498
499 def test_raw_wrong_cs(self, backend):
500 commit_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
501 f_path = 'vcs/nodes.py'
554 @pytest.mark.parametrize('commit_id', [
555 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
556 def test_archival_wrong_commit_id(self, backend, commit_id):
557 backend.enable_downloads()
558 fname = '%s.zip' % commit_id
502 559
503 response = self.app.get(url(controller='files', action='raw',
504 repo_name=backend.repo_name,
505 revision=commit_id,
506 f_path=f_path), status=404)
507
508 msg = """No such commit exists for this repository"""
509 response.mustcontain(msg)
510
511 def test_raw_wrong_f_path(self, backend):
512 commit = backend.repo.get_commit(commit_idx=173)
513 f_path = 'vcs/ERRORnodes.py'
514 response = self.app.get(url(controller='files', action='raw',
515 repo_name=backend.repo_name,
516 revision=commit.raw_id,
517 f_path=f_path), status=404)
518 msg = (
519 "There is no file nor directory at the given path: "
520 "`%s` at commit %s" % (f_path, commit.short_id))
521 response.mustcontain(msg)
522
523 def test_raw_svg_should_not_be_rendered(self, backend):
524 backend.create_repo()
525 backend.ensure_file("xss.svg")
526 response = self.app.get(url(controller='files', action='raw',
527 repo_name=backend.repo_name,
528 revision='tip',
529 f_path='xss.svg'))
530
531 # If the content type is image/svg+xml then it allows to render HTML
532 # and malicious SVG.
533 assert response.content_type == "text/plain"
560 response = self.app.get(
561 route_path('repo_archivefile',
562 repo_name=backend.repo_name,
563 fname=fname))
564 response.mustcontain('Unknown commit_id')
534 565
535 566
536 567 @pytest.mark.usefixtures("app")
537 class TestFilesDiff:
568 class TestFilesDiff(object):
538 569
539 570 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
540 571 def test_file_full_diff(self, backend, diff):
541 572 commit1 = backend.repo.get_commit(commit_idx=-1)
542 573 commit2 = backend.repo.get_commit(commit_idx=-2)
543 574
544 575 response = self.app.get(
545 url(
546 controller='files',
547 action='diff',
548 repo_name=backend.repo_name,
549 f_path='README'),
576 route_path('repo_files_diff',
577 repo_name=backend.repo_name,
578 f_path='README'),
550 579 params={
551 580 'diff1': commit2.raw_id,
552 581 'diff2': commit1.raw_id,
553 582 'fulldiff': '1',
554 583 'diff': diff,
555 584 })
556 585
557 586 if diff == 'diff':
558 587 # use redirect since this is OLD view redirecting to compare page
559 588 response = response.follow()
560 589
561 590 # It's a symlink to README.rst
562 591 response.mustcontain('README.rst')
563 592 response.mustcontain('No newline at end of file')
564 593
565 594 def test_file_binary_diff(self, backend):
566 595 commits = [
567 596 {'message': 'First commit'},
568 597 {'message': 'Commit with binary',
569 598 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
570 599 ]
571 600 repo = backend.create_repo(commits=commits)
572 601
573 602 response = self.app.get(
574 url(
575 controller='files',
576 action='diff',
577 repo_name=backend.repo_name,
578 f_path='file.bin'),
603 route_path('repo_files_diff',
604 repo_name=backend.repo_name,
605 f_path='file.bin'),
579 606 params={
580 607 'diff1': repo.get_commit(commit_idx=0).raw_id,
581 608 'diff2': repo.get_commit(commit_idx=1).raw_id,
582 609 'fulldiff': '1',
583 610 'diff': 'diff',
584 611 })
585 612 # use redirect since this is OLD view redirecting to compare page
586 613 response = response.follow()
587 614 response.mustcontain('Expand 1 commit')
588 615 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
589 616
590 617 if backend.alias == 'svn':
591 618 response.mustcontain('new file 10644')
592 619 # TODO(marcink): SVN doesn't yet detect binary changes
593 620 else:
594 621 response.mustcontain('new file 100644')
595 622 response.mustcontain('binary diff hidden')
596 623
597 624 def test_diff_2way(self, backend):
598 625 commit1 = backend.repo.get_commit(commit_idx=-1)
599 626 commit2 = backend.repo.get_commit(commit_idx=-2)
600 627 response = self.app.get(
601 url(
602 controller='files',
603 action='diff_2way',
604 repo_name=backend.repo_name,
605 f_path='README'),
628 route_path('repo_files_diff_2way_redirect',
629 repo_name=backend.repo_name,
630 f_path='README'),
606 631 params={
607 632 'diff1': commit2.raw_id,
608 633 'diff2': commit1.raw_id,
609 634 })
610 635 # use redirect since this is OLD view redirecting to compare page
611 636 response = response.follow()
612 637
613 638 # It's a symlink to README.rst
614 639 response.mustcontain('README.rst')
615 640 response.mustcontain('No newline at end of file')
616 641
617 642 def test_requires_one_commit_id(self, backend, autologin_user):
618 643 response = self.app.get(
619 url(
620 controller='files',
621 action='diff',
622 repo_name=backend.repo_name,
623 f_path='README.rst'),
644 route_path('repo_files_diff',
645 repo_name=backend.repo_name,
646 f_path='README.rst'),
624 647 status=400)
625 648 response.mustcontain(
626 649 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
627 650
628 651 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
629 652 repo = vcsbackend.repo
630 653 response = self.app.get(
631 url(
632 controller='files',
633 action='diff',
634 repo_name=repo.name,
635 f_path='does-not-exist-in-any-commit',
636 diff1=repo[0].raw_id,
637 diff2=repo[1].raw_id),)
654 route_path('repo_files_diff',
655 repo_name=repo.name,
656 f_path='does-not-exist-in-any-commit'),
657 params={
658 'diff1': repo[0].raw_id,
659 'diff2': repo[1].raw_id
660 })
638 661
639 662 response = response.follow()
640 663 response.mustcontain('No files')
641 664
642 665 def test_returns_redirect_if_file_not_changed(self, backend):
643 666 commit = backend.repo.get_commit(commit_idx=-1)
644 f_path = 'README'
645 667 response = self.app.get(
646 url(
647 controller='files',
648 action='diff_2way',
649 repo_name=backend.repo_name,
650 f_path=f_path,
651 diff1=commit.raw_id,
652 diff2=commit.raw_id,
653 ),
654 )
668 route_path('repo_files_diff_2way_redirect',
669 repo_name=backend.repo_name,
670 f_path='README'),
671 params={
672 'diff1': commit.raw_id,
673 'diff2': commit.raw_id,
674 })
675
655 676 response = response.follow()
656 677 response.mustcontain('No files')
657 678 response.mustcontain('No commits in this compare')
658 679
659 680 def test_supports_diff_to_different_path_svn(self, backend_svn):
660 681 #TODO: check this case
661 682 return
662 683
663 684 repo = backend_svn['svn-simple-layout'].scm_instance()
664 685 commit_id_1 = '24'
665 686 commit_id_2 = '26'
666 687
667
668 print( url(
669 controller='files',
670 action='diff',
671 repo_name=repo.name,
672 f_path='trunk/example.py',
673 diff1='tags/v0.2/example.py@' + commit_id_1,
674 diff2=commit_id_2))
675
676 688 response = self.app.get(
677 url(
678 controller='files',
679 action='diff',
680 repo_name=repo.name,
681 f_path='trunk/example.py',
682 diff1='tags/v0.2/example.py@' + commit_id_1,
683 diff2=commit_id_2))
689 route_path('repo_files_diff',
690 repo_name=backend_svn.repo_name,
691 f_path='trunk/example.py'),
692 params={
693 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
694 'diff2': commit_id_2,
695 })
684 696
685 697 response = response.follow()
686 698 response.mustcontain(
687 699 # diff contains this
688 700 "Will print out a useful message on invocation.")
689 701
690 702 # Note: Expecting that we indicate the user what's being compared
691 703 response.mustcontain("trunk/example.py")
692 704 response.mustcontain("tags/v0.2/example.py")
693 705
694 706 def test_show_rev_redirects_to_svn_path(self, backend_svn):
695 707 #TODO: check this case
696 708 return
697 709
698 710 repo = backend_svn['svn-simple-layout'].scm_instance()
699 711 commit_id = repo[-1].raw_id
712
700 713 response = self.app.get(
701 url(
702 controller='files',
703 action='diff',
704 repo_name=repo.name,
705 f_path='trunk/example.py',
706 diff1='branches/argparse/example.py@' + commit_id,
707 diff2=commit_id),
708 params={'show_rev': 'Show at Revision'},
714 route_path('repo_files_diff',
715 repo_name=backend_svn.repo_name,
716 f_path='trunk/example.py'),
717 params={
718 'diff1': 'branches/argparse/example.py@' + commit_id,
719 'diff2': commit_id,
720 },
709 721 status=302)
722 response = response.follow()
710 723 assert response.headers['Location'].endswith(
711 724 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
712 725
713 726 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
714 727 #TODO: check this case
715 728 return
716 729
717 730 repo = backend_svn['svn-simple-layout'].scm_instance()
718 731 commit_id = repo[-1].raw_id
719 732 response = self.app.get(
720 url(
721 controller='files',
722 action='diff',
723 repo_name=repo.name,
724 f_path='trunk/example.py',
725 diff1='branches/argparse/example.py@' + commit_id,
726 diff2=commit_id),
733 route_path('repo_files_diff',
734 repo_name=backend_svn.repo_name,
735 f_path='trunk/example.py'),
727 736 params={
737 'diff1': 'branches/argparse/example.py@' + commit_id,
738 'diff2': commit_id,
728 739 'show_rev': 'Show at Revision',
729 740 'annotate': 'true',
730 741 },
731 742 status=302)
743 response = response.follow()
732 744 assert response.headers['Location'].endswith(
733 745 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
734 746
735 747
736 748 @pytest.mark.usefixtures("app", "autologin_user")
737 class TestChangingFiles:
749 class TestModifyFilesWithWebInterface(object):
738 750
739 751 def test_add_file_view(self, backend):
740 self.app.get(url(
741 'files_add_home',
742 repo_name=backend.repo_name,
743 revision='tip', f_path='/'))
752 self.app.get(
753 route_path('repo_files_add_file',
754 repo_name=backend.repo_name,
755 commit_id='tip', f_path='/')
756 )
744 757
745 758 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
746 759 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
747 760 repo = backend.create_repo()
748 761 filename = 'init.py'
749 762 response = self.app.post(
750 url(
751 'files_add',
752 repo_name=repo.repo_name,
753 revision='tip', f_path='/'),
763 route_path('repo_files_create_file',
764 repo_name=backend.repo_name,
765 commit_id='tip', f_path='/'),
754 766 params={
755 767 'content': "",
756 768 'filename': filename,
757 769 'location': "",
758 770 'csrf_token': csrf_token,
759 771 },
760 772 status=302)
761 773 assert_session_flash(response,
762 'Successfully committed new file `{}`'.format(os.path.join(filename)))
774 'Successfully committed new file `{}`'.format(
775 os.path.join(filename)))
763 776
764 777 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
765 778 response = self.app.post(
766 url(
767 'files_add',
768 repo_name=backend.repo_name,
769 revision='tip', f_path='/'),
779 route_path('repo_files_create_file',
780 repo_name=backend.repo_name,
781 commit_id='tip', f_path='/'),
770 782 params={
771 783 'content': "foo",
772 784 'csrf_token': csrf_token,
773 785 },
774 786 status=302)
775 787
776 788 assert_session_flash(response, 'No filename')
777 789
778 790 def test_add_file_into_repo_errors_and_no_commits(
779 791 self, backend, csrf_token):
780 792 repo = backend.create_repo()
781 793 # Create a file with no filename, it will display an error but
782 794 # the repo has no commits yet
783 795 response = self.app.post(
784 url(
785 'files_add',
786 repo_name=repo.repo_name,
787 revision='tip', f_path='/'),
796 route_path('repo_files_create_file',
797 repo_name=repo.repo_name,
798 commit_id='tip', f_path='/'),
788 799 params={
789 800 'content': "foo",
790 801 'csrf_token': csrf_token,
791 802 },
792 803 status=302)
793 804
794 805 assert_session_flash(response, 'No filename')
795 806
796 807 # Not allowed, redirect to the summary
797 808 redirected = response.follow()
798 809 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
799 810
800 811 # As there are no commits, displays the summary page with the error of
801 812 # creating a file with no filename
802 813
803 814 assert redirected.request.path == summary_url
804 815
805 816 @pytest.mark.parametrize("location, filename", [
806 817 ('/abs', 'foo'),
807 818 ('../rel', 'foo'),
808 819 ('file/../foo', 'foo'),
809 820 ])
810 821 def test_add_file_into_repo_bad_filenames(
811 822 self, location, filename, backend, csrf_token):
812 823 response = self.app.post(
813 url(
814 'files_add',
815 repo_name=backend.repo_name,
816 revision='tip', f_path='/'),
824 route_path('repo_files_create_file',
825 repo_name=backend.repo_name,
826 commit_id='tip', f_path='/'),
817 827 params={
818 828 'content': "foo",
819 829 'filename': filename,
820 830 'location': location,
821 831 'csrf_token': csrf_token,
822 832 },
823 833 status=302)
824 834
825 835 assert_session_flash(
826 836 response,
827 837 'The location specified must be a relative path and must not '
828 838 'contain .. in the path')
829 839
830 840 @pytest.mark.parametrize("cnt, location, filename", [
831 841 (1, '', 'foo.txt'),
832 842 (2, 'dir', 'foo.rst'),
833 843 (3, 'rel/dir', 'foo.bar'),
834 844 ])
835 845 def test_add_file_into_repo(self, cnt, location, filename, backend,
836 846 csrf_token):
837 847 repo = backend.create_repo()
838 848 response = self.app.post(
839 url(
840 'files_add',
841 repo_name=repo.repo_name,
842 revision='tip', f_path='/'),
849 route_path('repo_files_create_file',
850 repo_name=repo.repo_name,
851 commit_id='tip', f_path='/'),
843 852 params={
844 853 'content': "foo",
845 854 'filename': filename,
846 855 'location': location,
847 856 'csrf_token': csrf_token,
848 857 },
849 858 status=302)
850 859 assert_session_flash(response,
851 860 'Successfully committed new file `{}`'.format(
852 861 os.path.join(location, filename)))
853 862
854 863 def test_edit_file_view(self, backend):
855 864 response = self.app.get(
856 url(
857 'files_edit_home',
858 repo_name=backend.repo_name,
859 revision=backend.default_head_id,
860 f_path='vcs/nodes.py'),
865 route_path('repo_files_edit_file',
866 repo_name=backend.repo_name,
867 commit_id=backend.default_head_id,
868 f_path='vcs/nodes.py'),
861 869 status=200)
862 870 response.mustcontain("Module holding everything related to vcs nodes.")
863 871
864 872 def test_edit_file_view_not_on_branch(self, backend):
865 873 repo = backend.create_repo()
866 874 backend.ensure_file("vcs/nodes.py")
867 875
868 876 response = self.app.get(
869 url(
870 'files_edit_home',
871 repo_name=repo.repo_name,
872 revision='tip', f_path='vcs/nodes.py'),
877 route_path('repo_files_edit_file',
878 repo_name=repo.repo_name,
879 commit_id='tip',
880 f_path='vcs/nodes.py'),
873 881 status=302)
874 882 assert_session_flash(
875 883 response,
876 'You can only edit files with revision being a valid branch')
884 'You can only edit files with commit being a valid branch')
877 885
878 886 def test_edit_file_view_commit_changes(self, backend, csrf_token):
879 887 repo = backend.create_repo()
880 888 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
881 889
882 890 response = self.app.post(
883 url(
884 'files_edit',
885 repo_name=repo.repo_name,
886 revision=backend.default_head_id,
887 f_path='vcs/nodes.py'),
891 route_path('repo_files_update_file',
892 repo_name=repo.repo_name,
893 commit_id=backend.default_head_id,
894 f_path='vcs/nodes.py'),
888 895 params={
889 896 'content': "print 'hello world'",
890 897 'message': 'I committed',
891 898 'filename': "vcs/nodes.py",
892 899 'csrf_token': csrf_token,
893 900 },
894 901 status=302)
895 902 assert_session_flash(
896 903 response, 'Successfully committed changes to file `vcs/nodes.py`')
897 904 tip = repo.get_commit(commit_idx=-1)
898 905 assert tip.message == 'I committed'
899 906
900 907 def test_edit_file_view_commit_changes_default_message(self, backend,
901 908 csrf_token):
902 909 repo = backend.create_repo()
903 910 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
904 911
905 912 commit_id = (
906 913 backend.default_branch_name or
907 914 backend.repo.scm_instance().commit_ids[-1])
908 915
909 916 response = self.app.post(
910 url(
911 'files_edit',
912 repo_name=repo.repo_name,
913 revision=commit_id,
914 f_path='vcs/nodes.py'),
917 route_path('repo_files_update_file',
918 repo_name=repo.repo_name,
919 commit_id=commit_id,
920 f_path='vcs/nodes.py'),
915 921 params={
916 922 'content': "print 'hello world'",
917 923 'message': '',
918 924 'filename': "vcs/nodes.py",
919 925 'csrf_token': csrf_token,
920 926 },
921 927 status=302)
922 928 assert_session_flash(
923 929 response, 'Successfully committed changes to file `vcs/nodes.py`')
924 930 tip = repo.get_commit(commit_idx=-1)
925 931 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
926 932
927 933 def test_delete_file_view(self, backend):
928 self.app.get(url(
929 'files_delete_home',
930 repo_name=backend.repo_name,
931 revision='tip', f_path='vcs/nodes.py'))
934 self.app.get(
935 route_path('repo_files_remove_file',
936 repo_name=backend.repo_name,
937 commit_id=backend.default_head_id,
938 f_path='vcs/nodes.py'),
939 status=200)
932 940
933 941 def test_delete_file_view_not_on_branch(self, backend):
934 942 repo = backend.create_repo()
935 943 backend.ensure_file('vcs/nodes.py')
936 944
937 945 response = self.app.get(
938 url(
939 'files_delete_home',
940 repo_name=repo.repo_name,
941 revision='tip', f_path='vcs/nodes.py'),
946 route_path('repo_files_remove_file',
947 repo_name=repo.repo_name,
948 commit_id='tip',
949 f_path='vcs/nodes.py'),
942 950 status=302)
943 951 assert_session_flash(
944 952 response,
945 'You can only delete files with revision being a valid branch')
953 'You can only delete files with commit being a valid branch')
946 954
947 955 def test_delete_file_view_commit_changes(self, backend, csrf_token):
948 956 repo = backend.create_repo()
949 957 backend.ensure_file("vcs/nodes.py")
950 958
951 959 response = self.app.post(
952 url(
953 'files_delete_home',
954 repo_name=repo.repo_name,
955 revision=backend.default_head_id,
956 f_path='vcs/nodes.py'),
960 route_path('repo_files_delete_file',
961 repo_name=repo.repo_name,
962 commit_id=backend.default_head_id,
963 f_path='vcs/nodes.py'),
957 964 params={
958 965 'message': 'i commited',
959 966 'csrf_token': csrf_token,
960 967 },
961 968 status=302)
962 969 assert_session_flash(
963 970 response, 'Successfully deleted file `vcs/nodes.py`')
964 971
965 972
966 def assert_files_in_response(response, files, params):
967 template = (
968 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
969 _assert_items_in_response(response, files, template, params)
973 @pytest.mark.usefixtures("app")
974 class TestFilesViewOtherCases(object):
975
976 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
977 self, backend_stub, autologin_regular_user, user_regular,
978 user_util):
979
980 repo = backend_stub.create_repo()
981 user_util.grant_user_permission_to_repo(
982 repo, user_regular, 'repository.write')
983 response = self.app.get(
984 route_path('repo_files',
985 repo_name=repo.repo_name,
986 commit_id='tip', f_path='/'))
987
988 repo_file_add_url = route_path(
989 'repo_files_add_file',
990 repo_name=repo.repo_name,
991 commit_id=0, f_path='') + '#edit'
992
993 assert_session_flash(
994 response,
995 'There are no files yet. <a class="alert-link" '
996 'href="{}">Click here to add a new file.</a>'
997 .format(repo_file_add_url))
998
999 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1000 self, backend_stub, user_util):
1001 repo = backend_stub.create_repo()
1002 repo_file_add_url = route_path(
1003 'repo_files_add_file',
1004 repo_name=repo.repo_name,
1005 commit_id=0, f_path='') + '#edit'
1006
1007 response = self.app.get(
1008 route_path('repo_files',
1009 repo_name=repo.repo_name,
1010 commit_id='tip', f_path='/'))
1011
1012 assert_session_flash(response, no_=repo_file_add_url)
1013
1014 @pytest.mark.parametrize('file_node', [
1015 'archive/file.zip',
1016 'diff/my-file.txt',
1017 'render.py',
1018 'render',
1019 'remove_file',
1020 'remove_file/to-delete.txt',
1021 ])
1022 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1023 backend.create_repo()
1024 backend.ensure_file(file_node)
1025
1026 self.app.get(
1027 route_path('repo_files',
1028 repo_name=backend.repo_name,
1029 commit_id='tip', f_path=file_node),
1030 status=200)
970 1031
971 1032
972 def assert_dirs_in_response(response, dirs, params):
973 template = (
974 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
975 _assert_items_in_response(response, dirs, template, params)
1033 class TestAdjustFilePathForSvn(object):
1034 """
1035 SVN specific adjustments of node history in RepoFilesView.
1036 """
976 1037
1038 def test_returns_path_relative_to_matched_reference(self):
1039 repo = self._repo(branches=['trunk'])
1040 self.assert_file_adjustment('trunk/file', 'file', repo)
1041
1042 def test_does_not_modify_file_if_no_reference_matches(self):
1043 repo = self._repo(branches=['trunk'])
1044 self.assert_file_adjustment('notes/file', 'notes/file', repo)
977 1045
978 def _assert_items_in_response(response, items, template, params):
979 for item in items:
980 item_params = {'name': item}
981 item_params.update(params)
982 response.mustcontain(template % item_params)
1046 def test_does_not_adjust_partial_directory_names(self):
1047 repo = self._repo(branches=['trun'])
1048 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1049
1050 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1051 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1052 self.assert_file_adjustment('trunk/new/file', 'file', repo)
983 1053
1054 def assert_file_adjustment(self, f_path, expected, repo):
1055 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1056 assert result == expected
984 1057
985 def assert_timeago_in_response(response, items, params):
986 for item in items:
987 response.mustcontain(h.age_component(params['date']))
1058 def _repo(self, branches=None):
1059 repo = mock.Mock()
1060 repo.branches = OrderedDict((name, '0') for name in branches or [])
1061 repo.tags = {}
1062 return repo
@@ -1,494 +1,494 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 31 from rhodecode.model.db import Repository
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.scm import ScmModel
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38 38
39 39
40 40 fixture = Fixture()
41 41
42 42
43 43 def route_path(name, params=None, **kwargs):
44 44 import urllib
45 45
46 46 base_url = {
47 47 'repo_summary': '/{repo_name}',
48 48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 49 'repo_refs_data': '/{repo_name}/refs-data',
50 50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog'
51 51
52 52 }[name].format(**kwargs)
53 53
54 54 if params:
55 55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
56 56 return base_url
57 57
58 58
59 59 @pytest.mark.usefixtures('app')
60 60 class TestSummaryView(object):
61 61 def test_index(self, autologin_user, backend, http_host_only_stub):
62 62 repo_id = backend.repo.repo_id
63 63 repo_name = backend.repo_name
64 64 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
65 65 return_value=False):
66 66 response = self.app.get(
67 67 route_path('repo_summary', repo_name=repo_name))
68 68
69 69 # repo type
70 70 response.mustcontain(
71 71 '<i class="icon-%s">' % (backend.alias, )
72 72 )
73 73 # public/private
74 74 response.mustcontain(
75 75 """<i class="icon-unlock-alt">"""
76 76 )
77 77
78 78 # clone url...
79 79 response.mustcontain(
80 80 'id="clone_url" readonly="readonly"'
81 81 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
82 82 response.mustcontain(
83 83 'id="clone_url_id" readonly="readonly"'
84 84 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
85 85
86 86 def test_index_svn_without_proxy(
87 87 self, autologin_user, backend_svn, http_host_only_stub):
88 88 repo_id = backend_svn.repo.repo_id
89 89 repo_name = backend_svn.repo_name
90 90 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
91 91 # clone url...
92 92 response.mustcontain(
93 93 'id="clone_url" disabled'
94 94 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
95 95 response.mustcontain(
96 96 'id="clone_url_id" disabled'
97 97 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
98 98
99 99 def test_index_with_trailing_slash(
100 100 self, autologin_user, backend, http_host_only_stub):
101 101
102 102 repo_id = backend.repo.repo_id
103 103 repo_name = backend.repo_name
104 104 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
105 105 return_value=False):
106 106 response = self.app.get(
107 107 route_path('repo_summary', repo_name=repo_name) + '/',
108 108 status=200)
109 109
110 110 # clone url...
111 111 response.mustcontain(
112 112 'id="clone_url" readonly="readonly"'
113 113 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
114 114 response.mustcontain(
115 115 'id="clone_url_id" readonly="readonly"'
116 116 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
117 117
118 118 def test_index_by_id(self, autologin_user, backend):
119 119 repo_id = backend.repo.repo_id
120 120 response = self.app.get(
121 121 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
122 122
123 123 # repo type
124 124 response.mustcontain(
125 125 '<i class="icon-%s">' % (backend.alias, )
126 126 )
127 127 # public/private
128 128 response.mustcontain(
129 129 """<i class="icon-unlock-alt">"""
130 130 )
131 131
132 132 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
133 133 fixture.create_repo(name='repo_1')
134 134 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
135 135
136 136 try:
137 137 response.mustcontain("repo_1")
138 138 finally:
139 139 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
140 140 Session().commit()
141 141
142 142 def test_index_with_anonymous_access_disabled(
143 143 self, backend, disable_anonymous_user):
144 144 response = self.app.get(
145 145 route_path('repo_summary', repo_name=backend.repo_name), status=302)
146 146 assert 'login' in response.location
147 147
148 148 def _enable_stats(self, repo):
149 149 r = Repository.get_by_repo_name(repo)
150 150 r.enable_statistics = True
151 151 Session().add(r)
152 152 Session().commit()
153 153
154 154 expected_trending = {
155 155 'hg': {
156 156 "py": {"count": 68, "desc": ["Python"]},
157 157 "rst": {"count": 16, "desc": ["Rst"]},
158 158 "css": {"count": 2, "desc": ["Css"]},
159 159 "sh": {"count": 2, "desc": ["Bash"]},
160 160 "bat": {"count": 1, "desc": ["Batch"]},
161 161 "cfg": {"count": 1, "desc": ["Ini"]},
162 162 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
163 163 "ini": {"count": 1, "desc": ["Ini"]},
164 164 "js": {"count": 1, "desc": ["Javascript"]},
165 165 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
166 166 },
167 167 'git': {
168 168 "py": {"count": 68, "desc": ["Python"]},
169 169 "rst": {"count": 16, "desc": ["Rst"]},
170 170 "css": {"count": 2, "desc": ["Css"]},
171 171 "sh": {"count": 2, "desc": ["Bash"]},
172 172 "bat": {"count": 1, "desc": ["Batch"]},
173 173 "cfg": {"count": 1, "desc": ["Ini"]},
174 174 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
175 175 "ini": {"count": 1, "desc": ["Ini"]},
176 176 "js": {"count": 1, "desc": ["Javascript"]},
177 177 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
178 178 },
179 179 'svn': {
180 180 "py": {"count": 75, "desc": ["Python"]},
181 181 "rst": {"count": 16, "desc": ["Rst"]},
182 182 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
183 183 "css": {"count": 2, "desc": ["Css"]},
184 184 "bat": {"count": 1, "desc": ["Batch"]},
185 185 "cfg": {"count": 1, "desc": ["Ini"]},
186 186 "ini": {"count": 1, "desc": ["Ini"]},
187 187 "js": {"count": 1, "desc": ["Javascript"]},
188 188 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
189 189 "sh": {"count": 1, "desc": ["Bash"]}
190 190 },
191 191 }
192 192
193 193 def test_repo_stats(self, autologin_user, backend, xhr_header):
194 194 response = self.app.get(
195 195 route_path(
196 196 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
197 197 extra_environ=xhr_header,
198 198 status=200)
199 199 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
200 200
201 201 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
202 202 repo_name = backend.repo_name
203 203
204 204 # codes stats
205 205 self._enable_stats(repo_name)
206 206 ScmModel().mark_for_invalidation(repo_name)
207 207
208 208 response = self.app.get(
209 209 route_path(
210 210 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
211 211 extra_environ=xhr_header,
212 212 status=200)
213 213
214 214 expected_data = self.expected_trending[backend.alias]
215 215 returned_stats = response.json['code_stats']
216 216 for k, v in expected_data.items():
217 217 assert v == returned_stats[k]
218 218
219 219 def test_repo_refs_data(self, backend):
220 220 response = self.app.get(
221 221 route_path('repo_refs_data', repo_name=backend.repo_name),
222 222 status=200)
223 223
224 224 # Ensure that there is the correct amount of items in the result
225 225 repo = backend.repo.scm_instance()
226 226 data = response.json['results']
227 227 items = sum(len(section['children']) for section in data)
228 228 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
229 229 assert items == repo_refs
230 230
231 231 def test_index_shows_missing_requirements_message(
232 232 self, backend, autologin_user):
233 233 repo_name = backend.repo_name
234 234 scm_patcher = mock.patch.object(
235 235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
236 236
237 237 with scm_patcher:
238 238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
239 239 assert_response = AssertResponse(response)
240 240 assert_response.element_contains(
241 241 '.main .alert-warning strong', 'Missing requirements')
242 242 assert_response.element_contains(
243 243 '.main .alert-warning',
244 244 'Commits cannot be displayed, because this repository '
245 245 'uses one or more extensions, which was not enabled.')
246 246
247 247 def test_missing_requirements_page_does_not_contains_switch_to(
248 248 self, autologin_user, backend):
249 249 repo_name = backend.repo_name
250 250 scm_patcher = mock.patch.object(
251 251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
252 252
253 253 with scm_patcher:
254 254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
255 255 response.mustcontain(no='Switch To')
256 256
257 257
258 258 @pytest.mark.usefixtures('app')
259 259 class TestRepoLocation(object):
260 260
261 261 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
262 262 def test_manual_delete(self, autologin_user, backend, suffix, csrf_token):
263 263 repo = backend.create_repo(name_suffix=suffix)
264 264 repo_name = repo.repo_name
265 265
266 266 # delete from file system
267 267 RepoModel()._delete_filesystem_repo(repo)
268 268
269 269 # test if the repo is still in the database
270 270 new_repo = RepoModel().get_by_repo_name(repo_name)
271 271 assert new_repo.repo_name == repo_name
272 272
273 273 # check if repo is not in the filesystem
274 274 assert not repo_on_filesystem(repo_name)
275 275 self.assert_repo_not_found_redirect(repo_name)
276 276
277 277 def assert_repo_not_found_redirect(self, repo_name):
278 278 # run the check page that triggers the other flash message
279 279 response = self.app.get(h.url('repo_check_home', repo_name=repo_name))
280 280 assert_session_flash(
281 281 response, 'The repository at %s cannot be located.' % repo_name)
282 282
283 283
284 284 @pytest.fixture()
285 285 def summary_view(context_stub, request_stub, user_util):
286 286 """
287 287 Bootstrap view to test the view functions
288 288 """
289 289 request_stub.matched_route = AttributeDict(name='test_view')
290 290
291 291 request_stub.user = user_util.create_user().AuthUser
292 292 request_stub.db_repo = user_util.create_repo()
293 293
294 294 view = RepoSummaryView(context=context_stub, request=request_stub)
295 295 return view
296 296
297 297
298 298 @pytest.mark.usefixtures('app')
299 299 class TestCreateReferenceData(object):
300 300
301 301 @pytest.fixture
302 302 def example_refs(self):
303 303 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
304 304 example_refs = [
305 305 ('section_1', section_1_refs, 't1'),
306 306 ('section_2', {'c': 'c_id'}, 't2'),
307 307 ]
308 308 return example_refs
309 309
310 310 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
311 311 repo = mock.Mock()
312 312 repo.name = 'test-repo'
313 313 repo.alias = 'git'
314 314 full_repo_name = 'pytest-repo-group/' + repo.name
315 315
316 316 result = summary_view._create_reference_data(
317 317 repo, full_repo_name, example_refs)
318 318
319 319 expected_files_url = '/{}/files/'.format(full_repo_name)
320 320 expected_result = [
321 321 {
322 322 'children': [
323 323 {
324 324 'id': 'a', 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
325 325 'files_url': expected_files_url + 'a/?at=a',
326 326 },
327 327 {
328 328 'id': 'b', 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
329 329 'files_url': expected_files_url + 'b/?at=b',
330 330 }
331 331 ],
332 332 'text': 'section_1'
333 333 },
334 334 {
335 335 'children': [
336 336 {
337 337 'id': 'c', 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
338 338 'files_url': expected_files_url + 'c/?at=c',
339 339 }
340 340 ],
341 341 'text': 'section_2'
342 342 }]
343 343 assert result == expected_result
344 344
345 345 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
346 346 repo = mock.Mock()
347 347 repo.name = 'test-repo'
348 348 repo.alias = 'svn'
349 349 full_repo_name = 'pytest-repo-group/' + repo.name
350 350
351 351 result = summary_view._create_reference_data(
352 352 repo, full_repo_name, example_refs)
353 353
354 354 expected_files_url = '/{}/files/'.format(full_repo_name)
355 355 expected_result = [
356 356 {
357 357 'children': [
358 358 {
359 359 'id': 'a@a_id', 'raw_id': 'a_id',
360 360 'text': 'a', 'type': 't1',
361 361 'files_url': expected_files_url + 'a_id/a?at=a',
362 362 },
363 363 {
364 364 'id': 'b@b_id', 'raw_id': 'b_id',
365 365 'text': 'b', 'type': 't1',
366 366 'files_url': expected_files_url + 'b_id/b?at=b',
367 367 }
368 368 ],
369 369 'text': 'section_1'
370 370 },
371 371 {
372 372 'children': [
373 373 {
374 374 'id': 'c@c_id', 'raw_id': 'c_id',
375 375 'text': 'c', 'type': 't2',
376 376 'files_url': expected_files_url + 'c_id/c?at=c',
377 377 }
378 378 ],
379 379 'text': 'section_2'
380 380 }
381 381 ]
382 382 assert result == expected_result
383 383
384 384
385 385 class TestCreateFilesUrl(object):
386 386
387 def test_creates_non_svn_url(self, summary_view):
387 def test_creates_non_svn_url(self, app, summary_view):
388 388 repo = mock.Mock()
389 389 repo.name = 'abcde'
390 390 full_repo_name = 'test-repo-group/' + repo.name
391 391 ref_name = 'branch1'
392 392 raw_id = 'deadbeef0123456789'
393 393 is_svn = False
394 394
395 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
395 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
396 396 result = summary_view._create_files_url(
397 397 repo, full_repo_name, ref_name, raw_id, is_svn)
398 398 url_mock.assert_called_once_with(
399 'files_home', repo_name=full_repo_name, f_path='',
400 revision=ref_name, at=ref_name)
399 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
400 f_path='', _query=dict(at=ref_name))
401 401 assert result == url_mock.return_value
402 402
403 def test_creates_svn_url(self, summary_view):
403 def test_creates_svn_url(self, app, summary_view):
404 404 repo = mock.Mock()
405 405 repo.name = 'abcde'
406 406 full_repo_name = 'test-repo-group/' + repo.name
407 407 ref_name = 'branch1'
408 408 raw_id = 'deadbeef0123456789'
409 409 is_svn = True
410 410
411 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
411 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
412 412 result = summary_view._create_files_url(
413 413 repo, full_repo_name, ref_name, raw_id, is_svn)
414 414 url_mock.assert_called_once_with(
415 'files_home', repo_name=full_repo_name, f_path=ref_name,
416 revision=raw_id, at=ref_name)
415 'repo_files', repo_name=full_repo_name, f_path=ref_name,
416 commit_id=raw_id, _query=dict(at=ref_name))
417 417 assert result == url_mock.return_value
418 418
419 def test_name_has_slashes(self, summary_view):
419 def test_name_has_slashes(self, app, summary_view):
420 420 repo = mock.Mock()
421 421 repo.name = 'abcde'
422 422 full_repo_name = 'test-repo-group/' + repo.name
423 423 ref_name = 'branch1/branch2'
424 424 raw_id = 'deadbeef0123456789'
425 425 is_svn = False
426 426
427 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
427 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
428 428 result = summary_view._create_files_url(
429 429 repo, full_repo_name, ref_name, raw_id, is_svn)
430 430 url_mock.assert_called_once_with(
431 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id,
432 at=ref_name)
431 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
432 f_path='', _query=dict(at=ref_name))
433 433 assert result == url_mock.return_value
434 434
435 435
436 436 class TestReferenceItems(object):
437 437 repo = mock.Mock()
438 438 repo.name = 'pytest-repo'
439 439 repo_full_name = 'pytest-repo-group/' + repo.name
440 440 ref_type = 'branch'
441 441 fake_url = '/abcde/'
442 442
443 443 @staticmethod
444 444 def _format_function(name, id_):
445 445 return 'format_function_{}_{}'.format(name, id_)
446 446
447 447 def test_creates_required_amount_of_items(self, summary_view):
448 448 amount = 100
449 449 refs = {
450 450 'ref{}'.format(i): '{0:040d}'.format(i)
451 451 for i in range(amount)
452 452 }
453 453
454 454 url_patcher = mock.patch.object(summary_view, '_create_files_url')
455 455 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
456 456 return_value=False)
457 457
458 458 with url_patcher as url_mock, svn_patcher:
459 459 result = summary_view._create_reference_items(
460 460 self.repo, self.repo_full_name, refs, self.ref_type,
461 461 self._format_function)
462 462 assert len(result) == amount
463 463 assert url_mock.call_count == amount
464 464
465 465 def test_single_item_details(self, summary_view):
466 466 ref_name = 'ref1'
467 467 ref_id = 'deadbeef'
468 468 refs = {
469 469 ref_name: ref_id
470 470 }
471 471
472 472 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
473 473 return_value=False)
474 474
475 475 url_patcher = mock.patch.object(
476 476 summary_view, '_create_files_url', return_value=self.fake_url)
477 477
478 478 with url_patcher as url_mock, svn_patcher:
479 479 result = summary_view._create_reference_items(
480 480 self.repo, self.repo_full_name, refs, self.ref_type,
481 481 self._format_function)
482 482
483 483 url_mock.assert_called_once_with(
484 484 self.repo, self.repo_full_name, ref_name, ref_id, False)
485 485 expected_result = [
486 486 {
487 487 'text': ref_name,
488 488 'id': self._format_function(ref_name, ref_id),
489 489 'raw_id': ref_id,
490 490 'type': self.ref_type,
491 491 'files_url': self.fake_url
492 492 }
493 493 ]
494 494 assert result == expected_result
@@ -1,368 +1,367 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import string
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from beaker.cache import cache_region
27 27
28 28
29 29 from rhodecode.controllers import utils
30 30
31 31 from rhodecode.apps._base import RepoAppView
32 32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 33 from rhodecode.lib import caches, helpers as h
34 34 from rhodecode.lib.helpers import RepoPage
35 35 from rhodecode.lib.utils2 import safe_str, safe_int
36 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 40 from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError
41 41 from rhodecode.model.db import Statistics, CacheKey, User
42 42 from rhodecode.model.meta import Session
43 43 from rhodecode.model.repo import ReadmeFinder
44 44 from rhodecode.model.scm import ScmModel
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class RepoSummaryView(RepoAppView):
50 50
51 51 def load_default_context(self):
52 52 c = self._get_local_tmpl_context(include_app_defaults=True)
53 53
54 54 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
55 55 c.repo_info = self.db_repo
56 56 c.rhodecode_repo = None
57 57 if not c.repository_requirements_missing:
58 58 c.rhodecode_repo = self.rhodecode_vcs_repo
59 59
60 60 self._register_global_c(c)
61 61 return c
62 62
63 63 def _get_readme_data(self, db_repo, default_renderer):
64 64 repo_name = db_repo.repo_name
65 65 log.debug('Looking for README file')
66 66
67 67 @cache_region('long_term')
68 68 def _generate_readme(cache_key):
69 69 readme_data = None
70 70 readme_node = None
71 71 readme_filename = None
72 72 commit = self._get_landing_commit_or_none(db_repo)
73 73 if commit:
74 74 log.debug("Searching for a README file.")
75 75 readme_node = ReadmeFinder(default_renderer).search(commit)
76 76 if readme_node:
77 relative_url = h.url('files_raw_home',
78 repo_name=repo_name,
79 revision=commit.raw_id,
80 f_path=readme_node.path)
77 relative_url = h.route_path(
78 'repo_file_raw', repo_name=repo_name,
79 commit_id=commit.raw_id, f_path=readme_node.path)
81 80 readme_data = self._render_readme_or_none(
82 81 commit, readme_node, relative_url)
83 82 readme_filename = readme_node.path
84 83 return readme_data, readme_filename
85 84
86 85 invalidator_context = CacheKey.repo_context_cache(
87 86 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
88 87
89 88 with invalidator_context as context:
90 89 context.invalidate()
91 90 computed = context.compute()
92 91
93 92 return computed
94 93
95 94 def _get_landing_commit_or_none(self, db_repo):
96 95 log.debug("Getting the landing commit.")
97 96 try:
98 97 commit = db_repo.get_landing_commit()
99 98 if not isinstance(commit, EmptyCommit):
100 99 return commit
101 100 else:
102 101 log.debug("Repository is empty, no README to render.")
103 102 except CommitError:
104 103 log.exception(
105 104 "Problem getting commit when trying to render the README.")
106 105
107 106 def _render_readme_or_none(self, commit, readme_node, relative_url):
108 107 log.debug(
109 108 'Found README file `%s` rendering...', readme_node.path)
110 109 renderer = MarkupRenderer()
111 110 try:
112 111 html_source = renderer.render(
113 112 readme_node.content, filename=readme_node.path)
114 113 if relative_url:
115 114 return relative_links(html_source, relative_url)
116 115 return html_source
117 116 except Exception:
118 117 log.exception(
119 118 "Exception while trying to render the README")
120 119
121 120 def _load_commits_context(self, c):
122 121 p = safe_int(self.request.GET.get('page'), 1)
123 122 size = safe_int(self.request.GET.get('size'), 10)
124 123
125 124 def url_generator(**kw):
126 125 query_params = {
127 126 'size': size
128 127 }
129 128 query_params.update(kw)
130 129 return h.route_path(
131 130 'repo_summary_commits',
132 131 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
133 132
134 133 pre_load = ['author', 'branch', 'date', 'message']
135 134 try:
136 135 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
137 136 except EmptyRepositoryError:
138 137 collection = self.rhodecode_vcs_repo
139 138
140 139 c.repo_commits = RepoPage(
141 140 collection, page=p, items_per_page=size, url=url_generator)
142 141 page_ids = [x.raw_id for x in c.repo_commits]
143 142 c.comments = self.db_repo.get_comments(page_ids)
144 143 c.statuses = self.db_repo.statuses(page_ids)
145 144
146 145 @LoginRequired()
147 146 @HasRepoPermissionAnyDecorator(
148 147 'repository.read', 'repository.write', 'repository.admin')
149 148 @view_config(
150 149 route_name='repo_summary_commits', request_method='GET',
151 150 renderer='rhodecode:templates/summary/summary_commits.mako')
152 151 def summary_commits(self):
153 152 c = self.load_default_context()
154 153 self._load_commits_context(c)
155 154 return self._get_template_context(c)
156 155
157 156 @LoginRequired()
158 157 @HasRepoPermissionAnyDecorator(
159 158 'repository.read', 'repository.write', 'repository.admin')
160 159 @view_config(
161 160 route_name='repo_summary', request_method='GET',
162 161 renderer='rhodecode:templates/summary/summary.mako')
163 162 @view_config(
164 163 route_name='repo_summary_slash', request_method='GET',
165 164 renderer='rhodecode:templates/summary/summary.mako')
166 165 def summary(self):
167 166 c = self.load_default_context()
168 167
169 168 # Prepare the clone URL
170 169 username = ''
171 170 if self._rhodecode_user.username != User.DEFAULT_USER:
172 171 username = safe_str(self._rhodecode_user.username)
173 172
174 173 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
175 174 if '{repo}' in _def_clone_uri:
176 175 _def_clone_uri_by_id = _def_clone_uri.replace(
177 176 '{repo}', '_{repoid}')
178 177 elif '{repoid}' in _def_clone_uri:
179 178 _def_clone_uri_by_id = _def_clone_uri.replace(
180 179 '_{repoid}', '{repo}')
181 180
182 181 c.clone_repo_url = self.db_repo.clone_url(
183 182 user=username, uri_tmpl=_def_clone_uri)
184 183 c.clone_repo_url_id = self.db_repo.clone_url(
185 184 user=username, uri_tmpl=_def_clone_uri_by_id)
186 185
187 186 # If enabled, get statistics data
188 187
189 188 c.show_stats = bool(self.db_repo.enable_statistics)
190 189
191 190 stats = Session().query(Statistics) \
192 191 .filter(Statistics.repository == self.db_repo) \
193 192 .scalar()
194 193
195 194 c.stats_percentage = 0
196 195
197 196 if stats and stats.languages:
198 197 c.no_data = False is self.db_repo.enable_statistics
199 198 lang_stats_d = json.loads(stats.languages)
200 199
201 200 # Sort first by decreasing count and second by the file extension,
202 201 # so we have a consistent output.
203 202 lang_stats_items = sorted(lang_stats_d.iteritems(),
204 203 key=lambda k: (-k[1], k[0]))[:10]
205 204 lang_stats = [(x, {"count": y,
206 205 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
207 206 for x, y in lang_stats_items]
208 207
209 208 c.trending_languages = json.dumps(lang_stats)
210 209 else:
211 210 c.no_data = True
212 211 c.trending_languages = json.dumps({})
213 212
214 213 scm_model = ScmModel()
215 214 c.enable_downloads = self.db_repo.enable_downloads
216 215 c.repository_followers = scm_model.get_followers(self.db_repo)
217 216 c.repository_forks = scm_model.get_forks(self.db_repo)
218 217 c.repository_is_user_following = scm_model.is_following_repo(
219 218 self.db_repo_name, self._rhodecode_user.user_id)
220 219
221 220 # first interaction with the VCS instance after here...
222 221 if c.repository_requirements_missing:
223 222 self.request.override_renderer = \
224 223 'rhodecode:templates/summary/missing_requirements.mako'
225 224 return self._get_template_context(c)
226 225
227 226 c.readme_data, c.readme_file = \
228 227 self._get_readme_data(self.db_repo, c.visual.default_renderer)
229 228
230 229 # loads the summary commits template context
231 230 self._load_commits_context(c)
232 231
233 232 return self._get_template_context(c)
234 233
235 234 def get_request_commit_id(self):
236 235 return self.request.matchdict['commit_id']
237 236
238 237 @LoginRequired()
239 238 @HasRepoPermissionAnyDecorator(
240 239 'repository.read', 'repository.write', 'repository.admin')
241 240 @view_config(
242 241 route_name='repo_stats', request_method='GET',
243 242 renderer='json_ext')
244 243 def repo_stats(self):
245 244 commit_id = self.get_request_commit_id()
246 245
247 246 _namespace = caches.get_repo_namespace_key(
248 247 caches.SUMMARY_STATS, self.db_repo_name)
249 248 show_stats = bool(self.db_repo.enable_statistics)
250 249 cache_manager = caches.get_cache_manager(
251 250 'repo_cache_long', _namespace)
252 251 _cache_key = caches.compute_key_from_params(
253 252 self.db_repo_name, commit_id, show_stats)
254 253
255 254 def compute_stats():
256 255 code_stats = {}
257 256 size = 0
258 257 try:
259 258 scm_instance = self.db_repo.scm_instance()
260 259 commit = scm_instance.get_commit(commit_id)
261 260
262 261 for node in commit.get_filenodes_generator():
263 262 size += node.size
264 263 if not show_stats:
265 264 continue
266 265 ext = string.lower(node.extension)
267 266 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
268 267 if ext_info:
269 268 if ext in code_stats:
270 269 code_stats[ext]['count'] += 1
271 270 else:
272 271 code_stats[ext] = {"count": 1, "desc": ext_info}
273 272 except EmptyRepositoryError:
274 273 pass
275 274 return {'size': h.format_byte_size_binary(size),
276 275 'code_stats': code_stats}
277 276
278 277 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
279 278 return stats
280 279
281 280 @LoginRequired()
282 281 @HasRepoPermissionAnyDecorator(
283 282 'repository.read', 'repository.write', 'repository.admin')
284 283 @view_config(
285 284 route_name='repo_refs_data', request_method='GET',
286 285 renderer='json_ext')
287 286 def repo_refs_data(self):
288 287 _ = self.request.translate
289 288 self.load_default_context()
290 289
291 290 repo = self.rhodecode_vcs_repo
292 291 refs_to_create = [
293 292 (_("Branch"), repo.branches, 'branch'),
294 293 (_("Tag"), repo.tags, 'tag'),
295 294 (_("Bookmark"), repo.bookmarks, 'book'),
296 295 ]
297 296 res = self._create_reference_data(
298 297 repo, self.db_repo_name, refs_to_create)
299 298 data = {
300 299 'more': False,
301 300 'results': res
302 301 }
303 302 return data
304 303
305 304 @LoginRequired()
306 305 @HasRepoPermissionAnyDecorator(
307 306 'repository.read', 'repository.write', 'repository.admin')
308 307 @view_config(
309 308 route_name='repo_refs_changelog_data', request_method='GET',
310 309 renderer='json_ext')
311 310 def repo_refs_changelog_data(self):
312 311 _ = self.request.translate
313 312 self.load_default_context()
314 313
315 314 repo = self.rhodecode_vcs_repo
316 315
317 316 refs_to_create = [
318 317 (_("Branches"), repo.branches, 'branch'),
319 318 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
320 319 # TODO: enable when vcs can handle bookmarks filters
321 320 # (_("Bookmarks"), repo.bookmarks, "book"),
322 321 ]
323 322 res = self._create_reference_data(
324 323 repo, self.db_repo_name, refs_to_create)
325 324 data = {
326 325 'more': False,
327 326 'results': res
328 327 }
329 328 return data
330 329
331 330 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
332 331 format_ref_id = utils.get_format_ref_id(repo)
333 332
334 333 result = []
335 334 for title, refs, ref_type in refs_to_create:
336 335 if refs:
337 336 result.append({
338 337 'text': title,
339 338 'children': self._create_reference_items(
340 339 repo, full_repo_name, refs, ref_type,
341 340 format_ref_id),
342 341 })
343 342 return result
344 343
345 344 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
346 345 format_ref_id):
347 346 result = []
348 347 is_svn = h.is_svn(repo)
349 348 for ref_name, raw_id in refs.iteritems():
350 349 files_url = self._create_files_url(
351 350 repo, full_repo_name, ref_name, raw_id, is_svn)
352 351 result.append({
353 352 'text': ref_name,
354 353 'id': format_ref_id(ref_name, raw_id),
355 354 'raw_id': raw_id,
356 355 'type': ref_type,
357 356 'files_url': files_url,
358 357 })
359 358 return result
360 359
361 360 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
362 361 use_commit_id = '/' in ref_name or is_svn
363 return h.url(
364 'files_home',
362 return h.route_path(
363 'repo_files',
365 364 repo_name=full_repo_name,
366 365 f_path=ref_name if is_svn else '',
367 revision=raw_id if use_commit_id else ref_name,
368 at=ref_name)
366 commit_id=raw_id if use_commit_id else ref_name,
367 _query=dict(at=ref_name))
@@ -1,527 +1,528 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 import traceback
26 26 from collections import OrderedDict
27 27
28 28 from paste.registry import RegistryManager
29 29 from paste.gzipper import make_gzip_middleware
30 30 from pylons.wsgiapp import PylonsApp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.wsgi import wsgiapp
35 35 from pyramid.httpexceptions import (
36 36 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
37 37 from pyramid.events import ApplicationCreated
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import rhodecode
41 41
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 47
48 48 from rhodecode.lib.vcs import VCSCommunicationError
49 49 from rhodecode.lib.exceptions import VCSServerUnavailable
50 50 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
51 51 from rhodecode.lib.middleware.error_handling import (
52 52 PylonsErrorHandlingMiddleware)
53 53 from rhodecode.lib.middleware.https_fixup import HttpsFixup
54 54 from rhodecode.lib.middleware.vcs import VCSMiddleware
55 55 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
56 56 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
57 57 from rhodecode.subscribers import (
58 58 scan_repositories_if_enabled, write_js_routes_if_enabled,
59 59 write_metadata_if_needed)
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 # this is used to avoid avoid the route lookup overhead in routesmiddleware
66 66 # for certain routes which won't go to pylons to - eg. static files, debugger
67 67 # it is only needed for the pylons migration and can be removed once complete
68 68 class SkippableRoutesMiddleware(RoutesMiddleware):
69 69 """ Routes middleware that allows you to skip prefixes """
70 70
71 71 def __init__(self, *args, **kw):
72 72 self.skip_prefixes = kw.pop('skip_prefixes', [])
73 73 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
74 74
75 75 def __call__(self, environ, start_response):
76 76 for prefix in self.skip_prefixes:
77 77 if environ['PATH_INFO'].startswith(prefix):
78 78 # added to avoid the case when a missing /_static route falls
79 79 # through to pylons and causes an exception as pylons is
80 80 # expecting wsgiorg.routingargs to be set in the environ
81 81 # by RoutesMiddleware.
82 82 if 'wsgiorg.routing_args' not in environ:
83 83 environ['wsgiorg.routing_args'] = (None, {})
84 84 return self.app(environ, start_response)
85 85
86 86 return super(SkippableRoutesMiddleware, self).__call__(
87 87 environ, start_response)
88 88
89 89
90 90 def make_app(global_conf, static_files=True, **app_conf):
91 91 """Create a Pylons WSGI application and return it
92 92
93 93 ``global_conf``
94 94 The inherited configuration for this application. Normally from
95 95 the [DEFAULT] section of the Paste ini file.
96 96
97 97 ``app_conf``
98 98 The application's local configuration. Normally specified in
99 99 the [app:<name>] section of the Paste ini file (where <name>
100 100 defaults to main).
101 101
102 102 """
103 103 # Apply compatibility patches
104 104 patches.kombu_1_5_1_python_2_7_11()
105 105 patches.inspect_getargspec()
106 106
107 107 # Configure the Pylons environment
108 108 config = load_environment(global_conf, app_conf)
109 109
110 110 # The Pylons WSGI app
111 111 app = PylonsApp(config=config)
112 112
113 113 # Establish the Registry for this application
114 114 app = RegistryManager(app)
115 115
116 116 app.config = config
117 117
118 118 return app
119 119
120 120
121 121 def make_pyramid_app(global_config, **settings):
122 122 """
123 123 Constructs the WSGI application based on Pyramid and wraps the Pylons based
124 124 application.
125 125
126 126 Specials:
127 127
128 128 * We migrate from Pylons to Pyramid. While doing this, we keep both
129 129 frameworks functional. This involves moving some WSGI middlewares around
130 130 and providing access to some data internals, so that the old code is
131 131 still functional.
132 132
133 133 * The application can also be integrated like a plugin via the call to
134 134 `includeme`. This is accompanied with the other utility functions which
135 135 are called. Changing this should be done with great care to not break
136 136 cases when these fragments are assembled from another place.
137 137
138 138 """
139 139 # The edition string should be available in pylons too, so we add it here
140 140 # before copying the settings.
141 141 settings.setdefault('rhodecode.edition', 'Community Edition')
142 142
143 143 # As long as our Pylons application does expect "unprepared" settings, make
144 144 # sure that we keep an unmodified copy. This avoids unintentional change of
145 145 # behavior in the old application.
146 146 settings_pylons = settings.copy()
147 147
148 148 sanitize_settings_and_apply_defaults(settings)
149 149 config = Configurator(settings=settings)
150 150 add_pylons_compat_data(config.registry, global_config, settings_pylons)
151 151
152 152 load_pyramid_environment(global_config, settings)
153 153
154 154 includeme_first(config)
155 155 includeme(config)
156
156 157 pyramid_app = config.make_wsgi_app()
157 158 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
158 159 pyramid_app.config = config
159 160
160 161 # creating the app uses a connection - return it after we are done
161 162 meta.Session.remove()
162 163
163 164 return pyramid_app
164 165
165 166
166 167 def make_not_found_view(config):
167 168 """
168 169 This creates the view which should be registered as not-found-view to
169 170 pyramid. Basically it contains of the old pylons app, converted to a view.
170 171 Additionally it is wrapped by some other middlewares.
171 172 """
172 173 settings = config.registry.settings
173 174 vcs_server_enabled = settings['vcs.server.enable']
174 175
175 176 # Make pylons app from unprepared settings.
176 177 pylons_app = make_app(
177 178 config.registry._pylons_compat_global_config,
178 179 **config.registry._pylons_compat_settings)
179 180 config.registry._pylons_compat_config = pylons_app.config
180 181
181 182 # Appenlight monitoring.
182 183 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
183 184 pylons_app, settings)
184 185
185 186 # The pylons app is executed inside of the pyramid 404 exception handler.
186 187 # Exceptions which are raised inside of it are not handled by pyramid
187 188 # again. Therefore we add a middleware that invokes the error handler in
188 189 # case of an exception or error response. This way we return proper error
189 190 # HTML pages in case of an error.
190 191 reraise = (settings.get('debugtoolbar.enabled', False) or
191 192 rhodecode.disable_error_handler)
192 193 pylons_app = PylonsErrorHandlingMiddleware(
193 194 pylons_app, error_handler, reraise)
194 195
195 196 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
196 197 # view to handle the request. Therefore it is wrapped around the pylons
197 198 # app. It has to be outside of the error handling otherwise error responses
198 199 # from the vcsserver are converted to HTML error pages. This confuses the
199 200 # command line tools and the user won't get a meaningful error message.
200 201 if vcs_server_enabled:
201 202 pylons_app = VCSMiddleware(
202 203 pylons_app, settings, appenlight_client, registry=config.registry)
203 204
204 205 # Convert WSGI app to pyramid view and return it.
205 206 return wsgiapp(pylons_app)
206 207
207 208
208 209 def add_pylons_compat_data(registry, global_config, settings):
209 210 """
210 211 Attach data to the registry to support the Pylons integration.
211 212 """
212 213 registry._pylons_compat_global_config = global_config
213 214 registry._pylons_compat_settings = settings
214 215
215 216
216 217 def error_handler(exception, request):
217 218 import rhodecode
218 219 from rhodecode.lib import helpers
219 220
220 221 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
221 222
222 223 base_response = HTTPInternalServerError()
223 224 # prefer original exception for the response since it may have headers set
224 225 if isinstance(exception, HTTPException):
225 226 base_response = exception
226 227 elif isinstance(exception, VCSCommunicationError):
227 228 base_response = VCSServerUnavailable()
228 229
229 230 def is_http_error(response):
230 231 # error which should have traceback
231 232 return response.status_code > 499
232 233
233 234 if is_http_error(base_response):
234 235 log.exception(
235 236 'error occurred handling this request for path: %s', request.path)
236 237
237 238 c = AttributeDict()
238 239 c.error_message = base_response.status
239 240 c.error_explanation = base_response.explanation or str(base_response)
240 241 c.visual = AttributeDict()
241 242
242 243 c.visual.rhodecode_support_url = (
243 244 request.registry.settings.get('rhodecode_support_url') or
244 245 request.route_url('rhodecode_support')
245 246 )
246 247 c.redirect_time = 0
247 248 c.rhodecode_name = rhodecode_title
248 249 if not c.rhodecode_name:
249 250 c.rhodecode_name = 'Rhodecode'
250 251
251 252 c.causes = []
252 253 if hasattr(base_response, 'causes'):
253 254 c.causes = base_response.causes
254 255 c.messages = helpers.flash.pop_messages(request=request)
255 256 c.traceback = traceback.format_exc()
256 257 response = render_to_response(
257 258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 259 response=base_response)
259 260
260 261 return response
261 262
262 263
263 264 def includeme(config):
264 265 settings = config.registry.settings
265 266
266 267 # plugin information
267 268 config.registry.rhodecode_plugins = OrderedDict()
268 269
269 270 config.add_directive(
270 271 'register_rhodecode_plugin', register_rhodecode_plugin)
271 272
272 273 if asbool(settings.get('appenlight', 'false')):
273 274 config.include('appenlight_client.ext.pyramid_tween')
274 275
275 276 # Includes which are required. The application would fail without them.
276 277 config.include('pyramid_mako')
277 278 config.include('pyramid_beaker')
278 279
279 280 config.include('rhodecode.authentication')
280 281 config.include('rhodecode.integrations')
281 282
282 283 # apps
283 284 config.include('rhodecode.apps._base')
284 285 config.include('rhodecode.apps.ops')
285 286
286 287 config.include('rhodecode.apps.admin')
287 288 config.include('rhodecode.apps.channelstream')
288 289 config.include('rhodecode.apps.login')
289 290 config.include('rhodecode.apps.home')
290 291 config.include('rhodecode.apps.repository')
291 292 config.include('rhodecode.apps.repo_group')
292 293 config.include('rhodecode.apps.search')
293 294 config.include('rhodecode.apps.user_profile')
294 295 config.include('rhodecode.apps.my_account')
295 296 config.include('rhodecode.apps.svn_support')
296 297 config.include('rhodecode.apps.gist')
297 298
298 299 config.include('rhodecode.apps.debug_style')
299 300 config.include('rhodecode.tweens')
300 301 config.include('rhodecode.api')
301 302
302 303 config.add_route(
303 304 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
304 305
305 306 config.add_translation_dirs('rhodecode:i18n/')
306 307 settings['default_locale_name'] = settings.get('lang', 'en')
307 308
308 309 # Add subscribers.
309 310 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
310 311 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
311 312 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
312 313
313 314 config.add_request_method(
314 315 'rhodecode.lib.partial_renderer.get_partial_renderer',
315 316 'get_partial_renderer')
316 317
317 318 # events
318 319 # TODO(marcink): this should be done when pyramid migration is finished
319 320 # config.add_subscriber(
320 321 # 'rhodecode.integrations.integrations_event_handler',
321 322 # 'rhodecode.events.RhodecodeEvent')
322 323
323 324 # Set the authorization policy.
324 325 authz_policy = ACLAuthorizationPolicy()
325 326 config.set_authorization_policy(authz_policy)
326 327
327 328 # Set the default renderer for HTML templates to mako.
328 329 config.add_mako_renderer('.html')
329 330
330 331 config.add_renderer(
331 332 name='json_ext',
332 333 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
333 334
334 335 # include RhodeCode plugins
335 336 includes = aslist(settings.get('rhodecode.includes', []))
336 337 for inc in includes:
337 338 config.include(inc)
338 339
339 340 # This is the glue which allows us to migrate in chunks. By registering the
340 341 # pylons based application as the "Not Found" view in Pyramid, we will
341 342 # fallback to the old application each time the new one does not yet know
342 343 # how to handle a request.
343 344 config.add_notfound_view(make_not_found_view(config))
344 345
345 346 if not settings.get('debugtoolbar.enabled', False):
346 347 # disabled debugtoolbar handle all exceptions via the error_handlers
347 348 config.add_view(error_handler, context=Exception)
348 349
349 350 config.add_view(error_handler, context=HTTPError)
350 351
351 352
352 353 def includeme_first(config):
353 354 # redirect automatic browser favicon.ico requests to correct place
354 355 def favicon_redirect(context, request):
355 356 return HTTPFound(
356 357 request.static_path('rhodecode:public/images/favicon.ico'))
357 358
358 359 config.add_view(favicon_redirect, route_name='favicon')
359 360 config.add_route('favicon', '/favicon.ico')
360 361
361 362 def robots_redirect(context, request):
362 363 return HTTPFound(
363 364 request.static_path('rhodecode:public/robots.txt'))
364 365
365 366 config.add_view(robots_redirect, route_name='robots')
366 367 config.add_route('robots', '/robots.txt')
367 368
368 369 config.add_static_view(
369 370 '_static/deform', 'deform:static')
370 371 config.add_static_view(
371 372 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
372 373
373 374
374 375 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
375 376 """
376 377 Apply outer WSGI middlewares around the application.
377 378
378 379 Part of this has been moved up from the Pylons layer, so that the
379 380 data is also available if old Pylons code is hit through an already ported
380 381 view.
381 382 """
382 383 settings = config.registry.settings
383 384
384 385 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
385 386 pyramid_app = HttpsFixup(pyramid_app, settings)
386 387
387 388 # Add RoutesMiddleware to support the pylons compatibility tween during
388 389 # migration to pyramid.
389 390
390 391 # TODO(marcink): remove after migration to pyramid
391 392 if hasattr(config.registry, '_pylons_compat_config'):
392 393 routes_map = config.registry._pylons_compat_config['routes.map']
393 394 pyramid_app = SkippableRoutesMiddleware(
394 395 pyramid_app, routes_map,
395 396 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
396 397
397 398 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
398 399
399 400 if settings['gzip_responses']:
400 401 pyramid_app = make_gzip_middleware(
401 402 pyramid_app, settings, compress_level=1)
402 403
403 404 # this should be the outer most middleware in the wsgi stack since
404 405 # middleware like Routes make database calls
405 406 def pyramid_app_with_cleanup(environ, start_response):
406 407 try:
407 408 return pyramid_app(environ, start_response)
408 409 finally:
409 410 # Dispose current database session and rollback uncommitted
410 411 # transactions.
411 412 meta.Session.remove()
412 413
413 414 # In a single threaded mode server, on non sqlite db we should have
414 415 # '0 Current Checked out connections' at the end of a request,
415 416 # if not, then something, somewhere is leaving a connection open
416 417 pool = meta.Base.metadata.bind.engine.pool
417 418 log.debug('sa pool status: %s', pool.status())
418 419
419 420 return pyramid_app_with_cleanup
420 421
421 422
422 423 def sanitize_settings_and_apply_defaults(settings):
423 424 """
424 425 Applies settings defaults and does all type conversion.
425 426
426 427 We would move all settings parsing and preparation into this place, so that
427 428 we have only one place left which deals with this part. The remaining parts
428 429 of the application would start to rely fully on well prepared settings.
429 430
430 431 This piece would later be split up per topic to avoid a big fat monster
431 432 function.
432 433 """
433 434
434 435 # Pyramid's mako renderer has to search in the templates folder so that the
435 436 # old templates still work. Ported and new templates are expected to use
436 437 # real asset specifications for the includes.
437 438 mako_directories = settings.setdefault('mako.directories', [
438 439 # Base templates of the original Pylons application
439 440 'rhodecode:templates',
440 441 ])
441 442 log.debug(
442 443 "Using the following Mako template directories: %s",
443 444 mako_directories)
444 445
445 446 # Default includes, possible to change as a user
446 447 pyramid_includes = settings.setdefault('pyramid.includes', [
447 448 'rhodecode.lib.middleware.request_wrapper',
448 449 ])
449 450 log.debug(
450 451 "Using the following pyramid.includes: %s",
451 452 pyramid_includes)
452 453
453 454 # TODO: johbo: Re-think this, usually the call to config.include
454 455 # should allow to pass in a prefix.
455 456 settings.setdefault('rhodecode.api.url', '/_admin/api')
456 457
457 458 # Sanitize generic settings.
458 459 _list_setting(settings, 'default_encoding', 'UTF-8')
459 460 _bool_setting(settings, 'is_test', 'false')
460 461 _bool_setting(settings, 'gzip_responses', 'false')
461 462
462 463 # Call split out functions that sanitize settings for each topic.
463 464 _sanitize_appenlight_settings(settings)
464 465 _sanitize_vcs_settings(settings)
465 466
466 467 return settings
467 468
468 469
469 470 def _sanitize_appenlight_settings(settings):
470 471 _bool_setting(settings, 'appenlight', 'false')
471 472
472 473
473 474 def _sanitize_vcs_settings(settings):
474 475 """
475 476 Applies settings defaults and does type conversion for all VCS related
476 477 settings.
477 478 """
478 479 _string_setting(settings, 'vcs.svn.compatible_version', '')
479 480 _string_setting(settings, 'git_rev_filter', '--all')
480 481 _string_setting(settings, 'vcs.hooks.protocol', 'http')
481 482 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
482 483 _string_setting(settings, 'vcs.server', '')
483 484 _string_setting(settings, 'vcs.server.log_level', 'debug')
484 485 _string_setting(settings, 'vcs.server.protocol', 'http')
485 486 _bool_setting(settings, 'startup.import_repos', 'false')
486 487 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
487 488 _bool_setting(settings, 'vcs.server.enable', 'true')
488 489 _bool_setting(settings, 'vcs.start_server', 'false')
489 490 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
490 491 _int_setting(settings, 'vcs.connection_timeout', 3600)
491 492
492 493 # Support legacy values of vcs.scm_app_implementation. Legacy
493 494 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
494 495 # which is now mapped to 'http'.
495 496 scm_app_impl = settings['vcs.scm_app_implementation']
496 497 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
497 498 settings['vcs.scm_app_implementation'] = 'http'
498 499
499 500
500 501 def _int_setting(settings, name, default):
501 502 settings[name] = int(settings.get(name, default))
502 503
503 504
504 505 def _bool_setting(settings, name, default):
505 506 input = settings.get(name, default)
506 507 if isinstance(input, unicode):
507 508 input = input.encode('utf8')
508 509 settings[name] = asbool(input)
509 510
510 511
511 512 def _list_setting(settings, name, default):
512 513 raw_value = settings.get(name, default)
513 514
514 515 old_separator = ','
515 516 if old_separator in raw_value:
516 517 # If we get a comma separated list, pass it to our own function.
517 518 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
518 519 else:
519 520 # Otherwise we assume it uses pyramids space/newline separation.
520 521 settings[name] = aslist(raw_value)
521 522
522 523
523 524 def _string_setting(settings, name, default, lower=True):
524 525 value = settings.get(name, default)
525 526 if lower:
526 527 value = value.lower()
527 528 settings[name] = value
@@ -1,870 +1,744 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 def add_route_requirements(route_path, requirements):
55 55 """
56 56 Adds regex requirements to pyramid routes using a mapping dict
57 57
58 58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 59 '/{action}/{id:\d+}'
60 60
61 61 """
62 62 for key, regex in requirements.items():
63 63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 64 return route_path
65 65
66 66
67 67 class JSRoutesMapper(Mapper):
68 68 """
69 69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
70 70 """
71 71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
72 72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
73 73 def __init__(self, *args, **kw):
74 74 super(JSRoutesMapper, self).__init__(*args, **kw)
75 75 self._jsroutes = []
76 76
77 77 def connect(self, *args, **kw):
78 78 """
79 79 Wrapper for connect to take an extra argument jsroute=True
80 80
81 81 :param jsroute: boolean, if True will add the route to the pyroutes list
82 82 """
83 83 if kw.pop('jsroute', False):
84 84 if not self._named_route_regex.match(args[0]):
85 85 raise Exception('only named routes can be added to pyroutes')
86 86 self._jsroutes.append(args[0])
87 87
88 88 super(JSRoutesMapper, self).connect(*args, **kw)
89 89
90 90 def _extract_route_information(self, route):
91 91 """
92 92 Convert a route into tuple(name, path, args), eg:
93 93 ('show_user', '/profile/%(username)s', ['username'])
94 94 """
95 95 routepath = route.routepath
96 96 def replace(matchobj):
97 97 if matchobj.group(1):
98 98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
99 99 else:
100 100 return "%%(%s)s" % matchobj.group(2)
101 101
102 102 routepath = self._argument_prog.sub(replace, routepath)
103 103 return (
104 104 route.name,
105 105 routepath,
106 106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
107 107 for arg in self._argument_prog.findall(route.routepath)]
108 108 )
109 109
110 110 def jsroutes(self):
111 111 """
112 112 Return a list of pyroutes.js compatible routes
113 113 """
114 114 for route_name in self._jsroutes:
115 115 yield self._extract_route_information(self._routenames[route_name])
116 116
117 117
118 118 def make_map(config):
119 119 """Create, configure and return the routes Mapper"""
120 120 rmap = JSRoutesMapper(
121 121 directory=config['pylons.paths']['controllers'],
122 122 always_scan=config['debug'])
123 123 rmap.minimization = False
124 124 rmap.explicit = False
125 125
126 126 from rhodecode.lib.utils2 import str2bool
127 127 from rhodecode.model import repo, repo_group
128 128
129 129 def check_repo(environ, match_dict):
130 130 """
131 131 check for valid repository for proper 404 handling
132 132
133 133 :param environ:
134 134 :param match_dict:
135 135 """
136 136 repo_name = match_dict.get('repo_name')
137 137
138 138 if match_dict.get('f_path'):
139 139 # fix for multiple initial slashes that causes errors
140 140 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 141 repo_model = repo.RepoModel()
142 142 by_name_match = repo_model.get_by_repo_name(repo_name)
143 143 # if we match quickly from database, short circuit the operation,
144 144 # and validate repo based on the type.
145 145 if by_name_match:
146 146 return True
147 147
148 148 by_id_match = repo_model.get_repo_by_id(repo_name)
149 149 if by_id_match:
150 150 repo_name = by_id_match.repo_name
151 151 match_dict['repo_name'] = repo_name
152 152 return True
153 153
154 154 return False
155 155
156 156 def check_group(environ, match_dict):
157 157 """
158 158 check for valid repository group path for proper 404 handling
159 159
160 160 :param environ:
161 161 :param match_dict:
162 162 """
163 163 repo_group_name = match_dict.get('group_name')
164 164 repo_group_model = repo_group.RepoGroupModel()
165 165 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 166 if by_name_match:
167 167 return True
168 168
169 169 return False
170 170
171 171 def check_user_group(environ, match_dict):
172 172 """
173 173 check for valid user group for proper 404 handling
174 174
175 175 :param environ:
176 176 :param match_dict:
177 177 """
178 178 return True
179 179
180 180 def check_int(environ, match_dict):
181 181 return match_dict.get('id').isdigit()
182 182
183 183
184 184 #==========================================================================
185 185 # CUSTOM ROUTES HERE
186 186 #==========================================================================
187 187
188 188 # ping and pylons error test
189 189 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
190 190 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
191 191
192 192 # ADMIN REPOSITORY ROUTES
193 193 with rmap.submapper(path_prefix=ADMIN_PREFIX,
194 194 controller='admin/repos') as m:
195 195 m.connect('repos', '/repos',
196 196 action='create', conditions={'method': ['POST']})
197 197 m.connect('repos', '/repos',
198 198 action='index', conditions={'method': ['GET']})
199 199 m.connect('new_repo', '/create_repository', jsroute=True,
200 200 action='create_repository', conditions={'method': ['GET']})
201 201 m.connect('delete_repo', '/repos/{repo_name}',
202 202 action='delete', conditions={'method': ['DELETE']},
203 203 requirements=URL_NAME_REQUIREMENTS)
204 204 m.connect('repo', '/repos/{repo_name}',
205 205 action='show', conditions={'method': ['GET'],
206 206 'function': check_repo},
207 207 requirements=URL_NAME_REQUIREMENTS)
208 208
209 209 # ADMIN REPOSITORY GROUPS ROUTES
210 210 with rmap.submapper(path_prefix=ADMIN_PREFIX,
211 211 controller='admin/repo_groups') as m:
212 212 m.connect('repo_groups', '/repo_groups',
213 213 action='create', conditions={'method': ['POST']})
214 214 m.connect('repo_groups', '/repo_groups',
215 215 action='index', conditions={'method': ['GET']})
216 216 m.connect('new_repo_group', '/repo_groups/new',
217 217 action='new', conditions={'method': ['GET']})
218 218 m.connect('update_repo_group', '/repo_groups/{group_name}',
219 219 action='update', conditions={'method': ['PUT'],
220 220 'function': check_group},
221 221 requirements=URL_NAME_REQUIREMENTS)
222 222
223 223 # EXTRAS REPO GROUP ROUTES
224 224 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
225 225 action='edit',
226 226 conditions={'method': ['GET'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
229 229 action='edit',
230 230 conditions={'method': ['PUT'], 'function': check_group},
231 231 requirements=URL_NAME_REQUIREMENTS)
232 232
233 233 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
234 234 action='edit_repo_group_advanced',
235 235 conditions={'method': ['GET'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
238 238 action='edit_repo_group_advanced',
239 239 conditions={'method': ['PUT'], 'function': check_group},
240 240 requirements=URL_NAME_REQUIREMENTS)
241 241
242 242 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
243 243 action='edit_repo_group_perms',
244 244 conditions={'method': ['GET'], 'function': check_group},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
247 247 action='update_perms',
248 248 conditions={'method': ['PUT'], 'function': check_group},
249 249 requirements=URL_NAME_REQUIREMENTS)
250 250
251 251 m.connect('delete_repo_group', '/repo_groups/{group_name}',
252 252 action='delete', conditions={'method': ['DELETE'],
253 253 'function': check_group},
254 254 requirements=URL_NAME_REQUIREMENTS)
255 255
256 256 # ADMIN USER ROUTES
257 257 with rmap.submapper(path_prefix=ADMIN_PREFIX,
258 258 controller='admin/users') as m:
259 259 m.connect('users', '/users',
260 260 action='create', conditions={'method': ['POST']})
261 261 m.connect('new_user', '/users/new',
262 262 action='new', conditions={'method': ['GET']})
263 263 m.connect('update_user', '/users/{user_id}',
264 264 action='update', conditions={'method': ['PUT']})
265 265 m.connect('delete_user', '/users/{user_id}',
266 266 action='delete', conditions={'method': ['DELETE']})
267 267 m.connect('edit_user', '/users/{user_id}/edit',
268 268 action='edit', conditions={'method': ['GET']}, jsroute=True)
269 269 m.connect('user', '/users/{user_id}',
270 270 action='show', conditions={'method': ['GET']})
271 271 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
272 272 action='reset_password', conditions={'method': ['POST']})
273 273 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
274 274 action='create_personal_repo_group', conditions={'method': ['POST']})
275 275
276 276 # EXTRAS USER ROUTES
277 277 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
278 278 action='edit_advanced', conditions={'method': ['GET']})
279 279 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
280 280 action='update_advanced', conditions={'method': ['PUT']})
281 281
282 282 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
283 283 action='edit_global_perms', conditions={'method': ['GET']})
284 284 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
285 285 action='update_global_perms', conditions={'method': ['PUT']})
286 286
287 287 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
288 288 action='edit_perms_summary', conditions={'method': ['GET']})
289 289
290 290
291 291 # ADMIN USER GROUPS REST ROUTES
292 292 with rmap.submapper(path_prefix=ADMIN_PREFIX,
293 293 controller='admin/user_groups') as m:
294 294 m.connect('users_groups', '/user_groups',
295 295 action='create', conditions={'method': ['POST']})
296 296 m.connect('users_groups', '/user_groups',
297 297 action='index', conditions={'method': ['GET']})
298 298 m.connect('new_users_group', '/user_groups/new',
299 299 action='new', conditions={'method': ['GET']})
300 300 m.connect('update_users_group', '/user_groups/{user_group_id}',
301 301 action='update', conditions={'method': ['PUT']})
302 302 m.connect('delete_users_group', '/user_groups/{user_group_id}',
303 303 action='delete', conditions={'method': ['DELETE']})
304 304 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
305 305 action='edit', conditions={'method': ['GET']},
306 306 function=check_user_group)
307 307
308 308 # EXTRAS USER GROUP ROUTES
309 309 m.connect('edit_user_group_global_perms',
310 310 '/user_groups/{user_group_id}/edit/global_permissions',
311 311 action='edit_global_perms', conditions={'method': ['GET']})
312 312 m.connect('edit_user_group_global_perms',
313 313 '/user_groups/{user_group_id}/edit/global_permissions',
314 314 action='update_global_perms', conditions={'method': ['PUT']})
315 315 m.connect('edit_user_group_perms_summary',
316 316 '/user_groups/{user_group_id}/edit/permissions_summary',
317 317 action='edit_perms_summary', conditions={'method': ['GET']})
318 318
319 319 m.connect('edit_user_group_perms',
320 320 '/user_groups/{user_group_id}/edit/permissions',
321 321 action='edit_perms', conditions={'method': ['GET']})
322 322 m.connect('edit_user_group_perms',
323 323 '/user_groups/{user_group_id}/edit/permissions',
324 324 action='update_perms', conditions={'method': ['PUT']})
325 325
326 326 m.connect('edit_user_group_advanced',
327 327 '/user_groups/{user_group_id}/edit/advanced',
328 328 action='edit_advanced', conditions={'method': ['GET']})
329 329
330 330 m.connect('edit_user_group_advanced_sync',
331 331 '/user_groups/{user_group_id}/edit/advanced/sync',
332 332 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
333 333
334 334 m.connect('edit_user_group_members',
335 335 '/user_groups/{user_group_id}/edit/members', jsroute=True,
336 336 action='user_group_members', conditions={'method': ['GET']})
337 337
338 338 # ADMIN PERMISSIONS ROUTES
339 339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
340 340 controller='admin/permissions') as m:
341 341 m.connect('admin_permissions_application', '/permissions/application',
342 342 action='permission_application_update', conditions={'method': ['POST']})
343 343 m.connect('admin_permissions_application', '/permissions/application',
344 344 action='permission_application', conditions={'method': ['GET']})
345 345
346 346 m.connect('admin_permissions_global', '/permissions/global',
347 347 action='permission_global_update', conditions={'method': ['POST']})
348 348 m.connect('admin_permissions_global', '/permissions/global',
349 349 action='permission_global', conditions={'method': ['GET']})
350 350
351 351 m.connect('admin_permissions_object', '/permissions/object',
352 352 action='permission_objects_update', conditions={'method': ['POST']})
353 353 m.connect('admin_permissions_object', '/permissions/object',
354 354 action='permission_objects', conditions={'method': ['GET']})
355 355
356 356 m.connect('admin_permissions_ips', '/permissions/ips',
357 357 action='permission_ips', conditions={'method': ['POST']})
358 358 m.connect('admin_permissions_ips', '/permissions/ips',
359 359 action='permission_ips', conditions={'method': ['GET']})
360 360
361 361 m.connect('admin_permissions_overview', '/permissions/overview',
362 362 action='permission_perms', conditions={'method': ['GET']})
363 363
364 364 # ADMIN DEFAULTS REST ROUTES
365 365 with rmap.submapper(path_prefix=ADMIN_PREFIX,
366 366 controller='admin/defaults') as m:
367 367 m.connect('admin_defaults_repositories', '/defaults/repositories',
368 368 action='update_repository_defaults', conditions={'method': ['POST']})
369 369 m.connect('admin_defaults_repositories', '/defaults/repositories',
370 370 action='index', conditions={'method': ['GET']})
371 371
372 372 # ADMIN SETTINGS ROUTES
373 373 with rmap.submapper(path_prefix=ADMIN_PREFIX,
374 374 controller='admin/settings') as m:
375 375
376 376 # default
377 377 m.connect('admin_settings', '/settings',
378 378 action='settings_global_update',
379 379 conditions={'method': ['POST']})
380 380 m.connect('admin_settings', '/settings',
381 381 action='settings_global', conditions={'method': ['GET']})
382 382
383 383 m.connect('admin_settings_vcs', '/settings/vcs',
384 384 action='settings_vcs_update',
385 385 conditions={'method': ['POST']})
386 386 m.connect('admin_settings_vcs', '/settings/vcs',
387 387 action='settings_vcs',
388 388 conditions={'method': ['GET']})
389 389 m.connect('admin_settings_vcs', '/settings/vcs',
390 390 action='delete_svn_pattern',
391 391 conditions={'method': ['DELETE']})
392 392
393 393 m.connect('admin_settings_mapping', '/settings/mapping',
394 394 action='settings_mapping_update',
395 395 conditions={'method': ['POST']})
396 396 m.connect('admin_settings_mapping', '/settings/mapping',
397 397 action='settings_mapping', conditions={'method': ['GET']})
398 398
399 399 m.connect('admin_settings_global', '/settings/global',
400 400 action='settings_global_update',
401 401 conditions={'method': ['POST']})
402 402 m.connect('admin_settings_global', '/settings/global',
403 403 action='settings_global', conditions={'method': ['GET']})
404 404
405 405 m.connect('admin_settings_visual', '/settings/visual',
406 406 action='settings_visual_update',
407 407 conditions={'method': ['POST']})
408 408 m.connect('admin_settings_visual', '/settings/visual',
409 409 action='settings_visual', conditions={'method': ['GET']})
410 410
411 411 m.connect('admin_settings_issuetracker',
412 412 '/settings/issue-tracker', action='settings_issuetracker',
413 413 conditions={'method': ['GET']})
414 414 m.connect('admin_settings_issuetracker_save',
415 415 '/settings/issue-tracker/save',
416 416 action='settings_issuetracker_save',
417 417 conditions={'method': ['POST']})
418 418 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
419 419 action='settings_issuetracker_test',
420 420 conditions={'method': ['POST']})
421 421 m.connect('admin_issuetracker_delete',
422 422 '/settings/issue-tracker/delete',
423 423 action='settings_issuetracker_delete',
424 424 conditions={'method': ['DELETE']})
425 425
426 426 m.connect('admin_settings_email', '/settings/email',
427 427 action='settings_email_update',
428 428 conditions={'method': ['POST']})
429 429 m.connect('admin_settings_email', '/settings/email',
430 430 action='settings_email', conditions={'method': ['GET']})
431 431
432 432 m.connect('admin_settings_hooks', '/settings/hooks',
433 433 action='settings_hooks_update',
434 434 conditions={'method': ['POST', 'DELETE']})
435 435 m.connect('admin_settings_hooks', '/settings/hooks',
436 436 action='settings_hooks', conditions={'method': ['GET']})
437 437
438 438 m.connect('admin_settings_search', '/settings/search',
439 439 action='settings_search', conditions={'method': ['GET']})
440 440
441 441 m.connect('admin_settings_supervisor', '/settings/supervisor',
442 442 action='settings_supervisor', conditions={'method': ['GET']})
443 443 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
444 444 action='settings_supervisor_log', conditions={'method': ['GET']})
445 445
446 446 m.connect('admin_settings_labs', '/settings/labs',
447 447 action='settings_labs_update',
448 448 conditions={'method': ['POST']})
449 449 m.connect('admin_settings_labs', '/settings/labs',
450 450 action='settings_labs', conditions={'method': ['GET']})
451 451
452 452 # ADMIN MY ACCOUNT
453 453 with rmap.submapper(path_prefix=ADMIN_PREFIX,
454 454 controller='admin/my_account') as m:
455 455
456 456 # NOTE(marcink): this needs to be kept for password force flag to be
457 457 # handled in pylons controllers, remove after full migration to pyramid
458 458 m.connect('my_account_password', '/my_account/password',
459 459 action='my_account_password', conditions={'method': ['GET']})
460 460
461 461 # USER JOURNAL
462 462 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
463 463 controller='journal', action='index')
464 464 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
465 465 controller='journal', action='journal_rss')
466 466 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
467 467 controller='journal', action='journal_atom')
468 468
469 469 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
470 470 controller='journal', action='public_journal')
471 471
472 472 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
473 473 controller='journal', action='public_journal_rss')
474 474
475 475 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
476 476 controller='journal', action='public_journal_rss')
477 477
478 478 rmap.connect('public_journal_atom',
479 479 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
480 480 action='public_journal_atom')
481 481
482 482 rmap.connect('public_journal_atom_old',
483 483 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
484 484 action='public_journal_atom')
485 485
486 486 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
487 487 controller='journal', action='toggle_following', jsroute=True,
488 488 conditions={'method': ['POST']})
489 489
490 490 #==========================================================================
491 491 # REPOSITORY ROUTES
492 492 #==========================================================================
493 493
494 494 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
495 495 controller='admin/repos', action='repo_creating',
496 496 requirements=URL_NAME_REQUIREMENTS)
497 497 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
498 498 controller='admin/repos', action='repo_check',
499 499 requirements=URL_NAME_REQUIREMENTS)
500 500
501 501 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
502 502 controller='changeset', revision='tip',
503 503 conditions={'function': check_repo},
504 504 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
505 505 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
506 506 controller='changeset', revision='tip', action='changeset_children',
507 507 conditions={'function': check_repo},
508 508 requirements=URL_NAME_REQUIREMENTS)
509 509 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
510 510 controller='changeset', revision='tip', action='changeset_parents',
511 511 conditions={'function': check_repo},
512 512 requirements=URL_NAME_REQUIREMENTS)
513 513
514 514 # repo edit options
515 515 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
516 516 controller='admin/repos', action='edit_fields',
517 517 conditions={'method': ['GET'], 'function': check_repo},
518 518 requirements=URL_NAME_REQUIREMENTS)
519 519 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
520 520 controller='admin/repos', action='create_repo_field',
521 521 conditions={'method': ['PUT'], 'function': check_repo},
522 522 requirements=URL_NAME_REQUIREMENTS)
523 523 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
524 524 controller='admin/repos', action='delete_repo_field',
525 525 conditions={'method': ['DELETE'], 'function': check_repo},
526 526 requirements=URL_NAME_REQUIREMENTS)
527 527
528 528 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
529 529 controller='admin/repos', action='toggle_locking',
530 530 conditions={'method': ['GET'], 'function': check_repo},
531 531 requirements=URL_NAME_REQUIREMENTS)
532 532
533 533 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
534 534 controller='admin/repos', action='edit_remote_form',
535 535 conditions={'method': ['GET'], 'function': check_repo},
536 536 requirements=URL_NAME_REQUIREMENTS)
537 537 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
538 538 controller='admin/repos', action='edit_remote',
539 539 conditions={'method': ['PUT'], 'function': check_repo},
540 540 requirements=URL_NAME_REQUIREMENTS)
541 541
542 542 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
543 543 controller='admin/repos', action='edit_statistics_form',
544 544 conditions={'method': ['GET'], 'function': check_repo},
545 545 requirements=URL_NAME_REQUIREMENTS)
546 546 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
547 547 controller='admin/repos', action='edit_statistics',
548 548 conditions={'method': ['PUT'], 'function': check_repo},
549 549 requirements=URL_NAME_REQUIREMENTS)
550 550 rmap.connect('repo_settings_issuetracker',
551 551 '/{repo_name}/settings/issue-tracker',
552 552 controller='admin/repos', action='repo_issuetracker',
553 553 conditions={'method': ['GET'], 'function': check_repo},
554 554 requirements=URL_NAME_REQUIREMENTS)
555 555 rmap.connect('repo_issuetracker_test',
556 556 '/{repo_name}/settings/issue-tracker/test',
557 557 controller='admin/repos', action='repo_issuetracker_test',
558 558 conditions={'method': ['POST'], 'function': check_repo},
559 559 requirements=URL_NAME_REQUIREMENTS)
560 560 rmap.connect('repo_issuetracker_delete',
561 561 '/{repo_name}/settings/issue-tracker/delete',
562 562 controller='admin/repos', action='repo_issuetracker_delete',
563 563 conditions={'method': ['DELETE'], 'function': check_repo},
564 564 requirements=URL_NAME_REQUIREMENTS)
565 565 rmap.connect('repo_issuetracker_save',
566 566 '/{repo_name}/settings/issue-tracker/save',
567 567 controller='admin/repos', action='repo_issuetracker_save',
568 568 conditions={'method': ['POST'], 'function': check_repo},
569 569 requirements=URL_NAME_REQUIREMENTS)
570 570 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
571 571 controller='admin/repos', action='repo_settings_vcs_update',
572 572 conditions={'method': ['POST'], 'function': check_repo},
573 573 requirements=URL_NAME_REQUIREMENTS)
574 574 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
575 575 controller='admin/repos', action='repo_settings_vcs',
576 576 conditions={'method': ['GET'], 'function': check_repo},
577 577 requirements=URL_NAME_REQUIREMENTS)
578 578 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
579 579 controller='admin/repos', action='repo_delete_svn_pattern',
580 580 conditions={'method': ['DELETE'], 'function': check_repo},
581 581 requirements=URL_NAME_REQUIREMENTS)
582 582 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
583 583 controller='admin/repos', action='repo_settings_pullrequest',
584 584 conditions={'method': ['GET', 'POST'], 'function': check_repo},
585 585 requirements=URL_NAME_REQUIREMENTS)
586 586
587 587 # still working url for backward compat.
588 588 rmap.connect('raw_changeset_home_depraced',
589 589 '/{repo_name}/raw-changeset/{revision}',
590 590 controller='changeset', action='changeset_raw',
591 591 revision='tip', conditions={'function': check_repo},
592 592 requirements=URL_NAME_REQUIREMENTS)
593 593
594 594 # new URLs
595 595 rmap.connect('changeset_raw_home',
596 596 '/{repo_name}/changeset-diff/{revision}',
597 597 controller='changeset', action='changeset_raw',
598 598 revision='tip', conditions={'function': check_repo},
599 599 requirements=URL_NAME_REQUIREMENTS)
600 600
601 601 rmap.connect('changeset_patch_home',
602 602 '/{repo_name}/changeset-patch/{revision}',
603 603 controller='changeset', action='changeset_patch',
604 604 revision='tip', conditions={'function': check_repo},
605 605 requirements=URL_NAME_REQUIREMENTS)
606 606
607 607 rmap.connect('changeset_download_home',
608 608 '/{repo_name}/changeset-download/{revision}',
609 609 controller='changeset', action='changeset_download',
610 610 revision='tip', conditions={'function': check_repo},
611 611 requirements=URL_NAME_REQUIREMENTS)
612 612
613 613 rmap.connect('changeset_comment',
614 614 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
615 615 controller='changeset', revision='tip', action='comment',
616 616 conditions={'function': check_repo},
617 617 requirements=URL_NAME_REQUIREMENTS)
618 618
619 619 rmap.connect('changeset_comment_preview',
620 620 '/{repo_name}/changeset/comment/preview', jsroute=True,
621 621 controller='changeset', action='preview_comment',
622 622 conditions={'function': check_repo, 'method': ['POST']},
623 623 requirements=URL_NAME_REQUIREMENTS)
624 624
625 625 rmap.connect('changeset_comment_delete',
626 626 '/{repo_name}/changeset/comment/{comment_id}/delete',
627 627 controller='changeset', action='delete_comment',
628 628 conditions={'function': check_repo, 'method': ['DELETE']},
629 629 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
630 630
631 631 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
632 632 controller='changeset', action='changeset_info',
633 633 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
634 634
635 635 rmap.connect('compare_home',
636 636 '/{repo_name}/compare',
637 637 controller='compare', action='index',
638 638 conditions={'function': check_repo},
639 639 requirements=URL_NAME_REQUIREMENTS)
640 640
641 641 rmap.connect('compare_url',
642 642 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
643 643 controller='compare', action='compare',
644 644 conditions={'function': check_repo},
645 645 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
646 646
647 647 rmap.connect('pullrequest_home',
648 648 '/{repo_name}/pull-request/new', controller='pullrequests',
649 649 action='index', conditions={'function': check_repo,
650 650 'method': ['GET']},
651 651 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
652 652
653 653 rmap.connect('pullrequest',
654 654 '/{repo_name}/pull-request/new', controller='pullrequests',
655 655 action='create', conditions={'function': check_repo,
656 656 'method': ['POST']},
657 657 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
658 658
659 659 rmap.connect('pullrequest_repo_refs',
660 660 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
661 661 controller='pullrequests',
662 662 action='get_repo_refs',
663 663 conditions={'function': check_repo, 'method': ['GET']},
664 664 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
665 665
666 666 rmap.connect('pullrequest_repo_destinations',
667 667 '/{repo_name}/pull-request/repo-destinations',
668 668 controller='pullrequests',
669 669 action='get_repo_destinations',
670 670 conditions={'function': check_repo, 'method': ['GET']},
671 671 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
672 672
673 673 rmap.connect('pullrequest_show',
674 674 '/{repo_name}/pull-request/{pull_request_id}',
675 675 controller='pullrequests',
676 676 action='show', conditions={'function': check_repo,
677 677 'method': ['GET']},
678 678 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
679 679
680 680 rmap.connect('pullrequest_update',
681 681 '/{repo_name}/pull-request/{pull_request_id}',
682 682 controller='pullrequests',
683 683 action='update', conditions={'function': check_repo,
684 684 'method': ['PUT']},
685 685 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
686 686
687 687 rmap.connect('pullrequest_merge',
688 688 '/{repo_name}/pull-request/{pull_request_id}',
689 689 controller='pullrequests',
690 690 action='merge', conditions={'function': check_repo,
691 691 'method': ['POST']},
692 692 requirements=URL_NAME_REQUIREMENTS)
693 693
694 694 rmap.connect('pullrequest_delete',
695 695 '/{repo_name}/pull-request/{pull_request_id}',
696 696 controller='pullrequests',
697 697 action='delete', conditions={'function': check_repo,
698 698 'method': ['DELETE']},
699 699 requirements=URL_NAME_REQUIREMENTS)
700 700
701 701 rmap.connect('pullrequest_comment',
702 702 '/{repo_name}/pull-request-comment/{pull_request_id}',
703 703 controller='pullrequests',
704 704 action='comment', conditions={'function': check_repo,
705 705 'method': ['POST']},
706 706 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
707 707
708 708 rmap.connect('pullrequest_comment_delete',
709 709 '/{repo_name}/pull-request-comment/{comment_id}/delete',
710 710 controller='pullrequests', action='delete_comment',
711 711 conditions={'function': check_repo, 'method': ['DELETE']},
712 712 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
713 713
714 714 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
715 715 controller='changelog', conditions={'function': check_repo},
716 716 requirements=URL_NAME_REQUIREMENTS)
717 717
718 718 rmap.connect('changelog_file_home',
719 719 '/{repo_name}/changelog/{revision}/{f_path}',
720 720 controller='changelog', f_path=None,
721 721 conditions={'function': check_repo},
722 722 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
723 723
724 724 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
725 725 controller='changelog', action='changelog_elements',
726 726 conditions={'function': check_repo},
727 727 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
728 728
729 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
730 controller='files', revision='tip', f_path='',
731 conditions={'function': check_repo},
732 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
733
734 rmap.connect('files_home_simple_catchrev',
735 '/{repo_name}/files/{revision}',
736 controller='files', revision='tip', f_path='',
737 conditions={'function': check_repo},
738 requirements=URL_NAME_REQUIREMENTS)
739
740 rmap.connect('files_home_simple_catchall',
741 '/{repo_name}/files',
742 controller='files', revision='tip', f_path='',
743 conditions={'function': check_repo},
744 requirements=URL_NAME_REQUIREMENTS)
745
746 rmap.connect('files_history_home',
747 '/{repo_name}/history/{revision}/{f_path}',
748 controller='files', action='history', revision='tip', f_path='',
749 conditions={'function': check_repo},
750 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
751
752 rmap.connect('files_authors_home',
753 '/{repo_name}/authors/{revision}/{f_path}',
754 controller='files', action='authors', revision='tip', f_path='',
755 conditions={'function': check_repo},
756 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
757
758 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
759 controller='files', action='diff', f_path='',
760 conditions={'function': check_repo},
761 requirements=URL_NAME_REQUIREMENTS)
762
763 rmap.connect('files_diff_2way_home',
764 '/{repo_name}/diff-2way/{f_path}',
765 controller='files', action='diff_2way', f_path='',
766 conditions={'function': check_repo},
767 requirements=URL_NAME_REQUIREMENTS)
768
769 rmap.connect('files_rawfile_home',
770 '/{repo_name}/rawfile/{revision}/{f_path}',
771 controller='files', action='rawfile', revision='tip',
772 f_path='', conditions={'function': check_repo},
773 requirements=URL_NAME_REQUIREMENTS)
774
775 rmap.connect('files_raw_home',
776 '/{repo_name}/raw/{revision}/{f_path}',
777 controller='files', action='raw', revision='tip', f_path='',
778 conditions={'function': check_repo},
779 requirements=URL_NAME_REQUIREMENTS)
780
781 rmap.connect('files_render_home',
782 '/{repo_name}/render/{revision}/{f_path}',
783 controller='files', action='index', revision='tip', f_path='',
784 rendered=True, conditions={'function': check_repo},
785 requirements=URL_NAME_REQUIREMENTS)
786
787 rmap.connect('files_annotate_home',
788 '/{repo_name}/annotate/{revision}/{f_path}',
789 controller='files', action='index', revision='tip',
790 f_path='', annotate=True, conditions={'function': check_repo},
791 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
792
793 rmap.connect('files_annotate_previous',
794 '/{repo_name}/annotate-previous/{revision}/{f_path}',
795 controller='files', action='annotate_previous', revision='tip',
796 f_path='', annotate=True, conditions={'function': check_repo},
797 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
798
799 rmap.connect('files_edit',
800 '/{repo_name}/edit/{revision}/{f_path}',
801 controller='files', action='edit', revision='tip',
802 f_path='',
803 conditions={'function': check_repo, 'method': ['POST']},
804 requirements=URL_NAME_REQUIREMENTS)
805
806 rmap.connect('files_edit_home',
807 '/{repo_name}/edit/{revision}/{f_path}',
808 controller='files', action='edit_home', revision='tip',
809 f_path='', conditions={'function': check_repo},
810 requirements=URL_NAME_REQUIREMENTS)
811
812 rmap.connect('files_add',
813 '/{repo_name}/add/{revision}/{f_path}',
814 controller='files', action='add', revision='tip',
815 f_path='',
816 conditions={'function': check_repo, 'method': ['POST']},
817 requirements=URL_NAME_REQUIREMENTS)
818
819 rmap.connect('files_add_home',
820 '/{repo_name}/add/{revision}/{f_path}',
821 controller='files', action='add_home', revision='tip',
822 f_path='', conditions={'function': check_repo},
823 requirements=URL_NAME_REQUIREMENTS)
824
825 rmap.connect('files_delete',
826 '/{repo_name}/delete/{revision}/{f_path}',
827 controller='files', action='delete', revision='tip',
828 f_path='',
829 conditions={'function': check_repo, 'method': ['POST']},
830 requirements=URL_NAME_REQUIREMENTS)
831
832 rmap.connect('files_delete_home',
833 '/{repo_name}/delete/{revision}/{f_path}',
834 controller='files', action='delete_home', revision='tip',
835 f_path='', conditions={'function': check_repo},
836 requirements=URL_NAME_REQUIREMENTS)
837
838 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
839 controller='files', action='archivefile',
840 conditions={'function': check_repo},
841 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
842
843 rmap.connect('files_nodelist_home',
844 '/{repo_name}/nodelist/{revision}/{f_path}',
845 controller='files', action='nodelist',
846 conditions={'function': check_repo},
847 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
848
849 rmap.connect('files_nodetree_full',
850 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
851 controller='files', action='nodetree_full',
852 conditions={'function': check_repo},
853 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
854
855 729 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
856 730 controller='forks', action='fork_create',
857 731 conditions={'function': check_repo, 'method': ['POST']},
858 732 requirements=URL_NAME_REQUIREMENTS)
859 733
860 734 rmap.connect('repo_fork_home', '/{repo_name}/fork',
861 735 controller='forks', action='fork',
862 736 conditions={'function': check_repo},
863 737 requirements=URL_NAME_REQUIREMENTS)
864 738
865 739 rmap.connect('repo_forks_home', '/{repo_name}/forks',
866 740 controller='forks', action='forks',
867 741 conditions={'function': check_repo},
868 742 requirements=URL_NAME_REQUIREMENTS)
869 743
870 744 return rmap
@@ -1,2040 +1,2046 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 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 random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from webhelpers.html import literal, HTML, escape
55 55 from webhelpers.html.tools import *
56 56 from webhelpers.html.builder import make_tag
57 57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 63 from webhelpers.pylonslib import Flash as _Flash
64 64 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
65 65 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
66 66 replace_whitespace, urlify, truncate, wrap_paragraphs
67 67 from webhelpers.date import time_ago_in_words
68 68 from webhelpers.paginate import Page as _Page
69 69 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
70 70 convert_boolean_attrs, NotGiven, _make_safe_id_component
71 71 from webhelpers2.number import format_byte_size
72 72
73 73 from rhodecode.lib.action_parser import action_parser
74 74 from rhodecode.lib.ext_json import json
75 75 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
76 76 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
77 77 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
78 78 AttributeDict, safe_int, md5, md5_safe
79 79 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
80 80 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
81 81 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
82 82 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
83 83 from rhodecode.model.changeset_status import ChangesetStatusModel
84 84 from rhodecode.model.db import Permission, User, Repository
85 85 from rhodecode.model.repo_group import RepoGroupModel
86 86 from rhodecode.model.settings import IssueTrackerSettingsModel
87 87
88 88 log = logging.getLogger(__name__)
89 89
90 90
91 91 DEFAULT_USER = User.DEFAULT_USER
92 92 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
93 93
94 94
95 95 def url(*args, **kw):
96 96 from pylons import url as pylons_url
97 97 return pylons_url(*args, **kw)
98 98
99 99
100 100 def pylons_url_current(*args, **kw):
101 101 """
102 102 This function overrides pylons.url.current() which returns the current
103 103 path so that it will also work from a pyramid only context. This
104 104 should be removed once port to pyramid is complete.
105 105 """
106 106 from pylons import url as pylons_url
107 107 if not args and not kw:
108 108 request = get_current_request()
109 109 return request.path
110 110 return pylons_url.current(*args, **kw)
111 111
112 112 url.current = pylons_url_current
113 113
114 114
115 115 def url_replace(**qargs):
116 116 """ Returns the current request url while replacing query string args """
117 117
118 118 request = get_current_request()
119 119 new_args = request.GET.mixed()
120 120 new_args.update(qargs)
121 121 return url('', **new_args)
122 122
123 123
124 124 def asset(path, ver=None, **kwargs):
125 125 """
126 126 Helper to generate a static asset file path for rhodecode assets
127 127
128 128 eg. h.asset('images/image.png', ver='3923')
129 129
130 130 :param path: path of asset
131 131 :param ver: optional version query param to append as ?ver=
132 132 """
133 133 request = get_current_request()
134 134 query = {}
135 135 query.update(kwargs)
136 136 if ver:
137 137 query = {'ver': ver}
138 138 return request.static_path(
139 139 'rhodecode:public/{}'.format(path), _query=query)
140 140
141 141
142 142 default_html_escape_table = {
143 143 ord('&'): u'&amp;',
144 144 ord('<'): u'&lt;',
145 145 ord('>'): u'&gt;',
146 146 ord('"'): u'&quot;',
147 147 ord("'"): u'&#39;',
148 148 }
149 149
150 150
151 151 def html_escape(text, html_escape_table=default_html_escape_table):
152 152 """Produce entities within text."""
153 153 return text.translate(html_escape_table)
154 154
155 155
156 156 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
157 157 """
158 158 Truncate string ``s`` at the first occurrence of ``sub``.
159 159
160 160 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
161 161 """
162 162 suffix_if_chopped = suffix_if_chopped or ''
163 163 pos = s.find(sub)
164 164 if pos == -1:
165 165 return s
166 166
167 167 if inclusive:
168 168 pos += len(sub)
169 169
170 170 chopped = s[:pos]
171 171 left = s[pos:].strip()
172 172
173 173 if left and suffix_if_chopped:
174 174 chopped += suffix_if_chopped
175 175
176 176 return chopped
177 177
178 178
179 179 def shorter(text, size=20):
180 180 postfix = '...'
181 181 if len(text) > size:
182 182 return text[:size - len(postfix)] + postfix
183 183 return text
184 184
185 185
186 186 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
187 187 """
188 188 Reset button
189 189 """
190 190 _set_input_attrs(attrs, type, name, value)
191 191 _set_id_attr(attrs, id, name)
192 192 convert_boolean_attrs(attrs, ["disabled"])
193 193 return HTML.input(**attrs)
194 194
195 195 reset = _reset
196 196 safeid = _make_safe_id_component
197 197
198 198
199 199 def branding(name, length=40):
200 200 return truncate(name, length, indicator="")
201 201
202 202
203 203 def FID(raw_id, path):
204 204 """
205 205 Creates a unique ID for filenode based on it's hash of path and commit
206 206 it's safe to use in urls
207 207
208 208 :param raw_id:
209 209 :param path:
210 210 """
211 211
212 212 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
213 213
214 214
215 215 class _GetError(object):
216 216 """Get error from form_errors, and represent it as span wrapped error
217 217 message
218 218
219 219 :param field_name: field to fetch errors for
220 220 :param form_errors: form errors dict
221 221 """
222 222
223 223 def __call__(self, field_name, form_errors):
224 224 tmpl = """<span class="error_msg">%s</span>"""
225 225 if form_errors and field_name in form_errors:
226 226 return literal(tmpl % form_errors.get(field_name))
227 227
228 228 get_error = _GetError()
229 229
230 230
231 231 class _ToolTip(object):
232 232
233 233 def __call__(self, tooltip_title, trim_at=50):
234 234 """
235 235 Special function just to wrap our text into nice formatted
236 236 autowrapped text
237 237
238 238 :param tooltip_title:
239 239 """
240 240 tooltip_title = escape(tooltip_title)
241 241 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
242 242 return tooltip_title
243 243 tooltip = _ToolTip()
244 244
245 245
246 246 def files_breadcrumbs(repo_name, commit_id, file_path):
247 247 if isinstance(file_path, str):
248 248 file_path = safe_unicode(file_path)
249 249
250 250 # TODO: johbo: Is this always a url like path, or is this operating
251 251 # system dependent?
252 252 path_segments = file_path.split('/')
253 253
254 254 repo_name_html = escape(repo_name)
255 255 if len(path_segments) == 1 and path_segments[0] == '':
256 256 url_segments = [repo_name_html]
257 257 else:
258 258 url_segments = [
259 259 link_to(
260 260 repo_name_html,
261 url('files_home',
261 route_path(
262 'repo_files',
262 263 repo_name=repo_name,
263 revision=commit_id,
264 commit_id=commit_id,
264 265 f_path=''),
265 266 class_='pjax-link')]
266 267
267 268 last_cnt = len(path_segments) - 1
268 269 for cnt, segment in enumerate(path_segments):
269 270 if not segment:
270 271 continue
271 272 segment_html = escape(segment)
272 273
273 274 if cnt != last_cnt:
274 275 url_segments.append(
275 276 link_to(
276 277 segment_html,
277 url('files_home',
278 route_path(
279 'repo_files',
278 280 repo_name=repo_name,
279 revision=commit_id,
281 commit_id=commit_id,
280 282 f_path='/'.join(path_segments[:cnt + 1])),
281 283 class_='pjax-link'))
282 284 else:
283 285 url_segments.append(segment_html)
284 286
285 287 return literal('/'.join(url_segments))
286 288
287 289
288 290 class CodeHtmlFormatter(HtmlFormatter):
289 291 """
290 292 My code Html Formatter for source codes
291 293 """
292 294
293 295 def wrap(self, source, outfile):
294 296 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
295 297
296 298 def _wrap_code(self, source):
297 299 for cnt, it in enumerate(source):
298 300 i, t = it
299 301 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
300 302 yield i, t
301 303
302 304 def _wrap_tablelinenos(self, inner):
303 305 dummyoutfile = StringIO.StringIO()
304 306 lncount = 0
305 307 for t, line in inner:
306 308 if t:
307 309 lncount += 1
308 310 dummyoutfile.write(line)
309 311
310 312 fl = self.linenostart
311 313 mw = len(str(lncount + fl - 1))
312 314 sp = self.linenospecial
313 315 st = self.linenostep
314 316 la = self.lineanchors
315 317 aln = self.anchorlinenos
316 318 nocls = self.noclasses
317 319 if sp:
318 320 lines = []
319 321
320 322 for i in range(fl, fl + lncount):
321 323 if i % st == 0:
322 324 if i % sp == 0:
323 325 if aln:
324 326 lines.append('<a href="#%s%d" class="special">%*d</a>' %
325 327 (la, i, mw, i))
326 328 else:
327 329 lines.append('<span class="special">%*d</span>' % (mw, i))
328 330 else:
329 331 if aln:
330 332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
331 333 else:
332 334 lines.append('%*d' % (mw, i))
333 335 else:
334 336 lines.append('')
335 337 ls = '\n'.join(lines)
336 338 else:
337 339 lines = []
338 340 for i in range(fl, fl + lncount):
339 341 if i % st == 0:
340 342 if aln:
341 343 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
342 344 else:
343 345 lines.append('%*d' % (mw, i))
344 346 else:
345 347 lines.append('')
346 348 ls = '\n'.join(lines)
347 349
348 350 # in case you wonder about the seemingly redundant <div> here: since the
349 351 # content in the other cell also is wrapped in a div, some browsers in
350 352 # some configurations seem to mess up the formatting...
351 353 if nocls:
352 354 yield 0, ('<table class="%stable">' % self.cssclass +
353 355 '<tr><td><div class="linenodiv" '
354 356 'style="background-color: #f0f0f0; padding-right: 10px">'
355 357 '<pre style="line-height: 125%">' +
356 358 ls + '</pre></div></td><td id="hlcode" class="code">')
357 359 else:
358 360 yield 0, ('<table class="%stable">' % self.cssclass +
359 361 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
360 362 ls + '</pre></div></td><td id="hlcode" class="code">')
361 363 yield 0, dummyoutfile.getvalue()
362 364 yield 0, '</td></tr></table>'
363 365
364 366
365 367 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
366 368 def __init__(self, **kw):
367 369 # only show these line numbers if set
368 370 self.only_lines = kw.pop('only_line_numbers', [])
369 371 self.query_terms = kw.pop('query_terms', [])
370 372 self.max_lines = kw.pop('max_lines', 5)
371 373 self.line_context = kw.pop('line_context', 3)
372 374 self.url = kw.pop('url', None)
373 375
374 376 super(CodeHtmlFormatter, self).__init__(**kw)
375 377
376 378 def _wrap_code(self, source):
377 379 for cnt, it in enumerate(source):
378 380 i, t = it
379 381 t = '<pre>%s</pre>' % t
380 382 yield i, t
381 383
382 384 def _wrap_tablelinenos(self, inner):
383 385 yield 0, '<table class="code-highlight %stable">' % self.cssclass
384 386
385 387 last_shown_line_number = 0
386 388 current_line_number = 1
387 389
388 390 for t, line in inner:
389 391 if not t:
390 392 yield t, line
391 393 continue
392 394
393 395 if current_line_number in self.only_lines:
394 396 if last_shown_line_number + 1 != current_line_number:
395 397 yield 0, '<tr>'
396 398 yield 0, '<td class="line">...</td>'
397 399 yield 0, '<td id="hlcode" class="code"></td>'
398 400 yield 0, '</tr>'
399 401
400 402 yield 0, '<tr>'
401 403 if self.url:
402 404 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
403 405 self.url, current_line_number, current_line_number)
404 406 else:
405 407 yield 0, '<td class="line"><a href="">%i</a></td>' % (
406 408 current_line_number)
407 409 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
408 410 yield 0, '</tr>'
409 411
410 412 last_shown_line_number = current_line_number
411 413
412 414 current_line_number += 1
413 415
414 416
415 417 yield 0, '</table>'
416 418
417 419
418 420 def extract_phrases(text_query):
419 421 """
420 422 Extracts phrases from search term string making sure phrases
421 423 contained in double quotes are kept together - and discarding empty values
422 424 or fully whitespace values eg.
423 425
424 426 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
425 427
426 428 """
427 429
428 430 in_phrase = False
429 431 buf = ''
430 432 phrases = []
431 433 for char in text_query:
432 434 if in_phrase:
433 435 if char == '"': # end phrase
434 436 phrases.append(buf)
435 437 buf = ''
436 438 in_phrase = False
437 439 continue
438 440 else:
439 441 buf += char
440 442 continue
441 443 else:
442 444 if char == '"': # start phrase
443 445 in_phrase = True
444 446 phrases.append(buf)
445 447 buf = ''
446 448 continue
447 449 elif char == ' ':
448 450 phrases.append(buf)
449 451 buf = ''
450 452 continue
451 453 else:
452 454 buf += char
453 455
454 456 phrases.append(buf)
455 457 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
456 458 return phrases
457 459
458 460
459 461 def get_matching_offsets(text, phrases):
460 462 """
461 463 Returns a list of string offsets in `text` that the list of `terms` match
462 464
463 465 >>> get_matching_offsets('some text here', ['some', 'here'])
464 466 [(0, 4), (10, 14)]
465 467
466 468 """
467 469 offsets = []
468 470 for phrase in phrases:
469 471 for match in re.finditer(phrase, text):
470 472 offsets.append((match.start(), match.end()))
471 473
472 474 return offsets
473 475
474 476
475 477 def normalize_text_for_matching(x):
476 478 """
477 479 Replaces all non alnum characters to spaces and lower cases the string,
478 480 useful for comparing two text strings without punctuation
479 481 """
480 482 return re.sub(r'[^\w]', ' ', x.lower())
481 483
482 484
483 485 def get_matching_line_offsets(lines, terms):
484 486 """ Return a set of `lines` indices (starting from 1) matching a
485 487 text search query, along with `context` lines above/below matching lines
486 488
487 489 :param lines: list of strings representing lines
488 490 :param terms: search term string to match in lines eg. 'some text'
489 491 :param context: number of lines above/below a matching line to add to result
490 492 :param max_lines: cut off for lines of interest
491 493 eg.
492 494
493 495 text = '''
494 496 words words words
495 497 words words words
496 498 some text some
497 499 words words words
498 500 words words words
499 501 text here what
500 502 '''
501 503 get_matching_line_offsets(text, 'text', context=1)
502 504 {3: [(5, 9)], 6: [(0, 4)]]
503 505
504 506 """
505 507 matching_lines = {}
506 508 phrases = [normalize_text_for_matching(phrase)
507 509 for phrase in extract_phrases(terms)]
508 510
509 511 for line_index, line in enumerate(lines, start=1):
510 512 match_offsets = get_matching_offsets(
511 513 normalize_text_for_matching(line), phrases)
512 514 if match_offsets:
513 515 matching_lines[line_index] = match_offsets
514 516
515 517 return matching_lines
516 518
517 519
518 520 def hsv_to_rgb(h, s, v):
519 521 """ Convert hsv color values to rgb """
520 522
521 523 if s == 0.0:
522 524 return v, v, v
523 525 i = int(h * 6.0) # XXX assume int() truncates!
524 526 f = (h * 6.0) - i
525 527 p = v * (1.0 - s)
526 528 q = v * (1.0 - s * f)
527 529 t = v * (1.0 - s * (1.0 - f))
528 530 i = i % 6
529 531 if i == 0:
530 532 return v, t, p
531 533 if i == 1:
532 534 return q, v, p
533 535 if i == 2:
534 536 return p, v, t
535 537 if i == 3:
536 538 return p, q, v
537 539 if i == 4:
538 540 return t, p, v
539 541 if i == 5:
540 542 return v, p, q
541 543
542 544
543 545 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
544 546 """
545 547 Generator for getting n of evenly distributed colors using
546 548 hsv color and golden ratio. It always return same order of colors
547 549
548 550 :param n: number of colors to generate
549 551 :param saturation: saturation of returned colors
550 552 :param lightness: lightness of returned colors
551 553 :returns: RGB tuple
552 554 """
553 555
554 556 golden_ratio = 0.618033988749895
555 557 h = 0.22717784590367374
556 558
557 559 for _ in xrange(n):
558 560 h += golden_ratio
559 561 h %= 1
560 562 HSV_tuple = [h, saturation, lightness]
561 563 RGB_tuple = hsv_to_rgb(*HSV_tuple)
562 564 yield map(lambda x: str(int(x * 256)), RGB_tuple)
563 565
564 566
565 567 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
566 568 """
567 569 Returns a function which when called with an argument returns a unique
568 570 color for that argument, eg.
569 571
570 572 :param n: number of colors to generate
571 573 :param saturation: saturation of returned colors
572 574 :param lightness: lightness of returned colors
573 575 :returns: css RGB string
574 576
575 577 >>> color_hash = color_hasher()
576 578 >>> color_hash('hello')
577 579 'rgb(34, 12, 59)'
578 580 >>> color_hash('hello')
579 581 'rgb(34, 12, 59)'
580 582 >>> color_hash('other')
581 583 'rgb(90, 224, 159)'
582 584 """
583 585
584 586 color_dict = {}
585 587 cgenerator = unique_color_generator(
586 588 saturation=saturation, lightness=lightness)
587 589
588 590 def get_color_string(thing):
589 591 if thing in color_dict:
590 592 col = color_dict[thing]
591 593 else:
592 594 col = color_dict[thing] = cgenerator.next()
593 595 return "rgb(%s)" % (', '.join(col))
594 596
595 597 return get_color_string
596 598
597 599
598 600 def get_lexer_safe(mimetype=None, filepath=None):
599 601 """
600 602 Tries to return a relevant pygments lexer using mimetype/filepath name,
601 603 defaulting to plain text if none could be found
602 604 """
603 605 lexer = None
604 606 try:
605 607 if mimetype:
606 608 lexer = get_lexer_for_mimetype(mimetype)
607 609 if not lexer:
608 610 lexer = get_lexer_for_filename(filepath)
609 611 except pygments.util.ClassNotFound:
610 612 pass
611 613
612 614 if not lexer:
613 615 lexer = get_lexer_by_name('text')
614 616
615 617 return lexer
616 618
617 619
618 620 def get_lexer_for_filenode(filenode):
619 621 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
620 622 return lexer
621 623
622 624
623 625 def pygmentize(filenode, **kwargs):
624 626 """
625 627 pygmentize function using pygments
626 628
627 629 :param filenode:
628 630 """
629 631 lexer = get_lexer_for_filenode(filenode)
630 632 return literal(code_highlight(filenode.content, lexer,
631 633 CodeHtmlFormatter(**kwargs)))
632 634
633 635
634 636 def is_following_repo(repo_name, user_id):
635 637 from rhodecode.model.scm import ScmModel
636 638 return ScmModel().is_following_repo(repo_name, user_id)
637 639
638 640
639 641 class _Message(object):
640 642 """A message returned by ``Flash.pop_messages()``.
641 643
642 644 Converting the message to a string returns the message text. Instances
643 645 also have the following attributes:
644 646
645 647 * ``message``: the message text.
646 648 * ``category``: the category specified when the message was created.
647 649 """
648 650
649 651 def __init__(self, category, message):
650 652 self.category = category
651 653 self.message = message
652 654
653 655 def __str__(self):
654 656 return self.message
655 657
656 658 __unicode__ = __str__
657 659
658 660 def __html__(self):
659 661 return escape(safe_unicode(self.message))
660 662
661 663
662 664 class Flash(_Flash):
663 665
664 666 def pop_messages(self, request=None):
665 667 """Return all accumulated messages and delete them from the session.
666 668
667 669 The return value is a list of ``Message`` objects.
668 670 """
669 671 messages = []
670 672
671 673 if request:
672 674 session = request.session
673 675 else:
674 676 from pylons import session
675 677
676 678 # Pop the 'old' pylons flash messages. They are tuples of the form
677 679 # (category, message)
678 680 for cat, msg in session.pop(self.session_key, []):
679 681 messages.append(_Message(cat, msg))
680 682
681 683 # Pop the 'new' pyramid flash messages for each category as list
682 684 # of strings.
683 685 for cat in self.categories:
684 686 for msg in session.pop_flash(queue=cat):
685 687 messages.append(_Message(cat, msg))
686 688 # Map messages from the default queue to the 'notice' category.
687 689 for msg in session.pop_flash():
688 690 messages.append(_Message('notice', msg))
689 691
690 692 session.save()
691 693 return messages
692 694
693 695 def json_alerts(self, request=None):
694 696 payloads = []
695 697 messages = flash.pop_messages(request=request)
696 698 if messages:
697 699 for message in messages:
698 700 subdata = {}
699 701 if hasattr(message.message, 'rsplit'):
700 702 flash_data = message.message.rsplit('|DELIM|', 1)
701 703 org_message = flash_data[0]
702 704 if len(flash_data) > 1:
703 705 subdata = json.loads(flash_data[1])
704 706 else:
705 707 org_message = message.message
706 708 payloads.append({
707 709 'message': {
708 710 'message': u'{}'.format(org_message),
709 711 'level': message.category,
710 712 'force': True,
711 713 'subdata': subdata
712 714 }
713 715 })
714 716 return json.dumps(payloads)
715 717
716 718 flash = Flash()
717 719
718 720 #==============================================================================
719 721 # SCM FILTERS available via h.
720 722 #==============================================================================
721 723 from rhodecode.lib.vcs.utils import author_name, author_email
722 724 from rhodecode.lib.utils2 import credentials_filter, age as _age
723 725 from rhodecode.model.db import User, ChangesetStatus
724 726
725 727 age = _age
726 728 capitalize = lambda x: x.capitalize()
727 729 email = author_email
728 730 short_id = lambda x: x[:12]
729 731 hide_credentials = lambda x: ''.join(credentials_filter(x))
730 732
731 733
732 734 def age_component(datetime_iso, value=None, time_is_local=False):
733 735 title = value or format_date(datetime_iso)
734 736 tzinfo = '+00:00'
735 737
736 738 # detect if we have a timezone info, otherwise, add it
737 739 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
738 740 if time_is_local:
739 741 tzinfo = time.strftime("+%H:%M",
740 742 time.gmtime(
741 743 (datetime.now() - datetime.utcnow()).seconds + 1
742 744 )
743 745 )
744 746
745 747 return literal(
746 748 '<time class="timeago tooltip" '
747 749 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
748 750 datetime_iso, title, tzinfo))
749 751
750 752
751 753 def _shorten_commit_id(commit_id):
752 754 from rhodecode import CONFIG
753 755 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
754 756 return commit_id[:def_len]
755 757
756 758
757 759 def show_id(commit):
758 760 """
759 761 Configurable function that shows ID
760 762 by default it's r123:fffeeefffeee
761 763
762 764 :param commit: commit instance
763 765 """
764 766 from rhodecode import CONFIG
765 767 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
766 768
767 769 raw_id = _shorten_commit_id(commit.raw_id)
768 770 if show_idx:
769 771 return 'r%s:%s' % (commit.idx, raw_id)
770 772 else:
771 773 return '%s' % (raw_id, )
772 774
773 775
774 776 def format_date(date):
775 777 """
776 778 use a standardized formatting for dates used in RhodeCode
777 779
778 780 :param date: date/datetime object
779 781 :return: formatted date
780 782 """
781 783
782 784 if date:
783 785 _fmt = "%a, %d %b %Y %H:%M:%S"
784 786 return safe_unicode(date.strftime(_fmt))
785 787
786 788 return u""
787 789
788 790
789 791 class _RepoChecker(object):
790 792
791 793 def __init__(self, backend_alias):
792 794 self._backend_alias = backend_alias
793 795
794 796 def __call__(self, repository):
795 797 if hasattr(repository, 'alias'):
796 798 _type = repository.alias
797 799 elif hasattr(repository, 'repo_type'):
798 800 _type = repository.repo_type
799 801 else:
800 802 _type = repository
801 803 return _type == self._backend_alias
802 804
803 805 is_git = _RepoChecker('git')
804 806 is_hg = _RepoChecker('hg')
805 807 is_svn = _RepoChecker('svn')
806 808
807 809
808 810 def get_repo_type_by_name(repo_name):
809 811 repo = Repository.get_by_repo_name(repo_name)
810 812 return repo.repo_type
811 813
812 814
813 815 def is_svn_without_proxy(repository):
814 816 if is_svn(repository):
815 817 from rhodecode.model.settings import VcsSettingsModel
816 818 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
817 819 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
818 820 return False
819 821
820 822
821 823 def discover_user(author):
822 824 """
823 825 Tries to discover RhodeCode User based on the autho string. Author string
824 826 is typically `FirstName LastName <email@address.com>`
825 827 """
826 828
827 829 # if author is already an instance use it for extraction
828 830 if isinstance(author, User):
829 831 return author
830 832
831 833 # Valid email in the attribute passed, see if they're in the system
832 834 _email = author_email(author)
833 835 if _email != '':
834 836 user = User.get_by_email(_email, case_insensitive=True, cache=True)
835 837 if user is not None:
836 838 return user
837 839
838 840 # Maybe it's a username, we try to extract it and fetch by username ?
839 841 _author = author_name(author)
840 842 user = User.get_by_username(_author, case_insensitive=True, cache=True)
841 843 if user is not None:
842 844 return user
843 845
844 846 return None
845 847
846 848
847 849 def email_or_none(author):
848 850 # extract email from the commit string
849 851 _email = author_email(author)
850 852
851 853 # If we have an email, use it, otherwise
852 854 # see if it contains a username we can get an email from
853 855 if _email != '':
854 856 return _email
855 857 else:
856 858 user = User.get_by_username(
857 859 author_name(author), case_insensitive=True, cache=True)
858 860
859 861 if user is not None:
860 862 return user.email
861 863
862 864 # No valid email, not a valid user in the system, none!
863 865 return None
864 866
865 867
866 868 def link_to_user(author, length=0, **kwargs):
867 869 user = discover_user(author)
868 870 # user can be None, but if we have it already it means we can re-use it
869 871 # in the person() function, so we save 1 intensive-query
870 872 if user:
871 873 author = user
872 874
873 875 display_person = person(author, 'username_or_name_or_email')
874 876 if length:
875 877 display_person = shorter(display_person, length)
876 878
877 879 if user:
878 880 return link_to(
879 881 escape(display_person),
880 882 route_path('user_profile', username=user.username),
881 883 **kwargs)
882 884 else:
883 885 return escape(display_person)
884 886
885 887
886 888 def person(author, show_attr="username_and_name"):
887 889 user = discover_user(author)
888 890 if user:
889 891 return getattr(user, show_attr)
890 892 else:
891 893 _author = author_name(author)
892 894 _email = email(author)
893 895 return _author or _email
894 896
895 897
896 898 def author_string(email):
897 899 if email:
898 900 user = User.get_by_email(email, case_insensitive=True, cache=True)
899 901 if user:
900 902 if user.first_name or user.last_name:
901 903 return '%s %s &lt;%s&gt;' % (
902 904 user.first_name, user.last_name, email)
903 905 else:
904 906 return email
905 907 else:
906 908 return email
907 909 else:
908 910 return None
909 911
910 912
911 913 def person_by_id(id_, show_attr="username_and_name"):
912 914 # attr to return from fetched user
913 915 person_getter = lambda usr: getattr(usr, show_attr)
914 916
915 917 #maybe it's an ID ?
916 918 if str(id_).isdigit() or isinstance(id_, int):
917 919 id_ = int(id_)
918 920 user = User.get(id_)
919 921 if user is not None:
920 922 return person_getter(user)
921 923 return id_
922 924
923 925
924 926 def gravatar_with_user(author, show_disabled=False):
925 927 from rhodecode.lib.utils import PartialRenderer
926 928 _render = PartialRenderer('base/base.mako')
927 929 return _render('gravatar_with_user', author, show_disabled=show_disabled)
928 930
929 931
930 932 def desc_stylize(value):
931 933 """
932 934 converts tags from value into html equivalent
933 935
934 936 :param value:
935 937 """
936 938 if not value:
937 939 return ''
938 940
939 941 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
940 942 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
941 943 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
942 944 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
943 945 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
944 946 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
945 947 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
946 948 '<div class="metatag" tag="lang">\\2</div>', value)
947 949 value = re.sub(r'\[([a-z]+)\]',
948 950 '<div class="metatag" tag="\\1">\\1</div>', value)
949 951
950 952 return value
951 953
952 954
953 955 def escaped_stylize(value):
954 956 """
955 957 converts tags from value into html equivalent, but escaping its value first
956 958 """
957 959 if not value:
958 960 return ''
959 961
960 962 # Using default webhelper escape method, but has to force it as a
961 963 # plain unicode instead of a markup tag to be used in regex expressions
962 964 value = unicode(escape(safe_unicode(value)))
963 965
964 966 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
965 967 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
966 968 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
967 969 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
968 970 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
969 971 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
970 972 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
971 973 '<div class="metatag" tag="lang">\\2</div>', value)
972 974 value = re.sub(r'\[([a-z]+)\]',
973 975 '<div class="metatag" tag="\\1">\\1</div>', value)
974 976
975 977 return value
976 978
977 979
978 980 def bool2icon(value):
979 981 """
980 982 Returns boolean value of a given value, represented as html element with
981 983 classes that will represent icons
982 984
983 985 :param value: given value to convert to html node
984 986 """
985 987
986 988 if value: # does bool conversion
987 989 return HTML.tag('i', class_="icon-true")
988 990 else: # not true as bool
989 991 return HTML.tag('i', class_="icon-false")
990 992
991 993
992 994 #==============================================================================
993 995 # PERMS
994 996 #==============================================================================
995 997 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
996 998 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
997 999 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
998 1000 csrf_token_key
999 1001
1000 1002
1001 1003 #==============================================================================
1002 1004 # GRAVATAR URL
1003 1005 #==============================================================================
1004 1006 class InitialsGravatar(object):
1005 1007 def __init__(self, email_address, first_name, last_name, size=30,
1006 1008 background=None, text_color='#fff'):
1007 1009 self.size = size
1008 1010 self.first_name = first_name
1009 1011 self.last_name = last_name
1010 1012 self.email_address = email_address
1011 1013 self.background = background or self.str2color(email_address)
1012 1014 self.text_color = text_color
1013 1015
1014 1016 def get_color_bank(self):
1015 1017 """
1016 1018 returns a predefined list of colors that gravatars can use.
1017 1019 Those are randomized distinct colors that guarantee readability and
1018 1020 uniqueness.
1019 1021
1020 1022 generated with: http://phrogz.net/css/distinct-colors.html
1021 1023 """
1022 1024 return [
1023 1025 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1024 1026 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1025 1027 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1026 1028 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1027 1029 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1028 1030 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1029 1031 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1030 1032 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1031 1033 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1032 1034 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1033 1035 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1034 1036 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1035 1037 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1036 1038 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1037 1039 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1038 1040 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1039 1041 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1040 1042 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1041 1043 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1042 1044 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1043 1045 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1044 1046 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1045 1047 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1046 1048 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1047 1049 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1048 1050 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1049 1051 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1050 1052 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1051 1053 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1052 1054 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1053 1055 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1054 1056 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1055 1057 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1056 1058 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1057 1059 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1058 1060 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1059 1061 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1060 1062 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1061 1063 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1062 1064 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1063 1065 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1064 1066 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1065 1067 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1066 1068 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1067 1069 '#4f8c46', '#368dd9', '#5c0073'
1068 1070 ]
1069 1071
1070 1072 def rgb_to_hex_color(self, rgb_tuple):
1071 1073 """
1072 1074 Converts an rgb_tuple passed to an hex color.
1073 1075
1074 1076 :param rgb_tuple: tuple with 3 ints represents rgb color space
1075 1077 """
1076 1078 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1077 1079
1078 1080 def email_to_int_list(self, email_str):
1079 1081 """
1080 1082 Get every byte of the hex digest value of email and turn it to integer.
1081 1083 It's going to be always between 0-255
1082 1084 """
1083 1085 digest = md5_safe(email_str.lower())
1084 1086 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1085 1087
1086 1088 def pick_color_bank_index(self, email_str, color_bank):
1087 1089 return self.email_to_int_list(email_str)[0] % len(color_bank)
1088 1090
1089 1091 def str2color(self, email_str):
1090 1092 """
1091 1093 Tries to map in a stable algorithm an email to color
1092 1094
1093 1095 :param email_str:
1094 1096 """
1095 1097 color_bank = self.get_color_bank()
1096 1098 # pick position (module it's length so we always find it in the
1097 1099 # bank even if it's smaller than 256 values
1098 1100 pos = self.pick_color_bank_index(email_str, color_bank)
1099 1101 return color_bank[pos]
1100 1102
1101 1103 def normalize_email(self, email_address):
1102 1104 import unicodedata
1103 1105 # default host used to fill in the fake/missing email
1104 1106 default_host = u'localhost'
1105 1107
1106 1108 if not email_address:
1107 1109 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1108 1110
1109 1111 email_address = safe_unicode(email_address)
1110 1112
1111 1113 if u'@' not in email_address:
1112 1114 email_address = u'%s@%s' % (email_address, default_host)
1113 1115
1114 1116 if email_address.endswith(u'@'):
1115 1117 email_address = u'%s%s' % (email_address, default_host)
1116 1118
1117 1119 email_address = unicodedata.normalize('NFKD', email_address)\
1118 1120 .encode('ascii', 'ignore')
1119 1121 return email_address
1120 1122
1121 1123 def get_initials(self):
1122 1124 """
1123 1125 Returns 2 letter initials calculated based on the input.
1124 1126 The algorithm picks first given email address, and takes first letter
1125 1127 of part before @, and then the first letter of server name. In case
1126 1128 the part before @ is in a format of `somestring.somestring2` it replaces
1127 1129 the server letter with first letter of somestring2
1128 1130
1129 1131 In case function was initialized with both first and lastname, this
1130 1132 overrides the extraction from email by first letter of the first and
1131 1133 last name. We add special logic to that functionality, In case Full name
1132 1134 is compound, like Guido Von Rossum, we use last part of the last name
1133 1135 (Von Rossum) picking `R`.
1134 1136
1135 1137 Function also normalizes the non-ascii characters to they ascii
1136 1138 representation, eg Ą => A
1137 1139 """
1138 1140 import unicodedata
1139 1141 # replace non-ascii to ascii
1140 1142 first_name = unicodedata.normalize(
1141 1143 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1142 1144 last_name = unicodedata.normalize(
1143 1145 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1144 1146
1145 1147 # do NFKD encoding, and also make sure email has proper format
1146 1148 email_address = self.normalize_email(self.email_address)
1147 1149
1148 1150 # first push the email initials
1149 1151 prefix, server = email_address.split('@', 1)
1150 1152
1151 1153 # check if prefix is maybe a 'first_name.last_name' syntax
1152 1154 _dot_split = prefix.rsplit('.', 1)
1153 1155 if len(_dot_split) == 2:
1154 1156 initials = [_dot_split[0][0], _dot_split[1][0]]
1155 1157 else:
1156 1158 initials = [prefix[0], server[0]]
1157 1159
1158 1160 # then try to replace either first_name or last_name
1159 1161 fn_letter = (first_name or " ")[0].strip()
1160 1162 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1161 1163
1162 1164 if fn_letter:
1163 1165 initials[0] = fn_letter
1164 1166
1165 1167 if ln_letter:
1166 1168 initials[1] = ln_letter
1167 1169
1168 1170 return ''.join(initials).upper()
1169 1171
1170 1172 def get_img_data_by_type(self, font_family, img_type):
1171 1173 default_user = """
1172 1174 <svg xmlns="http://www.w3.org/2000/svg"
1173 1175 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1174 1176 viewBox="-15 -10 439.165 429.164"
1175 1177
1176 1178 xml:space="preserve"
1177 1179 style="background:{background};" >
1178 1180
1179 1181 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1180 1182 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1181 1183 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1182 1184 168.596,153.916,216.671,
1183 1185 204.583,216.671z" fill="{text_color}"/>
1184 1186 <path d="M407.164,374.717L360.88,
1185 1187 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1186 1188 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1187 1189 15.366-44.203,23.488-69.076,23.488c-24.877,
1188 1190 0-48.762-8.122-69.078-23.488
1189 1191 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1190 1192 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1191 1193 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1192 1194 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1193 1195 19.402-10.527 C409.699,390.129,
1194 1196 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1195 1197 </svg>""".format(
1196 1198 size=self.size,
1197 1199 background='#979797', # @grey4
1198 1200 text_color=self.text_color,
1199 1201 font_family=font_family)
1200 1202
1201 1203 return {
1202 1204 "default_user": default_user
1203 1205 }[img_type]
1204 1206
1205 1207 def get_img_data(self, svg_type=None):
1206 1208 """
1207 1209 generates the svg metadata for image
1208 1210 """
1209 1211
1210 1212 font_family = ','.join([
1211 1213 'proximanovaregular',
1212 1214 'Proxima Nova Regular',
1213 1215 'Proxima Nova',
1214 1216 'Arial',
1215 1217 'Lucida Grande',
1216 1218 'sans-serif'
1217 1219 ])
1218 1220 if svg_type:
1219 1221 return self.get_img_data_by_type(font_family, svg_type)
1220 1222
1221 1223 initials = self.get_initials()
1222 1224 img_data = """
1223 1225 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1224 1226 width="{size}" height="{size}"
1225 1227 style="width: 100%; height: 100%; background-color: {background}"
1226 1228 viewBox="0 0 {size} {size}">
1227 1229 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1228 1230 pointer-events="auto" fill="{text_color}"
1229 1231 font-family="{font_family}"
1230 1232 style="font-weight: 400; font-size: {f_size}px;">{text}
1231 1233 </text>
1232 1234 </svg>""".format(
1233 1235 size=self.size,
1234 1236 f_size=self.size/1.85, # scale the text inside the box nicely
1235 1237 background=self.background,
1236 1238 text_color=self.text_color,
1237 1239 text=initials.upper(),
1238 1240 font_family=font_family)
1239 1241
1240 1242 return img_data
1241 1243
1242 1244 def generate_svg(self, svg_type=None):
1243 1245 img_data = self.get_img_data(svg_type)
1244 1246 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1245 1247
1246 1248
1247 1249 def initials_gravatar(email_address, first_name, last_name, size=30):
1248 1250 svg_type = None
1249 1251 if email_address == User.DEFAULT_USER_EMAIL:
1250 1252 svg_type = 'default_user'
1251 1253 klass = InitialsGravatar(email_address, first_name, last_name, size)
1252 1254 return klass.generate_svg(svg_type=svg_type)
1253 1255
1254 1256
1255 1257 def gravatar_url(email_address, size=30, request=None):
1256 1258 request = get_current_request()
1257 1259 if request and hasattr(request, 'call_context'):
1258 1260 _use_gravatar = request.call_context.visual.use_gravatar
1259 1261 _gravatar_url = request.call_context.visual.gravatar_url
1260 1262 else:
1261 1263 # doh, we need to re-import those to mock it later
1262 1264 from pylons import tmpl_context as c
1263 1265
1264 1266 _use_gravatar = c.visual.use_gravatar
1265 1267 _gravatar_url = c.visual.gravatar_url
1266 1268
1267 1269 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1268 1270
1269 1271 email_address = email_address or User.DEFAULT_USER_EMAIL
1270 1272 if isinstance(email_address, unicode):
1271 1273 # hashlib crashes on unicode items
1272 1274 email_address = safe_str(email_address)
1273 1275
1274 1276 # empty email or default user
1275 1277 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1276 1278 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1277 1279
1278 1280 if _use_gravatar:
1279 1281 # TODO: Disuse pyramid thread locals. Think about another solution to
1280 1282 # get the host and schema here.
1281 1283 request = get_current_request()
1282 1284 tmpl = safe_str(_gravatar_url)
1283 1285 tmpl = tmpl.replace('{email}', email_address)\
1284 1286 .replace('{md5email}', md5_safe(email_address.lower())) \
1285 1287 .replace('{netloc}', request.host)\
1286 1288 .replace('{scheme}', request.scheme)\
1287 1289 .replace('{size}', safe_str(size))
1288 1290 return tmpl
1289 1291 else:
1290 1292 return initials_gravatar(email_address, '', '', size=size)
1291 1293
1292 1294
1293 1295 class Page(_Page):
1294 1296 """
1295 1297 Custom pager to match rendering style with paginator
1296 1298 """
1297 1299
1298 1300 def _get_pos(self, cur_page, max_page, items):
1299 1301 edge = (items / 2) + 1
1300 1302 if (cur_page <= edge):
1301 1303 radius = max(items / 2, items - cur_page)
1302 1304 elif (max_page - cur_page) < edge:
1303 1305 radius = (items - 1) - (max_page - cur_page)
1304 1306 else:
1305 1307 radius = items / 2
1306 1308
1307 1309 left = max(1, (cur_page - (radius)))
1308 1310 right = min(max_page, cur_page + (radius))
1309 1311 return left, cur_page, right
1310 1312
1311 1313 def _range(self, regexp_match):
1312 1314 """
1313 1315 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1314 1316
1315 1317 Arguments:
1316 1318
1317 1319 regexp_match
1318 1320 A "re" (regular expressions) match object containing the
1319 1321 radius of linked pages around the current page in
1320 1322 regexp_match.group(1) as a string
1321 1323
1322 1324 This function is supposed to be called as a callable in
1323 1325 re.sub.
1324 1326
1325 1327 """
1326 1328 radius = int(regexp_match.group(1))
1327 1329
1328 1330 # Compute the first and last page number within the radius
1329 1331 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1330 1332 # -> leftmost_page = 5
1331 1333 # -> rightmost_page = 9
1332 1334 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1333 1335 self.last_page,
1334 1336 (radius * 2) + 1)
1335 1337 nav_items = []
1336 1338
1337 1339 # Create a link to the first page (unless we are on the first page
1338 1340 # or there would be no need to insert '..' spacers)
1339 1341 if self.page != self.first_page and self.first_page < leftmost_page:
1340 1342 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1341 1343
1342 1344 # Insert dots if there are pages between the first page
1343 1345 # and the currently displayed page range
1344 1346 if leftmost_page - self.first_page > 1:
1345 1347 # Wrap in a SPAN tag if nolink_attr is set
1346 1348 text = '..'
1347 1349 if self.dotdot_attr:
1348 1350 text = HTML.span(c=text, **self.dotdot_attr)
1349 1351 nav_items.append(text)
1350 1352
1351 1353 for thispage in xrange(leftmost_page, rightmost_page + 1):
1352 1354 # Hilight the current page number and do not use a link
1353 1355 if thispage == self.page:
1354 1356 text = '%s' % (thispage,)
1355 1357 # Wrap in a SPAN tag if nolink_attr is set
1356 1358 if self.curpage_attr:
1357 1359 text = HTML.span(c=text, **self.curpage_attr)
1358 1360 nav_items.append(text)
1359 1361 # Otherwise create just a link to that page
1360 1362 else:
1361 1363 text = '%s' % (thispage,)
1362 1364 nav_items.append(self._pagerlink(thispage, text))
1363 1365
1364 1366 # Insert dots if there are pages between the displayed
1365 1367 # page numbers and the end of the page range
1366 1368 if self.last_page - rightmost_page > 1:
1367 1369 text = '..'
1368 1370 # Wrap in a SPAN tag if nolink_attr is set
1369 1371 if self.dotdot_attr:
1370 1372 text = HTML.span(c=text, **self.dotdot_attr)
1371 1373 nav_items.append(text)
1372 1374
1373 1375 # Create a link to the very last page (unless we are on the last
1374 1376 # page or there would be no need to insert '..' spacers)
1375 1377 if self.page != self.last_page and rightmost_page < self.last_page:
1376 1378 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1377 1379
1378 1380 ## prerender links
1379 1381 #_page_link = url.current()
1380 1382 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1381 1383 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1382 1384 return self.separator.join(nav_items)
1383 1385
1384 1386 def pager(self, format='~2~', page_param='page', partial_param='partial',
1385 1387 show_if_single_page=False, separator=' ', onclick=None,
1386 1388 symbol_first='<<', symbol_last='>>',
1387 1389 symbol_previous='<', symbol_next='>',
1388 1390 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1389 1391 curpage_attr={'class': 'pager_curpage'},
1390 1392 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1391 1393
1392 1394 self.curpage_attr = curpage_attr
1393 1395 self.separator = separator
1394 1396 self.pager_kwargs = kwargs
1395 1397 self.page_param = page_param
1396 1398 self.partial_param = partial_param
1397 1399 self.onclick = onclick
1398 1400 self.link_attr = link_attr
1399 1401 self.dotdot_attr = dotdot_attr
1400 1402
1401 1403 # Don't show navigator if there is no more than one page
1402 1404 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1403 1405 return ''
1404 1406
1405 1407 from string import Template
1406 1408 # Replace ~...~ in token format by range of pages
1407 1409 result = re.sub(r'~(\d+)~', self._range, format)
1408 1410
1409 1411 # Interpolate '%' variables
1410 1412 result = Template(result).safe_substitute({
1411 1413 'first_page': self.first_page,
1412 1414 'last_page': self.last_page,
1413 1415 'page': self.page,
1414 1416 'page_count': self.page_count,
1415 1417 'items_per_page': self.items_per_page,
1416 1418 'first_item': self.first_item,
1417 1419 'last_item': self.last_item,
1418 1420 'item_count': self.item_count,
1419 1421 'link_first': self.page > self.first_page and \
1420 1422 self._pagerlink(self.first_page, symbol_first) or '',
1421 1423 'link_last': self.page < self.last_page and \
1422 1424 self._pagerlink(self.last_page, symbol_last) or '',
1423 1425 'link_previous': self.previous_page and \
1424 1426 self._pagerlink(self.previous_page, symbol_previous) \
1425 1427 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1426 1428 'link_next': self.next_page and \
1427 1429 self._pagerlink(self.next_page, symbol_next) \
1428 1430 or HTML.span(symbol_next, class_="pg-next disabled")
1429 1431 })
1430 1432
1431 1433 return literal(result)
1432 1434
1433 1435
1434 1436 #==============================================================================
1435 1437 # REPO PAGER, PAGER FOR REPOSITORY
1436 1438 #==============================================================================
1437 1439 class RepoPage(Page):
1438 1440
1439 1441 def __init__(self, collection, page=1, items_per_page=20,
1440 1442 item_count=None, url=None, **kwargs):
1441 1443
1442 1444 """Create a "RepoPage" instance. special pager for paging
1443 1445 repository
1444 1446 """
1445 1447 self._url_generator = url
1446 1448
1447 1449 # Safe the kwargs class-wide so they can be used in the pager() method
1448 1450 self.kwargs = kwargs
1449 1451
1450 1452 # Save a reference to the collection
1451 1453 self.original_collection = collection
1452 1454
1453 1455 self.collection = collection
1454 1456
1455 1457 # The self.page is the number of the current page.
1456 1458 # The first page has the number 1!
1457 1459 try:
1458 1460 self.page = int(page) # make it int() if we get it as a string
1459 1461 except (ValueError, TypeError):
1460 1462 self.page = 1
1461 1463
1462 1464 self.items_per_page = items_per_page
1463 1465
1464 1466 # Unless the user tells us how many items the collections has
1465 1467 # we calculate that ourselves.
1466 1468 if item_count is not None:
1467 1469 self.item_count = item_count
1468 1470 else:
1469 1471 self.item_count = len(self.collection)
1470 1472
1471 1473 # Compute the number of the first and last available page
1472 1474 if self.item_count > 0:
1473 1475 self.first_page = 1
1474 1476 self.page_count = int(math.ceil(float(self.item_count) /
1475 1477 self.items_per_page))
1476 1478 self.last_page = self.first_page + self.page_count - 1
1477 1479
1478 1480 # Make sure that the requested page number is the range of
1479 1481 # valid pages
1480 1482 if self.page > self.last_page:
1481 1483 self.page = self.last_page
1482 1484 elif self.page < self.first_page:
1483 1485 self.page = self.first_page
1484 1486
1485 1487 # Note: the number of items on this page can be less than
1486 1488 # items_per_page if the last page is not full
1487 1489 self.first_item = max(0, (self.item_count) - (self.page *
1488 1490 items_per_page))
1489 1491 self.last_item = ((self.item_count - 1) - items_per_page *
1490 1492 (self.page - 1))
1491 1493
1492 1494 self.items = list(self.collection[self.first_item:self.last_item + 1])
1493 1495
1494 1496 # Links to previous and next page
1495 1497 if self.page > self.first_page:
1496 1498 self.previous_page = self.page - 1
1497 1499 else:
1498 1500 self.previous_page = None
1499 1501
1500 1502 if self.page < self.last_page:
1501 1503 self.next_page = self.page + 1
1502 1504 else:
1503 1505 self.next_page = None
1504 1506
1505 1507 # No items available
1506 1508 else:
1507 1509 self.first_page = None
1508 1510 self.page_count = 0
1509 1511 self.last_page = None
1510 1512 self.first_item = None
1511 1513 self.last_item = None
1512 1514 self.previous_page = None
1513 1515 self.next_page = None
1514 1516 self.items = []
1515 1517
1516 1518 # This is a subclass of the 'list' type. Initialise the list now.
1517 1519 list.__init__(self, reversed(self.items))
1518 1520
1519 1521
1520 1522 def breadcrumb_repo_link(repo):
1521 1523 """
1522 1524 Makes a breadcrumbs path link to repo
1523 1525
1524 1526 ex::
1525 1527 group >> subgroup >> repo
1526 1528
1527 1529 :param repo: a Repository instance
1528 1530 """
1529 1531
1530 1532 path = [
1531 1533 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1532 1534 for group in repo.groups_with_parents
1533 1535 ] + [
1534 1536 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1535 1537 ]
1536 1538
1537 1539 return literal(' &raquo; '.join(path))
1538 1540
1539 1541
1540 1542 def format_byte_size_binary(file_size):
1541 1543 """
1542 1544 Formats file/folder sizes to standard.
1543 1545 """
1546 if file_size is None:
1547 file_size = 0
1548
1544 1549 formatted_size = format_byte_size(file_size, binary=True)
1545 1550 return formatted_size
1546 1551
1547 1552
1548 1553 def urlify_text(text_, safe=True):
1549 1554 """
1550 1555 Extrac urls from text and make html links out of them
1551 1556
1552 1557 :param text_:
1553 1558 """
1554 1559
1555 1560 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1556 1561 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1557 1562
1558 1563 def url_func(match_obj):
1559 1564 url_full = match_obj.groups()[0]
1560 1565 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1561 1566 _newtext = url_pat.sub(url_func, text_)
1562 1567 if safe:
1563 1568 return literal(_newtext)
1564 1569 return _newtext
1565 1570
1566 1571
1567 1572 def urlify_commits(text_, repository):
1568 1573 """
1569 1574 Extract commit ids from text and make link from them
1570 1575
1571 1576 :param text_:
1572 1577 :param repository: repo name to build the URL with
1573 1578 """
1574 1579 from pylons import url # doh, we need to re-import url to mock it later
1575 1580 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1576 1581
1577 1582 def url_func(match_obj):
1578 1583 commit_id = match_obj.groups()[1]
1579 1584 pref = match_obj.groups()[0]
1580 1585 suf = match_obj.groups()[2]
1581 1586
1582 1587 tmpl = (
1583 1588 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1584 1589 '%(commit_id)s</a>%(suf)s'
1585 1590 )
1586 1591 return tmpl % {
1587 1592 'pref': pref,
1588 1593 'cls': 'revision-link',
1589 1594 'url': url('changeset_home', repo_name=repository,
1590 1595 revision=commit_id, qualified=True),
1591 1596 'commit_id': commit_id,
1592 1597 'suf': suf
1593 1598 }
1594 1599
1595 1600 newtext = URL_PAT.sub(url_func, text_)
1596 1601
1597 1602 return newtext
1598 1603
1599 1604
1600 1605 def _process_url_func(match_obj, repo_name, uid, entry,
1601 1606 return_raw_data=False, link_format='html'):
1602 1607 pref = ''
1603 1608 if match_obj.group().startswith(' '):
1604 1609 pref = ' '
1605 1610
1606 1611 issue_id = ''.join(match_obj.groups())
1607 1612
1608 1613 if link_format == 'html':
1609 1614 tmpl = (
1610 1615 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1611 1616 '%(issue-prefix)s%(id-repr)s'
1612 1617 '</a>')
1613 1618 elif link_format == 'rst':
1614 1619 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1615 1620 elif link_format == 'markdown':
1616 1621 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1617 1622 else:
1618 1623 raise ValueError('Bad link_format:{}'.format(link_format))
1619 1624
1620 1625 (repo_name_cleaned,
1621 1626 parent_group_name) = RepoGroupModel().\
1622 1627 _get_group_name_and_parent(repo_name)
1623 1628
1624 1629 # variables replacement
1625 1630 named_vars = {
1626 1631 'id': issue_id,
1627 1632 'repo': repo_name,
1628 1633 'repo_name': repo_name_cleaned,
1629 1634 'group_name': parent_group_name
1630 1635 }
1631 1636 # named regex variables
1632 1637 named_vars.update(match_obj.groupdict())
1633 1638 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1634 1639
1635 1640 data = {
1636 1641 'pref': pref,
1637 1642 'cls': 'issue-tracker-link',
1638 1643 'url': _url,
1639 1644 'id-repr': issue_id,
1640 1645 'issue-prefix': entry['pref'],
1641 1646 'serv': entry['url'],
1642 1647 }
1643 1648 if return_raw_data:
1644 1649 return {
1645 1650 'id': issue_id,
1646 1651 'url': _url
1647 1652 }
1648 1653 return tmpl % data
1649 1654
1650 1655
1651 1656 def process_patterns(text_string, repo_name, link_format='html'):
1652 1657 allowed_formats = ['html', 'rst', 'markdown']
1653 1658 if link_format not in allowed_formats:
1654 1659 raise ValueError('Link format can be only one of:{} got {}'.format(
1655 1660 allowed_formats, link_format))
1656 1661
1657 1662 repo = None
1658 1663 if repo_name:
1659 1664 # Retrieving repo_name to avoid invalid repo_name to explode on
1660 1665 # IssueTrackerSettingsModel but still passing invalid name further down
1661 1666 repo = Repository.get_by_repo_name(repo_name, cache=True)
1662 1667
1663 1668 settings_model = IssueTrackerSettingsModel(repo=repo)
1664 1669 active_entries = settings_model.get_settings(cache=True)
1665 1670
1666 1671 issues_data = []
1667 1672 newtext = text_string
1668 1673
1669 1674 for uid, entry in active_entries.items():
1670 1675 log.debug('found issue tracker entry with uid %s' % (uid,))
1671 1676
1672 1677 if not (entry['pat'] and entry['url']):
1673 1678 log.debug('skipping due to missing data')
1674 1679 continue
1675 1680
1676 1681 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1677 1682 % (uid, entry['pat'], entry['url'], entry['pref']))
1678 1683
1679 1684 try:
1680 1685 pattern = re.compile(r'%s' % entry['pat'])
1681 1686 except re.error:
1682 1687 log.exception(
1683 1688 'issue tracker pattern: `%s` failed to compile',
1684 1689 entry['pat'])
1685 1690 continue
1686 1691
1687 1692 data_func = partial(
1688 1693 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1689 1694 return_raw_data=True)
1690 1695
1691 1696 for match_obj in pattern.finditer(text_string):
1692 1697 issues_data.append(data_func(match_obj))
1693 1698
1694 1699 url_func = partial(
1695 1700 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1696 1701 link_format=link_format)
1697 1702
1698 1703 newtext = pattern.sub(url_func, newtext)
1699 1704 log.debug('processed prefix:uid `%s`' % (uid,))
1700 1705
1701 1706 return newtext, issues_data
1702 1707
1703 1708
1704 1709 def urlify_commit_message(commit_text, repository=None):
1705 1710 """
1706 1711 Parses given text message and makes proper links.
1707 1712 issues are linked to given issue-server, and rest is a commit link
1708 1713
1709 1714 :param commit_text:
1710 1715 :param repository:
1711 1716 """
1712 1717 from pylons import url # doh, we need to re-import url to mock it later
1713 1718
1714 1719 def escaper(string):
1715 1720 return string.replace('<', '&lt;').replace('>', '&gt;')
1716 1721
1717 1722 newtext = escaper(commit_text)
1718 1723
1719 1724 # extract http/https links and make them real urls
1720 1725 newtext = urlify_text(newtext, safe=False)
1721 1726
1722 1727 # urlify commits - extract commit ids and make link out of them, if we have
1723 1728 # the scope of repository present.
1724 1729 if repository:
1725 1730 newtext = urlify_commits(newtext, repository)
1726 1731
1727 1732 # process issue tracker patterns
1728 1733 newtext, issues = process_patterns(newtext, repository or '')
1729 1734
1730 1735 return literal(newtext)
1731 1736
1732 1737
1733 1738 def render_binary(repo_name, file_obj):
1734 1739 """
1735 1740 Choose how to render a binary file
1736 1741 """
1737 1742 filename = file_obj.name
1738 1743
1739 1744 # images
1740 1745 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1741 1746 if fnmatch.fnmatch(filename, pat=ext):
1742 1747 alt = filename
1743 src = url('files_raw_home', repo_name=repo_name,
1744 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1748 src = route_path(
1749 'repo_file_raw', repo_name=repo_name,
1750 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1745 1751 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1746 1752
1747 1753
1748 1754 def renderer_from_filename(filename, exclude=None):
1749 1755 """
1750 1756 choose a renderer based on filename, this works only for text based files
1751 1757 """
1752 1758
1753 1759 # ipython
1754 1760 for ext in ['*.ipynb']:
1755 1761 if fnmatch.fnmatch(filename, pat=ext):
1756 1762 return 'jupyter'
1757 1763
1758 1764 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1759 1765 if is_markup:
1760 1766 return is_markup
1761 1767 return None
1762 1768
1763 1769
1764 1770 def render(source, renderer='rst', mentions=False, relative_url=None,
1765 1771 repo_name=None):
1766 1772
1767 1773 def maybe_convert_relative_links(html_source):
1768 1774 if relative_url:
1769 1775 return relative_links(html_source, relative_url)
1770 1776 return html_source
1771 1777
1772 1778 if renderer == 'rst':
1773 1779 if repo_name:
1774 1780 # process patterns on comments if we pass in repo name
1775 1781 source, issues = process_patterns(
1776 1782 source, repo_name, link_format='rst')
1777 1783
1778 1784 return literal(
1779 1785 '<div class="rst-block">%s</div>' %
1780 1786 maybe_convert_relative_links(
1781 1787 MarkupRenderer.rst(source, mentions=mentions)))
1782 1788 elif renderer == 'markdown':
1783 1789 if repo_name:
1784 1790 # process patterns on comments if we pass in repo name
1785 1791 source, issues = process_patterns(
1786 1792 source, repo_name, link_format='markdown')
1787 1793
1788 1794 return literal(
1789 1795 '<div class="markdown-block">%s</div>' %
1790 1796 maybe_convert_relative_links(
1791 1797 MarkupRenderer.markdown(source, flavored=True,
1792 1798 mentions=mentions)))
1793 1799 elif renderer == 'jupyter':
1794 1800 return literal(
1795 1801 '<div class="ipynb">%s</div>' %
1796 1802 maybe_convert_relative_links(
1797 1803 MarkupRenderer.jupyter(source)))
1798 1804
1799 1805 # None means just show the file-source
1800 1806 return None
1801 1807
1802 1808
1803 1809 def commit_status(repo, commit_id):
1804 1810 return ChangesetStatusModel().get_status(repo, commit_id)
1805 1811
1806 1812
1807 1813 def commit_status_lbl(commit_status):
1808 1814 return dict(ChangesetStatus.STATUSES).get(commit_status)
1809 1815
1810 1816
1811 1817 def commit_time(repo_name, commit_id):
1812 1818 repo = Repository.get_by_repo_name(repo_name)
1813 1819 commit = repo.get_commit(commit_id=commit_id)
1814 1820 return commit.date
1815 1821
1816 1822
1817 1823 def get_permission_name(key):
1818 1824 return dict(Permission.PERMS).get(key)
1819 1825
1820 1826
1821 1827 def journal_filter_help(request):
1822 1828 _ = request.translate
1823 1829
1824 1830 return _(
1825 1831 'Example filter terms:\n' +
1826 1832 ' repository:vcs\n' +
1827 1833 ' username:marcin\n' +
1828 1834 ' username:(NOT marcin)\n' +
1829 1835 ' action:*push*\n' +
1830 1836 ' ip:127.0.0.1\n' +
1831 1837 ' date:20120101\n' +
1832 1838 ' date:[20120101100000 TO 20120102]\n' +
1833 1839 '\n' +
1834 1840 'Generate wildcards using \'*\' character:\n' +
1835 1841 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1836 1842 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1837 1843 '\n' +
1838 1844 'Optional AND / OR operators in queries\n' +
1839 1845 ' "repository:vcs OR repository:test"\n' +
1840 1846 ' "username:test AND repository:test*"\n'
1841 1847 )
1842 1848
1843 1849
1844 1850 def search_filter_help(searcher, request):
1845 1851 _ = request.translate
1846 1852
1847 1853 terms = ''
1848 1854 return _(
1849 1855 'Example filter terms for `{searcher}` search:\n' +
1850 1856 '{terms}\n' +
1851 1857 'Generate wildcards using \'*\' character:\n' +
1852 1858 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1853 1859 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1854 1860 '\n' +
1855 1861 'Optional AND / OR operators in queries\n' +
1856 1862 ' "repo_name:vcs OR repo_name:test"\n' +
1857 1863 ' "owner:test AND repo_name:test*"\n' +
1858 1864 'More: {search_doc}'
1859 1865 ).format(searcher=searcher.name,
1860 1866 terms=terms, search_doc=searcher.query_lang_doc)
1861 1867
1862 1868
1863 1869 def not_mapped_error(repo_name):
1864 1870 from rhodecode.translation import _
1865 1871 flash(_('%s repository is not mapped to db perhaps'
1866 1872 ' it was created or renamed from the filesystem'
1867 1873 ' please run the application again'
1868 1874 ' in order to rescan repositories') % repo_name, category='error')
1869 1875
1870 1876
1871 1877 def ip_range(ip_addr):
1872 1878 from rhodecode.model.db import UserIpMap
1873 1879 s, e = UserIpMap._get_ip_range(ip_addr)
1874 1880 return '%s - %s' % (s, e)
1875 1881
1876 1882
1877 1883 def form(url, method='post', needs_csrf_token=True, **attrs):
1878 1884 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1879 1885 if method.lower() != 'get' and needs_csrf_token:
1880 1886 raise Exception(
1881 1887 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1882 1888 'CSRF token. If the endpoint does not require such token you can ' +
1883 1889 'explicitly set the parameter needs_csrf_token to false.')
1884 1890
1885 1891 return wh_form(url, method=method, **attrs)
1886 1892
1887 1893
1888 1894 def secure_form(url, method="POST", multipart=False, **attrs):
1889 1895 """Start a form tag that points the action to an url. This
1890 1896 form tag will also include the hidden field containing
1891 1897 the auth token.
1892 1898
1893 1899 The url options should be given either as a string, or as a
1894 1900 ``url()`` function. The method for the form defaults to POST.
1895 1901
1896 1902 Options:
1897 1903
1898 1904 ``multipart``
1899 1905 If set to True, the enctype is set to "multipart/form-data".
1900 1906 ``method``
1901 1907 The method to use when submitting the form, usually either
1902 1908 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1903 1909 hidden input with name _method is added to simulate the verb
1904 1910 over POST.
1905 1911
1906 1912 """
1907 1913 from webhelpers.pylonslib.secure_form import insecure_form
1908 1914 form = insecure_form(url, method, multipart, **attrs)
1909 1915
1910 1916 session = None
1911 1917 # TODO(marcink): after pyramid migration require request variable ALWAYS
1912 1918 if 'request' in attrs:
1913 1919 session = attrs['request'].session
1914 1920
1915 1921 token = literal(
1916 1922 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1917 1923 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1918 1924
1919 1925 return literal("%s\n%s" % (form, token))
1920 1926
1921 1927
1922 1928 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1923 1929 select_html = select(name, selected, options, **attrs)
1924 1930 select2 = """
1925 1931 <script>
1926 1932 $(document).ready(function() {
1927 1933 $('#%s').select2({
1928 1934 containerCssClass: 'drop-menu',
1929 1935 dropdownCssClass: 'drop-menu-dropdown',
1930 1936 dropdownAutoWidth: true%s
1931 1937 });
1932 1938 });
1933 1939 </script>
1934 1940 """
1935 1941 filter_option = """,
1936 1942 minimumResultsForSearch: -1
1937 1943 """
1938 1944 input_id = attrs.get('id') or name
1939 1945 filter_enabled = "" if enable_filter else filter_option
1940 1946 select_script = literal(select2 % (input_id, filter_enabled))
1941 1947
1942 1948 return literal(select_html+select_script)
1943 1949
1944 1950
1945 1951 def get_visual_attr(tmpl_context_var, attr_name):
1946 1952 """
1947 1953 A safe way to get a variable from visual variable of template context
1948 1954
1949 1955 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1950 1956 :param attr_name: name of the attribute we fetch from the c.visual
1951 1957 """
1952 1958 visual = getattr(tmpl_context_var, 'visual', None)
1953 1959 if not visual:
1954 1960 return
1955 1961 else:
1956 1962 return getattr(visual, attr_name, None)
1957 1963
1958 1964
1959 1965 def get_last_path_part(file_node):
1960 1966 if not file_node.path:
1961 1967 return u''
1962 1968
1963 1969 path = safe_unicode(file_node.path.split('/')[-1])
1964 1970 return u'../' + path
1965 1971
1966 1972
1967 1973 def route_url(*args, **kwargs):
1968 1974 """
1969 1975 Wrapper around pyramids `route_url` (fully qualified url) function.
1970 1976 It is used to generate URLs from within pylons views or templates.
1971 1977 This will be removed when pyramid migration if finished.
1972 1978 """
1973 1979 req = get_current_request()
1974 1980 return req.route_url(*args, **kwargs)
1975 1981
1976 1982
1977 1983 def route_path(*args, **kwargs):
1978 1984 """
1979 1985 Wrapper around pyramids `route_path` function. It is used to generate
1980 1986 URLs from within pylons views or templates. This will be removed when
1981 1987 pyramid migration if finished.
1982 1988 """
1983 1989 req = get_current_request()
1984 1990 return req.route_path(*args, **kwargs)
1985 1991
1986 1992
1987 1993 def route_path_or_none(*args, **kwargs):
1988 1994 try:
1989 1995 return route_path(*args, **kwargs)
1990 1996 except KeyError:
1991 1997 return None
1992 1998
1993 1999
1994 2000 def static_url(*args, **kwds):
1995 2001 """
1996 2002 Wrapper around pyramids `route_path` function. It is used to generate
1997 2003 URLs from within pylons views or templates. This will be removed when
1998 2004 pyramid migration if finished.
1999 2005 """
2000 2006 req = get_current_request()
2001 2007 return req.static_url(*args, **kwds)
2002 2008
2003 2009
2004 2010 def resource_path(*args, **kwds):
2005 2011 """
2006 2012 Wrapper around pyramids `route_path` function. It is used to generate
2007 2013 URLs from within pylons views or templates. This will be removed when
2008 2014 pyramid migration if finished.
2009 2015 """
2010 2016 req = get_current_request()
2011 2017 return req.resource_path(*args, **kwds)
2012 2018
2013 2019
2014 2020 def api_call_example(method, args):
2015 2021 """
2016 2022 Generates an API call example via CURL
2017 2023 """
2018 2024 args_json = json.dumps(OrderedDict([
2019 2025 ('id', 1),
2020 2026 ('auth_token', 'SECRET'),
2021 2027 ('method', method),
2022 2028 ('args', args)
2023 2029 ]))
2024 2030 return literal(
2025 2031 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2026 2032 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2027 2033 "and needs to be of `api calls` role."
2028 2034 .format(
2029 2035 api_url=route_url('apiv2'),
2030 2036 token_url=route_url('my_account_auth_tokens'),
2031 2037 data=args_json))
2032 2038
2033 2039
2034 2040 def notification_description(notification, request):
2035 2041 """
2036 2042 Generate notification human readable description based on notification type
2037 2043 """
2038 2044 from rhodecode.model.notification import NotificationModel
2039 2045 return NotificationModel().make_description(
2040 2046 notification, translate=request.translate)
@@ -1,101 +1,101 b''
1 1 // Global keyboard bindings
2 2
3 3 function setRCMouseBindings(repoName, repoLandingRev) {
4 4
5 5 /** custom callback for supressing mousetrap from firing */
6 6 Mousetrap.stopCallback = function(e, element) {
7 7 // if the element has the class "mousetrap" then no need to stop
8 8 if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
9 9 return false;
10 10 }
11 11
12 12 // stop for input, select, and textarea
13 13 return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
14 14 };
15 15
16 16 // general help "?"
17 17 Mousetrap.bind(['?'], function(e) {
18 18 $('#help_kb').modal({});
19 19 });
20 20
21 21 // / open the quick filter
22 22 Mousetrap.bind(['/'], function(e) {
23 23 $('#repo_switcher').select2('open');
24 24
25 25 // return false to prevent default browser behavior
26 26 // and stop event from bubbling
27 27 return false;
28 28 });
29 29
30 30 // ctrl/command+b, show the the main bar
31 31 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
32 32 var $headerInner = $('#header-inner'),
33 33 $content = $('#content');
34 34 if ($headerInner.hasClass('hover') && $content.hasClass('hover')) {
35 35 $headerInner.removeClass('hover');
36 36 $content.removeClass('hover');
37 37 } else {
38 38 $headerInner.addClass('hover');
39 39 $content.addClass('hover');
40 40 }
41 41 return false;
42 42 });
43 43
44 44 // general nav g + action
45 45 Mousetrap.bind(['g h'], function(e) {
46 46 window.location = pyroutes.url('home');
47 47 });
48 48 Mousetrap.bind(['g g'], function(e) {
49 49 window.location = pyroutes.url('gists_show', {'private': 1});
50 50 });
51 51 Mousetrap.bind(['g G'], function(e) {
52 52 window.location = pyroutes.url('gists_show', {'public': 1});
53 53 });
54 54 Mousetrap.bind(['n g'], function(e) {
55 55 window.location = pyroutes.url('gists_new');
56 56 });
57 57 Mousetrap.bind(['n r'], function(e) {
58 58 window.location = pyroutes.url('new_repo');
59 59 });
60 60
61 61 if (repoName && repoName != '') {
62 62 // nav in repo context
63 63 Mousetrap.bind(['g s'], function(e) {
64 64 window.location = pyroutes.url(
65 65 'repo_summary', {'repo_name': repoName});
66 66 });
67 67 Mousetrap.bind(['g c'], function(e) {
68 68 window.location = pyroutes.url(
69 69 'changelog_home', {'repo_name': repoName});
70 70 });
71 71 Mousetrap.bind(['g F'], function(e) {
72 72 window.location = pyroutes.url(
73 'files_home',
73 'repo_files',
74 74 {
75 75 'repo_name': repoName,
76 'revision': repoLandingRev,
76 'commit_id': repoLandingRev,
77 77 'f_path': '',
78 78 'search': '1'
79 79 });
80 80 });
81 81 Mousetrap.bind(['g f'], function(e) {
82 82 window.location = pyroutes.url(
83 'files_home',
83 'repo_files',
84 84 {
85 85 'repo_name': repoName,
86 'revision': repoLandingRev,
86 'commit_id': repoLandingRev,
87 87 'f_path': ''
88 88 });
89 89 });
90 90 Mousetrap.bind(['g o'], function(e) {
91 91 window.location = pyroutes.url(
92 92 'edit_repo', {'repo_name': repoName});
93 93 });
94 94 Mousetrap.bind(['g O'], function(e) {
95 95 window.location = pyroutes.url(
96 96 'edit_repo_perms', {'repo_name': repoName});
97 97 });
98 98 }
99 99 }
100 100
101 101 setRCMouseBindings(templateContext.repo_name, templateContext.repo_landing_commit);
@@ -1,183 +1,198 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 18 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
19 19 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
20 20 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
21 21 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
22 22 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
23 23 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
24 24 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
25 25 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
26 26 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
27 27 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
28 28 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
29 29 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
30 30 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
31 31 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
32 32 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
33 33 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
34 34 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
35 35 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
36 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
37 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
38 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
39 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
40 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
41 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
42 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
43 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
44 36 pyroutes.register('favicon', '/favicon.ico', []);
45 37 pyroutes.register('robots', '/robots.txt', []);
46 38 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
47 39 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
48 40 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
49 41 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
50 42 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
51 43 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
52 44 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
53 45 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
54 46 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
55 47 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
56 48 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
57 49 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
58 50 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
59 51 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
60 52 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
61 53 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
62 54 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
63 55 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
64 56 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
65 57 pyroutes.register('admin_home', '/_admin', []);
66 58 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
67 59 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
68 60 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
69 61 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
70 62 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
71 63 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
72 64 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
73 65 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
74 66 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
75 67 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
76 68 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
77 69 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
78 70 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
79 71 pyroutes.register('users', '/_admin/users', []);
80 72 pyroutes.register('users_data', '/_admin/users_data', []);
81 73 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
82 74 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
83 75 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
84 76 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
85 77 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
86 78 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
87 79 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
88 80 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
89 81 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
90 82 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
91 83 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
92 84 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
93 85 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
94 86 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
95 87 pyroutes.register('channelstream_proxy', '/_channelstream', []);
96 88 pyroutes.register('login', '/_admin/login', []);
97 89 pyroutes.register('logout', '/_admin/logout', []);
98 90 pyroutes.register('register', '/_admin/register', []);
99 91 pyroutes.register('reset_password', '/_admin/password_reset', []);
100 92 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
101 93 pyroutes.register('home', '/', []);
102 94 pyroutes.register('user_autocomplete_data', '/_users', []);
103 95 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
104 96 pyroutes.register('repo_list_data', '/_repos', []);
105 97 pyroutes.register('goto_switcher_data', '/_goto_data', []);
106 98 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
107 99 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
108 100 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
101 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
102 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
103 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
104 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
105 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
106 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
107 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
108 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
109 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
110 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
111 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
112 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
113 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
114 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
115 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
116 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
117 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
118 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
119 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
120 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
121 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
109 124 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
110 125 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
111 126 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
112 127 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
113 128 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
114 129 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
115 130 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
116 131 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
117 132 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
118 133 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
119 134 pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
120 135 pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
121 136 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
122 137 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
123 138 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
124 139 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
125 140 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
126 141 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
127 142 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
128 143 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
129 144 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
130 145 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
131 146 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
132 147 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
133 148 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
134 149 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
135 150 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
136 151 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
137 152 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
138 153 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
139 154 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
140 155 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
141 156 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
142 157 pyroutes.register('search', '/_admin/search', []);
143 158 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
144 159 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
145 160 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
146 161 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
147 162 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
148 163 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
149 164 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
150 165 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
151 166 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
152 167 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
153 168 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
154 169 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
155 170 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
156 171 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
157 172 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
158 173 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
159 174 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
160 175 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
161 176 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
162 177 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
163 178 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
164 179 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
165 180 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
166 181 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
167 182 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
168 183 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
169 184 pyroutes.register('gists_show', '/_admin/gists', []);
170 185 pyroutes.register('gists_new', '/_admin/gists/new', []);
171 186 pyroutes.register('gists_create', '/_admin/gists/create', []);
172 187 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
173 188 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
174 189 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
175 190 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
176 191 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
177 192 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
178 193 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
179 194 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
180 195 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
181 196 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
182 197 pyroutes.register('apiv2', '/_admin/api', []);
183 198 }
@@ -1,84 +1,84 b''
1 1 /* COMMON */
2 2 var select2RefFilterResults = function(queryTerm, data) {
3 3 var filteredData = {results: []};
4 4 //filter results
5 5 $.each(data.results, function() {
6 6 var section = this.text;
7 7 var children = [];
8 8 $.each(this.children, function() {
9 9 if (queryTerm.length === 0 || this.text.toUpperCase().indexOf(queryTerm.toUpperCase()) >= 0) {
10 10 children.push(this);
11 11 }
12 12 });
13 13
14 14 if (children.length > 0) {
15 15 filteredData.results.push({
16 16 'text': section,
17 17 'children': children
18 18 });
19 19 }
20 20 });
21 21
22 22 return filteredData
23 23 };
24 24
25 25
26 26 var select2RefBaseSwitcher = function(targetElement, loadUrl, initialData){
27 27 var formatResult = function(result, container, query) {
28 28 return formatSelect2SelectionRefs(result);
29 29 };
30 30
31 31 var formatSelection = function(data, container) {
32 32 return formatSelect2SelectionRefs(data);
33 33 };
34 34
35 35 $(targetElement).select2({
36 36 cachedDataSource: {},
37 37 dropdownAutoWidth: true,
38 38 formatResult: formatResult,
39 39 width: "resolve",
40 40 containerCssClass: "drop-menu",
41 41 dropdownCssClass: "drop-menu-dropdown",
42 42 query: function(query) {
43 43 var self = this;
44 44 var cacheKey = '__ALLREFS__';
45 45 var cachedData = self.cachedDataSource[cacheKey];
46 46 if (cachedData) {
47 47 var data = select2RefFilterResults(query.term, cachedData);
48 48 query.callback({results: data.results});
49 49 } else {
50 50 $.ajax({
51 51 url: loadUrl,
52 52 data: {},
53 53 dataType: 'json',
54 54 type: 'GET',
55 55 success: function(data) {
56 56 self.cachedDataSource[cacheKey] = data;
57 57 query.callback({results: data.results});
58 58 }
59 59 });
60 60 }
61 61 },
62 62
63 63 initSelection: function(element, callback) {
64 64 callback(initialData);
65 65 },
66 66
67 67 formatSelection: formatSelection
68 68 });
69 69
70 70 };
71 71
72 72 /* WIDGETS */
73 73 var select2RefSwitcher = function(targetElement, initialData) {
74 74 var loadUrl = pyroutes.url('repo_refs_data',
75 75 {'repo_name': templateContext.repo_name});
76 76 select2RefBaseSwitcher(targetElement, loadUrl, initialData);
77 77 };
78 78
79 79 var select2FileHistorySwitcher = function(targetElement, initialData, state) {
80 var loadUrl = pyroutes.url('files_history_home',
81 {'repo_name': templateContext.repo_name, 'revision': state.rev,
80 var loadUrl = pyroutes.url('repo_file_history',
81 {'repo_name': templateContext.repo_name, 'commit_id': state.rev,
82 82 'f_path': state.f_path});
83 83 select2RefBaseSwitcher(targetElement, loadUrl, initialData);
84 84 };
@@ -1,600 +1,600 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${h.tooltip(title)}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 <li class="${is_active('files')}"><a class="menulink" href="${h.url('files_home', repo_name=c.repo_name, revision=c.rhodecode_db_repo.landing_rev[1])}"><div class="menulabel">${_('Files')}</div></a></li>
230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}">
232 232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 233 </li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <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)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
255 255 ${_('Compare fork')}</a></li>
256 256 %endif
257 257
258 258 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
259 259
260 260 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
261 261 %if c.rhodecode_db_repo.locked[0]:
262 262 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
263 263 %else:
264 264 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
265 265 %endif
266 266 %endif
267 267 %if c.rhodecode_user.username != h.DEFAULT_USER:
268 268 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
269 269 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
270 270 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
271 271 %endif
272 272 %endif
273 273 </ul>
274 274 </li>
275 275 </ul>
276 276 </div>
277 277 <div class="clear"></div>
278 278 </div>
279 279 <!--- END CONTEXT BAR -->
280 280
281 281 </%def>
282 282
283 283 <%def name="usermenu(active=False)">
284 284 ## USER MENU
285 285 <li id="quick_login_li" class="${'active' if active else ''}">
286 286 <a id="quick_login_link" class="menulink childs">
287 287 ${gravatar(c.rhodecode_user.email, 20)}
288 288 <span class="user">
289 289 %if c.rhodecode_user.username != h.DEFAULT_USER:
290 290 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
291 291 %else:
292 292 <span>${_('Sign in')}</span>
293 293 %endif
294 294 </span>
295 295 </a>
296 296
297 297 <div class="user-menu submenu">
298 298 <div id="quick_login">
299 299 %if c.rhodecode_user.username == h.DEFAULT_USER:
300 300 <h4>${_('Sign in to your account')}</h4>
301 301 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
302 302 <div class="form form-vertical">
303 303 <div class="fields">
304 304 <div class="field">
305 305 <div class="label">
306 306 <label for="username">${_('Username')}:</label>
307 307 </div>
308 308 <div class="input">
309 309 ${h.text('username',class_='focus',tabindex=1)}
310 310 </div>
311 311
312 312 </div>
313 313 <div class="field">
314 314 <div class="label">
315 315 <label for="password">${_('Password')}:</label>
316 316 %if h.HasPermissionAny('hg.password_reset.enabled')():
317 317 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
318 318 %endif
319 319 </div>
320 320 <div class="input">
321 321 ${h.password('password',class_='focus',tabindex=2)}
322 322 </div>
323 323 </div>
324 324 <div class="buttons">
325 325 <div class="register">
326 326 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
327 327 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
328 328 %endif
329 329 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
330 330 </div>
331 331 <div class="submit">
332 332 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
333 333 </div>
334 334 </div>
335 335 </div>
336 336 </div>
337 337 ${h.end_form()}
338 338 %else:
339 339 <div class="">
340 340 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
341 341 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
342 342 <div class="email">${c.rhodecode_user.email}</div>
343 343 </div>
344 344 <div class="">
345 345 <ol class="links">
346 346 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
347 347 % if c.rhodecode_user.personal_repo_group:
348 348 <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>
349 349 % endif
350 350 <li class="logout">
351 351 ${h.secure_form(h.route_path('logout'), request=request)}
352 352 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
353 353 ${h.end_form()}
354 354 </li>
355 355 </ol>
356 356 </div>
357 357 %endif
358 358 </div>
359 359 </div>
360 360 %if c.rhodecode_user.username != h.DEFAULT_USER:
361 361 <div class="pill_container">
362 362 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
363 363 </div>
364 364 % endif
365 365 </li>
366 366 </%def>
367 367
368 368 <%def name="menu_items(active=None)">
369 369 <%
370 370 def is_active(selected):
371 371 if selected == active:
372 372 return "active"
373 373 return ""
374 374 %>
375 375 <ul id="quick" class="main_nav navigation horizontal-list">
376 376 <!-- repo switcher -->
377 377 <li class="${is_active('repositories')} repo_switcher_li has_select2">
378 378 <input id="repo_switcher" name="repo_switcher" type="hidden">
379 379 </li>
380 380
381 381 ## ROOT MENU
382 382 %if c.rhodecode_user.username != h.DEFAULT_USER:
383 383 <li class="${is_active('journal')}">
384 384 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
385 385 <div class="menulabel">${_('Journal')}</div>
386 386 </a>
387 387 </li>
388 388 %else:
389 389 <li class="${is_active('journal')}">
390 390 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
391 391 <div class="menulabel">${_('Public journal')}</div>
392 392 </a>
393 393 </li>
394 394 %endif
395 395 <li class="${is_active('gists')}">
396 396 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
397 397 <div class="menulabel">${_('Gists')}</div>
398 398 </a>
399 399 </li>
400 400 <li class="${is_active('search')}">
401 401 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
402 402 <div class="menulabel">${_('Search')}</div>
403 403 </a>
404 404 </li>
405 405 % if h.HasPermissionAll('hg.admin')('access admin main page'):
406 406 <li class="${is_active('admin')}">
407 407 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
408 408 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
409 409 </a>
410 410 ${admin_menu()}
411 411 </li>
412 412 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
413 413 <li class="${is_active('admin')}">
414 414 <a class="menulink childs" title="${_('Delegated Admin settings')}">
415 415 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
416 416 </a>
417 417 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
418 418 c.rhodecode_user.repository_groups_admin,
419 419 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
420 420 </li>
421 421 % endif
422 422 % if c.debug_style:
423 423 <li class="${is_active('debug_style')}">
424 424 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
425 425 <div class="menulabel">${_('Style')}</div>
426 426 </a>
427 427 </li>
428 428 % endif
429 429 ## render extra user menu
430 430 ${usermenu(active=(active=='my_account'))}
431 431 </ul>
432 432
433 433 <script type="text/javascript">
434 434 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
435 435
436 436 /*format the look of items in the list*/
437 437 var format = function(state, escapeMarkup){
438 438 if (!state.id){
439 439 return state.text; // optgroup
440 440 }
441 441 var obj_dict = state.obj;
442 442 var tmpl = '';
443 443
444 444 if(obj_dict && state.type == 'repo'){
445 445 if(obj_dict['repo_type'] === 'hg'){
446 446 tmpl += '<i class="icon-hg"></i> ';
447 447 }
448 448 else if(obj_dict['repo_type'] === 'git'){
449 449 tmpl += '<i class="icon-git"></i> ';
450 450 }
451 451 else if(obj_dict['repo_type'] === 'svn'){
452 452 tmpl += '<i class="icon-svn"></i> ';
453 453 }
454 454 if(obj_dict['private']){
455 455 tmpl += '<i class="icon-lock" ></i> ';
456 456 }
457 457 else if(visual_show_public_icon){
458 458 tmpl += '<i class="icon-unlock-alt"></i> ';
459 459 }
460 460 }
461 461 if(obj_dict && state.type == 'commit') {
462 462 tmpl += '<i class="icon-tag"></i>';
463 463 }
464 464 if(obj_dict && state.type == 'group'){
465 465 tmpl += '<i class="icon-folder-close"></i> ';
466 466 }
467 467 tmpl += escapeMarkup(state.text);
468 468 return tmpl;
469 469 };
470 470
471 471 var formatResult = function(result, container, query, escapeMarkup) {
472 472 return format(result, escapeMarkup);
473 473 };
474 474
475 475 var formatSelection = function(data, container, escapeMarkup) {
476 476 return format(data, escapeMarkup);
477 477 };
478 478
479 479 $("#repo_switcher").select2({
480 480 cachedDataSource: {},
481 481 minimumInputLength: 2,
482 482 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
483 483 dropdownAutoWidth: true,
484 484 formatResult: formatResult,
485 485 formatSelection: formatSelection,
486 486 containerCssClass: "repo-switcher",
487 487 dropdownCssClass: "repo-switcher-dropdown",
488 488 escapeMarkup: function(m){
489 489 // don't escape our custom placeholder
490 490 if(m.substr(0,23) == '<div class="menulabel">'){
491 491 return m;
492 492 }
493 493
494 494 return Select2.util.escapeMarkup(m);
495 495 },
496 496 query: $.debounce(250, function(query){
497 497 self = this;
498 498 var cacheKey = query.term;
499 499 var cachedData = self.cachedDataSource[cacheKey];
500 500
501 501 if (cachedData) {
502 502 query.callback({results: cachedData.results});
503 503 } else {
504 504 $.ajax({
505 505 url: pyroutes.url('goto_switcher_data'),
506 506 data: {'query': query.term},
507 507 dataType: 'json',
508 508 type: 'GET',
509 509 success: function(data) {
510 510 self.cachedDataSource[cacheKey] = data;
511 511 query.callback({results: data.results});
512 512 },
513 513 error: function(data, textStatus, errorThrown) {
514 514 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
515 515 }
516 516 })
517 517 }
518 518 })
519 519 });
520 520
521 521 $("#repo_switcher").on('select2-selecting', function(e){
522 522 e.preventDefault();
523 523 window.location = e.choice.url;
524 524 });
525 525
526 526 </script>
527 527 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
528 528 </%def>
529 529
530 530 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
531 531 <div class="modal-dialog">
532 532 <div class="modal-content">
533 533 <div class="modal-header">
534 534 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
535 535 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
536 536 </div>
537 537 <div class="modal-body">
538 538 <div class="block-left">
539 539 <table class="keyboard-mappings">
540 540 <tbody>
541 541 <tr>
542 542 <th></th>
543 543 <th>${_('Site-wide shortcuts')}</th>
544 544 </tr>
545 545 <%
546 546 elems = [
547 547 ('/', 'Open quick search box'),
548 548 ('g h', 'Goto home page'),
549 549 ('g g', 'Goto my private gists page'),
550 550 ('g G', 'Goto my public gists page'),
551 551 ('n r', 'New repository page'),
552 552 ('n g', 'New gist page'),
553 553 ]
554 554 %>
555 555 %for key, desc in elems:
556 556 <tr>
557 557 <td class="keys">
558 558 <span class="key tag">${key}</span>
559 559 </td>
560 560 <td>${desc}</td>
561 561 </tr>
562 562 %endfor
563 563 </tbody>
564 564 </table>
565 565 </div>
566 566 <div class="block-left">
567 567 <table class="keyboard-mappings">
568 568 <tbody>
569 569 <tr>
570 570 <th></th>
571 571 <th>${_('Repositories')}</th>
572 572 </tr>
573 573 <%
574 574 elems = [
575 575 ('g s', 'Goto summary page'),
576 576 ('g c', 'Goto changelog page'),
577 577 ('g f', 'Goto files page'),
578 578 ('g F', 'Goto files page with file search activated'),
579 579 ('g p', 'Goto pull requests page'),
580 580 ('g o', 'Goto repository settings'),
581 581 ('g O', 'Goto repository permissions settings'),
582 582 ]
583 583 %>
584 584 %for key, desc in elems:
585 585 <tr>
586 586 <td class="keys">
587 587 <span class="key tag">${key}</span>
588 588 </td>
589 589 <td>${desc}</td>
590 590 </tr>
591 591 %endfor
592 592 </tbody>
593 593 </table>
594 594 </div>
595 595 </div>
596 596 <div class="modal-footer">
597 597 </div>
598 598 </div><!-- /.modal-content -->
599 599 </div><!-- /.modal-dialog -->
600 600 </div><!-- /.modal -->
@@ -1,33 +1,33 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS FOR BOOKMARKS
2 2 ## usage:
3 3 ## <%namespace name="bookmarks" file="/bookmarks/bookmarks_data.mako"/>
4 4 ## bookmarks.<func_name>(arg,arg2)
5 5
6 6 <%def name="compare(commit_id)">
7 7 <input class="compare-radio-button" type="radio" name="compare_source" value="${commit_id}"/>
8 8 <input class="compare-radio-button" type="radio" name="compare_target" value="${commit_id}"/>
9 9 </%def>
10 10
11 11
12 12 <%def name="name(name, files_url, closed)">
13 13 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % (name,))}">
14 14 <a href="${files_url}">
15 15 <i class="icon-bookmark"></i>
16 16 ${name}
17 17 </a>
18 18 </span>
19 19 </%def>
20 20
21 21 <%def name="date(date)">
22 22 ${h.age_component(date)}
23 23 </%def>
24 24
25 25 <%def name="author(author)">
26 26 <span class="tooltip" title="${h.tooltip(author)}">${h.link_to_user(author)}</span>
27 27 </%def>
28 28
29 29 <%def name="commit(message, commit_id, commit_idx)">
30 30 <div>
31 <pre><a title="${h.tooltip(message)}" href="${h.url('files_home',repo_name=c.repo_name,revision=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
31 <pre><a title="${h.tooltip(message)}" href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
32 32 </div>
33 33 </%def>
@@ -1,33 +1,33 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS FOR BRANCHES
2 2 ## usage:
3 3 ## <%namespace name="branch" file="/branches/branches_data.mako"/>
4 4 ## branch.<func_name>(arg,arg2)
5 5
6 6 <%def name="compare(commit_id)">
7 7 <input class="compare-radio-button" type="radio" name="compare_source" value="${commit_id}"/>
8 8 <input class="compare-radio-button" type="radio" name="compare_target" value="${commit_id}"/>
9 9 </%def>
10 10
11 11 <%def name="name(name, files_url, closed)">
12 12 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % (name,))}">
13 13 <a href="${files_url}"><i class="icon-code-fork"></i>${name}
14 14 %if closed:
15 15 [closed]
16 16 %endif
17 17 </a>
18 18 </span>
19 19 </%def>
20 20
21 21 <%def name="date(date)">
22 22 ${h.age_component(date)}
23 23 </%def>
24 24
25 25 <%def name="author(author)">
26 26 <span class="tooltip" title="${h.tooltip(author)}">${h.link_to_user(author)}</span>
27 27 </%def>
28 28
29 29 <%def name="commit(message, commit_id, commit_idx)">
30 30 <div>
31 <pre><a title="${h.tooltip(message)}" href="${h.url('files_home',repo_name=c.repo_name,revision=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
31 <pre><a title="${h.tooltip(message)}" href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit_id)}">r${commit_idx}:${h.short_id(commit_id)}</a></pre>
32 32 </div>
33 33 </%def>
@@ -1,142 +1,142 b''
1 1 ## small box that displays changed/added/removed details fetched by AJAX
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4
5 5 % if c.prev_page:
6 6 <tr>
7 7 <td colspan="9" class="load-more-commits">
8 8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}');return false">
9 9 ${_('load previous')}
10 10 </a>
11 11 </td>
12 12 </tr>
13 13 % endif
14 14
15 15 % for cnt,commit in enumerate(c.pagination):
16 16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17 17
18 18 <td class="td-checkbox">
19 19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 20 </td>
21 21 <td class="td-status">
22 22
23 23 %if c.statuses.get(commit.raw_id):
24 24 <div class="changeset-status-ico">
25 25 %if c.statuses.get(commit.raw_id)[2]:
26 26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
27 27 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
28 28 </a>
29 29 %else:
30 30 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 31 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
32 32 </a>
33 33 %endif
34 34 </div>
35 35 %else:
36 36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 37 %endif
38 38 </td>
39 39 <td class="td-comments comments-col">
40 40 %if c.comments.get(commit.raw_id):
41 41 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
42 42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 43 </a>
44 44 %endif
45 45 </td>
46 46 <td class="td-hash">
47 47 <code>
48 48 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
49 49 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
50 50 </a>
51 51 % if hasattr(commit, 'phase'):
52 52 % if commit.phase != 'public':
53 53 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
54 54 % endif
55 55 % endif
56 56
57 57 ## obsolete commits
58 58 % if hasattr(commit, 'obsolete'):
59 59 % if commit.obsolete:
60 60 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
61 61 % endif
62 62 % endif
63 63
64 64 ## hidden commits
65 65 % if hasattr(commit, 'hidden'):
66 66 % if commit.hidden:
67 67 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
68 68 % endif
69 69 % endif
70 70
71 71 </code>
72 72 </td>
73 73 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
74 74 <div class="show_more_col">
75 75 <i class="show_more"></i>&nbsp;
76 76 </div>
77 77 </td>
78 78 <td class="td-description mid">
79 79 <div class="log-container truncate-wrap">
80 80 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
81 81 </div>
82 82 </td>
83 83
84 84 <td class="td-time">
85 85 ${h.age_component(commit.date)}
86 86 </td>
87 87 <td class="td-user">
88 88 ${base.gravatar_with_user(commit.author)}
89 89 </td>
90 90
91 91 <td class="td-tags tags-col">
92 92 <div id="t-${commit.raw_id}">
93 93
94 94 ## merge
95 95 %if commit.merge:
96 96 <span class="tag mergetag">
97 97 <i class="icon-merge"></i>${_('merge')}
98 98 </span>
99 99 %endif
100 100
101 101 ## branch
102 102 %if commit.branch:
103 103 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
104 104 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=commit.branch)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
105 105 </span>
106 106 %endif
107 107
108 108 ## bookmarks
109 109 %if h.is_hg(c.rhodecode_repo):
110 110 %for book in commit.bookmarks:
111 111 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
112 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
112 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
113 113 </span>
114 114 %endfor
115 115 %endif
116 116
117 117 ## tags
118 118 %for tag in commit.tags:
119 119 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
120 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
120 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
121 121 </span>
122 122 %endfor
123 123
124 124 </div>
125 125 </td>
126 126 </tr>
127 127 % endfor
128 128
129 129 % if c.next_page:
130 130 <tr>
131 131 <td colspan="9" class="load-more-commits">
132 132 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}');return false">
133 133 ${_('load next')}
134 134 </a>
135 135 </td>
136 136 </tr>
137 137 % endif
138 138 <tr class="chunk-graph-data" style="display:none"
139 139 data-graph='${c.graph_data|n}'
140 140 data-node='${c.prev_page}:${c.next_page}'
141 141 data-commits='${c.graph_commits|n}'>
142 142 </tr> No newline at end of file
@@ -1,39 +1,39 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <div class="table">
3 3
4 4 <table class="table rctable file_history">
5 5 %for cnt,cs in enumerate(c.pagination):
6 6 <tr id="chg_${cnt+1}" class="${'tablerow%s' % (cnt%2)}">
7 7 <td class="td-user">
8 8 ${base.gravatar_with_user(cs.author, 16)}
9 9 </td>
10 10 <td class="td-time">
11 11 <div class="date">
12 12 ${h.age_component(cs.date)}
13 13 </div>
14 14 </td>
15 15 <td class="td-message">
16 16 <div class="log-container">
17 17 <div class="message_history" title="${h.tooltip(cs.message)}">
18 18 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">
19 19 ${h.shorter(cs.message, 75)}
20 20 </a>
21 21 </div>
22 22 </div>
23 23 </td>
24 24 <td class="td-hash">
25 25 <code>
26 26 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">
27 27 <span>${h.show_id(cs)}</span>
28 28 </a>
29 29 </code>
30 30 </td>
31 31 <td class="td-actions">
32 <a href="${h.url('files_home',repo_name=c.repo_name,f_path=c.changelog_for_path,revision=cs.raw_id)}">
32 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=cs.raw_id,f_path=c.changelog_for_path)}">
33 33 ${_('Show File')}
34 34 </a>
35 35 </td>
36 36 </tr>
37 37 %endfor
38 38 </table>
39 39 </div>
@@ -1,351 +1,351 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 5
6 6 <%def name="title()">
7 7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 8 %if c.rhodecode_name:
9 9 &middot; ${h.branding(c.rhodecode_name)}
10 10 %endif
11 11 </%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <script>
23 23 // TODO: marcink switch this to pyroutes
24 24 AJAX_COMMENT_DELETE_URL = "${h.url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 26 </script>
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 30 </div>
31 31
32 32 <div id="changeset_compare_view_content" class="summary changeset">
33 33 <div class="summary-detail">
34 34 <div class="summary-detail-header">
35 35 <span class="breadcrumbs files_location">
36 36 <h4>${_('Commit')}
37 37 <code>
38 38 ${h.show_id(c.commit)}
39 39 % if hasattr(c.commit, 'phase'):
40 40 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">${c.commit.phase}</span>
41 41 % endif
42 42
43 43 ## obsolete commits
44 44 % if hasattr(c.commit, 'obsolete'):
45 45 % if c.commit.obsolete:
46 46 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
47 47 % endif
48 48 % endif
49 49
50 50 ## hidden commits
51 51 % if hasattr(c.commit, 'hidden'):
52 52 % if c.commit.hidden:
53 53 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
54 54 % endif
55 55 % endif
56 56
57 57 </code>
58 58 </h4>
59 59 </span>
60 60 <div class="pull-right">
61 61 <span id="parent_link">
62 62 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
63 63 </span>
64 64 |
65 65 <span id="child_link">
66 66 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
67 67 </span>
68 68 </div>
69 69 </div>
70 70
71 71 <div class="fieldset">
72 72 <div class="left-label">
73 73 ${_('Description')}:
74 74 </div>
75 75 <div class="right-content">
76 76 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
77 77 <div id="message_expand" style="display:none;">
78 78 ${_('Expand')}
79 79 </div>
80 80 </div>
81 81 </div>
82 82
83 83 %if c.statuses:
84 84 <div class="fieldset">
85 85 <div class="left-label">
86 86 ${_('Commit status')}:
87 87 </div>
88 88 <div class="right-content">
89 89 <div class="changeset-status-ico">
90 90 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
91 91 </div>
92 92 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
93 93 </div>
94 94 </div>
95 95 %endif
96 96
97 97 <div class="fieldset">
98 98 <div class="left-label">
99 99 ${_('References')}:
100 100 </div>
101 101 <div class="right-content">
102 102 <div class="tags">
103 103
104 104 %if c.commit.merge:
105 105 <span class="mergetag tag">
106 106 <i class="icon-merge"></i>${_('merge')}
107 107 </span>
108 108 %endif
109 109
110 110 %if h.is_hg(c.rhodecode_repo):
111 111 %for book in c.commit.bookmarks:
112 112 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
113 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
113 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
114 114 </span>
115 115 %endfor
116 116 %endif
117 117
118 118 %for tag in c.commit.tags:
119 119 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
120 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
120 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
121 121 </span>
122 122 %endfor
123 123
124 124 %if c.commit.branch:
125 125 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % c.commit.branch)}">
126 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
126 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=c.commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
127 127 </span>
128 128 %endif
129 129 </div>
130 130 </div>
131 131 </div>
132 132
133 133 <div class="fieldset">
134 134 <div class="left-label">
135 135 ${_('Diff options')}:
136 136 </div>
137 137 <div class="right-content">
138 138 <div class="diff-actions">
139 139 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
140 140 ${_('Raw Diff')}
141 141 </a>
142 142 |
143 143 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
144 144 ${_('Patch Diff')}
145 145 </a>
146 146 |
147 147 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
148 148 ${_('Download Diff')}
149 149 </a>
150 150 |
151 151 ${c.ignorews_url(request.GET)}
152 152 |
153 153 ${c.context_url(request.GET)}
154 154 </div>
155 155 </div>
156 156 </div>
157 157
158 158 <div class="fieldset">
159 159 <div class="left-label">
160 160 ${_('Comments')}:
161 161 </div>
162 162 <div class="right-content">
163 163 <div class="comments-number">
164 164 %if c.comments:
165 165 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
166 166 %else:
167 167 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
168 168 %endif
169 169 %if c.inline_cnt:
170 170 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
171 171 %else:
172 172 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
173 173 %endif
174 174 </div>
175 175 </div>
176 176 </div>
177 177
178 178 <div class="fieldset">
179 179 <div class="left-label">
180 180 ${_('Unresolved TODOs')}:
181 181 </div>
182 182 <div class="right-content">
183 183 <div class="comments-number">
184 184 % if c.unresolved_comments:
185 185 % for co in c.unresolved_comments:
186 186 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
187 187 % endfor
188 188 % else:
189 189 ${_('There are no unresolved TODOs')}
190 190 % endif
191 191 </div>
192 192 </div>
193 193 </div>
194 194
195 195 </div> <!-- end summary-detail -->
196 196
197 197 <div id="commit-stats" class="sidebar-right">
198 198 <div class="summary-detail-header">
199 199 <h4 class="item">
200 200 ${_('Author')}
201 201 </h4>
202 202 </div>
203 203 <div class="sidebar-right-content">
204 204 ${self.gravatar_with_user(c.commit.author)}
205 205 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
206 206 </div>
207 207 </div><!-- end sidebar -->
208 208 </div> <!-- end summary -->
209 209 <div class="cs_files">
210 210 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
211 211 ${cbdiffs.render_diffset_menu()}
212 212 ${cbdiffs.render_diffset(
213 213 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
214 214 </div>
215 215
216 216 ## template for inline comment form
217 217 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
218 218
219 219 ## render comments
220 220 ${comment.generate_comments(c.comments)}
221 221
222 222 ## main comment form and it status
223 223 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
224 224 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
225 225 </div>
226 226
227 227 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
228 228 <script type="text/javascript">
229 229
230 230 $(document).ready(function() {
231 231
232 232 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
233 233 if($('#trimmed_message_box').height() === boxmax){
234 234 $('#message_expand').show();
235 235 }
236 236
237 237 $('#message_expand').on('click', function(e){
238 238 $('#trimmed_message_box').css('max-height', 'none');
239 239 $(this).hide();
240 240 });
241 241
242 242 $('.show-inline-comments').on('click', function(e){
243 243 var boxid = $(this).attr('data-comment-id');
244 244 var button = $(this);
245 245
246 246 if(button.hasClass("comments-visible")) {
247 247 $('#{0} .inline-comments'.format(boxid)).each(function(index){
248 248 $(this).hide();
249 249 });
250 250 button.removeClass("comments-visible");
251 251 } else {
252 252 $('#{0} .inline-comments'.format(boxid)).each(function(index){
253 253 $(this).show();
254 254 });
255 255 button.addClass("comments-visible");
256 256 }
257 257 });
258 258
259 259
260 260 // next links
261 261 $('#child_link').on('click', function(e){
262 262 // fetch via ajax what is going to be the next link, if we have
263 263 // >1 links show them to user to choose
264 264 if(!$('#child_link').hasClass('disabled')){
265 265 $.ajax({
266 266 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
267 267 success: function(data) {
268 268 if(data.results.length === 0){
269 269 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
270 270 }
271 271 if(data.results.length === 1){
272 272 var commit = data.results[0];
273 273 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
274 274 }
275 275 else if(data.results.length === 2){
276 276 $('#child_link').addClass('disabled');
277 277 $('#child_link').addClass('double');
278 278 var _html = '';
279 279 _html +='<a title="__title__" href="__url__">__rev__</a> '
280 280 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
281 281 .replace('__title__', data.results[0].message)
282 282 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
283 283 _html +=' | ';
284 284 _html +='<a title="__title__" href="__url__">__rev__</a> '
285 285 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
286 286 .replace('__title__', data.results[1].message)
287 287 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
288 288 $('#child_link').html(_html);
289 289 }
290 290 }
291 291 });
292 292 e.preventDefault();
293 293 }
294 294 });
295 295
296 296 // prev links
297 297 $('#parent_link').on('click', function(e){
298 298 // fetch via ajax what is going to be the next link, if we have
299 299 // >1 links show them to user to choose
300 300 if(!$('#parent_link').hasClass('disabled')){
301 301 $.ajax({
302 302 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
303 303 success: function(data) {
304 304 if(data.results.length === 0){
305 305 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
306 306 }
307 307 if(data.results.length === 1){
308 308 var commit = data.results[0];
309 309 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
310 310 }
311 311 else if(data.results.length === 2){
312 312 $('#parent_link').addClass('disabled');
313 313 $('#parent_link').addClass('double');
314 314 var _html = '';
315 315 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
316 316 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
317 317 .replace('__title__', data.results[0].message)
318 318 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
319 319 _html +=' | ';
320 320 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
321 321 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
322 322 .replace('__title__', data.results[1].message)
323 323 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
324 324 $('#parent_link').html(_html);
325 325 }
326 326 }
327 327 });
328 328 e.preventDefault();
329 329 }
330 330 });
331 331
332 332 if (location.hash) {
333 333 var result = splitDelimitedHash(location.hash);
334 334 var line = $('html').find(result.loc);
335 335 if (line.length > 0){
336 336 offsetScroll(line, 70);
337 337 }
338 338 }
339 339
340 340 // browse tree @ revision
341 341 $('#files_link').on('click', function(e){
342 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
342 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
343 343 e.preventDefault();
344 344 });
345 345
346 346 // inject comments into their proper positions
347 347 var file_comments = $('.inline-comment-placeholder');
348 348 })
349 349 </script>
350 350
351 351 </%def>
@@ -1,671 +1,671 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26 <%def name="link_for(**kw)">
27 27 <%
28 28 new_args = request.GET.mixed()
29 29 new_args.update(kw)
30 30 return h.url('', **new_args)
31 31 %>
32 32 </%def>
33 33
34 34 <%def name="render_diffset(diffset, commit=None,
35 35
36 36 # collapse all file diff entries when there are more than this amount of files in the diff
37 37 collapse_when_files_over=20,
38 38
39 39 # collapse lines in the diff when more than this amount of lines changed in the file diff
40 40 lines_changed_limit=500,
41 41
42 42 # add a ruler at to the output
43 43 ruler_at_chars=0,
44 44
45 45 # show inline comments
46 46 use_comments=False,
47 47
48 48 # disable new comments
49 49 disable_new_comments=False,
50 50
51 51 # special file-comments that were deleted in previous versions
52 52 # it's used for showing outdated comments for deleted files in a PR
53 53 deleted_files_comments=None
54 54
55 55 )">
56 56
57 57 %if use_comments:
58 58 <div id="cb-comments-inline-container-template" class="js-template">
59 59 ${inline_comments_container([])}
60 60 </div>
61 61 <div class="js-template" id="cb-comment-inline-form-template">
62 62 <div class="comment-inline-form ac">
63 63
64 64 %if c.rhodecode_user.username != h.DEFAULT_USER:
65 65 ## render template for inline comments
66 66 ${commentblock.comment_form(form_type='inline')}
67 67 %else:
68 68 ${h.form('', class_='inline-form comment-form-login', method='get')}
69 69 <div class="pull-left">
70 70 <div class="comment-help pull-right">
71 71 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
72 72 </div>
73 73 </div>
74 74 <div class="comment-button pull-right">
75 75 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
76 76 ${_('Cancel')}
77 77 </button>
78 78 </div>
79 79 <div class="clearfix"></div>
80 80 ${h.end_form()}
81 81 %endif
82 82 </div>
83 83 </div>
84 84
85 85 %endif
86 86 <%
87 87 collapse_all = len(diffset.files) > collapse_when_files_over
88 88 %>
89 89
90 90 %if c.diffmode == 'sideside':
91 91 <style>
92 92 .wrapper {
93 93 max-width: 1600px !important;
94 94 }
95 95 </style>
96 96 %endif
97 97
98 98 %if ruler_at_chars:
99 99 <style>
100 100 .diff table.cb .cb-content:after {
101 101 content: "";
102 102 border-left: 1px solid blue;
103 103 position: absolute;
104 104 top: 0;
105 105 height: 18px;
106 106 opacity: .2;
107 107 z-index: 10;
108 108 //## +5 to account for diff action (+/-)
109 109 left: ${ruler_at_chars + 5}ch;
110 110 </style>
111 111 %endif
112 112
113 113 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
114 114 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
115 115 %if commit:
116 116 <div class="pull-right">
117 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
117 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
118 118 ${_('Browse Files')}
119 119 </a>
120 120 </div>
121 121 %endif
122 122 <h2 class="clearinner">
123 123 %if commit:
124 124 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
125 125 ${h.age_component(commit.date)} -
126 126 %endif
127 127 %if diffset.limited_diff:
128 128 ${_('The requested commit is too big and content was truncated.')}
129 129
130 130 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
131 131 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
132 132 %else:
133 133 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
134 134 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
135 135 %endif
136 136
137 137 </h2>
138 138 </div>
139 139
140 140 %if not diffset.files:
141 141 <p class="empty_data">${_('No files')}</p>
142 142 %endif
143 143
144 144 <div class="filediffs">
145 145 ## initial value could be marked as False later on
146 146 <% over_lines_changed_limit = False %>
147 147 %for i, filediff in enumerate(diffset.files):
148 148
149 149 <%
150 150 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
151 151 over_lines_changed_limit = lines_changed > lines_changed_limit
152 152 %>
153 153 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
154 154 <div
155 155 class="filediff"
156 156 data-f-path="${filediff.patch['filename']}"
157 157 id="a_${h.FID('', filediff.patch['filename'])}">
158 158 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
159 159 <div class="filediff-collapse-indicator"></div>
160 160 ${diff_ops(filediff)}
161 161 </label>
162 162 ${diff_menu(filediff, use_comments=use_comments)}
163 163 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
164 164 %if not filediff.hunks:
165 165 %for op_id, op_text in filediff.patch['stats']['ops'].items():
166 166 <tr>
167 167 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
168 168 %if op_id == DEL_FILENODE:
169 169 ${_('File was deleted')}
170 170 %elif op_id == BIN_FILENODE:
171 171 ${_('Binary file hidden')}
172 172 %else:
173 173 ${op_text}
174 174 %endif
175 175 </td>
176 176 </tr>
177 177 %endfor
178 178 %endif
179 179 %if filediff.limited_diff:
180 180 <tr class="cb-warning cb-collapser">
181 181 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
182 182 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
183 183 </td>
184 184 </tr>
185 185 %else:
186 186 %if over_lines_changed_limit:
187 187 <tr class="cb-warning cb-collapser">
188 188 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
189 189 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 190 <a href="#" class="cb-expand"
191 191 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
192 192 </a>
193 193 <a href="#" class="cb-collapse"
194 194 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
195 195 </a>
196 196 </td>
197 197 </tr>
198 198 %endif
199 199 %endif
200 200
201 201 %for hunk in filediff.hunks:
202 202 <tr class="cb-hunk">
203 203 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
204 204 ## TODO: dan: add ajax loading of more context here
205 205 ## <a href="#">
206 206 <i class="icon-more"></i>
207 207 ## </a>
208 208 </td>
209 209 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
210 210 @@
211 211 -${hunk.source_start},${hunk.source_length}
212 212 +${hunk.target_start},${hunk.target_length}
213 213 ${hunk.section_header}
214 214 </td>
215 215 </tr>
216 216 %if c.diffmode == 'unified':
217 217 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
218 218 %elif c.diffmode == 'sideside':
219 219 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
220 220 %else:
221 221 <tr class="cb-line">
222 222 <td>unknown diff mode</td>
223 223 </tr>
224 224 %endif
225 225 %endfor
226 226
227 227 ## outdated comments that do not fit into currently displayed lines
228 228 % for lineno, comments in filediff.left_comments.items():
229 229
230 230 %if c.diffmode == 'unified':
231 231 <tr class="cb-line">
232 232 <td class="cb-data cb-context"></td>
233 233 <td class="cb-lineno cb-context"></td>
234 234 <td class="cb-lineno cb-context"></td>
235 235 <td class="cb-content cb-context">
236 236 ${inline_comments_container(comments)}
237 237 </td>
238 238 </tr>
239 239 %elif c.diffmode == 'sideside':
240 240 <tr class="cb-line">
241 241 <td class="cb-data cb-context"></td>
242 242 <td class="cb-lineno cb-context"></td>
243 243 <td class="cb-content cb-context"></td>
244 244
245 245 <td class="cb-data cb-context"></td>
246 246 <td class="cb-lineno cb-context"></td>
247 247 <td class="cb-content cb-context">
248 248 ${inline_comments_container(comments)}
249 249 </td>
250 250 </tr>
251 251 %endif
252 252
253 253 % endfor
254 254
255 255 </table>
256 256 </div>
257 257 %endfor
258 258
259 259 ## outdated comments that are made for a file that has been deleted
260 260 % for filename, comments_dict in (deleted_files_comments or {}).items():
261 261
262 262 <div class="filediffs filediff-outdated" style="display: none">
263 263 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
264 264 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
265 265 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
266 266 <div class="filediff-collapse-indicator"></div>
267 267 <span class="pill">
268 268 ## file was deleted
269 269 <strong>${filename}</strong>
270 270 </span>
271 271 <span class="pill-group" style="float: left">
272 272 ## file op, doesn't need translation
273 273 <span class="pill" op="removed">removed in this version</span>
274 274 </span>
275 275 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}"></a>
276 276 <span class="pill-group" style="float: right">
277 277 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
278 278 </span>
279 279 </label>
280 280
281 281 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
282 282 <tr>
283 283 % if c.diffmode == 'unified':
284 284 <td></td>
285 285 %endif
286 286
287 287 <td></td>
288 288 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
289 289 ${_('File was deleted in this version, and outdated comments were made on it')}
290 290 </td>
291 291 </tr>
292 292 %if c.diffmode == 'unified':
293 293 <tr class="cb-line">
294 294 <td class="cb-data cb-context"></td>
295 295 <td class="cb-lineno cb-context"></td>
296 296 <td class="cb-lineno cb-context"></td>
297 297 <td class="cb-content cb-context">
298 298 ${inline_comments_container(comments_dict['comments'])}
299 299 </td>
300 300 </tr>
301 301 %elif c.diffmode == 'sideside':
302 302 <tr class="cb-line">
303 303 <td class="cb-data cb-context"></td>
304 304 <td class="cb-lineno cb-context"></td>
305 305 <td class="cb-content cb-context"></td>
306 306
307 307 <td class="cb-data cb-context"></td>
308 308 <td class="cb-lineno cb-context"></td>
309 309 <td class="cb-content cb-context">
310 310 ${inline_comments_container(comments_dict['comments'])}
311 311 </td>
312 312 </tr>
313 313 %endif
314 314 </table>
315 315 </div>
316 316 </div>
317 317 % endfor
318 318
319 319 </div>
320 320 </div>
321 321 </%def>
322 322
323 323 <%def name="diff_ops(filediff)">
324 324 <%
325 325 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
326 326 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
327 327 %>
328 328 <span class="pill">
329 329 %if filediff.source_file_path and filediff.target_file_path:
330 330 %if filediff.source_file_path != filediff.target_file_path:
331 331 ## file was renamed, or copied
332 332 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
333 333 <strong>${filediff.target_file_path}</strong><del>${filediff.source_file_path}</del>
334 334 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
335 335 <strong>${filediff.target_file_path}</strong>${filediff.source_file_path}
336 336 %endif
337 337 %else:
338 338 ## file was modified
339 339 <strong>${filediff.source_file_path}</strong>
340 340 %endif
341 341 %else:
342 342 %if filediff.source_file_path:
343 343 ## file was deleted
344 344 <strong>${filediff.source_file_path}</strong>
345 345 %else:
346 346 ## file was added
347 347 <strong>${filediff.target_file_path}</strong>
348 348 %endif
349 349 %endif
350 350 </span>
351 351 <span class="pill-group" style="float: left">
352 352 %if filediff.limited_diff:
353 353 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
354 354 %endif
355 355
356 356 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
357 357 <span class="pill" op="renamed">renamed</span>
358 358 %endif
359 359
360 360 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
361 361 <span class="pill" op="copied">copied</span>
362 362 %endif
363 363
364 364 %if NEW_FILENODE in filediff.patch['stats']['ops']:
365 365 <span class="pill" op="created">created</span>
366 366 %if filediff['target_mode'].startswith('120'):
367 367 <span class="pill" op="symlink">symlink</span>
368 368 %else:
369 369 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
370 370 %endif
371 371 %endif
372 372
373 373 %if DEL_FILENODE in filediff.patch['stats']['ops']:
374 374 <span class="pill" op="removed">removed</span>
375 375 %endif
376 376
377 377 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
378 378 <span class="pill" op="mode">
379 379 ${nice_mode(filediff['source_mode'])}${nice_mode(filediff['target_mode'])}
380 380 </span>
381 381 %endif
382 382 </span>
383 383
384 384 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}"></a>
385 385
386 386 <span class="pill-group" style="float: right">
387 387 %if BIN_FILENODE in filediff.patch['stats']['ops']:
388 388 <span class="pill" op="binary">binary</span>
389 389 %if MOD_FILENODE in filediff.patch['stats']['ops']:
390 390 <span class="pill" op="modified">modified</span>
391 391 %endif
392 392 %endif
393 393 %if filediff.patch['stats']['added']:
394 394 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
395 395 %endif
396 396 %if filediff.patch['stats']['deleted']:
397 397 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
398 398 %endif
399 399 </span>
400 400
401 401 </%def>
402 402
403 403 <%def name="nice_mode(filemode)">
404 404 ${filemode.startswith('100') and filemode[3:] or filemode}
405 405 </%def>
406 406
407 407 <%def name="diff_menu(filediff, use_comments=False)">
408 408 <div class="filediff-menu">
409 409 %if filediff.diffset.source_ref:
410 410 %if filediff.operation in ['D', 'M']:
411 411 <a
412 412 class="tooltip"
413 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
413 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
414 414 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
415 415 >
416 416 ${_('Show file before')}
417 417 </a> |
418 418 %else:
419 419 <span
420 420 class="tooltip"
421 421 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
422 422 >
423 423 ${_('Show file before')}
424 424 </span> |
425 425 %endif
426 426 %if filediff.operation in ['A', 'M']:
427 427 <a
428 428 class="tooltip"
429 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
429 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
430 430 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
431 431 >
432 432 ${_('Show file after')}
433 433 </a> |
434 434 %else:
435 435 <span
436 436 class="tooltip"
437 437 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
438 438 >
439 439 ${_('Show file after')}
440 440 </span> |
441 441 %endif
442 442 <a
443 443 class="tooltip"
444 444 title="${h.tooltip(_('Raw diff'))}"
445 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
445 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
446 446 >
447 447 ${_('Raw diff')}
448 448 </a> |
449 449 <a
450 450 class="tooltip"
451 451 title="${h.tooltip(_('Download diff'))}"
452 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
452 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
453 453 >
454 454 ${_('Download diff')}
455 455 </a>
456 456 % if use_comments:
457 457 |
458 458 % endif
459 459
460 460 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
461 461 %if hasattr(c, 'ignorews_url'):
462 462 ${c.ignorews_url(request.GET, h.FID('', filediff.patch['filename']))}
463 463 %endif
464 464 %if hasattr(c, 'context_url'):
465 465 ${c.context_url(request.GET, h.FID('', filediff.patch['filename']))}
466 466 %endif
467 467
468 468 %if use_comments:
469 469 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
470 470 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
471 471 </a>
472 472 %endif
473 473 %endif
474 474 </div>
475 475 </%def>
476 476
477 477
478 478 <%def name="inline_comments_container(comments)">
479 479 <div class="inline-comments">
480 480 %for comment in comments:
481 481 ${commentblock.comment_block(comment, inline=True)}
482 482 %endfor
483 483
484 484 % if comments and comments[-1].outdated:
485 485 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
486 486 style="display: none;}">
487 487 ${_('Add another comment')}
488 488 </span>
489 489 % else:
490 490 <span onclick="return Rhodecode.comments.createComment(this)"
491 491 class="btn btn-secondary cb-comment-add-button">
492 492 ${_('Add another comment')}
493 493 </span>
494 494 % endif
495 495
496 496 </div>
497 497 </%def>
498 498
499 499
500 500 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
501 501 %for i, line in enumerate(hunk.sideside):
502 502 <%
503 503 old_line_anchor, new_line_anchor = None, None
504 504 if line.original.lineno:
505 505 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
506 506 if line.modified.lineno:
507 507 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
508 508 %>
509 509
510 510 <tr class="cb-line">
511 511 <td class="cb-data ${action_class(line.original.action)}"
512 512 data-line-number="${line.original.lineno}"
513 513 >
514 514 <div>
515 515 %if line.original.comments:
516 516 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
517 517 %endif
518 518 </div>
519 519 </td>
520 520 <td class="cb-lineno ${action_class(line.original.action)}"
521 521 data-line-number="${line.original.lineno}"
522 522 %if old_line_anchor:
523 523 id="${old_line_anchor}"
524 524 %endif
525 525 >
526 526 %if line.original.lineno:
527 527 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
528 528 %endif
529 529 </td>
530 530 <td class="cb-content ${action_class(line.original.action)}"
531 531 data-line-number="o${line.original.lineno}"
532 532 >
533 533 %if use_comments and line.original.lineno:
534 534 ${render_add_comment_button()}
535 535 %endif
536 536 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
537 537 %if use_comments and line.original.lineno and line.original.comments:
538 538 ${inline_comments_container(line.original.comments)}
539 539 %endif
540 540 </td>
541 541 <td class="cb-data ${action_class(line.modified.action)}"
542 542 data-line-number="${line.modified.lineno}"
543 543 >
544 544 <div>
545 545 %if line.modified.comments:
546 546 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
547 547 %endif
548 548 </div>
549 549 </td>
550 550 <td class="cb-lineno ${action_class(line.modified.action)}"
551 551 data-line-number="${line.modified.lineno}"
552 552 %if new_line_anchor:
553 553 id="${new_line_anchor}"
554 554 %endif
555 555 >
556 556 %if line.modified.lineno:
557 557 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
558 558 %endif
559 559 </td>
560 560 <td class="cb-content ${action_class(line.modified.action)}"
561 561 data-line-number="n${line.modified.lineno}"
562 562 >
563 563 %if use_comments and line.modified.lineno:
564 564 ${render_add_comment_button()}
565 565 %endif
566 566 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
567 567 %if use_comments and line.modified.lineno and line.modified.comments:
568 568 ${inline_comments_container(line.modified.comments)}
569 569 %endif
570 570 </td>
571 571 </tr>
572 572 %endfor
573 573 </%def>
574 574
575 575
576 576 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
577 577 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
578 578 <%
579 579 old_line_anchor, new_line_anchor = None, None
580 580 if old_line_no:
581 581 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
582 582 if new_line_no:
583 583 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
584 584 %>
585 585 <tr class="cb-line">
586 586 <td class="cb-data ${action_class(action)}">
587 587 <div>
588 588 %if comments:
589 589 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
590 590 %endif
591 591 </div>
592 592 </td>
593 593 <td class="cb-lineno ${action_class(action)}"
594 594 data-line-number="${old_line_no}"
595 595 %if old_line_anchor:
596 596 id="${old_line_anchor}"
597 597 %endif
598 598 >
599 599 %if old_line_anchor:
600 600 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
601 601 %endif
602 602 </td>
603 603 <td class="cb-lineno ${action_class(action)}"
604 604 data-line-number="${new_line_no}"
605 605 %if new_line_anchor:
606 606 id="${new_line_anchor}"
607 607 %endif
608 608 >
609 609 %if new_line_anchor:
610 610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
611 611 %endif
612 612 </td>
613 613 <td class="cb-content ${action_class(action)}"
614 614 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
615 615 >
616 616 %if use_comments:
617 617 ${render_add_comment_button()}
618 618 %endif
619 619 <span class="cb-code">${action} ${content or '' | n}</span>
620 620 %if use_comments and comments:
621 621 ${inline_comments_container(comments)}
622 622 %endif
623 623 </td>
624 624 </tr>
625 625 %endfor
626 626 </%def>
627 627
628 628 <%def name="render_add_comment_button()">
629 629 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
630 630 <span><i class="icon-comment"></i></span>
631 631 </button>
632 632 </%def>
633 633
634 634 <%def name="render_diffset_menu()">
635 635
636 636 <div class="diffset-menu clearinner">
637 637 <div class="pull-right">
638 638 <div class="btn-group">
639 639
640 640 <a
641 641 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
642 642 title="${h.tooltip(_('View side by side'))}"
643 643 href="${h.url_replace(diffmode='sideside')}">
644 644 <span>${_('Side by Side')}</span>
645 645 </a>
646 646 <a
647 647 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
648 648 title="${h.tooltip(_('View unified'))}" href="${h.url_replace(diffmode='unified')}">
649 649 <span>${_('Unified')}</span>
650 650 </a>
651 651 </div>
652 652 </div>
653 653
654 654 <div class="pull-left">
655 655 <div class="btn-group">
656 656 <a
657 657 class="btn"
658 658 href="#"
659 659 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
660 660 <a
661 661 class="btn"
662 662 href="#"
663 663 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
664 664 <a
665 665 class="btn"
666 666 href="#"
667 667 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
668 668 </div>
669 669 </div>
670 670 </div>
671 671 </%def>
@@ -1,91 +1,92 b''
1 1 <%def name="render_line(line_num, tokens,
2 2 annotation=None,
3 3 bgcolor=None, show_annotation=None)">
4 4 <%
5 5 from rhodecode.lib.codeblocks import render_tokenstream
6 6 # avoid module lookup for performance
7 7 html_escape = h.html_escape
8 8 tooltip = h.tooltip
9 9 %>
10 10 <tr class="cb-line cb-line-fresh ${'cb-annotate' if show_annotation else ''}"
11 11 %if annotation:
12 12 data-revision="${annotation.revision}"
13 13 %endif
14 14 >
15 15
16 16 % if annotation:
17 17 % if show_annotation:
18 18 <td class="cb-annotate-info tooltip"
19 19 title="Author: ${tooltip(annotation.author) | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
20 20 >
21 21 ${h.gravatar_with_user(annotation.author, 16) | n}
22 22 <div class="cb-annotate-message truncate-wrap">${h.chop_at_smart(annotation.message, '\n', suffix_if_chopped='...')}</div>
23 23 </td>
24 24 <td class="cb-annotate-message-spacer">
25 <a class="tooltip" href="#show-previous-annotation" onclick="return annotationController.previousAnnotation('${annotation.raw_id}', '${c.f_path}')" title="${tooltip(_('view annotation from before this change'))}">
25 <a class="tooltip" href="#show-previous-annotation" onclick="return annotationController.previousAnnotation('${annotation.raw_id}', '${c.f_path}', ${line_num})" title="${tooltip(_('view annotation from before this change'))}">
26 26 <i class="icon-left"></i>
27 27 </a>
28 28 </td>
29 29 <td
30 30 class="cb-annotate-revision"
31 31 data-revision="${annotation.revision}"
32 32 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
33 33 style="background: ${bgcolor}">
34 34 <a class="cb-annotate" href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}">
35 35 r${annotation.revision}
36 36 </a>
37 37 </td>
38 38 % else:
39 39 <td></td>
40 40 <td class="cb-annotate-message-spacer"></td>
41 41 <td
42 42 class="cb-annotate-revision"
43 43 data-revision="${annotation.revision}"
44 44 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
45 45 style="background: ${bgcolor}">
46 46 </td>
47 47 % endif
48 48 % else:
49 49 <td colspan="3"></td>
50 50 % endif
51 51
52 52
53 53 <td class="cb-lineno" id="L${line_num}">
54 54 <a data-line-no="${line_num}" href="#L${line_num}"></a>
55 55 </td>
56 56 <td class="cb-content cb-content-fresh"
57 57 %if bgcolor:
58 58 style="background: ${bgcolor}"
59 59 %endif
60 60 >
61 61 ## newline at end is necessary for highlight to work when line is empty
62 62 ## and for copy pasting code to work as expected
63 63 <span class="cb-code">${render_tokenstream(tokens)|n}${'\n'}</span>
64 64 </td>
65 65 </tr>
66 66 </%def>
67 67
68 68 <%def name="render_annotation_lines(annotation, lines, color_hasher)">
69 69 % for line_num, tokens in lines:
70 70 ${render_line(line_num, tokens,
71 71 bgcolor=color_hasher(annotation and annotation.raw_id or ''),
72 72 annotation=annotation, show_annotation=loop.first
73 73 )}
74 74 % endfor
75 75 <script>
76 76 var AnnotationController = function() {
77 77 var self = this;
78 78
79 this.previousAnnotation = function(commitId, fPath) {
79 this.previousAnnotation = function(commitId, fPath, lineNo) {
80 80 var params = {
81 81 'repo_name': templateContext.repo_name,
82 'revision': commitId,
83 'f_path': fPath
82 'commit_id': commitId,
83 'f_path': fPath,
84 'line_anchor': lineNo
84 85 };
85 window.location = pyroutes.url('files_annotate_previous', params);
86 window.location = pyroutes.url('repo_files:annotated_previous', params);
86 87 return false;
87 88 };
88 89 };
89 90 var annotationController = new AnnotationController();
90 91 </script>
91 92 </%def>
@@ -1,317 +1,317 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
22 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="repo_desc(description)">
76 76 <div class="truncate-wrap">${description}</div>
77 77 </%def>
78 78
79 79 <%def name="last_change(last_change)">
80 80 ${h.age_component(last_change)}
81 81 </%def>
82 82
83 83 <%def name="revision(name,rev,tip,author,last_msg)">
84 84 <div>
85 85 %if rev >= 0:
86 86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
87 87 %else:
88 88 ${_('No commits yet')}
89 89 %endif
90 90 </div>
91 91 </%def>
92 92
93 93 <%def name="rss(name)">
94 94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 95 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
96 96 %else:
97 97 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
98 98 %endif
99 99 </%def>
100 100
101 101 <%def name="atom(name)">
102 102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 103 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
104 104 %else:
105 105 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
106 106 %endif
107 107 </%def>
108 108
109 109 <%def name="user_gravatar(email, size=16)">
110 110 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
111 111 ${base.gravatar(email, 16)}
112 112 </div>
113 113 </%def>
114 114
115 115 <%def name="repo_actions(repo_name, super_user=True)">
116 116 <div>
117 117 <div class="grid_edit">
118 118 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
119 119 <i class="icon-pencil"></i>Edit</a>
120 120 </div>
121 121 <div class="grid_delete">
122 122 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST', request=request)}
123 123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
124 124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
125 125 ${h.end_form()}
126 126 </div>
127 127 </div>
128 128 </%def>
129 129
130 130 <%def name="repo_state(repo_state)">
131 131 <div>
132 132 %if repo_state == 'repo_state_pending':
133 133 <div class="tag tag4">${_('Creating')}</div>
134 134 %elif repo_state == 'repo_state_created':
135 135 <div class="tag tag1">${_('Created')}</div>
136 136 %else:
137 137 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
138 138 %endif
139 139 </div>
140 140 </%def>
141 141
142 142
143 143 ## REPO GROUP RENDERERS
144 144 <%def name="quick_repo_group_menu(repo_group_name)">
145 145 <i class="pointer icon-more"></i>
146 146 <div class="menu_items_container hidden">
147 147 <ul class="menu_items">
148 148 <li>
149 149 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
150 150 <span class="icon">
151 151 <i class="icon-file-text"></i>
152 152 </span>
153 153 <span>${_('Summary')}</span>
154 154 </a>
155 155 </li>
156 156
157 157 </ul>
158 158 </div>
159 159 </%def>
160 160
161 161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
162 162 <div>
163 163 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
164 164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
165 165 %if children_groups:
166 166 ${h.literal(' &raquo; '.join(children_groups))}
167 167 %else:
168 168 ${repo_group_name}
169 169 %endif
170 170 </a>
171 171 </div>
172 172 </%def>
173 173
174 174 <%def name="repo_group_desc(description)">
175 175 <div class="truncate-wrap">${description}</div>
176 176 </%def>
177 177
178 178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
179 179 <div class="grid_edit">
180 180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
181 181 </div>
182 182 <div class="grid_delete">
183 183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
184 184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
185 185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
186 186 ${h.end_form()}
187 187 </div>
188 188 </%def>
189 189
190 190
191 191 <%def name="user_actions(user_id, username)">
192 192 <div class="grid_edit">
193 193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
194 194 <i class="icon-pencil"></i>Edit</a>
195 195 </div>
196 196 <div class="grid_delete">
197 197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
198 198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
199 199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
200 200 ${h.end_form()}
201 201 </div>
202 202 </%def>
203 203
204 204 <%def name="user_group_actions(user_group_id, user_group_name)">
205 205 <div class="grid_edit">
206 206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
207 207 </div>
208 208 <div class="grid_delete">
209 209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
210 210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
211 211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
212 212 ${h.end_form()}
213 213 </div>
214 214 </%def>
215 215
216 216
217 217 <%def name="user_name(user_id, username)">
218 218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
219 219 </%def>
220 220
221 221 <%def name="user_profile(username)">
222 222 ${base.gravatar_with_user(username, 16)}
223 223 </%def>
224 224
225 225 <%def name="user_group_name(user_group_id, user_group_name)">
226 226 <div>
227 227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
228 228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
229 229 </div>
230 230 </%def>
231 231
232 232
233 233 ## GISTS
234 234
235 235 <%def name="gist_gravatar(full_contact)">
236 236 <div class="gist_gravatar">
237 237 ${base.gravatar(full_contact, 30)}
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_access_id(gist_access_id, full_contact)">
242 242 <div>
243 243 <b>
244 244 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
245 245 </b>
246 246 </div>
247 247 </%def>
248 248
249 249 <%def name="gist_author(full_contact, created_on, expires)">
250 250 ${base.gravatar_with_user(full_contact, 16)}
251 251 </%def>
252 252
253 253
254 254 <%def name="gist_created(created_on)">
255 255 <div class="created">
256 256 ${h.age_component(created_on, time_is_local=True)}
257 257 </div>
258 258 </%def>
259 259
260 260 <%def name="gist_expires(expires)">
261 261 <div class="created">
262 262 %if expires == -1:
263 263 ${_('never')}
264 264 %else:
265 265 ${h.age_component(h.time_to_utcdatetime(expires))}
266 266 %endif
267 267 </div>
268 268 </%def>
269 269
270 270 <%def name="gist_type(gist_type)">
271 271 %if gist_type != 'public':
272 272 <div class="tag">${_('Private')}</div>
273 273 %endif
274 274 </%def>
275 275
276 276 <%def name="gist_description(gist_description)">
277 277 ${gist_description}
278 278 </%def>
279 279
280 280
281 281 ## PULL REQUESTS GRID RENDERERS
282 282
283 283 <%def name="pullrequest_target_repo(repo_name)">
284 284 <div class="truncate">
285 285 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
286 286 </div>
287 287 </%def>
288 288 <%def name="pullrequest_status(status)">
289 289 <div class="${'flag_status %s' % status} pull-left"></div>
290 290 </%def>
291 291
292 292 <%def name="pullrequest_title(title, description)">
293 293 ${title} <br/>
294 294 ${h.shorter(description, 40)}
295 295 </%def>
296 296
297 297 <%def name="pullrequest_comments(comments_nr)">
298 298 <i class="icon-comment"></i> ${comments_nr}
299 299 </%def>
300 300
301 301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
302 302 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
303 303 % if short:
304 304 #${pull_request_id}
305 305 % else:
306 306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
307 307 % endif
308 308 </a>
309 309 </%def>
310 310
311 311 <%def name="pullrequest_updated_on(updated_on)">
312 312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
313 313 </%def>
314 314
315 315 <%def name="pullrequest_author(full_contact)">
316 316 ${base.gravatar_with_user(full_contact, 16)}
317 317 </%def>
@@ -1,28 +1,28 b''
1 1 <%def name="refs(commit)">
2 2 %if commit.merge:
3 3 <span class="mergetag tag">
4 4 <i class="icon-merge">${_('merge')}</i>
5 5 </span>
6 6 %endif
7 7
8 8 %if h.is_hg(c.rhodecode_repo):
9 9 %for book in commit.bookmarks:
10 10 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
11 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
11 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
12 12 </span>
13 13 %endfor
14 14 %endif
15 15
16 16 %for tag in commit.tags:
17 17 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
18 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
18 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
19 19 </span>
20 20 %endfor
21 21
22 22 %if commit.branch:
23 23 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
24 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
24 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
25 25 </span>
26 26 %endif
27 27
28 28 </%def>
@@ -1,30 +1,42 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="summary-detail-header">
4 4 <h4 class="item">
5 5 % if c.file_author:
6 6 ${_('Last Author')}
7 7 % else:
8 ${h.literal(ungettext(u'File Author (%s)',u'File Authors (%s)',len(c.authors)) % ('<b>%s</b>' % len(c.authors))) }
8 ${h.literal(_ungettext(u'File Author (%s)',u'File Authors (%s)',len(c.authors)) % ('<b>%s</b>' % len(c.authors))) }
9 9 % endif
10 10 </h4>
11 11 <a href="#" id="show_authors" class="action_link">${_('Show All')}</a>
12 12 </div>
13 13
14 14 % if c.authors:
15 15 <ul class="sidebar-right-content">
16 % for email, user in sorted(c.authors, key=lambda e: c.file_last_commit.author_email!=e[0]):
16 % for email, user, commits in sorted(c.authors, key=lambda e: c.file_last_commit.author_email!=e[0]):
17 17 <li class="file_author">
18 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
18 <div class="tooltip" title="${h.tooltip(h.author_string(email))}">
19 19 ${base.gravatar(email, 16)}
20 <span class="user">${h.link_to_user(user)}</span>
20 <div class="user">${h.link_to_user(user)}</div>
21
22 % if c.file_author:
23 <span>- ${h.age_component(c.file_last_commit.date)}</span>
24 % elif c.file_last_commit.author_email==email:
25 <span> (${_('last author')})</span>
26 % endif
27
28 % if not c.file_author:
29 <span>
30 % if commits == 1:
31 ${commits} ${_('Commit')}
32 % else:
33 ${commits} ${_('Commits')}
34 % endif
35 </span>
36 % endif
37
21 38 </div>
22 % if c.file_author:
23 <div class="user-inline-data">- ${h.age_component(c.file_last_commit.date)}</div>
24 % elif c.file_last_commit.author_email==email:
25 <div class="user-inline-data"> (${_('last author')})</div>
26 % endif
27 39 </li>
28 40 % endfor
29 41 </ul>
30 42 % endif
@@ -1,324 +1,324 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title(*args)">
4 4 ${_('%s Files') % c.repo_name}
5 5 %if hasattr(c,'file'):
6 6 &middot; ${h.safe_unicode(c.file.path) or '\\'}
7 7 %endif
8 8
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Files')}
16 16 %if c.file:
17 17 @ ${h.show_id(c.commit)}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='files')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30 <div class="title">
31 31 ${self.repo_page_title(c.rhodecode_db_repo)}
32 32 </div>
33 33
34 34 <div id="pjax-container" class="summary">
35 35 <div id="files_data">
36 36 <%include file='files_pjax.mako'/>
37 37 </div>
38 38 </div>
39 39 <script>
40 40 var curState = {
41 41 commit_id: "${c.commit.raw_id}"
42 42 };
43 43
44 44 var getState = function(context) {
45 45 var url = $(location).attr('href');
46 var _base_url = '${h.url("files_home",repo_name=c.repo_name,revision='',f_path='')}';
47 var _annotate_url = '${h.url("files_annotate_home",repo_name=c.repo_name,revision='',f_path='')}';
46 var _base_url = '${h.route_path("repo_files",repo_name=c.repo_name,commit_id='',f_path='')}';
47 var _annotate_url = '${h.route_path("repo_files:annotated",repo_name=c.repo_name,commit_id='',f_path='')}';
48 48 _base_url = _base_url.replace('//', '/');
49 49 _annotate_url = _annotate_url.replace('//', '/');
50 50
51 51 //extract f_path from url.
52 52 var parts = url.split(_base_url);
53 53 if (parts.length != 2) {
54 54 parts = url.split(_annotate_url);
55 55 if (parts.length != 2) {
56 56 var rev = "tip";
57 57 var f_path = "";
58 58 } else {
59 59 var parts2 = parts[1].split('/');
60 60 var rev = parts2.shift(); // pop the first element which is the revision
61 61 var f_path = parts2.join('/');
62 62 }
63 63
64 64 } else {
65 65 var parts2 = parts[1].split('/');
66 66 var rev = parts2.shift(); // pop the first element which is the revision
67 67 var f_path = parts2.join('/');
68 68 }
69 69
70 var _node_list_url = pyroutes.url('files_nodelist_home',
70 var _node_list_url = pyroutes.url('repo_files_nodelist',
71 71 {repo_name: templateContext.repo_name,
72 revision: rev, f_path: f_path});
73 var _url_base = pyroutes.url('files_home',
72 commit_id: rev, f_path: f_path});
73 var _url_base = pyroutes.url('repo_files',
74 74 {repo_name: templateContext.repo_name,
75 revision: rev, f_path:'__FPATH__'});
75 commit_id: rev, f_path:'__FPATH__'});
76 76 return {
77 77 url: url,
78 78 f_path: f_path,
79 79 rev: rev,
80 80 commit_id: curState.commit_id,
81 81 node_list_url: _node_list_url,
82 82 url_base: _url_base
83 83 };
84 84 };
85 85
86 86 var metadataRequest = null;
87 87 var getFilesMetadata = function() {
88 88 if (metadataRequest && metadataRequest.readyState != 4) {
89 89 metadataRequest.abort();
90 90 }
91 91 if (fileSourcePage) {
92 92 return false;
93 93 }
94 94
95 95 if ($('#file-tree-wrapper').hasClass('full-load')) {
96 96 // in case our HTML wrapper has full-load class we don't
97 97 // trigger the async load of metadata
98 98 return false;
99 99 }
100 100
101 101 var state = getState('metadata');
102 102 var url_data = {
103 103 'repo_name': templateContext.repo_name,
104 104 'commit_id': state.commit_id,
105 105 'f_path': state.f_path
106 106 };
107 107
108 var url = pyroutes.url('files_nodetree_full', url_data);
108 var url = pyroutes.url('repo_nodetree_full', url_data);
109 109
110 110 metadataRequest = $.ajax({url: url});
111 111
112 112 metadataRequest.done(function(data) {
113 113 $('#file-tree').html(data);
114 114 timeagoActivate();
115 115 });
116 116 metadataRequest.fail(function (data, textStatus, errorThrown) {
117 117 console.log(data);
118 118 if (data.status != 0) {
119 119 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
120 120 }
121 121 });
122 122 };
123 123
124 124 var callbacks = function() {
125 125 var state = getState('callbacks');
126 126 timeagoActivate();
127 127
128 128 // used for history, and switch to
129 129 var initialCommitData = {
130 130 id: null,
131 131 text: '${_("Pick Commit")}',
132 132 type: 'sha',
133 133 raw_id: null,
134 134 files_url: null
135 135 };
136 136
137 137 if ($('#trimmed_message_box').height() < 50) {
138 138 $('#message_expand').hide();
139 139 }
140 140
141 141 $('#message_expand').on('click', function(e) {
142 142 $('#trimmed_message_box').css('max-height', 'none');
143 143 $(this).hide();
144 144 });
145 145
146 146 if (fileSourcePage) {
147 147 // variants for with source code, not tree view
148 148
149 149 // select code link event
150 150 $("#hlcode").mouseup(getSelectionLink);
151 151
152 152 // file history select2
153 153 select2FileHistorySwitcher('#diff1', initialCommitData, state);
154 154
155 155 // show at, diff to actions handlers
156 156 $('#diff1').on('change', function(e) {
157 157 $('#diff_to_commit').removeClass('disabled').removeAttr("disabled");
158 158 $('#diff_to_commit').val(_gettext('Diff to Commit ') + e.val.truncateAfter(8, '...'));
159 159
160 160 $('#show_at_commit').removeClass('disabled').removeAttr("disabled");
161 161 $('#show_at_commit').val(_gettext('Show at Commit ') + e.val.truncateAfter(8, '...'));
162 162 });
163 163
164 164 $('#diff_to_commit').on('click', function(e) {
165 165 var diff1 = $('#diff1').val();
166 166 var diff2 = $('#diff2').val();
167 167
168 168 var url_data = {
169 169 repo_name: templateContext.repo_name,
170 170 source_ref: diff1,
171 171 source_ref_type: 'rev',
172 172 target_ref: diff2,
173 173 target_ref_type: 'rev',
174 174 merge: 1,
175 175 f_path: state.f_path
176 176 };
177 177 window.location = pyroutes.url('compare_url', url_data);
178 178 });
179 179
180 180 $('#show_at_commit').on('click', function(e) {
181 181 var diff1 = $('#diff1').val();
182 182
183 183 var annotate = $('#annotate').val();
184 184 if (annotate === "True") {
185 var url = pyroutes.url('files_annotate_home',
185 var url = pyroutes.url('repo_files:annotated',
186 186 {'repo_name': templateContext.repo_name,
187 'revision': diff1, 'f_path': state.f_path});
187 'commit_id': diff1, 'f_path': state.f_path});
188 188 } else {
189 var url = pyroutes.url('files_home',
189 var url = pyroutes.url('repo_files',
190 190 {'repo_name': templateContext.repo_name,
191 'revision': diff1, 'f_path': state.f_path});
191 'commit_id': diff1, 'f_path': state.f_path});
192 192 }
193 193 window.location = url;
194 194
195 195 });
196 196
197 197 // show more authors
198 198 $('#show_authors').on('click', function(e) {
199 199 e.preventDefault();
200 var url = pyroutes.url('files_authors_home',
200 var url = pyroutes.url('repo_file_authors',
201 201 {'repo_name': templateContext.repo_name,
202 'revision': state.rev, 'f_path': state.f_path});
202 'commit_id': state.rev, 'f_path': state.f_path});
203 203
204 204 $.pjax({
205 205 url: url,
206 206 data: 'annotate=${"1" if c.annotate else "0"}',
207 207 container: '#file_authors',
208 208 push: false,
209 209 timeout: pjaxTimeout
210 210 }).complete(function(){
211 211 $('#show_authors').hide();
212 212 })
213 213 });
214 214
215 215 // load file short history
216 216 $('#file_history_overview').on('click', function(e) {
217 217 e.preventDefault();
218 218 path = state.f_path;
219 219 if (path.indexOf("#") >= 0) {
220 220 path = path.slice(0, path.indexOf("#"));
221 221 }
222 222 var url = pyroutes.url('changelog_file_home',
223 223 {'repo_name': templateContext.repo_name,
224 224 'revision': state.rev, 'f_path': path, 'limit': 6});
225 225 $('#file_history_container').show();
226 226 $('#file_history_container').html('<div class="file-history-inner">{0}</div>'.format(_gettext('Loading ...')));
227 227
228 228 $.pjax({
229 229 url: url,
230 230 container: '#file_history_container',
231 231 push: false,
232 232 timeout: pjaxTimeout
233 233 })
234 234 });
235 235
236 236 }
237 237 else {
238 238 getFilesMetadata();
239 239
240 240 // fuzzy file filter
241 241 fileBrowserListeners(state.node_list_url, state.url_base);
242 242
243 243 // switch to widget
244 244 select2RefSwitcher('#refs_filter', initialCommitData);
245 245 $('#refs_filter').on('change', function(e) {
246 246 var data = $('#refs_filter').select2('data');
247 247 curState.commit_id = data.raw_id;
248 248 $.pjax({url: data.files_url, container: '#pjax-container', timeout: pjaxTimeout});
249 249 });
250 250
251 251 $("#prev_commit_link").on('click', function(e) {
252 252 var data = $(this).data();
253 253 curState.commit_id = data.commitId;
254 254 });
255 255
256 256 $("#next_commit_link").on('click', function(e) {
257 257 var data = $(this).data();
258 258 curState.commit_id = data.commitId;
259 259 });
260 260
261 261 $('#at_rev').on("keypress", function(e) {
262 262 /* ENTER PRESSED */
263 263 if (e.keyCode === 13) {
264 264 var rev = $('#at_rev').val();
265 265 // explicit reload page here. with pjax entering bad input
266 266 // produces not so nice results
267 window.location = pyroutes.url('files_home',
267 window.location = pyroutes.url('repo_files',
268 268 {'repo_name': templateContext.repo_name,
269 'revision': rev, 'f_path': state.f_path});
269 'commit_id': rev, 'f_path': state.f_path});
270 270 }
271 271 });
272 272 }
273 273 };
274 274
275 275 var pjaxTimeout = 5000;
276 276
277 277 $(document).pjax(".pjax-link", "#pjax-container", {
278 278 "fragment": "#pjax-content",
279 279 "maxCacheLength": 1000,
280 280 "timeout": pjaxTimeout
281 281 });
282 282
283 283 // define global back/forward states
284 284 var isPjaxPopState = false;
285 285 $(document).on('pjax:popstate', function() {
286 286 isPjaxPopState = true;
287 287 });
288 288
289 289 $(document).on('pjax:end', function(xhr, options) {
290 290 if (isPjaxPopState) {
291 291 isPjaxPopState = false;
292 292 callbacks();
293 293 _NODEFILTER.resetFilter();
294 294 }
295 295
296 296 // run callback for tracking if defined for google analytics etc.
297 297 // this is used to trigger tracking on pjax
298 298 if (typeof window.rhodecode_statechange_callback !== 'undefined') {
299 299 var state = getState('statechange');
300 300 rhodecode_statechange_callback(state.url, null)
301 301 }
302 302 });
303 303
304 304 $(document).on('pjax:success', function(event, xhr, options) {
305 305 if (event.target.id == "file_history_container") {
306 306 $('#file_history_overview').hide();
307 307 $('#file_history_overview_full').show();
308 308 timeagoActivate();
309 309 } else {
310 310 callbacks();
311 311 }
312 312 });
313 313
314 314 $(document).ready(function() {
315 315 callbacks();
316 316 var search_GET = "${request.GET.get('search','')}";
317 317 if (search_GET == "1") {
318 318 _NODEFILTER.initFilter();
319 319 }
320 320 });
321 321
322 322 </script>
323 323
324 324 </%def>
@@ -1,236 +1,236 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Files Add') % 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()">
15 15 ${_('Add new file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <div class="title">
25 25 ${self.repo_page_title(c.rhodecode_db_repo)}
26 26 </div>
27 27 <div class="edit-file-title">
28 28 ${self.breadcrumbs()}
29 29 </div>
30 ${h.secure_form(h.url.current(),method='post',id='eform',enctype="multipart/form-data", class_="form-horizontal")}
30 ${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', method='POST', enctype="multipart/form-data", class_="form-horizontal")}
31 31 <div class="edit-file-fieldset">
32 32 <div class="fieldset">
33 33 <div id="destination-label" class="left-label">
34 34 ${_('Path')}:
35 35 </div>
36 36 <div class="right-content">
37 37 <div id="specify-custom-path-container">
38 38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
39 39 <a class="custom-path-link" id="specify-custom-path" href="#">${_('Specify Custom Path')}</a>
40 40 </div>
41 41 <div id="remove-custom-path-container" style="display: none;">
42 42 ${c.repo_name}/
43 43 <input type="input-small" value="${c.f_path}" size="46" name="location" id="location">
44 44 <a class="custom-path-link" id="remove-custom-path" href="#">${_('Remove Custom Path')}</a>
45 45 </div>
46 46 </div>
47 47 </div>
48 48 <div id="filename_container" class="fieldset">
49 49 <div class="filename-label left-label">
50 50 ${_('Filename')}:
51 51 </div>
52 52 <div class="right-content">
53 53 <input class="input-small" type="text" value="" size="46" name="filename" id="filename">
54 54 <p>${_('or')} <a id="upload_file_enable" href="#">${_('Upload File')}</a></p>
55 55 </div>
56 56 </div>
57 57 <div id="upload_file_container" class="fieldset" style="display: none;">
58 58 <div class="filename-label left-label">
59 59 ${_('Filename')}:
60 60 </div>
61 61 <div class="right-content">
62 62 <input class="input-small" type="text" value="" size="46" name="filename_upload" id="filename_upload" placeholder="${_('No file selected')}">
63 63 </div>
64 64 <div class="filename-label left-label file-upload-label">
65 65 ${_('Upload file')}:
66 66 </div>
67 67 <div class="right-content file-upload-input">
68 68 <label for="upload_file" class="btn btn-default">Browse</label>
69 69
70 70 <input type="file" name="upload_file" id="upload_file">
71 71 <p>${_('or')} <a id="file_enable" href="#">${_('Create New File')}</a></p>
72 72 </div>
73 73 </div>
74 74 </div>
75 75 <div class="table">
76 76 <div id="files_data">
77 77 <div id="codeblock" class="codeblock">
78 78 <div class="code-header form" id="set_mode_header">
79 79 <div class="fields">
80 80 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
81 81 <label for="line_wrap">${_('line wraps')}</label>
82 82 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
83 83
84 84 <div id="render_preview" class="btn btn-small preview hidden" >${_('Preview')}</div>
85 85 </div>
86 86 </div>
87 87 <div id="editor_container">
88 88 <pre id="editor_pre"></pre>
89 89 <textarea id="editor" name="content" ></textarea>
90 90 <div id="editor_preview"></div>
91 91 </div>
92 92 </div>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="edit-file-fieldset">
97 97 <div class="fieldset">
98 98 <div id="commit-message-label" class="commit-message-label left-label">
99 99 ${_('Commit Message')}:
100 100 </div>
101 101 <div class="right-content">
102 102 <div class="message">
103 103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
104 104 </div>
105 105 </div>
106 106 </div>
107 107 <div class="pull-right">
108 108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
109 109 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")}
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 </div>
114 114 <script type="text/javascript">
115 115
116 116 $('#commit_btn').on('click', function() {
117 117 var button = $(this);
118 118 if (button.hasClass('clicked')) {
119 119 button.attr('disabled', true);
120 120 } else {
121 121 button.addClass('clicked');
122 122 }
123 123 });
124 124
125 125 $('#specify-custom-path').on('click', function(e){
126 126 e.preventDefault();
127 127 $('#specify-custom-path-container').hide();
128 128 $('#remove-custom-path-container').show();
129 129 $('#destination-label').css('margin-top', '13px');
130 130 });
131 131
132 132 $('#remove-custom-path').on('click', function(e){
133 133 e.preventDefault();
134 134 $('#specify-custom-path-container').show();
135 135 $('#remove-custom-path-container').hide();
136 136 $('#location').val('${c.f_path}');
137 137 $('#destination-label').css('margin-top', '0');
138 138 });
139 139
140 140 var hide_upload = function(){
141 141 $('#files_data').show();
142 142 $('#upload_file_container').hide();
143 143 $('#filename_container').show();
144 144 };
145 145
146 146 $('#file_enable').on('click', function(e){
147 147 e.preventDefault();
148 148 hide_upload();
149 149 });
150 150
151 151 $('#upload_file_enable').on('click', function(e){
152 152 e.preventDefault();
153 153 $('#files_data').hide();
154 154 $('#upload_file_container').show();
155 155 $('#filename_container').hide();
156 156 if (detectIE() && detectIE() <= 9) {
157 157 $('#upload_file_container .file-upload-input label').hide();
158 158 $('#upload_file_container .file-upload-input span').hide();
159 159 $('#upload_file_container .file-upload-input input').show();
160 160 }
161 161 });
162 162
163 163 $('#upload_file').on('change', function() {
164 164 if (this.files && this.files[0]) {
165 165 $('#filename_upload').val(this.files[0].name);
166 166 }
167 167 });
168 168
169 169 hide_upload();
170 170
171 171 var renderer = "";
172 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path)}";
172 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}";
173 173 var myCodeMirror = initCodeMirror('editor', reset_url, false);
174 174
175 175 var modes_select = $('#set_mode');
176 176 fillCodeMirrorOptions(modes_select);
177 177
178 178 var filename_selector = '#filename';
179 179 var callback = function(filename, mimetype, mode){
180 180 CodeMirrorPreviewEnable(mode);
181 181 };
182 182 // on change of select field set mode
183 183 setCodeMirrorModeFromSelect(
184 184 modes_select, filename_selector, myCodeMirror, callback);
185 185
186 186 // on entering the new filename set mode, from given extension
187 187 setCodeMirrorModeFromInput(
188 188 modes_select, filename_selector, myCodeMirror, callback);
189 189
190 190 // if the file is renderable set line wraps automatically
191 191 if (renderer !== ""){
192 192 var line_wrap = 'on';
193 193 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
194 194 setCodeMirrorLineWrap(myCodeMirror, true);
195 195 }
196 196
197 197 // on select line wraps change the editor
198 198 $('#line_wrap').on('change', function(e){
199 199 var selected = e.currentTarget;
200 200 var line_wraps = {'on': true, 'off': false}[selected.value];
201 201 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
202 202 });
203 203
204 204 // render preview/edit button
205 205 $('#render_preview').on('click', function(e){
206 206 if($(this).hasClass('preview')){
207 207 $(this).removeClass('preview');
208 208 $(this).html("${_('Edit')}");
209 209 $('#editor_preview').show();
210 210 $(myCodeMirror.getWrapperElement()).hide();
211 211
212 212 var possible_renderer = {
213 213 'rst':'rst',
214 214 'markdown':'markdown',
215 215 'gfm': 'markdown'}[myCodeMirror.getMode().name];
216 216 var _text = myCodeMirror.getValue();
217 217 var _renderer = possible_renderer || DEFAULT_RENDERER;
218 218 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
219 219 $('#editor_preview').html(_gettext('Loading ...'));
220 220 var url = pyroutes.url('changeset_comment_preview', {'repo_name': '${c.repo_name}'});
221 221
222 222 ajaxPOST(url, post_data, function(o){
223 223 $('#editor_preview').html(o);
224 224 })
225 225 }
226 226 else{
227 227 $(this).addClass('preview');
228 228 $(this).html("${_('Preview')}");
229 229 $('#editor_preview').hide();
230 230 $(myCodeMirror.getWrapperElement()).show();
231 231 }
232 232 });
233 233 $('#filename').focus();
234 234
235 235 </script>
236 236 </%def>
@@ -1,49 +1,49 b''
1 1
2 2 <div id="codeblock" class="browserblock">
3 3 <div class="browser-header">
4 4 <div class="browser-nav">
5 5 ${h.form(h.url.current(), method='GET', id='at_rev_form')}
6 6 <div class="info_box">
7 7 ${h.hidden('refs_filter')}
8 8 <div class="info_box_elem previous">
9 9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class="pjax-link ${'disabled' if c.url_prev == '#' else ''}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-chevron-left"></i></a>
10 10 </div>
11 11 <div class="info_box_elem">${h.text('at_rev',value=c.commit.revision)}</div>
12 12 <div class="info_box_elem next">
13 13 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class="pjax-link ${'disabled' if c.url_next == '#' else ''}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-chevron-right"></i></a>
14 14 </div>
15 15 </div>
16 16 ${h.end_form()}
17 17
18 18 <div id="search_activate_id" class="search_activate">
19 19 <a class="btn btn-default" id="filter_activate" href="javascript:void(0)">${_('Search File List')}</a>
20 20 </div>
21 21 <div id="search_deactivate_id" class="search_activate hidden">
22 22 <a class="btn btn-default" id="filter_deactivate" href="javascript:void(0)">${_('Close File List')}</a>
23 23 </div>
24 24 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
25 25 <div title="${_('Add New File')}" class="btn btn-primary new-file">
26 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path, anchor='edit')}">
26 <a href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _anchor='edit')}">
27 27 ${_('Add File')}</a>
28 28 </div>
29 29 % endif
30 30 </div>
31 31
32 32 <div class="browser-search">
33 33 <div class="node-filter">
34 34 <div class="node_filter_box hidden" id="node_filter_box_loading" >${_('Loading file list...')}</div>
35 35 <div class="node_filter_box hidden" id="node_filter_box" >
36 36 <div class="node-filter-path">${h.get_last_path_part(c.file)}/</div>
37 37 <div class="node-filter-input">
38 38 <input class="init" type="text" name="filter" size="25" id="node_filter" autocomplete="off">
39 39 </div>
40 40 </div>
41 41 </div>
42 42 </div>
43 43 </div>
44 44 ## file tree is computed from caches, and filled in
45 45 <div id="file-tree">
46 ${c.file_tree}
46 ${c.file_tree |n}
47 47 </div>
48 48
49 49 </div>
@@ -1,82 +1,82 b''
1 1 <div id="file-tree-wrapper" class="browser-body ${'full-load' if c.full_load else ''}">
2 2 <table class="code-browser rctable">
3 3 <thead>
4 4 <tr>
5 5 <th>${_('Name')}</th>
6 6 <th>${_('Size')}</th>
7 7 <th>${_('Modified')}</th>
8 8 <th>${_('Last Commit')}</th>
9 9 <th>${_('Author')}</th>
10 10 </tr>
11 11 </thead>
12 12
13 13 <tbody id="tbody">
14 14 %if c.file.parent:
15 15 <tr class="parity0">
16 16 <td class="td-componentname">
17 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.file.parent.path)}" class="pjax-link">
17 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.parent.path)}" class="pjax-link">
18 18 <i class="icon-folder"></i>..
19 19 </a>
20 20 </td>
21 21 <td></td>
22 22 <td></td>
23 23 <td></td>
24 24 <td></td>
25 25 </tr>
26 26 %endif
27 27 %for cnt,node in enumerate(c.file):
28 28 <tr class="parity${cnt%2}">
29 29 <td class="td-componentname">
30 30 % if node.is_submodule():
31 31 <span class="submodule-dir">
32 32 % if node.url.startswith('http://') or node.url.startswith('https://'):
33 33 <a href="${node.url}">
34 34 <i class="icon-folder browser-dir"></i>${node.name}
35 35 </a>
36 36 % else:
37 37 <i class="icon-folder browser-dir"></i>${node.name}
38 38 % endif
39 39 </span>
40 40 % else:
41 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=h.safe_unicode(node.path))}" class="pjax-link">
41 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=h.safe_unicode(node.path))}" class="pjax-link">
42 42 <i class="${'icon-file browser-file' if node.is_file() else 'icon-folder browser-dir'}"></i>${node.name}
43 43 </a>
44 44 % endif
45 45 </td>
46 46 %if node.is_file():
47 47 <td class="td-size" data-attr-name="size">
48 48 % if c.full_load:
49 49 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
50 50 % else:
51 51 ${_('Loading ...')}
52 52 % endif
53 53 </td>
54 54 <td class="td-time" data-attr-name="modified_at">
55 55 % if c.full_load:
56 56 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
57 57 % endif
58 58 </td>
59 59 <td class="td-hash" data-attr-name="commit_id">
60 60 % if c.full_load:
61 61 <div class="tooltip" title="${h.tooltip(node.last_commit.message)}">
62 62 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.revision}:${node.last_commit.short_id}</pre>
63 63 </div>
64 64 % endif
65 65 </td>
66 66 <td class="td-user" data-attr-name="author">
67 67 % if c.full_load:
68 68 <span data-author="${node.last_commit.author}" title="${h.tooltip(node.last_commit.author)}">${h.gravatar_with_user(node.last_commit.author)|n}</span>
69 69 % endif
70 70 </td>
71 71 %else:
72 72 <td></td>
73 73 <td></td>
74 74 <td></td>
75 75 <td></td>
76 76 %endif
77 77 </tr>
78 78 %endfor
79 79 </tbody>
80 80 <tbody id="tbody_filtered"></tbody>
81 81 </table>
82 82 </div>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (1109 lines changed) Show them Hide them
General Comments 0
You need to be logged in to leave comments. Login now