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 TestFiles |
|
|
103 | class TestFilesViews(object): | |
|
49 | 104 | |
|
50 |
def test_ |
|
|
51 |
response = self.app.get( |
|
|
52 | controller='files', action='index', | |
|
53 |
repo_name=backend.repo_name, |
|
|
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_ |
|
|
136 | def test_show_files_links_submodules_with_absolute_url(self, backend_hg): | |
|
81 | 137 | repo = backend_hg['subrepos'] |
|
82 |
response = self.app.get( |
|
|
83 | controller='files', action='index', | |
|
84 |
repo_name=repo.repo_name, |
|
|
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_ |
|
|
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( |
|
|
93 | controller='files', action='index', | |
|
94 |
repo_name=repo.repo_name, |
|
|
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 |
|
|
112 |
response = self.app.get( |
|
|
113 | controller='files', action='index', | |
|
114 |
repo_name=backend.repo_name, |
|
|
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 |
|
|
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 |
|
|
|
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_ |
|
|
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( |
|
|
129 | controller='files', action='index', | |
|
130 | repo_name=backend.repo_name, | |
|
131 |
|
|
|
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_ |
|
|
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( |
|
|
155 | controller='files', action='index', | |
|
156 | repo_name=backend.repo_name, | |
|
157 |
|
|
|
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_ |
|
|
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( |
|
|
171 | controller='files', action='index', | |
|
172 | repo_name=backend.repo_name, | |
|
173 |
|
|
|
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( |
|
|
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( |
|
|
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_ |
|
|
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_ |
|
|
263 | response.mustcontain(expected_commits[backend.alias]) | |
|
243 | 264 | |
|
244 | def test_file_authors(self, backend): | |
|
245 |
response = self.app.get( |
|
|
246 |
|
|
|
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_ |
|
|
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_ |
|
|
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 |
|
|
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_ |
|
|
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_ |
|
|
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( |
|
|
412 | action='archivefile', | |
|
413 |
|
|
|
414 |
|
|
|
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 |
|
|
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( |
|
|
504 | repo_name=backend.repo_name, | |
|
505 |
|
|
|
506 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
682 |
|
|
|
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 |
|
|
|
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( |
|
|
741 |
'files_add_ |
|
|
742 | repo_name=backend.repo_name, | |
|
743 |
|
|
|
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( |
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
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( |
|
|
929 |
'files_ |
|
|
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 |
|
|
|
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 |
|
|
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. |
|
|
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 |
|
|
400 |
|
|
|
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. |
|
|
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 |
|
|
416 |
|
|
|
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. |
|
|
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 |
|
|
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. |
|
|
78 |
|
|
|
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. |
|
|
364 |
'files |
|
|
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 |
|
|
|
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'&', |
|
144 | 144 | ord('<'): u'<', |
|
145 | 145 | ord('>'): u'>', |
|
146 | 146 | ord('"'): u'"', |
|
147 | 147 | ord("'"): u''', |
|
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('<', '<').replace('>', '>') |
|
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 |
|
|
|
261 | route_path( | |
|
262 | 'repo_files', | |
|
262 | 263 | repo_name=repo_name, |
|
263 |
|
|
|
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 |
|
|
|
278 | route_path( | |
|
279 | 'repo_files', | |
|
278 | 280 | repo_name=repo_name, |
|
279 |
|
|
|
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 <%s>' % ( |
|
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 => \\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 => <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\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]', |
|
965 | 967 | '<div class="metatag" tag="see">see => \\1 </div>', value) |
|
966 | 968 | value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]', |
|
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)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]', |
|
969 | 971 | '<div class="metatag" tag="\\1">\\1 => <a href="/\\2">\\2</a></div>', value) |
|
970 | 972 | value = re.sub(r'\[(lang|language)\ \=\>\ *([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(' » '.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('<', '<').replace('>', '>') |
|
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 |
|
|
73 | 'repo_files', | |
|
74 | 74 | { |
|
75 | 75 | 'repo_name': repoName, |
|
76 |
' |
|
|
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 |
|
|
83 | 'repo_files', | |
|
84 | 84 | { |
|
85 | 85 | 'repo_name': repoName, |
|
86 |
' |
|
|
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('file |
|
|
81 |
{'repo_name': templateContext.repo_name, ' |
|
|
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 | © 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. |
|
|
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">×</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. |
|
|
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. |
|
|
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> |
|
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. |
|
|
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. |
|
|
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. |
|
|
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 | · ${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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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 |
' |
|
|
83 | 'f_path': fPath | |
|
82 | 'commit_id': commitId, | |
|
83 | 'f_path': fPath, | |
|
84 | 'line_anchor': lineNo | |
|
84 | 85 | }; |
|
85 |
window.location = pyroutes.url(' |
|
|
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. |
|
|
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(' » '.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. |
|
|
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. |
|
|
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. |
|
|
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=" |
|
|
18 | <div class="tooltip" title="${h.tooltip(h.author_string(email))}"> | |
|
19 | 19 | ${base.gravatar(email, 16)} |
|
20 |
< |
|
|
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 | · ${h.safe_unicode(c.file.path) or '\\'} |
|
7 | 7 | %endif |
|
8 | 8 | |
|
9 | 9 | %if c.rhodecode_name: |
|
10 | 10 | · ${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. |
|
|
47 |
var _annotate_url = '${h. |
|
|
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 |
|
|
70 | var _node_list_url = pyroutes.url('repo_files_nodelist', | |
|
71 | 71 | {repo_name: templateContext.repo_name, |
|
72 |
|
|
|
73 |
var _url_base = pyroutes.url('files |
|
|
72 | commit_id: rev, f_path: f_path}); | |
|
73 | var _url_base = pyroutes.url('repo_files', | |
|
74 | 74 | {repo_name: templateContext.repo_name, |
|
75 |
|
|
|
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(' |
|
|
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(' |
|
|
185 | var url = pyroutes.url('repo_files:annotated', | |
|
186 | 186 | {'repo_name': templateContext.repo_name, |
|
187 |
' |
|
|
187 | 'commit_id': diff1, 'f_path': state.f_path}); | |
|
188 | 188 | } else { |
|
189 |
var url = pyroutes.url('files |
|
|
189 | var url = pyroutes.url('repo_files', | |
|
190 | 190 | {'repo_name': templateContext.repo_name, |
|
191 |
' |
|
|
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('file |
|
|
200 | var url = pyroutes.url('repo_file_authors', | |
|
201 | 201 | {'repo_name': templateContext.repo_name, |
|
202 |
' |
|
|
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 |
|
|
267 | window.location = pyroutes.url('repo_files', | |
|
268 | 268 | {'repo_name': templateContext.repo_name, |
|
269 |
' |
|
|
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 | · ${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. |
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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 |
General Comments 0
You need to be logged in to leave comments.
Login now