##// END OF EJS Templates
annotations: replace annotated source code viewer with renderer...
dan -
r986:e7837355 default
parent child Browse files
Show More
@@ -0,0 +1,147 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2016 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
22 from itertools import groupby
23
24 from pygments import lex
25 # PYGMENTS_TOKEN_TYPES is used in a hot loop keep attribute lookups to a minimum
26 from pygments.token import STANDARD_TYPES as PYGMENTS_TOKEN_TYPES
27
28 from rhodecode.lib.helpers import get_lexer_for_filenode
29
30 def tokenize_file(content, lexer):
31 """
32 Use pygments to tokenize some content based on a lexer
33 ensuring all original new lines and whitespace is preserved
34 """
35
36 lexer.stripall = False
37 lexer.stripnl = False
38 lexer.ensurenl = False
39 return lex(content, lexer)
40
41
42 def pygment_token_class(token_type):
43 """ Convert a pygments token type to html class name """
44
45 fname = PYGMENTS_TOKEN_TYPES.get(token_type)
46 if fname:
47 return fname
48
49 aname = ''
50 while fname is None:
51 aname = '-' + token_type[-1] + aname
52 token_type = token_type.parent
53 fname = PYGMENTS_TOKEN_TYPES.get(token_type)
54
55 return fname + aname
56
57
58 def tokens_as_lines(tokens, split_string=u'\n'):
59 """
60 Take a list of (TokenType, text) tuples and split them by a string
61
62 eg. [(TEXT, 'some\ntext')] => [(TEXT, 'some'), (TEXT, 'text')]
63 """
64
65 buffer = []
66 for token_type, token_text in tokens:
67 parts = token_text.split(split_string)
68 for part in parts[:-1]:
69 buffer.append((token_type, part))
70 yield buffer
71 buffer = []
72
73 buffer.append((token_type, parts[-1]))
74
75 if buffer:
76 yield buffer
77
78
79 def filenode_as_lines_tokens(filenode):
80 """
81 Return a generator of lines with pygment tokens for a filenode eg:
82
83 [
84 (1, line1_tokens_list),
85 (2, line1_tokens_list]),
86 ]
87 """
88
89 return enumerate(
90 tokens_as_lines(
91 tokenize_file(
92 filenode.content, get_lexer_for_filenode(filenode)
93 )
94 ),
95 1)
96
97
98 def filenode_as_annotated_lines_tokens(filenode):
99 """
100 Take a file node and return a list of annotations => lines, if no annotation
101 is found, it will be None.
102
103 eg:
104
105 [
106 (annotation1, [
107 (1, line1_tokens_list),
108 (2, line2_tokens_list),
109 ]),
110 (annotation2, [
111 (3, line1_tokens_list),
112 ]),
113 (None, [
114 (4, line1_tokens_list),
115 ]),
116 (annotation1, [
117 (5, line1_tokens_list),
118 (6, line2_tokens_list),
119 ])
120 ]
121 """
122
123
124 # cache commit_getter lookups
125 commit_cache = {}
126 def _get_annotation(commit_id, commit_getter):
127 if commit_id not in commit_cache:
128 commit_cache[commit_id] = commit_getter()
129 return commit_cache[commit_id]
130
131 annotation_lookup = {
132 line_no: _get_annotation(commit_id, commit_getter)
133 for line_no, commit_id, commit_getter, line_content
134 in filenode.annotate
135 }
136
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 for line_no, tokens
139 in filenode_as_lines_tokens(filenode))
140
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142
143 for annotation, group in grouped_annotations_lines:
144 yield (
145 annotation, [(line_no, tokens)
146 for (_, line_no, tokens) in group]
147 )
@@ -0,0 +1,70 b''
1 <%def name="render_line(line_num, tokens,
2 annotation=None,
3 bgcolor=None)">
4 <%
5 # avoid module lookups for performance
6 from rhodecode.lib.codeblocks import pygment_token_class
7 from rhodecode.lib.helpers import html_escape
8 %>
9 <tr class="cb-line cb-line-fresh"
10 %if annotation:
11 data-revision="${annotation.revision}"
12 %endif
13 >
14 <td class="cb-lineno" id="L${line_num}">
15 <a data-line-no="${line_num}" href="#L${line_num}"></a>
16 </td>
17 <td class="cb-content cb-content-fresh"
18 %if bgcolor:
19 style="background: ${bgcolor}"
20 %endif
21 >${
22 ''.join(
23 '<span class="%s">%s</span>' %
24 (pygment_token_class(token_type), html_escape(token_text))
25 for token_type, token_text in tokens) + '\n' | n
26 }</td>
27 ## this ugly list comp is necessary for performance
28 </tr>
29 </%def>
30
31 <%def name="render_annotation_lines(annotation, lines, color_hasher)">
32 <%
33 rowspan = len(lines) + 1 # span the line's <tr> and annotation <tr>
34 %>
35 %if not annotation:
36 <tr class="cb-annotate">
37 <td class="cb-annotate-message" rowspan="${rowspan}"></td>
38 <td class="cb-annotate-revision" rowspan="${rowspan}"></td>
39 </tr>
40 %else:
41 <tr class="cb-annotate">
42 <td class="cb-annotate-info tooltip"
43 rowspan="${rowspan}"
44 title="Author: ${annotation.author | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
45 >
46 ${h.gravatar_with_user(annotation.author, 16) | n}
47 <strong class="cb-annotate-message">${
48 h.truncate(annotation.message, len(lines) * 30)
49 }</strong>
50 </td>
51 <td
52 class="cb-annotate-revision"
53 rowspan="${rowspan}"
54 data-revision="${annotation.revision}"
55 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
56 style="background: ${color_hasher(annotation.raw_id)}">
57 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}">
58 r${annotation.revision}
59 </a>
60 </td>
61 </tr>
62 %endif
63
64 %for line_num, tokens in lines:
65 ${render_line(line_num, tokens,
66 bgcolor=color_hasher(annotation and annotation.raw_id or ''),
67 annotation=annotation,
68 )}
69 %endfor
70 </%def>
@@ -1,1114 +1,1122 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Files controller for RhodeCode Enterprise
22 Files controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 import os
27 import os
28 import shutil
28 import shutil
29 import tempfile
29 import tempfile
30
30
31 from pylons import request, response, tmpl_context as c, url
31 from pylons import request, response, tmpl_context as c, url
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from webob.exc import HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPNotFound, HTTPBadRequest
35
35
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import diffs, helpers as h, caches
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
39 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils import jsonify, action_logger
40 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
41 convert_line_endings, detect_mode, safe_str, str2bool)
43 convert_line_endings, detect_mode, safe_str, str2bool)
42 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
44 from rhodecode.lib.base import BaseRepoController, render
46 from rhodecode.lib.base import BaseRepoController, render
45 from rhodecode.lib.vcs import path as vcspath
47 from rhodecode.lib.vcs import path as vcspath
46 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 from rhodecode.lib.vcs.conf import settings
49 from rhodecode.lib.vcs.conf import settings
48 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
49 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
50 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
51 NodeDoesNotExistError, CommitError, NodeError)
53 NodeDoesNotExistError, CommitError, NodeError)
52 from rhodecode.lib.vcs.nodes import FileNode
54 from rhodecode.lib.vcs.nodes import FileNode
53
55
54 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.repo import RepoModel
55 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
57
59
58 from rhodecode.controllers.changeset import (
60 from rhodecode.controllers.changeset import (
59 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
60 from rhodecode.lib.exceptions import NonRelativePathError
62 from rhodecode.lib.exceptions import NonRelativePathError
61
63
62 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
63
65
64
66
65 class FilesController(BaseRepoController):
67 class FilesController(BaseRepoController):
66
68
67 def __before__(self):
69 def __before__(self):
68 super(FilesController, self).__before__()
70 super(FilesController, self).__before__()
69 c.cut_off_limit = self.cut_off_limit_file
71 c.cut_off_limit = self.cut_off_limit_file
70
72
71 def _get_default_encoding(self):
73 def _get_default_encoding(self):
72 enc_list = getattr(c, 'default_encodings', [])
74 enc_list = getattr(c, 'default_encodings', [])
73 return enc_list[0] if enc_list else 'UTF-8'
75 return enc_list[0] if enc_list else 'UTF-8'
74
76
75 def __get_commit_or_redirect(self, commit_id, repo_name,
77 def __get_commit_or_redirect(self, commit_id, repo_name,
76 redirect_after=True):
78 redirect_after=True):
77 """
79 """
78 This is a safe way to get commit. If an error occurs it redirects to
80 This is a safe way to get commit. If an error occurs it redirects to
79 tip with proper message
81 tip with proper message
80
82
81 :param commit_id: id of commit to fetch
83 :param commit_id: id of commit to fetch
82 :param repo_name: repo name to redirect after
84 :param repo_name: repo name to redirect after
83 :param redirect_after: toggle redirection
85 :param redirect_after: toggle redirection
84 """
86 """
85 try:
87 try:
86 return c.rhodecode_repo.get_commit(commit_id)
88 return c.rhodecode_repo.get_commit(commit_id)
87 except EmptyRepositoryError:
89 except EmptyRepositoryError:
88 if not redirect_after:
90 if not redirect_after:
89 return None
91 return None
90 url_ = url('files_add_home',
92 url_ = url('files_add_home',
91 repo_name=c.repo_name,
93 repo_name=c.repo_name,
92 revision=0, f_path='', anchor='edit')
94 revision=0, f_path='', anchor='edit')
93 if h.HasRepoPermissionAny(
95 if h.HasRepoPermissionAny(
94 'repository.write', 'repository.admin')(c.repo_name):
96 'repository.write', 'repository.admin')(c.repo_name):
95 add_new = h.link_to(
97 add_new = h.link_to(
96 _('Click here to add a new file.'),
98 _('Click here to add a new file.'),
97 url_, class_="alert-link")
99 url_, class_="alert-link")
98 else:
100 else:
99 add_new = ""
101 add_new = ""
100 h.flash(h.literal(
102 h.flash(h.literal(
101 _('There are no files yet. %s') % add_new), category='warning')
103 _('There are no files yet. %s') % add_new), category='warning')
102 redirect(h.url('summary_home', repo_name=repo_name))
104 redirect(h.url('summary_home', repo_name=repo_name))
103 except (CommitDoesNotExistError, LookupError):
105 except (CommitDoesNotExistError, LookupError):
104 msg = _('No such commit exists for this repository')
106 msg = _('No such commit exists for this repository')
105 h.flash(msg, category='error')
107 h.flash(msg, category='error')
106 raise HTTPNotFound()
108 raise HTTPNotFound()
107 except RepositoryError as e:
109 except RepositoryError as e:
108 h.flash(safe_str(e), category='error')
110 h.flash(safe_str(e), category='error')
109 raise HTTPNotFound()
111 raise HTTPNotFound()
110
112
111 def __get_filenode_or_redirect(self, repo_name, commit, path):
113 def __get_filenode_or_redirect(self, repo_name, commit, path):
112 """
114 """
113 Returns file_node, if error occurs or given path is directory,
115 Returns file_node, if error occurs or given path is directory,
114 it'll redirect to top level path
116 it'll redirect to top level path
115
117
116 :param repo_name: repo_name
118 :param repo_name: repo_name
117 :param commit: given commit
119 :param commit: given commit
118 :param path: path to lookup
120 :param path: path to lookup
119 """
121 """
120 try:
122 try:
121 file_node = commit.get_node(path)
123 file_node = commit.get_node(path)
122 if file_node.is_dir():
124 if file_node.is_dir():
123 raise RepositoryError('The given path is a directory')
125 raise RepositoryError('The given path is a directory')
124 except CommitDoesNotExistError:
126 except CommitDoesNotExistError:
125 msg = _('No such commit exists for this repository')
127 msg = _('No such commit exists for this repository')
126 log.exception(msg)
128 log.exception(msg)
127 h.flash(msg, category='error')
129 h.flash(msg, category='error')
128 raise HTTPNotFound()
130 raise HTTPNotFound()
129 except RepositoryError as e:
131 except RepositoryError as e:
130 h.flash(safe_str(e), category='error')
132 h.flash(safe_str(e), category='error')
131 raise HTTPNotFound()
133 raise HTTPNotFound()
132
134
133 return file_node
135 return file_node
134
136
135 def __get_tree_cache_manager(self, repo_name, namespace_type):
137 def __get_tree_cache_manager(self, repo_name, namespace_type):
136 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
137 return caches.get_cache_manager('repo_cache_long', _namespace)
139 return caches.get_cache_manager('repo_cache_long', _namespace)
138
140
139 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
140 full_load=False, force=False):
142 full_load=False, force=False):
141 def _cached_tree():
143 def _cached_tree():
142 log.debug('Generating cached file tree for %s, %s, %s',
144 log.debug('Generating cached file tree for %s, %s, %s',
143 repo_name, commit_id, f_path)
145 repo_name, commit_id, f_path)
144 c.full_load = full_load
146 c.full_load = full_load
145 return render('files/files_browser_tree.html')
147 return render('files/files_browser_tree.html')
146
148
147 cache_manager = self.__get_tree_cache_manager(
149 cache_manager = self.__get_tree_cache_manager(
148 repo_name, caches.FILE_TREE)
150 repo_name, caches.FILE_TREE)
149
151
150 cache_key = caches.compute_key_from_params(
152 cache_key = caches.compute_key_from_params(
151 repo_name, commit_id, f_path)
153 repo_name, commit_id, f_path)
152
154
153 if force:
155 if force:
154 # we want to force recompute of caches
156 # we want to force recompute of caches
155 cache_manager.remove_value(cache_key)
157 cache_manager.remove_value(cache_key)
156
158
157 return cache_manager.get(cache_key, createfunc=_cached_tree)
159 return cache_manager.get(cache_key, createfunc=_cached_tree)
158
160
159 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
160 def _cached_nodes():
162 def _cached_nodes():
161 log.debug('Generating cached nodelist for %s, %s, %s',
163 log.debug('Generating cached nodelist for %s, %s, %s',
162 repo_name, commit_id, f_path)
164 repo_name, commit_id, f_path)
163 _d, _f = ScmModel().get_nodes(
165 _d, _f = ScmModel().get_nodes(
164 repo_name, commit_id, f_path, flat=False)
166 repo_name, commit_id, f_path, flat=False)
165 return _d + _f
167 return _d + _f
166
168
167 cache_manager = self.__get_tree_cache_manager(
169 cache_manager = self.__get_tree_cache_manager(
168 repo_name, caches.FILE_SEARCH_TREE_META)
170 repo_name, caches.FILE_SEARCH_TREE_META)
169
171
170 cache_key = caches.compute_key_from_params(
172 cache_key = caches.compute_key_from_params(
171 repo_name, commit_id, f_path)
173 repo_name, commit_id, f_path)
172 return cache_manager.get(cache_key, createfunc=_cached_nodes)
174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
173
175
174 @LoginRequired()
176 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
177 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
178 'repository.read', 'repository.write', 'repository.admin')
177 def index(
179 def index(
178 self, repo_name, revision, f_path, annotate=False, rendered=False):
180 self, repo_name, revision, f_path, annotate=False, rendered=False):
179 commit_id = revision
181 commit_id = revision
180
182
181 # redirect to given commit_id from form if given
183 # redirect to given commit_id from form if given
182 get_commit_id = request.GET.get('at_rev', None)
184 get_commit_id = request.GET.get('at_rev', None)
183 if get_commit_id:
185 if get_commit_id:
184 self.__get_commit_or_redirect(get_commit_id, repo_name)
186 self.__get_commit_or_redirect(get_commit_id, repo_name)
185
187
186 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
187 c.branch = request.GET.get('branch', None)
189 c.branch = request.GET.get('branch', None)
188 c.f_path = f_path
190 c.f_path = f_path
189 c.annotate = annotate
191 c.annotate = annotate
190 # default is false, but .rst/.md files later are autorendered, we can
192 # default is false, but .rst/.md files later are autorendered, we can
191 # overwrite autorendering by setting this GET flag
193 # overwrite autorendering by setting this GET flag
192 c.renderer = rendered or not request.GET.get('no-render', False)
194 c.renderer = rendered or not request.GET.get('no-render', False)
193
195
194 # prev link
196 # prev link
195 try:
197 try:
196 prev_commit = c.commit.prev(c.branch)
198 prev_commit = c.commit.prev(c.branch)
197 c.prev_commit = prev_commit
199 c.prev_commit = prev_commit
198 c.url_prev = url('files_home', repo_name=c.repo_name,
200 c.url_prev = url('files_home', repo_name=c.repo_name,
199 revision=prev_commit.raw_id, f_path=f_path)
201 revision=prev_commit.raw_id, f_path=f_path)
200 if c.branch:
202 if c.branch:
201 c.url_prev += '?branch=%s' % c.branch
203 c.url_prev += '?branch=%s' % c.branch
202 except (CommitDoesNotExistError, VCSError):
204 except (CommitDoesNotExistError, VCSError):
203 c.url_prev = '#'
205 c.url_prev = '#'
204 c.prev_commit = EmptyCommit()
206 c.prev_commit = EmptyCommit()
205
207
206 # next link
208 # next link
207 try:
209 try:
208 next_commit = c.commit.next(c.branch)
210 next_commit = c.commit.next(c.branch)
209 c.next_commit = next_commit
211 c.next_commit = next_commit
210 c.url_next = url('files_home', repo_name=c.repo_name,
212 c.url_next = url('files_home', repo_name=c.repo_name,
211 revision=next_commit.raw_id, f_path=f_path)
213 revision=next_commit.raw_id, f_path=f_path)
212 if c.branch:
214 if c.branch:
213 c.url_next += '?branch=%s' % c.branch
215 c.url_next += '?branch=%s' % c.branch
214 except (CommitDoesNotExistError, VCSError):
216 except (CommitDoesNotExistError, VCSError):
215 c.url_next = '#'
217 c.url_next = '#'
216 c.next_commit = EmptyCommit()
218 c.next_commit = EmptyCommit()
217
219
218 # files or dirs
220 # files or dirs
219 try:
221 try:
220 c.file = c.commit.get_node(f_path)
222 c.file = c.commit.get_node(f_path)
221 c.file_author = True
223 c.file_author = True
222 c.file_tree = ''
224 c.file_tree = ''
223 if c.file.is_file():
225 if c.file.is_file():
224 c.renderer = (
225 c.renderer and h.renderer_from_filename(c.file.path))
226 c.file_last_commit = c.file.last_commit
226 c.file_last_commit = c.file.last_commit
227 if c.annotate: # annotation has precedence over renderer
228 c.annotated_lines = filenode_as_annotated_lines_tokens(
229 c.file)
230 else:
231 c.renderer = (
232 c.renderer and h.renderer_from_filename(c.file.path))
233 if not c.renderer:
234 c.lines = filenode_as_lines_tokens(c.file)
227
235
228 c.on_branch_head = self._is_valid_head(
236 c.on_branch_head = self._is_valid_head(
229 commit_id, c.rhodecode_repo)
237 commit_id, c.rhodecode_repo)
230 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
238 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
231
239
232 author = c.file_last_commit.author
240 author = c.file_last_commit.author
233 c.authors = [(h.email(author),
241 c.authors = [(h.email(author),
234 h.person(author, 'username_or_name_or_email'))]
242 h.person(author, 'username_or_name_or_email'))]
235 else:
243 else:
236 c.authors = []
244 c.authors = []
237 c.file_tree = self._get_tree_at_commit(
245 c.file_tree = self._get_tree_at_commit(
238 repo_name, c.commit.raw_id, f_path)
246 repo_name, c.commit.raw_id, f_path)
239
247
240 except RepositoryError as e:
248 except RepositoryError as e:
241 h.flash(safe_str(e), category='error')
249 h.flash(safe_str(e), category='error')
242 raise HTTPNotFound()
250 raise HTTPNotFound()
243
251
244 if request.environ.get('HTTP_X_PJAX'):
252 if request.environ.get('HTTP_X_PJAX'):
245 return render('files/files_pjax.html')
253 return render('files/files_pjax.html')
246
254
247 return render('files/files.html')
255 return render('files/files.html')
248
256
249 @LoginRequired()
257 @LoginRequired()
250 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
258 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
251 'repository.admin')
259 'repository.admin')
252 @jsonify
260 @jsonify
253 def history(self, repo_name, revision, f_path):
261 def history(self, repo_name, revision, f_path):
254 commit = self.__get_commit_or_redirect(revision, repo_name)
262 commit = self.__get_commit_or_redirect(revision, repo_name)
255 f_path = f_path
263 f_path = f_path
256 _file = commit.get_node(f_path)
264 _file = commit.get_node(f_path)
257 if _file.is_file():
265 if _file.is_file():
258 file_history, _hist = self._get_node_history(commit, f_path)
266 file_history, _hist = self._get_node_history(commit, f_path)
259
267
260 res = []
268 res = []
261 for obj in file_history:
269 for obj in file_history:
262 res.append({
270 res.append({
263 'text': obj[1],
271 'text': obj[1],
264 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
272 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
265 })
273 })
266
274
267 data = {
275 data = {
268 'more': False,
276 'more': False,
269 'results': res
277 'results': res
270 }
278 }
271 return data
279 return data
272
280
273 @LoginRequired()
281 @LoginRequired()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
282 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
283 'repository.admin')
276 def authors(self, repo_name, revision, f_path):
284 def authors(self, repo_name, revision, f_path):
277 commit = self.__get_commit_or_redirect(revision, repo_name)
285 commit = self.__get_commit_or_redirect(revision, repo_name)
278 file_node = commit.get_node(f_path)
286 file_node = commit.get_node(f_path)
279 if file_node.is_file():
287 if file_node.is_file():
280 c.file_last_commit = file_node.last_commit
288 c.file_last_commit = file_node.last_commit
281 if request.GET.get('annotate') == '1':
289 if request.GET.get('annotate') == '1':
282 # use _hist from annotation if annotation mode is on
290 # use _hist from annotation if annotation mode is on
283 commit_ids = set(x[1] for x in file_node.annotate)
291 commit_ids = set(x[1] for x in file_node.annotate)
284 _hist = (
292 _hist = (
285 c.rhodecode_repo.get_commit(commit_id)
293 c.rhodecode_repo.get_commit(commit_id)
286 for commit_id in commit_ids)
294 for commit_id in commit_ids)
287 else:
295 else:
288 _f_history, _hist = self._get_node_history(commit, f_path)
296 _f_history, _hist = self._get_node_history(commit, f_path)
289 c.file_author = False
297 c.file_author = False
290 c.authors = []
298 c.authors = []
291 for author in set(commit.author for commit in _hist):
299 for author in set(commit.author for commit in _hist):
292 c.authors.append((
300 c.authors.append((
293 h.email(author),
301 h.email(author),
294 h.person(author, 'username_or_name_or_email')))
302 h.person(author, 'username_or_name_or_email')))
295 return render('files/file_authors_box.html')
303 return render('files/file_authors_box.html')
296
304
297 @LoginRequired()
305 @LoginRequired()
298 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
299 'repository.admin')
307 'repository.admin')
300 def rawfile(self, repo_name, revision, f_path):
308 def rawfile(self, repo_name, revision, f_path):
301 """
309 """
302 Action for download as raw
310 Action for download as raw
303 """
311 """
304 commit = self.__get_commit_or_redirect(revision, repo_name)
312 commit = self.__get_commit_or_redirect(revision, repo_name)
305 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
313 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
306
314
307 response.content_disposition = 'attachment; filename=%s' % \
315 response.content_disposition = 'attachment; filename=%s' % \
308 safe_str(f_path.split(Repository.NAME_SEP)[-1])
316 safe_str(f_path.split(Repository.NAME_SEP)[-1])
309
317
310 response.content_type = file_node.mimetype
318 response.content_type = file_node.mimetype
311 charset = self._get_default_encoding()
319 charset = self._get_default_encoding()
312 if charset:
320 if charset:
313 response.charset = charset
321 response.charset = charset
314
322
315 return file_node.content
323 return file_node.content
316
324
317 @LoginRequired()
325 @LoginRequired()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
327 'repository.admin')
320 def raw(self, repo_name, revision, f_path):
328 def raw(self, repo_name, revision, f_path):
321 """
329 """
322 Action for show as raw, some mimetypes are "rendered",
330 Action for show as raw, some mimetypes are "rendered",
323 those include images, icons.
331 those include images, icons.
324 """
332 """
325 commit = self.__get_commit_or_redirect(revision, repo_name)
333 commit = self.__get_commit_or_redirect(revision, repo_name)
326 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
334 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
327
335
328 raw_mimetype_mapping = {
336 raw_mimetype_mapping = {
329 # map original mimetype to a mimetype used for "show as raw"
337 # map original mimetype to a mimetype used for "show as raw"
330 # you can also provide a content-disposition to override the
338 # you can also provide a content-disposition to override the
331 # default "attachment" disposition.
339 # default "attachment" disposition.
332 # orig_type: (new_type, new_dispo)
340 # orig_type: (new_type, new_dispo)
333
341
334 # show images inline:
342 # show images inline:
335 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
343 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
336 # for example render an SVG with javascript inside or even render
344 # for example render an SVG with javascript inside or even render
337 # HTML.
345 # HTML.
338 'image/x-icon': ('image/x-icon', 'inline'),
346 'image/x-icon': ('image/x-icon', 'inline'),
339 'image/png': ('image/png', 'inline'),
347 'image/png': ('image/png', 'inline'),
340 'image/gif': ('image/gif', 'inline'),
348 'image/gif': ('image/gif', 'inline'),
341 'image/jpeg': ('image/jpeg', 'inline'),
349 'image/jpeg': ('image/jpeg', 'inline'),
342 }
350 }
343
351
344 mimetype = file_node.mimetype
352 mimetype = file_node.mimetype
345 try:
353 try:
346 mimetype, dispo = raw_mimetype_mapping[mimetype]
354 mimetype, dispo = raw_mimetype_mapping[mimetype]
347 except KeyError:
355 except KeyError:
348 # we don't know anything special about this, handle it safely
356 # we don't know anything special about this, handle it safely
349 if file_node.is_binary:
357 if file_node.is_binary:
350 # do same as download raw for binary files
358 # do same as download raw for binary files
351 mimetype, dispo = 'application/octet-stream', 'attachment'
359 mimetype, dispo = 'application/octet-stream', 'attachment'
352 else:
360 else:
353 # do not just use the original mimetype, but force text/plain,
361 # do not just use the original mimetype, but force text/plain,
354 # otherwise it would serve text/html and that might be unsafe.
362 # otherwise it would serve text/html and that might be unsafe.
355 # Note: underlying vcs library fakes text/plain mimetype if the
363 # Note: underlying vcs library fakes text/plain mimetype if the
356 # mimetype can not be determined and it thinks it is not
364 # mimetype can not be determined and it thinks it is not
357 # binary.This might lead to erroneous text display in some
365 # binary.This might lead to erroneous text display in some
358 # cases, but helps in other cases, like with text files
366 # cases, but helps in other cases, like with text files
359 # without extension.
367 # without extension.
360 mimetype, dispo = 'text/plain', 'inline'
368 mimetype, dispo = 'text/plain', 'inline'
361
369
362 if dispo == 'attachment':
370 if dispo == 'attachment':
363 dispo = 'attachment; filename=%s' % safe_str(
371 dispo = 'attachment; filename=%s' % safe_str(
364 f_path.split(os.sep)[-1])
372 f_path.split(os.sep)[-1])
365
373
366 response.content_disposition = dispo
374 response.content_disposition = dispo
367 response.content_type = mimetype
375 response.content_type = mimetype
368 charset = self._get_default_encoding()
376 charset = self._get_default_encoding()
369 if charset:
377 if charset:
370 response.charset = charset
378 response.charset = charset
371 return file_node.content
379 return file_node.content
372
380
373 @CSRFRequired()
381 @CSRFRequired()
374 @LoginRequired()
382 @LoginRequired()
375 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
383 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
376 def delete(self, repo_name, revision, f_path):
384 def delete(self, repo_name, revision, f_path):
377 commit_id = revision
385 commit_id = revision
378
386
379 repo = c.rhodecode_db_repo
387 repo = c.rhodecode_db_repo
380 if repo.enable_locking and repo.locked[0]:
388 if repo.enable_locking and repo.locked[0]:
381 h.flash(_('This repository has been locked by %s on %s')
389 h.flash(_('This repository has been locked by %s on %s')
382 % (h.person_by_id(repo.locked[0]),
390 % (h.person_by_id(repo.locked[0]),
383 h.format_date(h.time_to_datetime(repo.locked[1]))),
391 h.format_date(h.time_to_datetime(repo.locked[1]))),
384 'warning')
392 'warning')
385 return redirect(h.url('files_home',
393 return redirect(h.url('files_home',
386 repo_name=repo_name, revision='tip'))
394 repo_name=repo_name, revision='tip'))
387
395
388 if not self._is_valid_head(commit_id, repo.scm_instance()):
396 if not self._is_valid_head(commit_id, repo.scm_instance()):
389 h.flash(_('You can only delete files with revision '
397 h.flash(_('You can only delete files with revision '
390 'being a valid branch '), category='warning')
398 'being a valid branch '), category='warning')
391 return redirect(h.url('files_home',
399 return redirect(h.url('files_home',
392 repo_name=repo_name, revision='tip',
400 repo_name=repo_name, revision='tip',
393 f_path=f_path))
401 f_path=f_path))
394
402
395 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
403 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
396 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
404 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
397
405
398 c.default_message = _(
406 c.default_message = _(
399 'Deleted file %s via RhodeCode Enterprise') % (f_path)
407 'Deleted file %s via RhodeCode Enterprise') % (f_path)
400 c.f_path = f_path
408 c.f_path = f_path
401 node_path = f_path
409 node_path = f_path
402 author = c.rhodecode_user.full_contact
410 author = c.rhodecode_user.full_contact
403 message = request.POST.get('message') or c.default_message
411 message = request.POST.get('message') or c.default_message
404 try:
412 try:
405 nodes = {
413 nodes = {
406 node_path: {
414 node_path: {
407 'content': ''
415 'content': ''
408 }
416 }
409 }
417 }
410 self.scm_model.delete_nodes(
418 self.scm_model.delete_nodes(
411 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
419 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
412 message=message,
420 message=message,
413 nodes=nodes,
421 nodes=nodes,
414 parent_commit=c.commit,
422 parent_commit=c.commit,
415 author=author,
423 author=author,
416 )
424 )
417
425
418 h.flash(_('Successfully deleted file %s') % f_path,
426 h.flash(_('Successfully deleted file %s') % f_path,
419 category='success')
427 category='success')
420 except Exception:
428 except Exception:
421 msg = _('Error occurred during commit')
429 msg = _('Error occurred during commit')
422 log.exception(msg)
430 log.exception(msg)
423 h.flash(msg, category='error')
431 h.flash(msg, category='error')
424 return redirect(url('changeset_home',
432 return redirect(url('changeset_home',
425 repo_name=c.repo_name, revision='tip'))
433 repo_name=c.repo_name, revision='tip'))
426
434
427 @LoginRequired()
435 @LoginRequired()
428 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
436 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
429 def delete_home(self, repo_name, revision, f_path):
437 def delete_home(self, repo_name, revision, f_path):
430 commit_id = revision
438 commit_id = revision
431
439
432 repo = c.rhodecode_db_repo
440 repo = c.rhodecode_db_repo
433 if repo.enable_locking and repo.locked[0]:
441 if repo.enable_locking and repo.locked[0]:
434 h.flash(_('This repository has been locked by %s on %s')
442 h.flash(_('This repository has been locked by %s on %s')
435 % (h.person_by_id(repo.locked[0]),
443 % (h.person_by_id(repo.locked[0]),
436 h.format_date(h.time_to_datetime(repo.locked[1]))),
444 h.format_date(h.time_to_datetime(repo.locked[1]))),
437 'warning')
445 'warning')
438 return redirect(h.url('files_home',
446 return redirect(h.url('files_home',
439 repo_name=repo_name, revision='tip'))
447 repo_name=repo_name, revision='tip'))
440
448
441 if not self._is_valid_head(commit_id, repo.scm_instance()):
449 if not self._is_valid_head(commit_id, repo.scm_instance()):
442 h.flash(_('You can only delete files with revision '
450 h.flash(_('You can only delete files with revision '
443 'being a valid branch '), category='warning')
451 'being a valid branch '), category='warning')
444 return redirect(h.url('files_home',
452 return redirect(h.url('files_home',
445 repo_name=repo_name, revision='tip',
453 repo_name=repo_name, revision='tip',
446 f_path=f_path))
454 f_path=f_path))
447
455
448 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
456 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
449 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
457 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
450
458
451 c.default_message = _(
459 c.default_message = _(
452 'Deleted file %s via RhodeCode Enterprise') % (f_path)
460 'Deleted file %s via RhodeCode Enterprise') % (f_path)
453 c.f_path = f_path
461 c.f_path = f_path
454
462
455 return render('files/files_delete.html')
463 return render('files/files_delete.html')
456
464
457 @CSRFRequired()
465 @CSRFRequired()
458 @LoginRequired()
466 @LoginRequired()
459 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
467 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
460 def edit(self, repo_name, revision, f_path):
468 def edit(self, repo_name, revision, f_path):
461 commit_id = revision
469 commit_id = revision
462
470
463 repo = c.rhodecode_db_repo
471 repo = c.rhodecode_db_repo
464 if repo.enable_locking and repo.locked[0]:
472 if repo.enable_locking and repo.locked[0]:
465 h.flash(_('This repository has been locked by %s on %s')
473 h.flash(_('This repository has been locked by %s on %s')
466 % (h.person_by_id(repo.locked[0]),
474 % (h.person_by_id(repo.locked[0]),
467 h.format_date(h.time_to_datetime(repo.locked[1]))),
475 h.format_date(h.time_to_datetime(repo.locked[1]))),
468 'warning')
476 'warning')
469 return redirect(h.url('files_home',
477 return redirect(h.url('files_home',
470 repo_name=repo_name, revision='tip'))
478 repo_name=repo_name, revision='tip'))
471
479
472 if not self._is_valid_head(commit_id, repo.scm_instance()):
480 if not self._is_valid_head(commit_id, repo.scm_instance()):
473 h.flash(_('You can only edit files with revision '
481 h.flash(_('You can only edit files with revision '
474 'being a valid branch '), category='warning')
482 'being a valid branch '), category='warning')
475 return redirect(h.url('files_home',
483 return redirect(h.url('files_home',
476 repo_name=repo_name, revision='tip',
484 repo_name=repo_name, revision='tip',
477 f_path=f_path))
485 f_path=f_path))
478
486
479 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
487 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
480 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
488 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
481
489
482 if c.file.is_binary:
490 if c.file.is_binary:
483 return redirect(url('files_home', repo_name=c.repo_name,
491 return redirect(url('files_home', repo_name=c.repo_name,
484 revision=c.commit.raw_id, f_path=f_path))
492 revision=c.commit.raw_id, f_path=f_path))
485 c.default_message = _(
493 c.default_message = _(
486 'Edited file %s via RhodeCode Enterprise') % (f_path)
494 'Edited file %s via RhodeCode Enterprise') % (f_path)
487 c.f_path = f_path
495 c.f_path = f_path
488 old_content = c.file.content
496 old_content = c.file.content
489 sl = old_content.splitlines(1)
497 sl = old_content.splitlines(1)
490 first_line = sl[0] if sl else ''
498 first_line = sl[0] if sl else ''
491
499
492 # modes: 0 - Unix, 1 - Mac, 2 - DOS
500 # modes: 0 - Unix, 1 - Mac, 2 - DOS
493 mode = detect_mode(first_line, 0)
501 mode = detect_mode(first_line, 0)
494 content = convert_line_endings(request.POST.get('content', ''), mode)
502 content = convert_line_endings(request.POST.get('content', ''), mode)
495
503
496 message = request.POST.get('message') or c.default_message
504 message = request.POST.get('message') or c.default_message
497 org_f_path = c.file.unicode_path
505 org_f_path = c.file.unicode_path
498 filename = request.POST['filename']
506 filename = request.POST['filename']
499 org_filename = c.file.name
507 org_filename = c.file.name
500
508
501 if content == old_content and filename == org_filename:
509 if content == old_content and filename == org_filename:
502 h.flash(_('No changes'), category='warning')
510 h.flash(_('No changes'), category='warning')
503 return redirect(url('changeset_home', repo_name=c.repo_name,
511 return redirect(url('changeset_home', repo_name=c.repo_name,
504 revision='tip'))
512 revision='tip'))
505 try:
513 try:
506 mapping = {
514 mapping = {
507 org_f_path: {
515 org_f_path: {
508 'org_filename': org_f_path,
516 'org_filename': org_f_path,
509 'filename': os.path.join(c.file.dir_path, filename),
517 'filename': os.path.join(c.file.dir_path, filename),
510 'content': content,
518 'content': content,
511 'lexer': '',
519 'lexer': '',
512 'op': 'mod',
520 'op': 'mod',
513 }
521 }
514 }
522 }
515
523
516 ScmModel().update_nodes(
524 ScmModel().update_nodes(
517 user=c.rhodecode_user.user_id,
525 user=c.rhodecode_user.user_id,
518 repo=c.rhodecode_db_repo,
526 repo=c.rhodecode_db_repo,
519 message=message,
527 message=message,
520 nodes=mapping,
528 nodes=mapping,
521 parent_commit=c.commit,
529 parent_commit=c.commit,
522 )
530 )
523
531
524 h.flash(_('Successfully committed to %s') % f_path,
532 h.flash(_('Successfully committed to %s') % f_path,
525 category='success')
533 category='success')
526 except Exception:
534 except Exception:
527 msg = _('Error occurred during commit')
535 msg = _('Error occurred during commit')
528 log.exception(msg)
536 log.exception(msg)
529 h.flash(msg, category='error')
537 h.flash(msg, category='error')
530 return redirect(url('changeset_home',
538 return redirect(url('changeset_home',
531 repo_name=c.repo_name, revision='tip'))
539 repo_name=c.repo_name, revision='tip'))
532
540
533 @LoginRequired()
541 @LoginRequired()
534 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
542 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
535 def edit_home(self, repo_name, revision, f_path):
543 def edit_home(self, repo_name, revision, f_path):
536 commit_id = revision
544 commit_id = revision
537
545
538 repo = c.rhodecode_db_repo
546 repo = c.rhodecode_db_repo
539 if repo.enable_locking and repo.locked[0]:
547 if repo.enable_locking and repo.locked[0]:
540 h.flash(_('This repository has been locked by %s on %s')
548 h.flash(_('This repository has been locked by %s on %s')
541 % (h.person_by_id(repo.locked[0]),
549 % (h.person_by_id(repo.locked[0]),
542 h.format_date(h.time_to_datetime(repo.locked[1]))),
550 h.format_date(h.time_to_datetime(repo.locked[1]))),
543 'warning')
551 'warning')
544 return redirect(h.url('files_home',
552 return redirect(h.url('files_home',
545 repo_name=repo_name, revision='tip'))
553 repo_name=repo_name, revision='tip'))
546
554
547 if not self._is_valid_head(commit_id, repo.scm_instance()):
555 if not self._is_valid_head(commit_id, repo.scm_instance()):
548 h.flash(_('You can only edit files with revision '
556 h.flash(_('You can only edit files with revision '
549 'being a valid branch '), category='warning')
557 'being a valid branch '), category='warning')
550 return redirect(h.url('files_home',
558 return redirect(h.url('files_home',
551 repo_name=repo_name, revision='tip',
559 repo_name=repo_name, revision='tip',
552 f_path=f_path))
560 f_path=f_path))
553
561
554 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
562 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
555 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
563 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
556
564
557 if c.file.is_binary:
565 if c.file.is_binary:
558 return redirect(url('files_home', repo_name=c.repo_name,
566 return redirect(url('files_home', repo_name=c.repo_name,
559 revision=c.commit.raw_id, f_path=f_path))
567 revision=c.commit.raw_id, f_path=f_path))
560 c.default_message = _(
568 c.default_message = _(
561 'Edited file %s via RhodeCode Enterprise') % (f_path)
569 'Edited file %s via RhodeCode Enterprise') % (f_path)
562 c.f_path = f_path
570 c.f_path = f_path
563
571
564 return render('files/files_edit.html')
572 return render('files/files_edit.html')
565
573
566 def _is_valid_head(self, commit_id, repo):
574 def _is_valid_head(self, commit_id, repo):
567 # check if commit is a branch identifier- basically we cannot
575 # check if commit is a branch identifier- basically we cannot
568 # create multiple heads via file editing
576 # create multiple heads via file editing
569 valid_heads = repo.branches.keys() + repo.branches.values()
577 valid_heads = repo.branches.keys() + repo.branches.values()
570
578
571 if h.is_svn(repo) and not repo.is_empty():
579 if h.is_svn(repo) and not repo.is_empty():
572 # Note: Subversion only has one head, we add it here in case there
580 # Note: Subversion only has one head, we add it here in case there
573 # is no branch matched.
581 # is no branch matched.
574 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
582 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
575
583
576 # check if commit is a branch name or branch hash
584 # check if commit is a branch name or branch hash
577 return commit_id in valid_heads
585 return commit_id in valid_heads
578
586
579 @CSRFRequired()
587 @CSRFRequired()
580 @LoginRequired()
588 @LoginRequired()
581 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
589 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
582 def add(self, repo_name, revision, f_path):
590 def add(self, repo_name, revision, f_path):
583 repo = Repository.get_by_repo_name(repo_name)
591 repo = Repository.get_by_repo_name(repo_name)
584 if repo.enable_locking and repo.locked[0]:
592 if repo.enable_locking and repo.locked[0]:
585 h.flash(_('This repository has been locked by %s on %s')
593 h.flash(_('This repository has been locked by %s on %s')
586 % (h.person_by_id(repo.locked[0]),
594 % (h.person_by_id(repo.locked[0]),
587 h.format_date(h.time_to_datetime(repo.locked[1]))),
595 h.format_date(h.time_to_datetime(repo.locked[1]))),
588 'warning')
596 'warning')
589 return redirect(h.url('files_home',
597 return redirect(h.url('files_home',
590 repo_name=repo_name, revision='tip'))
598 repo_name=repo_name, revision='tip'))
591
599
592 r_post = request.POST
600 r_post = request.POST
593
601
594 c.commit = self.__get_commit_or_redirect(
602 c.commit = self.__get_commit_or_redirect(
595 revision, repo_name, redirect_after=False)
603 revision, repo_name, redirect_after=False)
596 if c.commit is None:
604 if c.commit is None:
597 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
605 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
598 c.default_message = (_('Added file via RhodeCode Enterprise'))
606 c.default_message = (_('Added file via RhodeCode Enterprise'))
599 c.f_path = f_path
607 c.f_path = f_path
600 unix_mode = 0
608 unix_mode = 0
601 content = convert_line_endings(r_post.get('content', ''), unix_mode)
609 content = convert_line_endings(r_post.get('content', ''), unix_mode)
602
610
603 message = r_post.get('message') or c.default_message
611 message = r_post.get('message') or c.default_message
604 filename = r_post.get('filename')
612 filename = r_post.get('filename')
605 location = r_post.get('location', '') # dir location
613 location = r_post.get('location', '') # dir location
606 file_obj = r_post.get('upload_file', None)
614 file_obj = r_post.get('upload_file', None)
607
615
608 if file_obj is not None and hasattr(file_obj, 'filename'):
616 if file_obj is not None and hasattr(file_obj, 'filename'):
609 filename = file_obj.filename
617 filename = file_obj.filename
610 content = file_obj.file
618 content = file_obj.file
611
619
612 if hasattr(content, 'file'):
620 if hasattr(content, 'file'):
613 # non posix systems store real file under file attr
621 # non posix systems store real file under file attr
614 content = content.file
622 content = content.file
615
623
616 # If there's no commit, redirect to repo summary
624 # If there's no commit, redirect to repo summary
617 if type(c.commit) is EmptyCommit:
625 if type(c.commit) is EmptyCommit:
618 redirect_url = "summary_home"
626 redirect_url = "summary_home"
619 else:
627 else:
620 redirect_url = "changeset_home"
628 redirect_url = "changeset_home"
621
629
622 if not filename:
630 if not filename:
623 h.flash(_('No filename'), category='warning')
631 h.flash(_('No filename'), category='warning')
624 return redirect(url(redirect_url, repo_name=c.repo_name,
632 return redirect(url(redirect_url, repo_name=c.repo_name,
625 revision='tip'))
633 revision='tip'))
626
634
627 # extract the location from filename,
635 # extract the location from filename,
628 # allows using foo/bar.txt syntax to create subdirectories
636 # allows using foo/bar.txt syntax to create subdirectories
629 subdir_loc = filename.rsplit('/', 1)
637 subdir_loc = filename.rsplit('/', 1)
630 if len(subdir_loc) == 2:
638 if len(subdir_loc) == 2:
631 location = os.path.join(location, subdir_loc[0])
639 location = os.path.join(location, subdir_loc[0])
632
640
633 # strip all crap out of file, just leave the basename
641 # strip all crap out of file, just leave the basename
634 filename = os.path.basename(filename)
642 filename = os.path.basename(filename)
635 node_path = os.path.join(location, filename)
643 node_path = os.path.join(location, filename)
636 author = c.rhodecode_user.full_contact
644 author = c.rhodecode_user.full_contact
637
645
638 try:
646 try:
639 nodes = {
647 nodes = {
640 node_path: {
648 node_path: {
641 'content': content
649 'content': content
642 }
650 }
643 }
651 }
644 self.scm_model.create_nodes(
652 self.scm_model.create_nodes(
645 user=c.rhodecode_user.user_id,
653 user=c.rhodecode_user.user_id,
646 repo=c.rhodecode_db_repo,
654 repo=c.rhodecode_db_repo,
647 message=message,
655 message=message,
648 nodes=nodes,
656 nodes=nodes,
649 parent_commit=c.commit,
657 parent_commit=c.commit,
650 author=author,
658 author=author,
651 )
659 )
652
660
653 h.flash(_('Successfully committed to %s') % node_path,
661 h.flash(_('Successfully committed to %s') % node_path,
654 category='success')
662 category='success')
655 except NonRelativePathError as e:
663 except NonRelativePathError as e:
656 h.flash(_(
664 h.flash(_(
657 'The location specified must be a relative path and must not '
665 'The location specified must be a relative path and must not '
658 'contain .. in the path'), category='warning')
666 'contain .. in the path'), category='warning')
659 return redirect(url('changeset_home', repo_name=c.repo_name,
667 return redirect(url('changeset_home', repo_name=c.repo_name,
660 revision='tip'))
668 revision='tip'))
661 except (NodeError, NodeAlreadyExistsError) as e:
669 except (NodeError, NodeAlreadyExistsError) as e:
662 h.flash(_(e), category='error')
670 h.flash(_(e), category='error')
663 except Exception:
671 except Exception:
664 msg = _('Error occurred during commit')
672 msg = _('Error occurred during commit')
665 log.exception(msg)
673 log.exception(msg)
666 h.flash(msg, category='error')
674 h.flash(msg, category='error')
667 return redirect(url('changeset_home',
675 return redirect(url('changeset_home',
668 repo_name=c.repo_name, revision='tip'))
676 repo_name=c.repo_name, revision='tip'))
669
677
670 @LoginRequired()
678 @LoginRequired()
671 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
679 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
672 def add_home(self, repo_name, revision, f_path):
680 def add_home(self, repo_name, revision, f_path):
673
681
674 repo = Repository.get_by_repo_name(repo_name)
682 repo = Repository.get_by_repo_name(repo_name)
675 if repo.enable_locking and repo.locked[0]:
683 if repo.enable_locking and repo.locked[0]:
676 h.flash(_('This repository has been locked by %s on %s')
684 h.flash(_('This repository has been locked by %s on %s')
677 % (h.person_by_id(repo.locked[0]),
685 % (h.person_by_id(repo.locked[0]),
678 h.format_date(h.time_to_datetime(repo.locked[1]))),
686 h.format_date(h.time_to_datetime(repo.locked[1]))),
679 'warning')
687 'warning')
680 return redirect(h.url('files_home',
688 return redirect(h.url('files_home',
681 repo_name=repo_name, revision='tip'))
689 repo_name=repo_name, revision='tip'))
682
690
683 c.commit = self.__get_commit_or_redirect(
691 c.commit = self.__get_commit_or_redirect(
684 revision, repo_name, redirect_after=False)
692 revision, repo_name, redirect_after=False)
685 if c.commit is None:
693 if c.commit is None:
686 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
694 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
687 c.default_message = (_('Added file via RhodeCode Enterprise'))
695 c.default_message = (_('Added file via RhodeCode Enterprise'))
688 c.f_path = f_path
696 c.f_path = f_path
689
697
690 return render('files/files_add.html')
698 return render('files/files_add.html')
691
699
692 @LoginRequired()
700 @LoginRequired()
693 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
701 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
694 'repository.admin')
702 'repository.admin')
695 def archivefile(self, repo_name, fname):
703 def archivefile(self, repo_name, fname):
696 fileformat = None
704 fileformat = None
697 commit_id = None
705 commit_id = None
698 ext = None
706 ext = None
699 subrepos = request.GET.get('subrepos') == 'true'
707 subrepos = request.GET.get('subrepos') == 'true'
700
708
701 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
709 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
702 archive_spec = fname.split(ext_data[1])
710 archive_spec = fname.split(ext_data[1])
703 if len(archive_spec) == 2 and archive_spec[1] == '':
711 if len(archive_spec) == 2 and archive_spec[1] == '':
704 fileformat = a_type or ext_data[1]
712 fileformat = a_type or ext_data[1]
705 commit_id = archive_spec[0]
713 commit_id = archive_spec[0]
706 ext = ext_data[1]
714 ext = ext_data[1]
707
715
708 dbrepo = RepoModel().get_by_repo_name(repo_name)
716 dbrepo = RepoModel().get_by_repo_name(repo_name)
709 if not dbrepo.enable_downloads:
717 if not dbrepo.enable_downloads:
710 return _('Downloads disabled')
718 return _('Downloads disabled')
711
719
712 try:
720 try:
713 commit = c.rhodecode_repo.get_commit(commit_id)
721 commit = c.rhodecode_repo.get_commit(commit_id)
714 content_type = settings.ARCHIVE_SPECS[fileformat][0]
722 content_type = settings.ARCHIVE_SPECS[fileformat][0]
715 except CommitDoesNotExistError:
723 except CommitDoesNotExistError:
716 return _('Unknown revision %s') % commit_id
724 return _('Unknown revision %s') % commit_id
717 except EmptyRepositoryError:
725 except EmptyRepositoryError:
718 return _('Empty repository')
726 return _('Empty repository')
719 except KeyError:
727 except KeyError:
720 return _('Unknown archive type')
728 return _('Unknown archive type')
721
729
722 # archive cache
730 # archive cache
723 from rhodecode import CONFIG
731 from rhodecode import CONFIG
724
732
725 archive_name = '%s-%s%s%s' % (
733 archive_name = '%s-%s%s%s' % (
726 safe_str(repo_name.replace('/', '_')),
734 safe_str(repo_name.replace('/', '_')),
727 '-sub' if subrepos else '',
735 '-sub' if subrepos else '',
728 safe_str(commit.short_id), ext)
736 safe_str(commit.short_id), ext)
729
737
730 use_cached_archive = False
738 use_cached_archive = False
731 archive_cache_enabled = CONFIG.get(
739 archive_cache_enabled = CONFIG.get(
732 'archive_cache_dir') and not request.GET.get('no_cache')
740 'archive_cache_dir') and not request.GET.get('no_cache')
733
741
734 if archive_cache_enabled:
742 if archive_cache_enabled:
735 # check if we it's ok to write
743 # check if we it's ok to write
736 if not os.path.isdir(CONFIG['archive_cache_dir']):
744 if not os.path.isdir(CONFIG['archive_cache_dir']):
737 os.makedirs(CONFIG['archive_cache_dir'])
745 os.makedirs(CONFIG['archive_cache_dir'])
738 cached_archive_path = os.path.join(
746 cached_archive_path = os.path.join(
739 CONFIG['archive_cache_dir'], archive_name)
747 CONFIG['archive_cache_dir'], archive_name)
740 if os.path.isfile(cached_archive_path):
748 if os.path.isfile(cached_archive_path):
741 log.debug('Found cached archive in %s', cached_archive_path)
749 log.debug('Found cached archive in %s', cached_archive_path)
742 fd, archive = None, cached_archive_path
750 fd, archive = None, cached_archive_path
743 use_cached_archive = True
751 use_cached_archive = True
744 else:
752 else:
745 log.debug('Archive %s is not yet cached', archive_name)
753 log.debug('Archive %s is not yet cached', archive_name)
746
754
747 if not use_cached_archive:
755 if not use_cached_archive:
748 # generate new archive
756 # generate new archive
749 fd, archive = tempfile.mkstemp()
757 fd, archive = tempfile.mkstemp()
750 log.debug('Creating new temp archive in %s' % (archive,))
758 log.debug('Creating new temp archive in %s' % (archive,))
751 try:
759 try:
752 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
760 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
753 except ImproperArchiveTypeError:
761 except ImproperArchiveTypeError:
754 return _('Unknown archive type')
762 return _('Unknown archive type')
755 if archive_cache_enabled:
763 if archive_cache_enabled:
756 # if we generated the archive and we have cache enabled
764 # if we generated the archive and we have cache enabled
757 # let's use this for future
765 # let's use this for future
758 log.debug('Storing new archive in %s' % (cached_archive_path,))
766 log.debug('Storing new archive in %s' % (cached_archive_path,))
759 shutil.move(archive, cached_archive_path)
767 shutil.move(archive, cached_archive_path)
760 archive = cached_archive_path
768 archive = cached_archive_path
761
769
762 def get_chunked_archive(archive):
770 def get_chunked_archive(archive):
763 with open(archive, 'rb') as stream:
771 with open(archive, 'rb') as stream:
764 while True:
772 while True:
765 data = stream.read(16 * 1024)
773 data = stream.read(16 * 1024)
766 if not data:
774 if not data:
767 if fd: # fd means we used temporary file
775 if fd: # fd means we used temporary file
768 os.close(fd)
776 os.close(fd)
769 if not archive_cache_enabled:
777 if not archive_cache_enabled:
770 log.debug('Destroying temp archive %s', archive)
778 log.debug('Destroying temp archive %s', archive)
771 os.remove(archive)
779 os.remove(archive)
772 break
780 break
773 yield data
781 yield data
774
782
775 # store download action
783 # store download action
776 action_logger(user=c.rhodecode_user,
784 action_logger(user=c.rhodecode_user,
777 action='user_downloaded_archive:%s' % archive_name,
785 action='user_downloaded_archive:%s' % archive_name,
778 repo=repo_name, ipaddr=self.ip_addr, commit=True)
786 repo=repo_name, ipaddr=self.ip_addr, commit=True)
779 response.content_disposition = str(
787 response.content_disposition = str(
780 'attachment; filename=%s' % archive_name)
788 'attachment; filename=%s' % archive_name)
781 response.content_type = str(content_type)
789 response.content_type = str(content_type)
782
790
783 return get_chunked_archive(archive)
791 return get_chunked_archive(archive)
784
792
785 @LoginRequired()
793 @LoginRequired()
786 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
794 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
787 'repository.admin')
795 'repository.admin')
788 def diff(self, repo_name, f_path):
796 def diff(self, repo_name, f_path):
789 ignore_whitespace = request.GET.get('ignorews') == '1'
797 ignore_whitespace = request.GET.get('ignorews') == '1'
790 line_context = request.GET.get('context', 3)
798 line_context = request.GET.get('context', 3)
791 diff1 = request.GET.get('diff1', '')
799 diff1 = request.GET.get('diff1', '')
792
800
793 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
801 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
794
802
795 diff2 = request.GET.get('diff2', '')
803 diff2 = request.GET.get('diff2', '')
796 c.action = request.GET.get('diff')
804 c.action = request.GET.get('diff')
797 c.no_changes = diff1 == diff2
805 c.no_changes = diff1 == diff2
798 c.f_path = f_path
806 c.f_path = f_path
799 c.big_diff = False
807 c.big_diff = False
800 c.ignorews_url = _ignorews_url
808 c.ignorews_url = _ignorews_url
801 c.context_url = _context_url
809 c.context_url = _context_url
802 c.changes = OrderedDict()
810 c.changes = OrderedDict()
803 c.changes[diff2] = []
811 c.changes[diff2] = []
804
812
805 if not any((diff1, diff2)):
813 if not any((diff1, diff2)):
806 h.flash(
814 h.flash(
807 'Need query parameter "diff1" or "diff2" to generate a diff.',
815 'Need query parameter "diff1" or "diff2" to generate a diff.',
808 category='error')
816 category='error')
809 raise HTTPBadRequest()
817 raise HTTPBadRequest()
810
818
811 # special case if we want a show commit_id only, it's impl here
819 # special case if we want a show commit_id only, it's impl here
812 # to reduce JS and callbacks
820 # to reduce JS and callbacks
813
821
814 if request.GET.get('show_rev') and diff1:
822 if request.GET.get('show_rev') and diff1:
815 if str2bool(request.GET.get('annotate', 'False')):
823 if str2bool(request.GET.get('annotate', 'False')):
816 _url = url('files_annotate_home', repo_name=c.repo_name,
824 _url = url('files_annotate_home', repo_name=c.repo_name,
817 revision=diff1, f_path=path1)
825 revision=diff1, f_path=path1)
818 else:
826 else:
819 _url = url('files_home', repo_name=c.repo_name,
827 _url = url('files_home', repo_name=c.repo_name,
820 revision=diff1, f_path=path1)
828 revision=diff1, f_path=path1)
821
829
822 return redirect(_url)
830 return redirect(_url)
823
831
824 try:
832 try:
825 node1 = self._get_file_node(diff1, path1)
833 node1 = self._get_file_node(diff1, path1)
826 node2 = self._get_file_node(diff2, f_path)
834 node2 = self._get_file_node(diff2, f_path)
827 except (RepositoryError, NodeError):
835 except (RepositoryError, NodeError):
828 log.exception("Exception while trying to get node from repository")
836 log.exception("Exception while trying to get node from repository")
829 return redirect(url(
837 return redirect(url(
830 'files_home', repo_name=c.repo_name, f_path=f_path))
838 'files_home', repo_name=c.repo_name, f_path=f_path))
831
839
832 if all(isinstance(node.commit, EmptyCommit)
840 if all(isinstance(node.commit, EmptyCommit)
833 for node in (node1, node2)):
841 for node in (node1, node2)):
834 raise HTTPNotFound
842 raise HTTPNotFound
835
843
836 c.commit_1 = node1.commit
844 c.commit_1 = node1.commit
837 c.commit_2 = node2.commit
845 c.commit_2 = node2.commit
838
846
839 if c.action == 'download':
847 if c.action == 'download':
840 _diff = diffs.get_gitdiff(node1, node2,
848 _diff = diffs.get_gitdiff(node1, node2,
841 ignore_whitespace=ignore_whitespace,
849 ignore_whitespace=ignore_whitespace,
842 context=line_context)
850 context=line_context)
843 diff = diffs.DiffProcessor(_diff, format='gitdiff')
851 diff = diffs.DiffProcessor(_diff, format='gitdiff')
844
852
845 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
853 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
846 response.content_type = 'text/plain'
854 response.content_type = 'text/plain'
847 response.content_disposition = (
855 response.content_disposition = (
848 'attachment; filename=%s' % (diff_name,)
856 'attachment; filename=%s' % (diff_name,)
849 )
857 )
850 charset = self._get_default_encoding()
858 charset = self._get_default_encoding()
851 if charset:
859 if charset:
852 response.charset = charset
860 response.charset = charset
853 return diff.as_raw()
861 return diff.as_raw()
854
862
855 elif c.action == 'raw':
863 elif c.action == 'raw':
856 _diff = diffs.get_gitdiff(node1, node2,
864 _diff = diffs.get_gitdiff(node1, node2,
857 ignore_whitespace=ignore_whitespace,
865 ignore_whitespace=ignore_whitespace,
858 context=line_context)
866 context=line_context)
859 diff = diffs.DiffProcessor(_diff, format='gitdiff')
867 diff = diffs.DiffProcessor(_diff, format='gitdiff')
860 response.content_type = 'text/plain'
868 response.content_type = 'text/plain'
861 charset = self._get_default_encoding()
869 charset = self._get_default_encoding()
862 if charset:
870 if charset:
863 response.charset = charset
871 response.charset = charset
864 return diff.as_raw()
872 return diff.as_raw()
865
873
866 else:
874 else:
867 fid = h.FID(diff2, node2.path)
875 fid = h.FID(diff2, node2.path)
868 line_context_lcl = get_line_ctx(fid, request.GET)
876 line_context_lcl = get_line_ctx(fid, request.GET)
869 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
877 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
870
878
871 __, commit1, commit2, diff, st, data = diffs.wrapped_diff(
879 __, commit1, commit2, diff, st, data = diffs.wrapped_diff(
872 filenode_old=node1,
880 filenode_old=node1,
873 filenode_new=node2,
881 filenode_new=node2,
874 diff_limit=self.cut_off_limit_diff,
882 diff_limit=self.cut_off_limit_diff,
875 file_limit=self.cut_off_limit_file,
883 file_limit=self.cut_off_limit_file,
876 show_full_diff=request.GET.get('fulldiff'),
884 show_full_diff=request.GET.get('fulldiff'),
877 ignore_whitespace=ign_whitespace_lcl,
885 ignore_whitespace=ign_whitespace_lcl,
878 line_context=line_context_lcl,)
886 line_context=line_context_lcl,)
879
887
880 c.lines_added = data['stats']['added'] if data else 0
888 c.lines_added = data['stats']['added'] if data else 0
881 c.lines_deleted = data['stats']['deleted'] if data else 0
889 c.lines_deleted = data['stats']['deleted'] if data else 0
882 c.files = [data]
890 c.files = [data]
883 c.commit_ranges = [c.commit_1, c.commit_2]
891 c.commit_ranges = [c.commit_1, c.commit_2]
884 c.ancestor = None
892 c.ancestor = None
885 c.statuses = []
893 c.statuses = []
886 c.target_repo = c.rhodecode_db_repo
894 c.target_repo = c.rhodecode_db_repo
887 c.filename1 = node1.path
895 c.filename1 = node1.path
888 c.filename = node2.path
896 c.filename = node2.path
889 c.binary_file = node1.is_binary or node2.is_binary
897 c.binary_file = node1.is_binary or node2.is_binary
890 operation = data['operation'] if data else ''
898 operation = data['operation'] if data else ''
891
899
892 commit_changes = {
900 commit_changes = {
893 # TODO: it's passing the old file to the diff to keep the
901 # TODO: it's passing the old file to the diff to keep the
894 # standard but this is not being used for this template,
902 # standard but this is not being used for this template,
895 # but might need both files in the future or a more standard
903 # but might need both files in the future or a more standard
896 # way to work with that
904 # way to work with that
897 'fid': [commit1, commit2, operation,
905 'fid': [commit1, commit2, operation,
898 c.filename, diff, st, data]
906 c.filename, diff, st, data]
899 }
907 }
900
908
901 c.changes = commit_changes
909 c.changes = commit_changes
902
910
903 return render('files/file_diff.html')
911 return render('files/file_diff.html')
904
912
905 @LoginRequired()
913 @LoginRequired()
906 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
914 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
907 'repository.admin')
915 'repository.admin')
908 def diff_2way(self, repo_name, f_path):
916 def diff_2way(self, repo_name, f_path):
909 diff1 = request.GET.get('diff1', '')
917 diff1 = request.GET.get('diff1', '')
910 diff2 = request.GET.get('diff2', '')
918 diff2 = request.GET.get('diff2', '')
911
919
912 nodes = []
920 nodes = []
913 unknown_commits = []
921 unknown_commits = []
914 for commit in [diff1, diff2]:
922 for commit in [diff1, diff2]:
915 try:
923 try:
916 nodes.append(self._get_file_node(commit, f_path))
924 nodes.append(self._get_file_node(commit, f_path))
917 except (RepositoryError, NodeError):
925 except (RepositoryError, NodeError):
918 log.exception('%(commit)s does not exist' % {'commit': commit})
926 log.exception('%(commit)s does not exist' % {'commit': commit})
919 unknown_commits.append(commit)
927 unknown_commits.append(commit)
920 h.flash(h.literal(
928 h.flash(h.literal(
921 _('Commit %(commit)s does not exist.') % {'commit': commit}
929 _('Commit %(commit)s does not exist.') % {'commit': commit}
922 ), category='error')
930 ), category='error')
923
931
924 if unknown_commits:
932 if unknown_commits:
925 return redirect(url('files_home', repo_name=c.repo_name,
933 return redirect(url('files_home', repo_name=c.repo_name,
926 f_path=f_path))
934 f_path=f_path))
927
935
928 if all(isinstance(node.commit, EmptyCommit) for node in nodes):
936 if all(isinstance(node.commit, EmptyCommit) for node in nodes):
929 raise HTTPNotFound
937 raise HTTPNotFound
930
938
931 node1, node2 = nodes
939 node1, node2 = nodes
932
940
933 f_gitdiff = diffs.get_gitdiff(node1, node2, ignore_whitespace=False)
941 f_gitdiff = diffs.get_gitdiff(node1, node2, ignore_whitespace=False)
934 diff_processor = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
942 diff_processor = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
935 diff_data = diff_processor.prepare()
943 diff_data = diff_processor.prepare()
936
944
937 if not diff_data or diff_data[0]['raw_diff'] == '':
945 if not diff_data or diff_data[0]['raw_diff'] == '':
938 h.flash(h.literal(_('%(file_path)s has not changed '
946 h.flash(h.literal(_('%(file_path)s has not changed '
939 'between %(commit_1)s and %(commit_2)s.') % {
947 'between %(commit_1)s and %(commit_2)s.') % {
940 'file_path': f_path,
948 'file_path': f_path,
941 'commit_1': node1.commit.id,
949 'commit_1': node1.commit.id,
942 'commit_2': node2.commit.id
950 'commit_2': node2.commit.id
943 }), category='error')
951 }), category='error')
944 return redirect(url('files_home', repo_name=c.repo_name,
952 return redirect(url('files_home', repo_name=c.repo_name,
945 f_path=f_path))
953 f_path=f_path))
946
954
947 c.diff_data = diff_data[0]
955 c.diff_data = diff_data[0]
948 c.FID = h.FID(diff2, node2.path)
956 c.FID = h.FID(diff2, node2.path)
949 # cleanup some unneeded data
957 # cleanup some unneeded data
950 del c.diff_data['raw_diff']
958 del c.diff_data['raw_diff']
951 del c.diff_data['chunks']
959 del c.diff_data['chunks']
952
960
953 c.node1 = node1
961 c.node1 = node1
954 c.commit_1 = node1.commit
962 c.commit_1 = node1.commit
955 c.node2 = node2
963 c.node2 = node2
956 c.commit_2 = node2.commit
964 c.commit_2 = node2.commit
957
965
958 return render('files/diff_2way.html')
966 return render('files/diff_2way.html')
959
967
960 def _get_file_node(self, commit_id, f_path):
968 def _get_file_node(self, commit_id, f_path):
961 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
969 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
962 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
970 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
963 try:
971 try:
964 node = commit.get_node(f_path)
972 node = commit.get_node(f_path)
965 if node.is_dir():
973 if node.is_dir():
966 raise NodeError('%s path is a %s not a file'
974 raise NodeError('%s path is a %s not a file'
967 % (node, type(node)))
975 % (node, type(node)))
968 except NodeDoesNotExistError:
976 except NodeDoesNotExistError:
969 commit = EmptyCommit(
977 commit = EmptyCommit(
970 commit_id=commit_id,
978 commit_id=commit_id,
971 idx=commit.idx,
979 idx=commit.idx,
972 repo=commit.repository,
980 repo=commit.repository,
973 alias=commit.repository.alias,
981 alias=commit.repository.alias,
974 message=commit.message,
982 message=commit.message,
975 author=commit.author,
983 author=commit.author,
976 date=commit.date)
984 date=commit.date)
977 node = FileNode(f_path, '', commit=commit)
985 node = FileNode(f_path, '', commit=commit)
978 else:
986 else:
979 commit = EmptyCommit(
987 commit = EmptyCommit(
980 repo=c.rhodecode_repo,
988 repo=c.rhodecode_repo,
981 alias=c.rhodecode_repo.alias)
989 alias=c.rhodecode_repo.alias)
982 node = FileNode(f_path, '', commit=commit)
990 node = FileNode(f_path, '', commit=commit)
983 return node
991 return node
984
992
985 def _get_node_history(self, commit, f_path, commits=None):
993 def _get_node_history(self, commit, f_path, commits=None):
986 """
994 """
987 get commit history for given node
995 get commit history for given node
988
996
989 :param commit: commit to calculate history
997 :param commit: commit to calculate history
990 :param f_path: path for node to calculate history for
998 :param f_path: path for node to calculate history for
991 :param commits: if passed don't calculate history and take
999 :param commits: if passed don't calculate history and take
992 commits defined in this list
1000 commits defined in this list
993 """
1001 """
994 # calculate history based on tip
1002 # calculate history based on tip
995 tip = c.rhodecode_repo.get_commit()
1003 tip = c.rhodecode_repo.get_commit()
996 if commits is None:
1004 if commits is None:
997 pre_load = ["author", "branch"]
1005 pre_load = ["author", "branch"]
998 try:
1006 try:
999 commits = tip.get_file_history(f_path, pre_load=pre_load)
1007 commits = tip.get_file_history(f_path, pre_load=pre_load)
1000 except (NodeDoesNotExistError, CommitError):
1008 except (NodeDoesNotExistError, CommitError):
1001 # this node is not present at tip!
1009 # this node is not present at tip!
1002 commits = commit.get_file_history(f_path, pre_load=pre_load)
1010 commits = commit.get_file_history(f_path, pre_load=pre_load)
1003
1011
1004 history = []
1012 history = []
1005 commits_group = ([], _("Changesets"))
1013 commits_group = ([], _("Changesets"))
1006 for commit in commits:
1014 for commit in commits:
1007 branch = ' (%s)' % commit.branch if commit.branch else ''
1015 branch = ' (%s)' % commit.branch if commit.branch else ''
1008 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1016 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1009 commits_group[0].append((commit.raw_id, n_desc,))
1017 commits_group[0].append((commit.raw_id, n_desc,))
1010 history.append(commits_group)
1018 history.append(commits_group)
1011
1019
1012 symbolic_reference = self._symbolic_reference
1020 symbolic_reference = self._symbolic_reference
1013
1021
1014 if c.rhodecode_repo.alias == 'svn':
1022 if c.rhodecode_repo.alias == 'svn':
1015 adjusted_f_path = self._adjust_file_path_for_svn(
1023 adjusted_f_path = self._adjust_file_path_for_svn(
1016 f_path, c.rhodecode_repo)
1024 f_path, c.rhodecode_repo)
1017 if adjusted_f_path != f_path:
1025 if adjusted_f_path != f_path:
1018 log.debug(
1026 log.debug(
1019 'Recognized svn tag or branch in file "%s", using svn '
1027 'Recognized svn tag or branch in file "%s", using svn '
1020 'specific symbolic references', f_path)
1028 'specific symbolic references', f_path)
1021 f_path = adjusted_f_path
1029 f_path = adjusted_f_path
1022 symbolic_reference = self._symbolic_reference_svn
1030 symbolic_reference = self._symbolic_reference_svn
1023
1031
1024 branches = self._create_references(
1032 branches = self._create_references(
1025 c.rhodecode_repo.branches, symbolic_reference, f_path)
1033 c.rhodecode_repo.branches, symbolic_reference, f_path)
1026 branches_group = (branches, _("Branches"))
1034 branches_group = (branches, _("Branches"))
1027
1035
1028 tags = self._create_references(
1036 tags = self._create_references(
1029 c.rhodecode_repo.tags, symbolic_reference, f_path)
1037 c.rhodecode_repo.tags, symbolic_reference, f_path)
1030 tags_group = (tags, _("Tags"))
1038 tags_group = (tags, _("Tags"))
1031
1039
1032 history.append(branches_group)
1040 history.append(branches_group)
1033 history.append(tags_group)
1041 history.append(tags_group)
1034
1042
1035 return history, commits
1043 return history, commits
1036
1044
1037 def _adjust_file_path_for_svn(self, f_path, repo):
1045 def _adjust_file_path_for_svn(self, f_path, repo):
1038 """
1046 """
1039 Computes the relative path of `f_path`.
1047 Computes the relative path of `f_path`.
1040
1048
1041 This is mainly based on prefix matching of the recognized tags and
1049 This is mainly based on prefix matching of the recognized tags and
1042 branches in the underlying repository.
1050 branches in the underlying repository.
1043 """
1051 """
1044 tags_and_branches = itertools.chain(
1052 tags_and_branches = itertools.chain(
1045 repo.branches.iterkeys(),
1053 repo.branches.iterkeys(),
1046 repo.tags.iterkeys())
1054 repo.tags.iterkeys())
1047 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1055 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1048
1056
1049 for name in tags_and_branches:
1057 for name in tags_and_branches:
1050 if f_path.startswith(name + '/'):
1058 if f_path.startswith(name + '/'):
1051 f_path = vcspath.relpath(f_path, name)
1059 f_path = vcspath.relpath(f_path, name)
1052 break
1060 break
1053 return f_path
1061 return f_path
1054
1062
1055 def _create_references(
1063 def _create_references(
1056 self, branches_or_tags, symbolic_reference, f_path):
1064 self, branches_or_tags, symbolic_reference, f_path):
1057 items = []
1065 items = []
1058 for name, commit_id in branches_or_tags.items():
1066 for name, commit_id in branches_or_tags.items():
1059 sym_ref = symbolic_reference(commit_id, name, f_path)
1067 sym_ref = symbolic_reference(commit_id, name, f_path)
1060 items.append((sym_ref, name))
1068 items.append((sym_ref, name))
1061 return items
1069 return items
1062
1070
1063 def _symbolic_reference(self, commit_id, name, f_path):
1071 def _symbolic_reference(self, commit_id, name, f_path):
1064 return commit_id
1072 return commit_id
1065
1073
1066 def _symbolic_reference_svn(self, commit_id, name, f_path):
1074 def _symbolic_reference_svn(self, commit_id, name, f_path):
1067 new_f_path = vcspath.join(name, f_path)
1075 new_f_path = vcspath.join(name, f_path)
1068 return u'%s@%s' % (new_f_path, commit_id)
1076 return u'%s@%s' % (new_f_path, commit_id)
1069
1077
1070 @LoginRequired()
1078 @LoginRequired()
1071 @XHRRequired()
1079 @XHRRequired()
1072 @HasRepoPermissionAnyDecorator(
1080 @HasRepoPermissionAnyDecorator(
1073 'repository.read', 'repository.write', 'repository.admin')
1081 'repository.read', 'repository.write', 'repository.admin')
1074 @jsonify
1082 @jsonify
1075 def nodelist(self, repo_name, revision, f_path):
1083 def nodelist(self, repo_name, revision, f_path):
1076 commit = self.__get_commit_or_redirect(revision, repo_name)
1084 commit = self.__get_commit_or_redirect(revision, repo_name)
1077
1085
1078 metadata = self._get_nodelist_at_commit(
1086 metadata = self._get_nodelist_at_commit(
1079 repo_name, commit.raw_id, f_path)
1087 repo_name, commit.raw_id, f_path)
1080 return {'nodes': metadata}
1088 return {'nodes': metadata}
1081
1089
1082 @LoginRequired()
1090 @LoginRequired()
1083 @XHRRequired()
1091 @XHRRequired()
1084 @HasRepoPermissionAnyDecorator(
1092 @HasRepoPermissionAnyDecorator(
1085 'repository.read', 'repository.write', 'repository.admin')
1093 'repository.read', 'repository.write', 'repository.admin')
1086 def nodetree_full(self, repo_name, commit_id, f_path):
1094 def nodetree_full(self, repo_name, commit_id, f_path):
1087 """
1095 """
1088 Returns rendered html of file tree that contains commit date,
1096 Returns rendered html of file tree that contains commit date,
1089 author, revision for the specified combination of
1097 author, revision for the specified combination of
1090 repo, commit_id and file path
1098 repo, commit_id and file path
1091
1099
1092 :param repo_name: name of the repository
1100 :param repo_name: name of the repository
1093 :param commit_id: commit_id of file tree
1101 :param commit_id: commit_id of file tree
1094 :param f_path: file path of the requested directory
1102 :param f_path: file path of the requested directory
1095 """
1103 """
1096
1104
1097 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1105 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1098 try:
1106 try:
1099 dir_node = commit.get_node(f_path)
1107 dir_node = commit.get_node(f_path)
1100 except RepositoryError as e:
1108 except RepositoryError as e:
1101 return 'error {}'.format(safe_str(e))
1109 return 'error {}'.format(safe_str(e))
1102
1110
1103 if dir_node.is_file():
1111 if dir_node.is_file():
1104 return ''
1112 return ''
1105
1113
1106 c.file = dir_node
1114 c.file = dir_node
1107 c.commit = commit
1115 c.commit = commit
1108
1116
1109 # using force=True here, make a little trick. We flush the cache and
1117 # using force=True here, make a little trick. We flush the cache and
1110 # compute it using the same key as without full_load, so the fully
1118 # compute it using the same key as without full_load, so the fully
1111 # loaded cached tree is now returned instead of partial
1119 # loaded cached tree is now returned instead of partial
1112 return self._get_tree_at_commit(
1120 return self._get_tree_at_commit(
1113 repo_name, commit.raw_id, dir_node.path, full_load=True,
1121 repo_name, commit.raw_id, dir_node.path, full_load=True,
1114 force=True)
1122 force=True)
@@ -1,2001 +1,1995 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 import pygments
39 import pygments
40
40
41 from datetime import datetime
41 from datetime import datetime
42 from functools import partial
42 from functools import partial
43 from pygments.formatters.html import HtmlFormatter
43 from pygments.formatters.html import HtmlFormatter
44 from pygments import highlight as code_highlight
44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 from pylons import url as pylons_url
47 from pylons import url as pylons_url
48 from pylons.i18n.translation import _, ungettext
48 from pylons.i18n.translation import _, ungettext
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from webhelpers.html import literal, HTML, escape
51 from webhelpers.html import literal, HTML, escape
52 from webhelpers.html.tools import *
52 from webhelpers.html.tools import *
53 from webhelpers.html.builder import make_tag
53 from webhelpers.html.builder import make_tag
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 submit, text, password, textarea, title, ul, xml_declaration, radio
57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 from webhelpers.pylonslib import Flash as _Flash
60 from webhelpers.pylonslib import Flash as _Flash
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 replace_whitespace, urlify, truncate, wrap_paragraphs
63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 from webhelpers.date import time_ago_in_words
64 from webhelpers.date import time_ago_in_words
65 from webhelpers.paginate import Page as _Page
65 from webhelpers.paginate import Page as _Page
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 from webhelpers2.number import format_byte_size
68 from webhelpers2.number import format_byte_size
69
69
70 from rhodecode.lib.annotate import annotate_highlight
71 from rhodecode.lib.action_parser import action_parser
70 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.ext_json import json
71 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 AttributeDict, safe_int, md5, md5_safe
75 AttributeDict, safe_int, md5, md5_safe
77 from rhodecode.lib.markup_renderer import MarkupRenderer
76 from rhodecode.lib.markup_renderer import MarkupRenderer
78 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.model.changeset_status import ChangesetStatusModel
80 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.db import Permission, User, Repository
81 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.repo_group import RepoGroupModel
82 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.settings import IssueTrackerSettingsModel
83 from rhodecode.model.settings import IssueTrackerSettingsModel
85
84
86 log = logging.getLogger(__name__)
85 log = logging.getLogger(__name__)
87
86
88
87
89 DEFAULT_USER = User.DEFAULT_USER
88 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
89 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91
90
92
91
93 def url(*args, **kw):
92 def url(*args, **kw):
94 return pylons_url(*args, **kw)
93 return pylons_url(*args, **kw)
95
94
96
95
97 def pylons_url_current(*args, **kw):
96 def pylons_url_current(*args, **kw):
98 """
97 """
99 This function overrides pylons.url.current() which returns the current
98 This function overrides pylons.url.current() which returns the current
100 path so that it will also work from a pyramid only context. This
99 path so that it will also work from a pyramid only context. This
101 should be removed once port to pyramid is complete.
100 should be removed once port to pyramid is complete.
102 """
101 """
103 if not args and not kw:
102 if not args and not kw:
104 request = get_current_request()
103 request = get_current_request()
105 return request.path
104 return request.path
106 return pylons_url.current(*args, **kw)
105 return pylons_url.current(*args, **kw)
107
106
108 url.current = pylons_url_current
107 url.current = pylons_url_current
109
108
110
109
111 def asset(path, ver=None):
110 def asset(path, ver=None):
112 """
111 """
113 Helper to generate a static asset file path for rhodecode assets
112 Helper to generate a static asset file path for rhodecode assets
114
113
115 eg. h.asset('images/image.png', ver='3923')
114 eg. h.asset('images/image.png', ver='3923')
116
115
117 :param path: path of asset
116 :param path: path of asset
118 :param ver: optional version query param to append as ?ver=
117 :param ver: optional version query param to append as ?ver=
119 """
118 """
120 request = get_current_request()
119 request = get_current_request()
121 query = {}
120 query = {}
122 if ver:
121 if ver:
123 query = {'ver': ver}
122 query = {'ver': ver}
124 return request.static_path(
123 return request.static_path(
125 'rhodecode:public/{}'.format(path), _query=query)
124 'rhodecode:public/{}'.format(path), _query=query)
126
125
127
126
128 def html_escape(text, html_escape_table=None):
127 default_html_escape_table = {
128 ord('&'): u'&amp;',
129 ord('<'): u'&lt;',
130 ord('>'): u'&gt;',
131 ord('"'): u'&quot;',
132 ord("'"): u'&#39;',
133 }
134
135
136 def html_escape(text, html_escape_table=default_html_escape_table):
129 """Produce entities within text."""
137 """Produce entities within text."""
130 if not html_escape_table:
138 return text.translate(html_escape_table)
131 html_escape_table = {
132 "&": "&amp;",
133 '"': "&quot;",
134 "'": "&apos;",
135 ">": "&gt;",
136 "<": "&lt;",
137 }
138 return "".join(html_escape_table.get(c, c) for c in text)
139
139
140
140
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
142 """
142 """
143 Truncate string ``s`` at the first occurrence of ``sub``.
143 Truncate string ``s`` at the first occurrence of ``sub``.
144
144
145 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
145 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
146 """
146 """
147 suffix_if_chopped = suffix_if_chopped or ''
147 suffix_if_chopped = suffix_if_chopped or ''
148 pos = s.find(sub)
148 pos = s.find(sub)
149 if pos == -1:
149 if pos == -1:
150 return s
150 return s
151
151
152 if inclusive:
152 if inclusive:
153 pos += len(sub)
153 pos += len(sub)
154
154
155 chopped = s[:pos]
155 chopped = s[:pos]
156 left = s[pos:].strip()
156 left = s[pos:].strip()
157
157
158 if left and suffix_if_chopped:
158 if left and suffix_if_chopped:
159 chopped += suffix_if_chopped
159 chopped += suffix_if_chopped
160
160
161 return chopped
161 return chopped
162
162
163
163
164 def shorter(text, size=20):
164 def shorter(text, size=20):
165 postfix = '...'
165 postfix = '...'
166 if len(text) > size:
166 if len(text) > size:
167 return text[:size - len(postfix)] + postfix
167 return text[:size - len(postfix)] + postfix
168 return text
168 return text
169
169
170
170
171 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 """
172 """
173 Reset button
173 Reset button
174 """
174 """
175 _set_input_attrs(attrs, type, name, value)
175 _set_input_attrs(attrs, type, name, value)
176 _set_id_attr(attrs, id, name)
176 _set_id_attr(attrs, id, name)
177 convert_boolean_attrs(attrs, ["disabled"])
177 convert_boolean_attrs(attrs, ["disabled"])
178 return HTML.input(**attrs)
178 return HTML.input(**attrs)
179
179
180 reset = _reset
180 reset = _reset
181 safeid = _make_safe_id_component
181 safeid = _make_safe_id_component
182
182
183
183
184 def branding(name, length=40):
184 def branding(name, length=40):
185 return truncate(name, length, indicator="")
185 return truncate(name, length, indicator="")
186
186
187
187
188 def FID(raw_id, path):
188 def FID(raw_id, path):
189 """
189 """
190 Creates a unique ID for filenode based on it's hash of path and commit
190 Creates a unique ID for filenode based on it's hash of path and commit
191 it's safe to use in urls
191 it's safe to use in urls
192
192
193 :param raw_id:
193 :param raw_id:
194 :param path:
194 :param path:
195 """
195 """
196
196
197 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
197 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
198
198
199
199
200 class _GetError(object):
200 class _GetError(object):
201 """Get error from form_errors, and represent it as span wrapped error
201 """Get error from form_errors, and represent it as span wrapped error
202 message
202 message
203
203
204 :param field_name: field to fetch errors for
204 :param field_name: field to fetch errors for
205 :param form_errors: form errors dict
205 :param form_errors: form errors dict
206 """
206 """
207
207
208 def __call__(self, field_name, form_errors):
208 def __call__(self, field_name, form_errors):
209 tmpl = """<span class="error_msg">%s</span>"""
209 tmpl = """<span class="error_msg">%s</span>"""
210 if form_errors and field_name in form_errors:
210 if form_errors and field_name in form_errors:
211 return literal(tmpl % form_errors.get(field_name))
211 return literal(tmpl % form_errors.get(field_name))
212
212
213 get_error = _GetError()
213 get_error = _GetError()
214
214
215
215
216 class _ToolTip(object):
216 class _ToolTip(object):
217
217
218 def __call__(self, tooltip_title, trim_at=50):
218 def __call__(self, tooltip_title, trim_at=50):
219 """
219 """
220 Special function just to wrap our text into nice formatted
220 Special function just to wrap our text into nice formatted
221 autowrapped text
221 autowrapped text
222
222
223 :param tooltip_title:
223 :param tooltip_title:
224 """
224 """
225 tooltip_title = escape(tooltip_title)
225 tooltip_title = escape(tooltip_title)
226 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
226 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
227 return tooltip_title
227 return tooltip_title
228 tooltip = _ToolTip()
228 tooltip = _ToolTip()
229
229
230
230
231 def files_breadcrumbs(repo_name, commit_id, file_path):
231 def files_breadcrumbs(repo_name, commit_id, file_path):
232 if isinstance(file_path, str):
232 if isinstance(file_path, str):
233 file_path = safe_unicode(file_path)
233 file_path = safe_unicode(file_path)
234
234
235 # TODO: johbo: Is this always a url like path, or is this operating
235 # TODO: johbo: Is this always a url like path, or is this operating
236 # system dependent?
236 # system dependent?
237 path_segments = file_path.split('/')
237 path_segments = file_path.split('/')
238
238
239 repo_name_html = escape(repo_name)
239 repo_name_html = escape(repo_name)
240 if len(path_segments) == 1 and path_segments[0] == '':
240 if len(path_segments) == 1 and path_segments[0] == '':
241 url_segments = [repo_name_html]
241 url_segments = [repo_name_html]
242 else:
242 else:
243 url_segments = [
243 url_segments = [
244 link_to(
244 link_to(
245 repo_name_html,
245 repo_name_html,
246 url('files_home',
246 url('files_home',
247 repo_name=repo_name,
247 repo_name=repo_name,
248 revision=commit_id,
248 revision=commit_id,
249 f_path=''),
249 f_path=''),
250 class_='pjax-link')]
250 class_='pjax-link')]
251
251
252 last_cnt = len(path_segments) - 1
252 last_cnt = len(path_segments) - 1
253 for cnt, segment in enumerate(path_segments):
253 for cnt, segment in enumerate(path_segments):
254 if not segment:
254 if not segment:
255 continue
255 continue
256 segment_html = escape(segment)
256 segment_html = escape(segment)
257
257
258 if cnt != last_cnt:
258 if cnt != last_cnt:
259 url_segments.append(
259 url_segments.append(
260 link_to(
260 link_to(
261 segment_html,
261 segment_html,
262 url('files_home',
262 url('files_home',
263 repo_name=repo_name,
263 repo_name=repo_name,
264 revision=commit_id,
264 revision=commit_id,
265 f_path='/'.join(path_segments[:cnt + 1])),
265 f_path='/'.join(path_segments[:cnt + 1])),
266 class_='pjax-link'))
266 class_='pjax-link'))
267 else:
267 else:
268 url_segments.append(segment_html)
268 url_segments.append(segment_html)
269
269
270 return literal('/'.join(url_segments))
270 return literal('/'.join(url_segments))
271
271
272
272
273 class CodeHtmlFormatter(HtmlFormatter):
273 class CodeHtmlFormatter(HtmlFormatter):
274 """
274 """
275 My code Html Formatter for source codes
275 My code Html Formatter for source codes
276 """
276 """
277
277
278 def wrap(self, source, outfile):
278 def wrap(self, source, outfile):
279 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
279 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
280
280
281 def _wrap_code(self, source):
281 def _wrap_code(self, source):
282 for cnt, it in enumerate(source):
282 for cnt, it in enumerate(source):
283 i, t = it
283 i, t = it
284 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
284 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
285 yield i, t
285 yield i, t
286
286
287 def _wrap_tablelinenos(self, inner):
287 def _wrap_tablelinenos(self, inner):
288 dummyoutfile = StringIO.StringIO()
288 dummyoutfile = StringIO.StringIO()
289 lncount = 0
289 lncount = 0
290 for t, line in inner:
290 for t, line in inner:
291 if t:
291 if t:
292 lncount += 1
292 lncount += 1
293 dummyoutfile.write(line)
293 dummyoutfile.write(line)
294
294
295 fl = self.linenostart
295 fl = self.linenostart
296 mw = len(str(lncount + fl - 1))
296 mw = len(str(lncount + fl - 1))
297 sp = self.linenospecial
297 sp = self.linenospecial
298 st = self.linenostep
298 st = self.linenostep
299 la = self.lineanchors
299 la = self.lineanchors
300 aln = self.anchorlinenos
300 aln = self.anchorlinenos
301 nocls = self.noclasses
301 nocls = self.noclasses
302 if sp:
302 if sp:
303 lines = []
303 lines = []
304
304
305 for i in range(fl, fl + lncount):
305 for i in range(fl, fl + lncount):
306 if i % st == 0:
306 if i % st == 0:
307 if i % sp == 0:
307 if i % sp == 0:
308 if aln:
308 if aln:
309 lines.append('<a href="#%s%d" class="special">%*d</a>' %
309 lines.append('<a href="#%s%d" class="special">%*d</a>' %
310 (la, i, mw, i))
310 (la, i, mw, i))
311 else:
311 else:
312 lines.append('<span class="special">%*d</span>' % (mw, i))
312 lines.append('<span class="special">%*d</span>' % (mw, i))
313 else:
313 else:
314 if aln:
314 if aln:
315 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
315 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
316 else:
316 else:
317 lines.append('%*d' % (mw, i))
317 lines.append('%*d' % (mw, i))
318 else:
318 else:
319 lines.append('')
319 lines.append('')
320 ls = '\n'.join(lines)
320 ls = '\n'.join(lines)
321 else:
321 else:
322 lines = []
322 lines = []
323 for i in range(fl, fl + lncount):
323 for i in range(fl, fl + lncount):
324 if i % st == 0:
324 if i % st == 0:
325 if aln:
325 if aln:
326 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
326 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
327 else:
327 else:
328 lines.append('%*d' % (mw, i))
328 lines.append('%*d' % (mw, i))
329 else:
329 else:
330 lines.append('')
330 lines.append('')
331 ls = '\n'.join(lines)
331 ls = '\n'.join(lines)
332
332
333 # in case you wonder about the seemingly redundant <div> here: since the
333 # in case you wonder about the seemingly redundant <div> here: since the
334 # content in the other cell also is wrapped in a div, some browsers in
334 # content in the other cell also is wrapped in a div, some browsers in
335 # some configurations seem to mess up the formatting...
335 # some configurations seem to mess up the formatting...
336 if nocls:
336 if nocls:
337 yield 0, ('<table class="%stable">' % self.cssclass +
337 yield 0, ('<table class="%stable">' % self.cssclass +
338 '<tr><td><div class="linenodiv" '
338 '<tr><td><div class="linenodiv" '
339 'style="background-color: #f0f0f0; padding-right: 10px">'
339 'style="background-color: #f0f0f0; padding-right: 10px">'
340 '<pre style="line-height: 125%">' +
340 '<pre style="line-height: 125%">' +
341 ls + '</pre></div></td><td id="hlcode" class="code">')
341 ls + '</pre></div></td><td id="hlcode" class="code">')
342 else:
342 else:
343 yield 0, ('<table class="%stable">' % self.cssclass +
343 yield 0, ('<table class="%stable">' % self.cssclass +
344 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
344 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
345 ls + '</pre></div></td><td id="hlcode" class="code">')
345 ls + '</pre></div></td><td id="hlcode" class="code">')
346 yield 0, dummyoutfile.getvalue()
346 yield 0, dummyoutfile.getvalue()
347 yield 0, '</td></tr></table>'
347 yield 0, '</td></tr></table>'
348
348
349
349
350 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
350 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
351 def __init__(self, **kw):
351 def __init__(self, **kw):
352 # only show these line numbers if set
352 # only show these line numbers if set
353 self.only_lines = kw.pop('only_line_numbers', [])
353 self.only_lines = kw.pop('only_line_numbers', [])
354 self.query_terms = kw.pop('query_terms', [])
354 self.query_terms = kw.pop('query_terms', [])
355 self.max_lines = kw.pop('max_lines', 5)
355 self.max_lines = kw.pop('max_lines', 5)
356 self.line_context = kw.pop('line_context', 3)
356 self.line_context = kw.pop('line_context', 3)
357 self.url = kw.pop('url', None)
357 self.url = kw.pop('url', None)
358
358
359 super(CodeHtmlFormatter, self).__init__(**kw)
359 super(CodeHtmlFormatter, self).__init__(**kw)
360
360
361 def _wrap_code(self, source):
361 def _wrap_code(self, source):
362 for cnt, it in enumerate(source):
362 for cnt, it in enumerate(source):
363 i, t = it
363 i, t = it
364 t = '<pre>%s</pre>' % t
364 t = '<pre>%s</pre>' % t
365 yield i, t
365 yield i, t
366
366
367 def _wrap_tablelinenos(self, inner):
367 def _wrap_tablelinenos(self, inner):
368 yield 0, '<table class="code-highlight %stable">' % self.cssclass
368 yield 0, '<table class="code-highlight %stable">' % self.cssclass
369
369
370 last_shown_line_number = 0
370 last_shown_line_number = 0
371 current_line_number = 1
371 current_line_number = 1
372
372
373 for t, line in inner:
373 for t, line in inner:
374 if not t:
374 if not t:
375 yield t, line
375 yield t, line
376 continue
376 continue
377
377
378 if current_line_number in self.only_lines:
378 if current_line_number in self.only_lines:
379 if last_shown_line_number + 1 != current_line_number:
379 if last_shown_line_number + 1 != current_line_number:
380 yield 0, '<tr>'
380 yield 0, '<tr>'
381 yield 0, '<td class="line">...</td>'
381 yield 0, '<td class="line">...</td>'
382 yield 0, '<td id="hlcode" class="code"></td>'
382 yield 0, '<td id="hlcode" class="code"></td>'
383 yield 0, '</tr>'
383 yield 0, '</tr>'
384
384
385 yield 0, '<tr>'
385 yield 0, '<tr>'
386 if self.url:
386 if self.url:
387 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
387 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
388 self.url, current_line_number, current_line_number)
388 self.url, current_line_number, current_line_number)
389 else:
389 else:
390 yield 0, '<td class="line"><a href="">%i</a></td>' % (
390 yield 0, '<td class="line"><a href="">%i</a></td>' % (
391 current_line_number)
391 current_line_number)
392 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
392 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
393 yield 0, '</tr>'
393 yield 0, '</tr>'
394
394
395 last_shown_line_number = current_line_number
395 last_shown_line_number = current_line_number
396
396
397 current_line_number += 1
397 current_line_number += 1
398
398
399
399
400 yield 0, '</table>'
400 yield 0, '</table>'
401
401
402
402
403 def extract_phrases(text_query):
403 def extract_phrases(text_query):
404 """
404 """
405 Extracts phrases from search term string making sure phrases
405 Extracts phrases from search term string making sure phrases
406 contained in double quotes are kept together - and discarding empty values
406 contained in double quotes are kept together - and discarding empty values
407 or fully whitespace values eg.
407 or fully whitespace values eg.
408
408
409 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
409 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
410
410
411 """
411 """
412
412
413 in_phrase = False
413 in_phrase = False
414 buf = ''
414 buf = ''
415 phrases = []
415 phrases = []
416 for char in text_query:
416 for char in text_query:
417 if in_phrase:
417 if in_phrase:
418 if char == '"': # end phrase
418 if char == '"': # end phrase
419 phrases.append(buf)
419 phrases.append(buf)
420 buf = ''
420 buf = ''
421 in_phrase = False
421 in_phrase = False
422 continue
422 continue
423 else:
423 else:
424 buf += char
424 buf += char
425 continue
425 continue
426 else:
426 else:
427 if char == '"': # start phrase
427 if char == '"': # start phrase
428 in_phrase = True
428 in_phrase = True
429 phrases.append(buf)
429 phrases.append(buf)
430 buf = ''
430 buf = ''
431 continue
431 continue
432 elif char == ' ':
432 elif char == ' ':
433 phrases.append(buf)
433 phrases.append(buf)
434 buf = ''
434 buf = ''
435 continue
435 continue
436 else:
436 else:
437 buf += char
437 buf += char
438
438
439 phrases.append(buf)
439 phrases.append(buf)
440 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
440 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
441 return phrases
441 return phrases
442
442
443
443
444 def get_matching_offsets(text, phrases):
444 def get_matching_offsets(text, phrases):
445 """
445 """
446 Returns a list of string offsets in `text` that the list of `terms` match
446 Returns a list of string offsets in `text` that the list of `terms` match
447
447
448 >>> get_matching_offsets('some text here', ['some', 'here'])
448 >>> get_matching_offsets('some text here', ['some', 'here'])
449 [(0, 4), (10, 14)]
449 [(0, 4), (10, 14)]
450
450
451 """
451 """
452 offsets = []
452 offsets = []
453 for phrase in phrases:
453 for phrase in phrases:
454 for match in re.finditer(phrase, text):
454 for match in re.finditer(phrase, text):
455 offsets.append((match.start(), match.end()))
455 offsets.append((match.start(), match.end()))
456
456
457 return offsets
457 return offsets
458
458
459
459
460 def normalize_text_for_matching(x):
460 def normalize_text_for_matching(x):
461 """
461 """
462 Replaces all non alnum characters to spaces and lower cases the string,
462 Replaces all non alnum characters to spaces and lower cases the string,
463 useful for comparing two text strings without punctuation
463 useful for comparing two text strings without punctuation
464 """
464 """
465 return re.sub(r'[^\w]', ' ', x.lower())
465 return re.sub(r'[^\w]', ' ', x.lower())
466
466
467
467
468 def get_matching_line_offsets(lines, terms):
468 def get_matching_line_offsets(lines, terms):
469 """ Return a set of `lines` indices (starting from 1) matching a
469 """ Return a set of `lines` indices (starting from 1) matching a
470 text search query, along with `context` lines above/below matching lines
470 text search query, along with `context` lines above/below matching lines
471
471
472 :param lines: list of strings representing lines
472 :param lines: list of strings representing lines
473 :param terms: search term string to match in lines eg. 'some text'
473 :param terms: search term string to match in lines eg. 'some text'
474 :param context: number of lines above/below a matching line to add to result
474 :param context: number of lines above/below a matching line to add to result
475 :param max_lines: cut off for lines of interest
475 :param max_lines: cut off for lines of interest
476 eg.
476 eg.
477
477
478 text = '''
478 text = '''
479 words words words
479 words words words
480 words words words
480 words words words
481 some text some
481 some text some
482 words words words
482 words words words
483 words words words
483 words words words
484 text here what
484 text here what
485 '''
485 '''
486 get_matching_line_offsets(text, 'text', context=1)
486 get_matching_line_offsets(text, 'text', context=1)
487 {3: [(5, 9)], 6: [(0, 4)]]
487 {3: [(5, 9)], 6: [(0, 4)]]
488
488
489 """
489 """
490 matching_lines = {}
490 matching_lines = {}
491 phrases = [normalize_text_for_matching(phrase)
491 phrases = [normalize_text_for_matching(phrase)
492 for phrase in extract_phrases(terms)]
492 for phrase in extract_phrases(terms)]
493
493
494 for line_index, line in enumerate(lines, start=1):
494 for line_index, line in enumerate(lines, start=1):
495 match_offsets = get_matching_offsets(
495 match_offsets = get_matching_offsets(
496 normalize_text_for_matching(line), phrases)
496 normalize_text_for_matching(line), phrases)
497 if match_offsets:
497 if match_offsets:
498 matching_lines[line_index] = match_offsets
498 matching_lines[line_index] = match_offsets
499
499
500 return matching_lines
500 return matching_lines
501
501
502
502
503 def hsv_to_rgb(h, s, v):
504 """ Convert hsv color values to rgb """
505
506 if s == 0.0:
507 return v, v, v
508 i = int(h * 6.0) # XXX assume int() truncates!
509 f = (h * 6.0) - i
510 p = v * (1.0 - s)
511 q = v * (1.0 - s * f)
512 t = v * (1.0 - s * (1.0 - f))
513 i = i % 6
514 if i == 0:
515 return v, t, p
516 if i == 1:
517 return q, v, p
518 if i == 2:
519 return p, v, t
520 if i == 3:
521 return p, q, v
522 if i == 4:
523 return t, p, v
524 if i == 5:
525 return v, p, q
526
527
528 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
529 """
530 Generator for getting n of evenly distributed colors using
531 hsv color and golden ratio. It always return same order of colors
532
533 :param n: number of colors to generate
534 :param saturation: saturation of returned colors
535 :param lightness: lightness of returned colors
536 :returns: RGB tuple
537 """
538
539 golden_ratio = 0.618033988749895
540 h = 0.22717784590367374
541
542 for _ in xrange(n):
543 h += golden_ratio
544 h %= 1
545 HSV_tuple = [h, saturation, lightness]
546 RGB_tuple = hsv_to_rgb(*HSV_tuple)
547 yield map(lambda x: str(int(x * 256)), RGB_tuple)
548
549
550 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
551 """
552 Returns a function which when called with an argument returns a unique
553 color for that argument, eg.
554
555 :param n: number of colors to generate
556 :param saturation: saturation of returned colors
557 :param lightness: lightness of returned colors
558 :returns: css RGB string
559
560 >>> color_hash = color_hasher()
561 >>> color_hash('hello')
562 'rgb(34, 12, 59)'
563 >>> color_hash('hello')
564 'rgb(34, 12, 59)'
565 >>> color_hash('other')
566 'rgb(90, 224, 159)'
567 """
568
569 color_dict = {}
570 cgenerator = unique_color_generator(
571 saturation=saturation, lightness=lightness)
572
573 def get_color_string(thing):
574 if thing in color_dict:
575 col = color_dict[thing]
576 else:
577 col = color_dict[thing] = cgenerator.next()
578 return "rgb(%s)" % (', '.join(col))
579
580 return get_color_string
581
582
503 def get_lexer_safe(mimetype=None, filepath=None):
583 def get_lexer_safe(mimetype=None, filepath=None):
504 """
584 """
505 Tries to return a relevant pygments lexer using mimetype/filepath name,
585 Tries to return a relevant pygments lexer using mimetype/filepath name,
506 defaulting to plain text if none could be found
586 defaulting to plain text if none could be found
507 """
587 """
508 lexer = None
588 lexer = None
509 try:
589 try:
510 if mimetype:
590 if mimetype:
511 lexer = get_lexer_for_mimetype(mimetype)
591 lexer = get_lexer_for_mimetype(mimetype)
512 if not lexer:
592 if not lexer:
513 lexer = get_lexer_for_filename(filepath)
593 lexer = get_lexer_for_filename(filepath)
514 except pygments.util.ClassNotFound:
594 except pygments.util.ClassNotFound:
515 pass
595 pass
516
596
517 if not lexer:
597 if not lexer:
518 lexer = get_lexer_by_name('text')
598 lexer = get_lexer_by_name('text')
519
599
520 return lexer
600 return lexer
521
601
522
602
523 def get_lexer_for_filenode(filenode):
603 def get_lexer_for_filenode(filenode):
524 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
604 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
525 return lexer
605 return lexer
526
606
527
607
528 def pygmentize(filenode, **kwargs):
608 def pygmentize(filenode, **kwargs):
529 """
609 """
530 pygmentize function using pygments
610 pygmentize function using pygments
531
611
532 :param filenode:
612 :param filenode:
533 """
613 """
534 lexer = get_lexer_for_filenode(filenode)
614 lexer = get_lexer_for_filenode(filenode)
535 return literal(code_highlight(filenode.content, lexer,
615 return literal(code_highlight(filenode.content, lexer,
536 CodeHtmlFormatter(**kwargs)))
616 CodeHtmlFormatter(**kwargs)))
537
617
538
618
539 def pygmentize_annotation(repo_name, filenode, **kwargs):
540 """
541 pygmentize function for annotation
542
543 :param filenode:
544 """
545
546 color_dict = {}
547
548 def gen_color(n=10000):
549 """generator for getting n of evenly distributed colors using
550 hsv color and golden ratio. It always return same order of colors
551
552 :returns: RGB tuple
553 """
554
555 def hsv_to_rgb(h, s, v):
556 if s == 0.0:
557 return v, v, v
558 i = int(h * 6.0) # XXX assume int() truncates!
559 f = (h * 6.0) - i
560 p = v * (1.0 - s)
561 q = v * (1.0 - s * f)
562 t = v * (1.0 - s * (1.0 - f))
563 i = i % 6
564 if i == 0:
565 return v, t, p
566 if i == 1:
567 return q, v, p
568 if i == 2:
569 return p, v, t
570 if i == 3:
571 return p, q, v
572 if i == 4:
573 return t, p, v
574 if i == 5:
575 return v, p, q
576
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
579
580 for _ in xrange(n):
581 h += golden_ratio
582 h %= 1
583 HSV_tuple = [h, 0.95, 0.95]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
587 cgenerator = gen_color()
588
589 def get_color_string(commit_id):
590 if commit_id in color_dict:
591 col = color_dict[commit_id]
592 else:
593 col = color_dict[commit_id] = cgenerator.next()
594 return "color: rgb(%s)! important;" % (', '.join(col))
595
596 def url_func(repo_name):
597
598 def _url_func(commit):
599 author = commit.author
600 date = commit.date
601 message = tooltip(commit.message)
602
603 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
604 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
605 "</b> %s<br/></div>")
606
607 tooltip_html = tooltip_html % (author, date, message)
608 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
609 uri = link_to(
610 lnk_format,
611 url('changeset_home', repo_name=repo_name,
612 revision=commit.raw_id),
613 style=get_color_string(commit.raw_id),
614 class_='tooltip',
615 title=tooltip_html
616 )
617
618 uri += '\n'
619 return uri
620 return _url_func
621
622 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
623
624
625 def is_following_repo(repo_name, user_id):
619 def is_following_repo(repo_name, user_id):
626 from rhodecode.model.scm import ScmModel
620 from rhodecode.model.scm import ScmModel
627 return ScmModel().is_following_repo(repo_name, user_id)
621 return ScmModel().is_following_repo(repo_name, user_id)
628
622
629
623
630 class _Message(object):
624 class _Message(object):
631 """A message returned by ``Flash.pop_messages()``.
625 """A message returned by ``Flash.pop_messages()``.
632
626
633 Converting the message to a string returns the message text. Instances
627 Converting the message to a string returns the message text. Instances
634 also have the following attributes:
628 also have the following attributes:
635
629
636 * ``message``: the message text.
630 * ``message``: the message text.
637 * ``category``: the category specified when the message was created.
631 * ``category``: the category specified when the message was created.
638 """
632 """
639
633
640 def __init__(self, category, message):
634 def __init__(self, category, message):
641 self.category = category
635 self.category = category
642 self.message = message
636 self.message = message
643
637
644 def __str__(self):
638 def __str__(self):
645 return self.message
639 return self.message
646
640
647 __unicode__ = __str__
641 __unicode__ = __str__
648
642
649 def __html__(self):
643 def __html__(self):
650 return escape(safe_unicode(self.message))
644 return escape(safe_unicode(self.message))
651
645
652
646
653 class Flash(_Flash):
647 class Flash(_Flash):
654
648
655 def pop_messages(self):
649 def pop_messages(self):
656 """Return all accumulated messages and delete them from the session.
650 """Return all accumulated messages and delete them from the session.
657
651
658 The return value is a list of ``Message`` objects.
652 The return value is a list of ``Message`` objects.
659 """
653 """
660 from pylons import session
654 from pylons import session
661
655
662 messages = []
656 messages = []
663
657
664 # Pop the 'old' pylons flash messages. They are tuples of the form
658 # Pop the 'old' pylons flash messages. They are tuples of the form
665 # (category, message)
659 # (category, message)
666 for cat, msg in session.pop(self.session_key, []):
660 for cat, msg in session.pop(self.session_key, []):
667 messages.append(_Message(cat, msg))
661 messages.append(_Message(cat, msg))
668
662
669 # Pop the 'new' pyramid flash messages for each category as list
663 # Pop the 'new' pyramid flash messages for each category as list
670 # of strings.
664 # of strings.
671 for cat in self.categories:
665 for cat in self.categories:
672 for msg in session.pop_flash(queue=cat):
666 for msg in session.pop_flash(queue=cat):
673 messages.append(_Message(cat, msg))
667 messages.append(_Message(cat, msg))
674 # Map messages from the default queue to the 'notice' category.
668 # Map messages from the default queue to the 'notice' category.
675 for msg in session.pop_flash():
669 for msg in session.pop_flash():
676 messages.append(_Message('notice', msg))
670 messages.append(_Message('notice', msg))
677
671
678 session.save()
672 session.save()
679 return messages
673 return messages
680
674
681 def json_alerts(self):
675 def json_alerts(self):
682 payloads = []
676 payloads = []
683 messages = flash.pop_messages()
677 messages = flash.pop_messages()
684 if messages:
678 if messages:
685 for message in messages:
679 for message in messages:
686 subdata = {}
680 subdata = {}
687 if hasattr(message.message, 'rsplit'):
681 if hasattr(message.message, 'rsplit'):
688 flash_data = message.message.rsplit('|DELIM|', 1)
682 flash_data = message.message.rsplit('|DELIM|', 1)
689 org_message = flash_data[0]
683 org_message = flash_data[0]
690 if len(flash_data) > 1:
684 if len(flash_data) > 1:
691 subdata = json.loads(flash_data[1])
685 subdata = json.loads(flash_data[1])
692 else:
686 else:
693 org_message = message.message
687 org_message = message.message
694 payloads.append({
688 payloads.append({
695 'message': {
689 'message': {
696 'message': u'{}'.format(org_message),
690 'message': u'{}'.format(org_message),
697 'level': message.category,
691 'level': message.category,
698 'force': True,
692 'force': True,
699 'subdata': subdata
693 'subdata': subdata
700 }
694 }
701 })
695 })
702 return json.dumps(payloads)
696 return json.dumps(payloads)
703
697
704 flash = Flash()
698 flash = Flash()
705
699
706 #==============================================================================
700 #==============================================================================
707 # SCM FILTERS available via h.
701 # SCM FILTERS available via h.
708 #==============================================================================
702 #==============================================================================
709 from rhodecode.lib.vcs.utils import author_name, author_email
703 from rhodecode.lib.vcs.utils import author_name, author_email
710 from rhodecode.lib.utils2 import credentials_filter, age as _age
704 from rhodecode.lib.utils2 import credentials_filter, age as _age
711 from rhodecode.model.db import User, ChangesetStatus
705 from rhodecode.model.db import User, ChangesetStatus
712
706
713 age = _age
707 age = _age
714 capitalize = lambda x: x.capitalize()
708 capitalize = lambda x: x.capitalize()
715 email = author_email
709 email = author_email
716 short_id = lambda x: x[:12]
710 short_id = lambda x: x[:12]
717 hide_credentials = lambda x: ''.join(credentials_filter(x))
711 hide_credentials = lambda x: ''.join(credentials_filter(x))
718
712
719
713
720 def age_component(datetime_iso, value=None, time_is_local=False):
714 def age_component(datetime_iso, value=None, time_is_local=False):
721 title = value or format_date(datetime_iso)
715 title = value or format_date(datetime_iso)
722
716
723 # detect if we have a timezone info, otherwise, add it
717 # detect if we have a timezone info, otherwise, add it
724 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
718 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
725 tzinfo = '+00:00'
719 tzinfo = '+00:00'
726
720
727 if time_is_local:
721 if time_is_local:
728 tzinfo = time.strftime("+%H:%M",
722 tzinfo = time.strftime("+%H:%M",
729 time.gmtime(
723 time.gmtime(
730 (datetime.now() - datetime.utcnow()).seconds + 1
724 (datetime.now() - datetime.utcnow()).seconds + 1
731 )
725 )
732 )
726 )
733
727
734 return literal(
728 return literal(
735 '<time class="timeago tooltip" '
729 '<time class="timeago tooltip" '
736 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
730 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
737 datetime_iso, title, tzinfo))
731 datetime_iso, title, tzinfo))
738
732
739
733
740 def _shorten_commit_id(commit_id):
734 def _shorten_commit_id(commit_id):
741 from rhodecode import CONFIG
735 from rhodecode import CONFIG
742 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
736 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
743 return commit_id[:def_len]
737 return commit_id[:def_len]
744
738
745
739
746 def show_id(commit):
740 def show_id(commit):
747 """
741 """
748 Configurable function that shows ID
742 Configurable function that shows ID
749 by default it's r123:fffeeefffeee
743 by default it's r123:fffeeefffeee
750
744
751 :param commit: commit instance
745 :param commit: commit instance
752 """
746 """
753 from rhodecode import CONFIG
747 from rhodecode import CONFIG
754 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
748 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
755
749
756 raw_id = _shorten_commit_id(commit.raw_id)
750 raw_id = _shorten_commit_id(commit.raw_id)
757 if show_idx:
751 if show_idx:
758 return 'r%s:%s' % (commit.idx, raw_id)
752 return 'r%s:%s' % (commit.idx, raw_id)
759 else:
753 else:
760 return '%s' % (raw_id, )
754 return '%s' % (raw_id, )
761
755
762
756
763 def format_date(date):
757 def format_date(date):
764 """
758 """
765 use a standardized formatting for dates used in RhodeCode
759 use a standardized formatting for dates used in RhodeCode
766
760
767 :param date: date/datetime object
761 :param date: date/datetime object
768 :return: formatted date
762 :return: formatted date
769 """
763 """
770
764
771 if date:
765 if date:
772 _fmt = "%a, %d %b %Y %H:%M:%S"
766 _fmt = "%a, %d %b %Y %H:%M:%S"
773 return safe_unicode(date.strftime(_fmt))
767 return safe_unicode(date.strftime(_fmt))
774
768
775 return u""
769 return u""
776
770
777
771
778 class _RepoChecker(object):
772 class _RepoChecker(object):
779
773
780 def __init__(self, backend_alias):
774 def __init__(self, backend_alias):
781 self._backend_alias = backend_alias
775 self._backend_alias = backend_alias
782
776
783 def __call__(self, repository):
777 def __call__(self, repository):
784 if hasattr(repository, 'alias'):
778 if hasattr(repository, 'alias'):
785 _type = repository.alias
779 _type = repository.alias
786 elif hasattr(repository, 'repo_type'):
780 elif hasattr(repository, 'repo_type'):
787 _type = repository.repo_type
781 _type = repository.repo_type
788 else:
782 else:
789 _type = repository
783 _type = repository
790 return _type == self._backend_alias
784 return _type == self._backend_alias
791
785
792 is_git = _RepoChecker('git')
786 is_git = _RepoChecker('git')
793 is_hg = _RepoChecker('hg')
787 is_hg = _RepoChecker('hg')
794 is_svn = _RepoChecker('svn')
788 is_svn = _RepoChecker('svn')
795
789
796
790
797 def get_repo_type_by_name(repo_name):
791 def get_repo_type_by_name(repo_name):
798 repo = Repository.get_by_repo_name(repo_name)
792 repo = Repository.get_by_repo_name(repo_name)
799 return repo.repo_type
793 return repo.repo_type
800
794
801
795
802 def is_svn_without_proxy(repository):
796 def is_svn_without_proxy(repository):
803 if is_svn(repository):
797 if is_svn(repository):
804 from rhodecode.model.settings import VcsSettingsModel
798 from rhodecode.model.settings import VcsSettingsModel
805 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
799 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
806 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
800 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
807 return False
801 return False
808
802
809
803
810 def discover_user(author):
804 def discover_user(author):
811 """
805 """
812 Tries to discover RhodeCode User based on the autho string. Author string
806 Tries to discover RhodeCode User based on the autho string. Author string
813 is typically `FirstName LastName <email@address.com>`
807 is typically `FirstName LastName <email@address.com>`
814 """
808 """
815
809
816 # if author is already an instance use it for extraction
810 # if author is already an instance use it for extraction
817 if isinstance(author, User):
811 if isinstance(author, User):
818 return author
812 return author
819
813
820 # Valid email in the attribute passed, see if they're in the system
814 # Valid email in the attribute passed, see if they're in the system
821 _email = author_email(author)
815 _email = author_email(author)
822 if _email != '':
816 if _email != '':
823 user = User.get_by_email(_email, case_insensitive=True, cache=True)
817 user = User.get_by_email(_email, case_insensitive=True, cache=True)
824 if user is not None:
818 if user is not None:
825 return user
819 return user
826
820
827 # Maybe it's a username, we try to extract it and fetch by username ?
821 # Maybe it's a username, we try to extract it and fetch by username ?
828 _author = author_name(author)
822 _author = author_name(author)
829 user = User.get_by_username(_author, case_insensitive=True, cache=True)
823 user = User.get_by_username(_author, case_insensitive=True, cache=True)
830 if user is not None:
824 if user is not None:
831 return user
825 return user
832
826
833 return None
827 return None
834
828
835
829
836 def email_or_none(author):
830 def email_or_none(author):
837 # extract email from the commit string
831 # extract email from the commit string
838 _email = author_email(author)
832 _email = author_email(author)
839
833
840 # If we have an email, use it, otherwise
834 # If we have an email, use it, otherwise
841 # see if it contains a username we can get an email from
835 # see if it contains a username we can get an email from
842 if _email != '':
836 if _email != '':
843 return _email
837 return _email
844 else:
838 else:
845 user = User.get_by_username(
839 user = User.get_by_username(
846 author_name(author), case_insensitive=True, cache=True)
840 author_name(author), case_insensitive=True, cache=True)
847
841
848 if user is not None:
842 if user is not None:
849 return user.email
843 return user.email
850
844
851 # No valid email, not a valid user in the system, none!
845 # No valid email, not a valid user in the system, none!
852 return None
846 return None
853
847
854
848
855 def link_to_user(author, length=0, **kwargs):
849 def link_to_user(author, length=0, **kwargs):
856 user = discover_user(author)
850 user = discover_user(author)
857 # user can be None, but if we have it already it means we can re-use it
851 # user can be None, but if we have it already it means we can re-use it
858 # in the person() function, so we save 1 intensive-query
852 # in the person() function, so we save 1 intensive-query
859 if user:
853 if user:
860 author = user
854 author = user
861
855
862 display_person = person(author, 'username_or_name_or_email')
856 display_person = person(author, 'username_or_name_or_email')
863 if length:
857 if length:
864 display_person = shorter(display_person, length)
858 display_person = shorter(display_person, length)
865
859
866 if user:
860 if user:
867 return link_to(
861 return link_to(
868 escape(display_person),
862 escape(display_person),
869 url('user_profile', username=user.username),
863 url('user_profile', username=user.username),
870 **kwargs)
864 **kwargs)
871 else:
865 else:
872 return escape(display_person)
866 return escape(display_person)
873
867
874
868
875 def person(author, show_attr="username_and_name"):
869 def person(author, show_attr="username_and_name"):
876 user = discover_user(author)
870 user = discover_user(author)
877 if user:
871 if user:
878 return getattr(user, show_attr)
872 return getattr(user, show_attr)
879 else:
873 else:
880 _author = author_name(author)
874 _author = author_name(author)
881 _email = email(author)
875 _email = email(author)
882 return _author or _email
876 return _author or _email
883
877
884
878
885 def author_string(email):
879 def author_string(email):
886 if email:
880 if email:
887 user = User.get_by_email(email, case_insensitive=True, cache=True)
881 user = User.get_by_email(email, case_insensitive=True, cache=True)
888 if user:
882 if user:
889 if user.firstname or user.lastname:
883 if user.firstname or user.lastname:
890 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
884 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
891 else:
885 else:
892 return email
886 return email
893 else:
887 else:
894 return email
888 return email
895 else:
889 else:
896 return None
890 return None
897
891
898
892
899 def person_by_id(id_, show_attr="username_and_name"):
893 def person_by_id(id_, show_attr="username_and_name"):
900 # attr to return from fetched user
894 # attr to return from fetched user
901 person_getter = lambda usr: getattr(usr, show_attr)
895 person_getter = lambda usr: getattr(usr, show_attr)
902
896
903 #maybe it's an ID ?
897 #maybe it's an ID ?
904 if str(id_).isdigit() or isinstance(id_, int):
898 if str(id_).isdigit() or isinstance(id_, int):
905 id_ = int(id_)
899 id_ = int(id_)
906 user = User.get(id_)
900 user = User.get(id_)
907 if user is not None:
901 if user is not None:
908 return person_getter(user)
902 return person_getter(user)
909 return id_
903 return id_
910
904
911
905
912 def gravatar_with_user(author, show_disabled=False):
906 def gravatar_with_user(author, show_disabled=False):
913 from rhodecode.lib.utils import PartialRenderer
907 from rhodecode.lib.utils import PartialRenderer
914 _render = PartialRenderer('base/base.html')
908 _render = PartialRenderer('base/base.html')
915 return _render('gravatar_with_user', author, show_disabled=show_disabled)
909 return _render('gravatar_with_user', author, show_disabled=show_disabled)
916
910
917
911
918 def desc_stylize(value):
912 def desc_stylize(value):
919 """
913 """
920 converts tags from value into html equivalent
914 converts tags from value into html equivalent
921
915
922 :param value:
916 :param value:
923 """
917 """
924 if not value:
918 if not value:
925 return ''
919 return ''
926
920
927 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
921 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
928 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
922 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
929 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
923 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
930 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
924 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
931 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
925 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
932 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
926 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
933 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
927 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
934 '<div class="metatag" tag="lang">\\2</div>', value)
928 '<div class="metatag" tag="lang">\\2</div>', value)
935 value = re.sub(r'\[([a-z]+)\]',
929 value = re.sub(r'\[([a-z]+)\]',
936 '<div class="metatag" tag="\\1">\\1</div>', value)
930 '<div class="metatag" tag="\\1">\\1</div>', value)
937
931
938 return value
932 return value
939
933
940
934
941 def escaped_stylize(value):
935 def escaped_stylize(value):
942 """
936 """
943 converts tags from value into html equivalent, but escaping its value first
937 converts tags from value into html equivalent, but escaping its value first
944 """
938 """
945 if not value:
939 if not value:
946 return ''
940 return ''
947
941
948 # Using default webhelper escape method, but has to force it as a
942 # Using default webhelper escape method, but has to force it as a
949 # plain unicode instead of a markup tag to be used in regex expressions
943 # plain unicode instead of a markup tag to be used in regex expressions
950 value = unicode(escape(safe_unicode(value)))
944 value = unicode(escape(safe_unicode(value)))
951
945
952 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
946 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
953 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
947 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
954 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
948 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
955 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
949 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
956 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
950 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
957 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
951 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
958 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
952 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
959 '<div class="metatag" tag="lang">\\2</div>', value)
953 '<div class="metatag" tag="lang">\\2</div>', value)
960 value = re.sub(r'\[([a-z]+)\]',
954 value = re.sub(r'\[([a-z]+)\]',
961 '<div class="metatag" tag="\\1">\\1</div>', value)
955 '<div class="metatag" tag="\\1">\\1</div>', value)
962
956
963 return value
957 return value
964
958
965
959
966 def bool2icon(value):
960 def bool2icon(value):
967 """
961 """
968 Returns boolean value of a given value, represented as html element with
962 Returns boolean value of a given value, represented as html element with
969 classes that will represent icons
963 classes that will represent icons
970
964
971 :param value: given value to convert to html node
965 :param value: given value to convert to html node
972 """
966 """
973
967
974 if value: # does bool conversion
968 if value: # does bool conversion
975 return HTML.tag('i', class_="icon-true")
969 return HTML.tag('i', class_="icon-true")
976 else: # not true as bool
970 else: # not true as bool
977 return HTML.tag('i', class_="icon-false")
971 return HTML.tag('i', class_="icon-false")
978
972
979
973
980 #==============================================================================
974 #==============================================================================
981 # PERMS
975 # PERMS
982 #==============================================================================
976 #==============================================================================
983 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
977 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
984 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
978 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
985 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
979 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
986 csrf_token_key
980 csrf_token_key
987
981
988
982
989 #==============================================================================
983 #==============================================================================
990 # GRAVATAR URL
984 # GRAVATAR URL
991 #==============================================================================
985 #==============================================================================
992 class InitialsGravatar(object):
986 class InitialsGravatar(object):
993 def __init__(self, email_address, first_name, last_name, size=30,
987 def __init__(self, email_address, first_name, last_name, size=30,
994 background=None, text_color='#fff'):
988 background=None, text_color='#fff'):
995 self.size = size
989 self.size = size
996 self.first_name = first_name
990 self.first_name = first_name
997 self.last_name = last_name
991 self.last_name = last_name
998 self.email_address = email_address
992 self.email_address = email_address
999 self.background = background or self.str2color(email_address)
993 self.background = background or self.str2color(email_address)
1000 self.text_color = text_color
994 self.text_color = text_color
1001
995
1002 def get_color_bank(self):
996 def get_color_bank(self):
1003 """
997 """
1004 returns a predefined list of colors that gravatars can use.
998 returns a predefined list of colors that gravatars can use.
1005 Those are randomized distinct colors that guarantee readability and
999 Those are randomized distinct colors that guarantee readability and
1006 uniqueness.
1000 uniqueness.
1007
1001
1008 generated with: http://phrogz.net/css/distinct-colors.html
1002 generated with: http://phrogz.net/css/distinct-colors.html
1009 """
1003 """
1010 return [
1004 return [
1011 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1005 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1012 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1006 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1013 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1007 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1014 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1008 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1015 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1009 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1016 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1010 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1017 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1011 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1018 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1012 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1019 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1013 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1020 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1014 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1021 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1015 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1022 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1016 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1023 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1017 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1024 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1018 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1025 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1019 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1026 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1020 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1027 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1021 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1028 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1022 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1029 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1023 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1030 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1024 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1031 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1025 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1032 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1026 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1033 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1027 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1034 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1028 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1035 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1029 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1036 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1030 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1037 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1031 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1038 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1032 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1039 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1033 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1040 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1034 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1041 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1035 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1042 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1036 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1043 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1037 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1044 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1038 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1045 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1039 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1046 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1040 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1047 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1041 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1048 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1042 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1049 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1043 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1050 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1044 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1051 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1045 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1052 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1046 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1053 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1047 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1054 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1048 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1055 '#4f8c46', '#368dd9', '#5c0073'
1049 '#4f8c46', '#368dd9', '#5c0073'
1056 ]
1050 ]
1057
1051
1058 def rgb_to_hex_color(self, rgb_tuple):
1052 def rgb_to_hex_color(self, rgb_tuple):
1059 """
1053 """
1060 Converts an rgb_tuple passed to an hex color.
1054 Converts an rgb_tuple passed to an hex color.
1061
1055
1062 :param rgb_tuple: tuple with 3 ints represents rgb color space
1056 :param rgb_tuple: tuple with 3 ints represents rgb color space
1063 """
1057 """
1064 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1058 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1065
1059
1066 def email_to_int_list(self, email_str):
1060 def email_to_int_list(self, email_str):
1067 """
1061 """
1068 Get every byte of the hex digest value of email and turn it to integer.
1062 Get every byte of the hex digest value of email and turn it to integer.
1069 It's going to be always between 0-255
1063 It's going to be always between 0-255
1070 """
1064 """
1071 digest = md5_safe(email_str.lower())
1065 digest = md5_safe(email_str.lower())
1072 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1066 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1073
1067
1074 def pick_color_bank_index(self, email_str, color_bank):
1068 def pick_color_bank_index(self, email_str, color_bank):
1075 return self.email_to_int_list(email_str)[0] % len(color_bank)
1069 return self.email_to_int_list(email_str)[0] % len(color_bank)
1076
1070
1077 def str2color(self, email_str):
1071 def str2color(self, email_str):
1078 """
1072 """
1079 Tries to map in a stable algorithm an email to color
1073 Tries to map in a stable algorithm an email to color
1080
1074
1081 :param email_str:
1075 :param email_str:
1082 """
1076 """
1083 color_bank = self.get_color_bank()
1077 color_bank = self.get_color_bank()
1084 # pick position (module it's length so we always find it in the
1078 # pick position (module it's length so we always find it in the
1085 # bank even if it's smaller than 256 values
1079 # bank even if it's smaller than 256 values
1086 pos = self.pick_color_bank_index(email_str, color_bank)
1080 pos = self.pick_color_bank_index(email_str, color_bank)
1087 return color_bank[pos]
1081 return color_bank[pos]
1088
1082
1089 def normalize_email(self, email_address):
1083 def normalize_email(self, email_address):
1090 import unicodedata
1084 import unicodedata
1091 # default host used to fill in the fake/missing email
1085 # default host used to fill in the fake/missing email
1092 default_host = u'localhost'
1086 default_host = u'localhost'
1093
1087
1094 if not email_address:
1088 if not email_address:
1095 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1089 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1096
1090
1097 email_address = safe_unicode(email_address)
1091 email_address = safe_unicode(email_address)
1098
1092
1099 if u'@' not in email_address:
1093 if u'@' not in email_address:
1100 email_address = u'%s@%s' % (email_address, default_host)
1094 email_address = u'%s@%s' % (email_address, default_host)
1101
1095
1102 if email_address.endswith(u'@'):
1096 if email_address.endswith(u'@'):
1103 email_address = u'%s%s' % (email_address, default_host)
1097 email_address = u'%s%s' % (email_address, default_host)
1104
1098
1105 email_address = unicodedata.normalize('NFKD', email_address)\
1099 email_address = unicodedata.normalize('NFKD', email_address)\
1106 .encode('ascii', 'ignore')
1100 .encode('ascii', 'ignore')
1107 return email_address
1101 return email_address
1108
1102
1109 def get_initials(self):
1103 def get_initials(self):
1110 """
1104 """
1111 Returns 2 letter initials calculated based on the input.
1105 Returns 2 letter initials calculated based on the input.
1112 The algorithm picks first given email address, and takes first letter
1106 The algorithm picks first given email address, and takes first letter
1113 of part before @, and then the first letter of server name. In case
1107 of part before @, and then the first letter of server name. In case
1114 the part before @ is in a format of `somestring.somestring2` it replaces
1108 the part before @ is in a format of `somestring.somestring2` it replaces
1115 the server letter with first letter of somestring2
1109 the server letter with first letter of somestring2
1116
1110
1117 In case function was initialized with both first and lastname, this
1111 In case function was initialized with both first and lastname, this
1118 overrides the extraction from email by first letter of the first and
1112 overrides the extraction from email by first letter of the first and
1119 last name. We add special logic to that functionality, In case Full name
1113 last name. We add special logic to that functionality, In case Full name
1120 is compound, like Guido Von Rossum, we use last part of the last name
1114 is compound, like Guido Von Rossum, we use last part of the last name
1121 (Von Rossum) picking `R`.
1115 (Von Rossum) picking `R`.
1122
1116
1123 Function also normalizes the non-ascii characters to they ascii
1117 Function also normalizes the non-ascii characters to they ascii
1124 representation, eg Ą => A
1118 representation, eg Ą => A
1125 """
1119 """
1126 import unicodedata
1120 import unicodedata
1127 # replace non-ascii to ascii
1121 # replace non-ascii to ascii
1128 first_name = unicodedata.normalize(
1122 first_name = unicodedata.normalize(
1129 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1123 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1130 last_name = unicodedata.normalize(
1124 last_name = unicodedata.normalize(
1131 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1125 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1132
1126
1133 # do NFKD encoding, and also make sure email has proper format
1127 # do NFKD encoding, and also make sure email has proper format
1134 email_address = self.normalize_email(self.email_address)
1128 email_address = self.normalize_email(self.email_address)
1135
1129
1136 # first push the email initials
1130 # first push the email initials
1137 prefix, server = email_address.split('@', 1)
1131 prefix, server = email_address.split('@', 1)
1138
1132
1139 # check if prefix is maybe a 'firstname.lastname' syntax
1133 # check if prefix is maybe a 'firstname.lastname' syntax
1140 _dot_split = prefix.rsplit('.', 1)
1134 _dot_split = prefix.rsplit('.', 1)
1141 if len(_dot_split) == 2:
1135 if len(_dot_split) == 2:
1142 initials = [_dot_split[0][0], _dot_split[1][0]]
1136 initials = [_dot_split[0][0], _dot_split[1][0]]
1143 else:
1137 else:
1144 initials = [prefix[0], server[0]]
1138 initials = [prefix[0], server[0]]
1145
1139
1146 # then try to replace either firtname or lastname
1140 # then try to replace either firtname or lastname
1147 fn_letter = (first_name or " ")[0].strip()
1141 fn_letter = (first_name or " ")[0].strip()
1148 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1142 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1149
1143
1150 if fn_letter:
1144 if fn_letter:
1151 initials[0] = fn_letter
1145 initials[0] = fn_letter
1152
1146
1153 if ln_letter:
1147 if ln_letter:
1154 initials[1] = ln_letter
1148 initials[1] = ln_letter
1155
1149
1156 return ''.join(initials).upper()
1150 return ''.join(initials).upper()
1157
1151
1158 def get_img_data_by_type(self, font_family, img_type):
1152 def get_img_data_by_type(self, font_family, img_type):
1159 default_user = """
1153 default_user = """
1160 <svg xmlns="http://www.w3.org/2000/svg"
1154 <svg xmlns="http://www.w3.org/2000/svg"
1161 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1155 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1162 viewBox="-15 -10 439.165 429.164"
1156 viewBox="-15 -10 439.165 429.164"
1163
1157
1164 xml:space="preserve"
1158 xml:space="preserve"
1165 style="background:{background};" >
1159 style="background:{background};" >
1166
1160
1167 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1161 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1168 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1162 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1169 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1163 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1170 168.596,153.916,216.671,
1164 168.596,153.916,216.671,
1171 204.583,216.671z" fill="{text_color}"/>
1165 204.583,216.671z" fill="{text_color}"/>
1172 <path d="M407.164,374.717L360.88,
1166 <path d="M407.164,374.717L360.88,
1173 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1167 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1174 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1168 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1175 15.366-44.203,23.488-69.076,23.488c-24.877,
1169 15.366-44.203,23.488-69.076,23.488c-24.877,
1176 0-48.762-8.122-69.078-23.488
1170 0-48.762-8.122-69.078-23.488
1177 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1171 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1178 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1172 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1179 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1173 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1180 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1174 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1181 19.402-10.527 C409.699,390.129,
1175 19.402-10.527 C409.699,390.129,
1182 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1176 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1183 </svg>""".format(
1177 </svg>""".format(
1184 size=self.size,
1178 size=self.size,
1185 background='#979797', # @grey4
1179 background='#979797', # @grey4
1186 text_color=self.text_color,
1180 text_color=self.text_color,
1187 font_family=font_family)
1181 font_family=font_family)
1188
1182
1189 return {
1183 return {
1190 "default_user": default_user
1184 "default_user": default_user
1191 }[img_type]
1185 }[img_type]
1192
1186
1193 def get_img_data(self, svg_type=None):
1187 def get_img_data(self, svg_type=None):
1194 """
1188 """
1195 generates the svg metadata for image
1189 generates the svg metadata for image
1196 """
1190 """
1197
1191
1198 font_family = ','.join([
1192 font_family = ','.join([
1199 'proximanovaregular',
1193 'proximanovaregular',
1200 'Proxima Nova Regular',
1194 'Proxima Nova Regular',
1201 'Proxima Nova',
1195 'Proxima Nova',
1202 'Arial',
1196 'Arial',
1203 'Lucida Grande',
1197 'Lucida Grande',
1204 'sans-serif'
1198 'sans-serif'
1205 ])
1199 ])
1206 if svg_type:
1200 if svg_type:
1207 return self.get_img_data_by_type(font_family, svg_type)
1201 return self.get_img_data_by_type(font_family, svg_type)
1208
1202
1209 initials = self.get_initials()
1203 initials = self.get_initials()
1210 img_data = """
1204 img_data = """
1211 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1205 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1212 width="{size}" height="{size}"
1206 width="{size}" height="{size}"
1213 style="width: 100%; height: 100%; background-color: {background}"
1207 style="width: 100%; height: 100%; background-color: {background}"
1214 viewBox="0 0 {size} {size}">
1208 viewBox="0 0 {size} {size}">
1215 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1209 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1216 pointer-events="auto" fill="{text_color}"
1210 pointer-events="auto" fill="{text_color}"
1217 font-family="{font_family}"
1211 font-family="{font_family}"
1218 style="font-weight: 400; font-size: {f_size}px;">{text}
1212 style="font-weight: 400; font-size: {f_size}px;">{text}
1219 </text>
1213 </text>
1220 </svg>""".format(
1214 </svg>""".format(
1221 size=self.size,
1215 size=self.size,
1222 f_size=self.size/1.85, # scale the text inside the box nicely
1216 f_size=self.size/1.85, # scale the text inside the box nicely
1223 background=self.background,
1217 background=self.background,
1224 text_color=self.text_color,
1218 text_color=self.text_color,
1225 text=initials.upper(),
1219 text=initials.upper(),
1226 font_family=font_family)
1220 font_family=font_family)
1227
1221
1228 return img_data
1222 return img_data
1229
1223
1230 def generate_svg(self, svg_type=None):
1224 def generate_svg(self, svg_type=None):
1231 img_data = self.get_img_data(svg_type)
1225 img_data = self.get_img_data(svg_type)
1232 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1226 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1233
1227
1234
1228
1235 def initials_gravatar(email_address, first_name, last_name, size=30):
1229 def initials_gravatar(email_address, first_name, last_name, size=30):
1236 svg_type = None
1230 svg_type = None
1237 if email_address == User.DEFAULT_USER_EMAIL:
1231 if email_address == User.DEFAULT_USER_EMAIL:
1238 svg_type = 'default_user'
1232 svg_type = 'default_user'
1239 klass = InitialsGravatar(email_address, first_name, last_name, size)
1233 klass = InitialsGravatar(email_address, first_name, last_name, size)
1240 return klass.generate_svg(svg_type=svg_type)
1234 return klass.generate_svg(svg_type=svg_type)
1241
1235
1242
1236
1243 def gravatar_url(email_address, size=30):
1237 def gravatar_url(email_address, size=30):
1244 # doh, we need to re-import those to mock it later
1238 # doh, we need to re-import those to mock it later
1245 from pylons import tmpl_context as c
1239 from pylons import tmpl_context as c
1246
1240
1247 _use_gravatar = c.visual.use_gravatar
1241 _use_gravatar = c.visual.use_gravatar
1248 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1242 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1249
1243
1250 email_address = email_address or User.DEFAULT_USER_EMAIL
1244 email_address = email_address or User.DEFAULT_USER_EMAIL
1251 if isinstance(email_address, unicode):
1245 if isinstance(email_address, unicode):
1252 # hashlib crashes on unicode items
1246 # hashlib crashes on unicode items
1253 email_address = safe_str(email_address)
1247 email_address = safe_str(email_address)
1254
1248
1255 # empty email or default user
1249 # empty email or default user
1256 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1250 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1257 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1251 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1258
1252
1259 if _use_gravatar:
1253 if _use_gravatar:
1260 # TODO: Disuse pyramid thread locals. Think about another solution to
1254 # TODO: Disuse pyramid thread locals. Think about another solution to
1261 # get the host and schema here.
1255 # get the host and schema here.
1262 request = get_current_request()
1256 request = get_current_request()
1263 tmpl = safe_str(_gravatar_url)
1257 tmpl = safe_str(_gravatar_url)
1264 tmpl = tmpl.replace('{email}', email_address)\
1258 tmpl = tmpl.replace('{email}', email_address)\
1265 .replace('{md5email}', md5_safe(email_address.lower())) \
1259 .replace('{md5email}', md5_safe(email_address.lower())) \
1266 .replace('{netloc}', request.host)\
1260 .replace('{netloc}', request.host)\
1267 .replace('{scheme}', request.scheme)\
1261 .replace('{scheme}', request.scheme)\
1268 .replace('{size}', safe_str(size))
1262 .replace('{size}', safe_str(size))
1269 return tmpl
1263 return tmpl
1270 else:
1264 else:
1271 return initials_gravatar(email_address, '', '', size=size)
1265 return initials_gravatar(email_address, '', '', size=size)
1272
1266
1273
1267
1274 class Page(_Page):
1268 class Page(_Page):
1275 """
1269 """
1276 Custom pager to match rendering style with paginator
1270 Custom pager to match rendering style with paginator
1277 """
1271 """
1278
1272
1279 def _get_pos(self, cur_page, max_page, items):
1273 def _get_pos(self, cur_page, max_page, items):
1280 edge = (items / 2) + 1
1274 edge = (items / 2) + 1
1281 if (cur_page <= edge):
1275 if (cur_page <= edge):
1282 radius = max(items / 2, items - cur_page)
1276 radius = max(items / 2, items - cur_page)
1283 elif (max_page - cur_page) < edge:
1277 elif (max_page - cur_page) < edge:
1284 radius = (items - 1) - (max_page - cur_page)
1278 radius = (items - 1) - (max_page - cur_page)
1285 else:
1279 else:
1286 radius = items / 2
1280 radius = items / 2
1287
1281
1288 left = max(1, (cur_page - (radius)))
1282 left = max(1, (cur_page - (radius)))
1289 right = min(max_page, cur_page + (radius))
1283 right = min(max_page, cur_page + (radius))
1290 return left, cur_page, right
1284 return left, cur_page, right
1291
1285
1292 def _range(self, regexp_match):
1286 def _range(self, regexp_match):
1293 """
1287 """
1294 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1288 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1295
1289
1296 Arguments:
1290 Arguments:
1297
1291
1298 regexp_match
1292 regexp_match
1299 A "re" (regular expressions) match object containing the
1293 A "re" (regular expressions) match object containing the
1300 radius of linked pages around the current page in
1294 radius of linked pages around the current page in
1301 regexp_match.group(1) as a string
1295 regexp_match.group(1) as a string
1302
1296
1303 This function is supposed to be called as a callable in
1297 This function is supposed to be called as a callable in
1304 re.sub.
1298 re.sub.
1305
1299
1306 """
1300 """
1307 radius = int(regexp_match.group(1))
1301 radius = int(regexp_match.group(1))
1308
1302
1309 # Compute the first and last page number within the radius
1303 # Compute the first and last page number within the radius
1310 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1304 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1311 # -> leftmost_page = 5
1305 # -> leftmost_page = 5
1312 # -> rightmost_page = 9
1306 # -> rightmost_page = 9
1313 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1307 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1314 self.last_page,
1308 self.last_page,
1315 (radius * 2) + 1)
1309 (radius * 2) + 1)
1316 nav_items = []
1310 nav_items = []
1317
1311
1318 # Create a link to the first page (unless we are on the first page
1312 # Create a link to the first page (unless we are on the first page
1319 # or there would be no need to insert '..' spacers)
1313 # or there would be no need to insert '..' spacers)
1320 if self.page != self.first_page and self.first_page < leftmost_page:
1314 if self.page != self.first_page and self.first_page < leftmost_page:
1321 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1315 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1322
1316
1323 # Insert dots if there are pages between the first page
1317 # Insert dots if there are pages between the first page
1324 # and the currently displayed page range
1318 # and the currently displayed page range
1325 if leftmost_page - self.first_page > 1:
1319 if leftmost_page - self.first_page > 1:
1326 # Wrap in a SPAN tag if nolink_attr is set
1320 # Wrap in a SPAN tag if nolink_attr is set
1327 text = '..'
1321 text = '..'
1328 if self.dotdot_attr:
1322 if self.dotdot_attr:
1329 text = HTML.span(c=text, **self.dotdot_attr)
1323 text = HTML.span(c=text, **self.dotdot_attr)
1330 nav_items.append(text)
1324 nav_items.append(text)
1331
1325
1332 for thispage in xrange(leftmost_page, rightmost_page + 1):
1326 for thispage in xrange(leftmost_page, rightmost_page + 1):
1333 # Hilight the current page number and do not use a link
1327 # Hilight the current page number and do not use a link
1334 if thispage == self.page:
1328 if thispage == self.page:
1335 text = '%s' % (thispage,)
1329 text = '%s' % (thispage,)
1336 # Wrap in a SPAN tag if nolink_attr is set
1330 # Wrap in a SPAN tag if nolink_attr is set
1337 if self.curpage_attr:
1331 if self.curpage_attr:
1338 text = HTML.span(c=text, **self.curpage_attr)
1332 text = HTML.span(c=text, **self.curpage_attr)
1339 nav_items.append(text)
1333 nav_items.append(text)
1340 # Otherwise create just a link to that page
1334 # Otherwise create just a link to that page
1341 else:
1335 else:
1342 text = '%s' % (thispage,)
1336 text = '%s' % (thispage,)
1343 nav_items.append(self._pagerlink(thispage, text))
1337 nav_items.append(self._pagerlink(thispage, text))
1344
1338
1345 # Insert dots if there are pages between the displayed
1339 # Insert dots if there are pages between the displayed
1346 # page numbers and the end of the page range
1340 # page numbers and the end of the page range
1347 if self.last_page - rightmost_page > 1:
1341 if self.last_page - rightmost_page > 1:
1348 text = '..'
1342 text = '..'
1349 # Wrap in a SPAN tag if nolink_attr is set
1343 # Wrap in a SPAN tag if nolink_attr is set
1350 if self.dotdot_attr:
1344 if self.dotdot_attr:
1351 text = HTML.span(c=text, **self.dotdot_attr)
1345 text = HTML.span(c=text, **self.dotdot_attr)
1352 nav_items.append(text)
1346 nav_items.append(text)
1353
1347
1354 # Create a link to the very last page (unless we are on the last
1348 # Create a link to the very last page (unless we are on the last
1355 # page or there would be no need to insert '..' spacers)
1349 # page or there would be no need to insert '..' spacers)
1356 if self.page != self.last_page and rightmost_page < self.last_page:
1350 if self.page != self.last_page and rightmost_page < self.last_page:
1357 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1351 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1358
1352
1359 ## prerender links
1353 ## prerender links
1360 #_page_link = url.current()
1354 #_page_link = url.current()
1361 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1355 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1362 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1356 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1363 return self.separator.join(nav_items)
1357 return self.separator.join(nav_items)
1364
1358
1365 def pager(self, format='~2~', page_param='page', partial_param='partial',
1359 def pager(self, format='~2~', page_param='page', partial_param='partial',
1366 show_if_single_page=False, separator=' ', onclick=None,
1360 show_if_single_page=False, separator=' ', onclick=None,
1367 symbol_first='<<', symbol_last='>>',
1361 symbol_first='<<', symbol_last='>>',
1368 symbol_previous='<', symbol_next='>',
1362 symbol_previous='<', symbol_next='>',
1369 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1363 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1370 curpage_attr={'class': 'pager_curpage'},
1364 curpage_attr={'class': 'pager_curpage'},
1371 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1365 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1372
1366
1373 self.curpage_attr = curpage_attr
1367 self.curpage_attr = curpage_attr
1374 self.separator = separator
1368 self.separator = separator
1375 self.pager_kwargs = kwargs
1369 self.pager_kwargs = kwargs
1376 self.page_param = page_param
1370 self.page_param = page_param
1377 self.partial_param = partial_param
1371 self.partial_param = partial_param
1378 self.onclick = onclick
1372 self.onclick = onclick
1379 self.link_attr = link_attr
1373 self.link_attr = link_attr
1380 self.dotdot_attr = dotdot_attr
1374 self.dotdot_attr = dotdot_attr
1381
1375
1382 # Don't show navigator if there is no more than one page
1376 # Don't show navigator if there is no more than one page
1383 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1377 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1384 return ''
1378 return ''
1385
1379
1386 from string import Template
1380 from string import Template
1387 # Replace ~...~ in token format by range of pages
1381 # Replace ~...~ in token format by range of pages
1388 result = re.sub(r'~(\d+)~', self._range, format)
1382 result = re.sub(r'~(\d+)~', self._range, format)
1389
1383
1390 # Interpolate '%' variables
1384 # Interpolate '%' variables
1391 result = Template(result).safe_substitute({
1385 result = Template(result).safe_substitute({
1392 'first_page': self.first_page,
1386 'first_page': self.first_page,
1393 'last_page': self.last_page,
1387 'last_page': self.last_page,
1394 'page': self.page,
1388 'page': self.page,
1395 'page_count': self.page_count,
1389 'page_count': self.page_count,
1396 'items_per_page': self.items_per_page,
1390 'items_per_page': self.items_per_page,
1397 'first_item': self.first_item,
1391 'first_item': self.first_item,
1398 'last_item': self.last_item,
1392 'last_item': self.last_item,
1399 'item_count': self.item_count,
1393 'item_count': self.item_count,
1400 'link_first': self.page > self.first_page and \
1394 'link_first': self.page > self.first_page and \
1401 self._pagerlink(self.first_page, symbol_first) or '',
1395 self._pagerlink(self.first_page, symbol_first) or '',
1402 'link_last': self.page < self.last_page and \
1396 'link_last': self.page < self.last_page and \
1403 self._pagerlink(self.last_page, symbol_last) or '',
1397 self._pagerlink(self.last_page, symbol_last) or '',
1404 'link_previous': self.previous_page and \
1398 'link_previous': self.previous_page and \
1405 self._pagerlink(self.previous_page, symbol_previous) \
1399 self._pagerlink(self.previous_page, symbol_previous) \
1406 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1400 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1407 'link_next': self.next_page and \
1401 'link_next': self.next_page and \
1408 self._pagerlink(self.next_page, symbol_next) \
1402 self._pagerlink(self.next_page, symbol_next) \
1409 or HTML.span(symbol_next, class_="pg-next disabled")
1403 or HTML.span(symbol_next, class_="pg-next disabled")
1410 })
1404 })
1411
1405
1412 return literal(result)
1406 return literal(result)
1413
1407
1414
1408
1415 #==============================================================================
1409 #==============================================================================
1416 # REPO PAGER, PAGER FOR REPOSITORY
1410 # REPO PAGER, PAGER FOR REPOSITORY
1417 #==============================================================================
1411 #==============================================================================
1418 class RepoPage(Page):
1412 class RepoPage(Page):
1419
1413
1420 def __init__(self, collection, page=1, items_per_page=20,
1414 def __init__(self, collection, page=1, items_per_page=20,
1421 item_count=None, url=None, **kwargs):
1415 item_count=None, url=None, **kwargs):
1422
1416
1423 """Create a "RepoPage" instance. special pager for paging
1417 """Create a "RepoPage" instance. special pager for paging
1424 repository
1418 repository
1425 """
1419 """
1426 self._url_generator = url
1420 self._url_generator = url
1427
1421
1428 # Safe the kwargs class-wide so they can be used in the pager() method
1422 # Safe the kwargs class-wide so they can be used in the pager() method
1429 self.kwargs = kwargs
1423 self.kwargs = kwargs
1430
1424
1431 # Save a reference to the collection
1425 # Save a reference to the collection
1432 self.original_collection = collection
1426 self.original_collection = collection
1433
1427
1434 self.collection = collection
1428 self.collection = collection
1435
1429
1436 # The self.page is the number of the current page.
1430 # The self.page is the number of the current page.
1437 # The first page has the number 1!
1431 # The first page has the number 1!
1438 try:
1432 try:
1439 self.page = int(page) # make it int() if we get it as a string
1433 self.page = int(page) # make it int() if we get it as a string
1440 except (ValueError, TypeError):
1434 except (ValueError, TypeError):
1441 self.page = 1
1435 self.page = 1
1442
1436
1443 self.items_per_page = items_per_page
1437 self.items_per_page = items_per_page
1444
1438
1445 # Unless the user tells us how many items the collections has
1439 # Unless the user tells us how many items the collections has
1446 # we calculate that ourselves.
1440 # we calculate that ourselves.
1447 if item_count is not None:
1441 if item_count is not None:
1448 self.item_count = item_count
1442 self.item_count = item_count
1449 else:
1443 else:
1450 self.item_count = len(self.collection)
1444 self.item_count = len(self.collection)
1451
1445
1452 # Compute the number of the first and last available page
1446 # Compute the number of the first and last available page
1453 if self.item_count > 0:
1447 if self.item_count > 0:
1454 self.first_page = 1
1448 self.first_page = 1
1455 self.page_count = int(math.ceil(float(self.item_count) /
1449 self.page_count = int(math.ceil(float(self.item_count) /
1456 self.items_per_page))
1450 self.items_per_page))
1457 self.last_page = self.first_page + self.page_count - 1
1451 self.last_page = self.first_page + self.page_count - 1
1458
1452
1459 # Make sure that the requested page number is the range of
1453 # Make sure that the requested page number is the range of
1460 # valid pages
1454 # valid pages
1461 if self.page > self.last_page:
1455 if self.page > self.last_page:
1462 self.page = self.last_page
1456 self.page = self.last_page
1463 elif self.page < self.first_page:
1457 elif self.page < self.first_page:
1464 self.page = self.first_page
1458 self.page = self.first_page
1465
1459
1466 # Note: the number of items on this page can be less than
1460 # Note: the number of items on this page can be less than
1467 # items_per_page if the last page is not full
1461 # items_per_page if the last page is not full
1468 self.first_item = max(0, (self.item_count) - (self.page *
1462 self.first_item = max(0, (self.item_count) - (self.page *
1469 items_per_page))
1463 items_per_page))
1470 self.last_item = ((self.item_count - 1) - items_per_page *
1464 self.last_item = ((self.item_count - 1) - items_per_page *
1471 (self.page - 1))
1465 (self.page - 1))
1472
1466
1473 self.items = list(self.collection[self.first_item:self.last_item + 1])
1467 self.items = list(self.collection[self.first_item:self.last_item + 1])
1474
1468
1475 # Links to previous and next page
1469 # Links to previous and next page
1476 if self.page > self.first_page:
1470 if self.page > self.first_page:
1477 self.previous_page = self.page - 1
1471 self.previous_page = self.page - 1
1478 else:
1472 else:
1479 self.previous_page = None
1473 self.previous_page = None
1480
1474
1481 if self.page < self.last_page:
1475 if self.page < self.last_page:
1482 self.next_page = self.page + 1
1476 self.next_page = self.page + 1
1483 else:
1477 else:
1484 self.next_page = None
1478 self.next_page = None
1485
1479
1486 # No items available
1480 # No items available
1487 else:
1481 else:
1488 self.first_page = None
1482 self.first_page = None
1489 self.page_count = 0
1483 self.page_count = 0
1490 self.last_page = None
1484 self.last_page = None
1491 self.first_item = None
1485 self.first_item = None
1492 self.last_item = None
1486 self.last_item = None
1493 self.previous_page = None
1487 self.previous_page = None
1494 self.next_page = None
1488 self.next_page = None
1495 self.items = []
1489 self.items = []
1496
1490
1497 # This is a subclass of the 'list' type. Initialise the list now.
1491 # This is a subclass of the 'list' type. Initialise the list now.
1498 list.__init__(self, reversed(self.items))
1492 list.__init__(self, reversed(self.items))
1499
1493
1500
1494
1501 def changed_tooltip(nodes):
1495 def changed_tooltip(nodes):
1502 """
1496 """
1503 Generates a html string for changed nodes in commit page.
1497 Generates a html string for changed nodes in commit page.
1504 It limits the output to 30 entries
1498 It limits the output to 30 entries
1505
1499
1506 :param nodes: LazyNodesGenerator
1500 :param nodes: LazyNodesGenerator
1507 """
1501 """
1508 if nodes:
1502 if nodes:
1509 pref = ': <br/> '
1503 pref = ': <br/> '
1510 suf = ''
1504 suf = ''
1511 if len(nodes) > 30:
1505 if len(nodes) > 30:
1512 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1506 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1513 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1507 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1514 for x in nodes[:30]]) + suf)
1508 for x in nodes[:30]]) + suf)
1515 else:
1509 else:
1516 return ': ' + _('No Files')
1510 return ': ' + _('No Files')
1517
1511
1518
1512
1519 def breadcrumb_repo_link(repo):
1513 def breadcrumb_repo_link(repo):
1520 """
1514 """
1521 Makes a breadcrumbs path link to repo
1515 Makes a breadcrumbs path link to repo
1522
1516
1523 ex::
1517 ex::
1524 group >> subgroup >> repo
1518 group >> subgroup >> repo
1525
1519
1526 :param repo: a Repository instance
1520 :param repo: a Repository instance
1527 """
1521 """
1528
1522
1529 path = [
1523 path = [
1530 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1524 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1531 for group in repo.groups_with_parents
1525 for group in repo.groups_with_parents
1532 ] + [
1526 ] + [
1533 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1527 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1534 ]
1528 ]
1535
1529
1536 return literal(' &raquo; '.join(path))
1530 return literal(' &raquo; '.join(path))
1537
1531
1538
1532
1539 def format_byte_size_binary(file_size):
1533 def format_byte_size_binary(file_size):
1540 """
1534 """
1541 Formats file/folder sizes to standard.
1535 Formats file/folder sizes to standard.
1542 """
1536 """
1543 formatted_size = format_byte_size(file_size, binary=True)
1537 formatted_size = format_byte_size(file_size, binary=True)
1544 return formatted_size
1538 return formatted_size
1545
1539
1546
1540
1547 def fancy_file_stats(stats):
1541 def fancy_file_stats(stats):
1548 """
1542 """
1549 Displays a fancy two colored bar for number of added/deleted
1543 Displays a fancy two colored bar for number of added/deleted
1550 lines of code on file
1544 lines of code on file
1551
1545
1552 :param stats: two element list of added/deleted lines of code
1546 :param stats: two element list of added/deleted lines of code
1553 """
1547 """
1554 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1548 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1555 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1549 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1556
1550
1557 def cgen(l_type, a_v, d_v):
1551 def cgen(l_type, a_v, d_v):
1558 mapping = {'tr': 'top-right-rounded-corner-mid',
1552 mapping = {'tr': 'top-right-rounded-corner-mid',
1559 'tl': 'top-left-rounded-corner-mid',
1553 'tl': 'top-left-rounded-corner-mid',
1560 'br': 'bottom-right-rounded-corner-mid',
1554 'br': 'bottom-right-rounded-corner-mid',
1561 'bl': 'bottom-left-rounded-corner-mid'}
1555 'bl': 'bottom-left-rounded-corner-mid'}
1562 map_getter = lambda x: mapping[x]
1556 map_getter = lambda x: mapping[x]
1563
1557
1564 if l_type == 'a' and d_v:
1558 if l_type == 'a' and d_v:
1565 #case when added and deleted are present
1559 #case when added and deleted are present
1566 return ' '.join(map(map_getter, ['tl', 'bl']))
1560 return ' '.join(map(map_getter, ['tl', 'bl']))
1567
1561
1568 if l_type == 'a' and not d_v:
1562 if l_type == 'a' and not d_v:
1569 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1563 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1570
1564
1571 if l_type == 'd' and a_v:
1565 if l_type == 'd' and a_v:
1572 return ' '.join(map(map_getter, ['tr', 'br']))
1566 return ' '.join(map(map_getter, ['tr', 'br']))
1573
1567
1574 if l_type == 'd' and not a_v:
1568 if l_type == 'd' and not a_v:
1575 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1569 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1576
1570
1577 a, d = stats['added'], stats['deleted']
1571 a, d = stats['added'], stats['deleted']
1578 width = 100
1572 width = 100
1579
1573
1580 if stats['binary']: # binary operations like chmod/rename etc
1574 if stats['binary']: # binary operations like chmod/rename etc
1581 lbl = []
1575 lbl = []
1582 bin_op = 0 # undefined
1576 bin_op = 0 # undefined
1583
1577
1584 # prefix with bin for binary files
1578 # prefix with bin for binary files
1585 if BIN_FILENODE in stats['ops']:
1579 if BIN_FILENODE in stats['ops']:
1586 lbl += ['bin']
1580 lbl += ['bin']
1587
1581
1588 if NEW_FILENODE in stats['ops']:
1582 if NEW_FILENODE in stats['ops']:
1589 lbl += [_('new file')]
1583 lbl += [_('new file')]
1590 bin_op = NEW_FILENODE
1584 bin_op = NEW_FILENODE
1591 elif MOD_FILENODE in stats['ops']:
1585 elif MOD_FILENODE in stats['ops']:
1592 lbl += [_('mod')]
1586 lbl += [_('mod')]
1593 bin_op = MOD_FILENODE
1587 bin_op = MOD_FILENODE
1594 elif DEL_FILENODE in stats['ops']:
1588 elif DEL_FILENODE in stats['ops']:
1595 lbl += [_('del')]
1589 lbl += [_('del')]
1596 bin_op = DEL_FILENODE
1590 bin_op = DEL_FILENODE
1597 elif RENAMED_FILENODE in stats['ops']:
1591 elif RENAMED_FILENODE in stats['ops']:
1598 lbl += [_('rename')]
1592 lbl += [_('rename')]
1599 bin_op = RENAMED_FILENODE
1593 bin_op = RENAMED_FILENODE
1600
1594
1601 # chmod can go with other operations, so we add a + to lbl if needed
1595 # chmod can go with other operations, so we add a + to lbl if needed
1602 if CHMOD_FILENODE in stats['ops']:
1596 if CHMOD_FILENODE in stats['ops']:
1603 lbl += [_('chmod')]
1597 lbl += [_('chmod')]
1604 if bin_op == 0:
1598 if bin_op == 0:
1605 bin_op = CHMOD_FILENODE
1599 bin_op = CHMOD_FILENODE
1606
1600
1607 lbl = '+'.join(lbl)
1601 lbl = '+'.join(lbl)
1608 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1602 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1609 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1603 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1610 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1604 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1611 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1605 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1612
1606
1613 t = stats['added'] + stats['deleted']
1607 t = stats['added'] + stats['deleted']
1614 unit = float(width) / (t or 1)
1608 unit = float(width) / (t or 1)
1615
1609
1616 # needs > 9% of width to be visible or 0 to be hidden
1610 # needs > 9% of width to be visible or 0 to be hidden
1617 a_p = max(9, unit * a) if a > 0 else 0
1611 a_p = max(9, unit * a) if a > 0 else 0
1618 d_p = max(9, unit * d) if d > 0 else 0
1612 d_p = max(9, unit * d) if d > 0 else 0
1619 p_sum = a_p + d_p
1613 p_sum = a_p + d_p
1620
1614
1621 if p_sum > width:
1615 if p_sum > width:
1622 #adjust the percentage to be == 100% since we adjusted to 9
1616 #adjust the percentage to be == 100% since we adjusted to 9
1623 if a_p > d_p:
1617 if a_p > d_p:
1624 a_p = a_p - (p_sum - width)
1618 a_p = a_p - (p_sum - width)
1625 else:
1619 else:
1626 d_p = d_p - (p_sum - width)
1620 d_p = d_p - (p_sum - width)
1627
1621
1628 a_v = a if a > 0 else ''
1622 a_v = a if a > 0 else ''
1629 d_v = d if d > 0 else ''
1623 d_v = d if d > 0 else ''
1630
1624
1631 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1625 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1632 cgen('a', a_v, d_v), a_p, a_v
1626 cgen('a', a_v, d_v), a_p, a_v
1633 )
1627 )
1634 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1628 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1635 cgen('d', a_v, d_v), d_p, d_v
1629 cgen('d', a_v, d_v), d_p, d_v
1636 )
1630 )
1637 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1631 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1638
1632
1639
1633
1640 def urlify_text(text_, safe=True):
1634 def urlify_text(text_, safe=True):
1641 """
1635 """
1642 Extrac urls from text and make html links out of them
1636 Extrac urls from text and make html links out of them
1643
1637
1644 :param text_:
1638 :param text_:
1645 """
1639 """
1646
1640
1647 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1641 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1648 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1642 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1649
1643
1650 def url_func(match_obj):
1644 def url_func(match_obj):
1651 url_full = match_obj.groups()[0]
1645 url_full = match_obj.groups()[0]
1652 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1646 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1653 _newtext = url_pat.sub(url_func, text_)
1647 _newtext = url_pat.sub(url_func, text_)
1654 if safe:
1648 if safe:
1655 return literal(_newtext)
1649 return literal(_newtext)
1656 return _newtext
1650 return _newtext
1657
1651
1658
1652
1659 def urlify_commits(text_, repository):
1653 def urlify_commits(text_, repository):
1660 """
1654 """
1661 Extract commit ids from text and make link from them
1655 Extract commit ids from text and make link from them
1662
1656
1663 :param text_:
1657 :param text_:
1664 :param repository: repo name to build the URL with
1658 :param repository: repo name to build the URL with
1665 """
1659 """
1666 from pylons import url # doh, we need to re-import url to mock it later
1660 from pylons import url # doh, we need to re-import url to mock it later
1667 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1661 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1668
1662
1669 def url_func(match_obj):
1663 def url_func(match_obj):
1670 commit_id = match_obj.groups()[1]
1664 commit_id = match_obj.groups()[1]
1671 pref = match_obj.groups()[0]
1665 pref = match_obj.groups()[0]
1672 suf = match_obj.groups()[2]
1666 suf = match_obj.groups()[2]
1673
1667
1674 tmpl = (
1668 tmpl = (
1675 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1669 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1676 '%(commit_id)s</a>%(suf)s'
1670 '%(commit_id)s</a>%(suf)s'
1677 )
1671 )
1678 return tmpl % {
1672 return tmpl % {
1679 'pref': pref,
1673 'pref': pref,
1680 'cls': 'revision-link',
1674 'cls': 'revision-link',
1681 'url': url('changeset_home', repo_name=repository,
1675 'url': url('changeset_home', repo_name=repository,
1682 revision=commit_id, qualified=True),
1676 revision=commit_id, qualified=True),
1683 'commit_id': commit_id,
1677 'commit_id': commit_id,
1684 'suf': suf
1678 'suf': suf
1685 }
1679 }
1686
1680
1687 newtext = URL_PAT.sub(url_func, text_)
1681 newtext = URL_PAT.sub(url_func, text_)
1688
1682
1689 return newtext
1683 return newtext
1690
1684
1691
1685
1692 def _process_url_func(match_obj, repo_name, uid, entry,
1686 def _process_url_func(match_obj, repo_name, uid, entry,
1693 return_raw_data=False):
1687 return_raw_data=False):
1694 pref = ''
1688 pref = ''
1695 if match_obj.group().startswith(' '):
1689 if match_obj.group().startswith(' '):
1696 pref = ' '
1690 pref = ' '
1697
1691
1698 issue_id = ''.join(match_obj.groups())
1692 issue_id = ''.join(match_obj.groups())
1699 tmpl = (
1693 tmpl = (
1700 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1694 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1701 '%(issue-prefix)s%(id-repr)s'
1695 '%(issue-prefix)s%(id-repr)s'
1702 '</a>')
1696 '</a>')
1703
1697
1704 (repo_name_cleaned,
1698 (repo_name_cleaned,
1705 parent_group_name) = RepoGroupModel().\
1699 parent_group_name) = RepoGroupModel().\
1706 _get_group_name_and_parent(repo_name)
1700 _get_group_name_and_parent(repo_name)
1707
1701
1708 # variables replacement
1702 # variables replacement
1709 named_vars = {
1703 named_vars = {
1710 'id': issue_id,
1704 'id': issue_id,
1711 'repo': repo_name,
1705 'repo': repo_name,
1712 'repo_name': repo_name_cleaned,
1706 'repo_name': repo_name_cleaned,
1713 'group_name': parent_group_name
1707 'group_name': parent_group_name
1714 }
1708 }
1715 # named regex variables
1709 # named regex variables
1716 named_vars.update(match_obj.groupdict())
1710 named_vars.update(match_obj.groupdict())
1717 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1711 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1718
1712
1719 data = {
1713 data = {
1720 'pref': pref,
1714 'pref': pref,
1721 'cls': 'issue-tracker-link',
1715 'cls': 'issue-tracker-link',
1722 'url': _url,
1716 'url': _url,
1723 'id-repr': issue_id,
1717 'id-repr': issue_id,
1724 'issue-prefix': entry['pref'],
1718 'issue-prefix': entry['pref'],
1725 'serv': entry['url'],
1719 'serv': entry['url'],
1726 }
1720 }
1727 if return_raw_data:
1721 if return_raw_data:
1728 return {
1722 return {
1729 'id': issue_id,
1723 'id': issue_id,
1730 'url': _url
1724 'url': _url
1731 }
1725 }
1732 return tmpl % data
1726 return tmpl % data
1733
1727
1734
1728
1735 def process_patterns(text_string, repo_name, config=None):
1729 def process_patterns(text_string, repo_name, config=None):
1736 repo = None
1730 repo = None
1737 if repo_name:
1731 if repo_name:
1738 # Retrieving repo_name to avoid invalid repo_name to explode on
1732 # Retrieving repo_name to avoid invalid repo_name to explode on
1739 # IssueTrackerSettingsModel but still passing invalid name further down
1733 # IssueTrackerSettingsModel but still passing invalid name further down
1740 repo = Repository.get_by_repo_name(repo_name, cache=True)
1734 repo = Repository.get_by_repo_name(repo_name, cache=True)
1741
1735
1742 settings_model = IssueTrackerSettingsModel(repo=repo)
1736 settings_model = IssueTrackerSettingsModel(repo=repo)
1743 active_entries = settings_model.get_settings(cache=True)
1737 active_entries = settings_model.get_settings(cache=True)
1744
1738
1745 issues_data = []
1739 issues_data = []
1746 newtext = text_string
1740 newtext = text_string
1747 for uid, entry in active_entries.items():
1741 for uid, entry in active_entries.items():
1748 log.debug('found issue tracker entry with uid %s' % (uid,))
1742 log.debug('found issue tracker entry with uid %s' % (uid,))
1749
1743
1750 if not (entry['pat'] and entry['url']):
1744 if not (entry['pat'] and entry['url']):
1751 log.debug('skipping due to missing data')
1745 log.debug('skipping due to missing data')
1752 continue
1746 continue
1753
1747
1754 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1748 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1755 % (uid, entry['pat'], entry['url'], entry['pref']))
1749 % (uid, entry['pat'], entry['url'], entry['pref']))
1756
1750
1757 try:
1751 try:
1758 pattern = re.compile(r'%s' % entry['pat'])
1752 pattern = re.compile(r'%s' % entry['pat'])
1759 except re.error:
1753 except re.error:
1760 log.exception(
1754 log.exception(
1761 'issue tracker pattern: `%s` failed to compile',
1755 'issue tracker pattern: `%s` failed to compile',
1762 entry['pat'])
1756 entry['pat'])
1763 continue
1757 continue
1764
1758
1765 data_func = partial(
1759 data_func = partial(
1766 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1760 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1767 return_raw_data=True)
1761 return_raw_data=True)
1768
1762
1769 for match_obj in pattern.finditer(text_string):
1763 for match_obj in pattern.finditer(text_string):
1770 issues_data.append(data_func(match_obj))
1764 issues_data.append(data_func(match_obj))
1771
1765
1772 url_func = partial(
1766 url_func = partial(
1773 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1767 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1774
1768
1775 newtext = pattern.sub(url_func, newtext)
1769 newtext = pattern.sub(url_func, newtext)
1776 log.debug('processed prefix:uid `%s`' % (uid,))
1770 log.debug('processed prefix:uid `%s`' % (uid,))
1777
1771
1778 return newtext, issues_data
1772 return newtext, issues_data
1779
1773
1780
1774
1781 def urlify_commit_message(commit_text, repository=None):
1775 def urlify_commit_message(commit_text, repository=None):
1782 """
1776 """
1783 Parses given text message and makes proper links.
1777 Parses given text message and makes proper links.
1784 issues are linked to given issue-server, and rest is a commit link
1778 issues are linked to given issue-server, and rest is a commit link
1785
1779
1786 :param commit_text:
1780 :param commit_text:
1787 :param repository:
1781 :param repository:
1788 """
1782 """
1789 from pylons import url # doh, we need to re-import url to mock it later
1783 from pylons import url # doh, we need to re-import url to mock it later
1790
1784
1791 def escaper(string):
1785 def escaper(string):
1792 return string.replace('<', '&lt;').replace('>', '&gt;')
1786 return string.replace('<', '&lt;').replace('>', '&gt;')
1793
1787
1794 newtext = escaper(commit_text)
1788 newtext = escaper(commit_text)
1795
1789
1796 # extract http/https links and make them real urls
1790 # extract http/https links and make them real urls
1797 newtext = urlify_text(newtext, safe=False)
1791 newtext = urlify_text(newtext, safe=False)
1798
1792
1799 # urlify commits - extract commit ids and make link out of them, if we have
1793 # urlify commits - extract commit ids and make link out of them, if we have
1800 # the scope of repository present.
1794 # the scope of repository present.
1801 if repository:
1795 if repository:
1802 newtext = urlify_commits(newtext, repository)
1796 newtext = urlify_commits(newtext, repository)
1803
1797
1804 # process issue tracker patterns
1798 # process issue tracker patterns
1805 newtext, issues = process_patterns(newtext, repository or '')
1799 newtext, issues = process_patterns(newtext, repository or '')
1806
1800
1807 return literal(newtext)
1801 return literal(newtext)
1808
1802
1809
1803
1810 def rst(source, mentions=False):
1804 def rst(source, mentions=False):
1811 return literal('<div class="rst-block">%s</div>' %
1805 return literal('<div class="rst-block">%s</div>' %
1812 MarkupRenderer.rst(source, mentions=mentions))
1806 MarkupRenderer.rst(source, mentions=mentions))
1813
1807
1814
1808
1815 def markdown(source, mentions=False):
1809 def markdown(source, mentions=False):
1816 return literal('<div class="markdown-block">%s</div>' %
1810 return literal('<div class="markdown-block">%s</div>' %
1817 MarkupRenderer.markdown(source, flavored=True,
1811 MarkupRenderer.markdown(source, flavored=True,
1818 mentions=mentions))
1812 mentions=mentions))
1819
1813
1820 def renderer_from_filename(filename, exclude=None):
1814 def renderer_from_filename(filename, exclude=None):
1821 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1815 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1822
1816
1823
1817
1824 def render(source, renderer='rst', mentions=False):
1818 def render(source, renderer='rst', mentions=False):
1825 if renderer == 'rst':
1819 if renderer == 'rst':
1826 return rst(source, mentions=mentions)
1820 return rst(source, mentions=mentions)
1827 if renderer == 'markdown':
1821 if renderer == 'markdown':
1828 return markdown(source, mentions=mentions)
1822 return markdown(source, mentions=mentions)
1829
1823
1830
1824
1831 def commit_status(repo, commit_id):
1825 def commit_status(repo, commit_id):
1832 return ChangesetStatusModel().get_status(repo, commit_id)
1826 return ChangesetStatusModel().get_status(repo, commit_id)
1833
1827
1834
1828
1835 def commit_status_lbl(commit_status):
1829 def commit_status_lbl(commit_status):
1836 return dict(ChangesetStatus.STATUSES).get(commit_status)
1830 return dict(ChangesetStatus.STATUSES).get(commit_status)
1837
1831
1838
1832
1839 def commit_time(repo_name, commit_id):
1833 def commit_time(repo_name, commit_id):
1840 repo = Repository.get_by_repo_name(repo_name)
1834 repo = Repository.get_by_repo_name(repo_name)
1841 commit = repo.get_commit(commit_id=commit_id)
1835 commit = repo.get_commit(commit_id=commit_id)
1842 return commit.date
1836 return commit.date
1843
1837
1844
1838
1845 def get_permission_name(key):
1839 def get_permission_name(key):
1846 return dict(Permission.PERMS).get(key)
1840 return dict(Permission.PERMS).get(key)
1847
1841
1848
1842
1849 def journal_filter_help():
1843 def journal_filter_help():
1850 return _(
1844 return _(
1851 'Example filter terms:\n' +
1845 'Example filter terms:\n' +
1852 ' repository:vcs\n' +
1846 ' repository:vcs\n' +
1853 ' username:marcin\n' +
1847 ' username:marcin\n' +
1854 ' action:*push*\n' +
1848 ' action:*push*\n' +
1855 ' ip:127.0.0.1\n' +
1849 ' ip:127.0.0.1\n' +
1856 ' date:20120101\n' +
1850 ' date:20120101\n' +
1857 ' date:[20120101100000 TO 20120102]\n' +
1851 ' date:[20120101100000 TO 20120102]\n' +
1858 '\n' +
1852 '\n' +
1859 'Generate wildcards using \'*\' character:\n' +
1853 'Generate wildcards using \'*\' character:\n' +
1860 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1854 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1861 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1855 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1862 '\n' +
1856 '\n' +
1863 'Optional AND / OR operators in queries\n' +
1857 'Optional AND / OR operators in queries\n' +
1864 ' "repository:vcs OR repository:test"\n' +
1858 ' "repository:vcs OR repository:test"\n' +
1865 ' "username:test AND repository:test*"\n'
1859 ' "username:test AND repository:test*"\n'
1866 )
1860 )
1867
1861
1868
1862
1869 def not_mapped_error(repo_name):
1863 def not_mapped_error(repo_name):
1870 flash(_('%s repository is not mapped to db perhaps'
1864 flash(_('%s repository is not mapped to db perhaps'
1871 ' it was created or renamed from the filesystem'
1865 ' it was created or renamed from the filesystem'
1872 ' please run the application again'
1866 ' please run the application again'
1873 ' in order to rescan repositories') % repo_name, category='error')
1867 ' in order to rescan repositories') % repo_name, category='error')
1874
1868
1875
1869
1876 def ip_range(ip_addr):
1870 def ip_range(ip_addr):
1877 from rhodecode.model.db import UserIpMap
1871 from rhodecode.model.db import UserIpMap
1878 s, e = UserIpMap._get_ip_range(ip_addr)
1872 s, e = UserIpMap._get_ip_range(ip_addr)
1879 return '%s - %s' % (s, e)
1873 return '%s - %s' % (s, e)
1880
1874
1881
1875
1882 def form(url, method='post', needs_csrf_token=True, **attrs):
1876 def form(url, method='post', needs_csrf_token=True, **attrs):
1883 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1877 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1884 if method.lower() != 'get' and needs_csrf_token:
1878 if method.lower() != 'get' and needs_csrf_token:
1885 raise Exception(
1879 raise Exception(
1886 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1880 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1887 'CSRF token. If the endpoint does not require such token you can ' +
1881 'CSRF token. If the endpoint does not require such token you can ' +
1888 'explicitly set the parameter needs_csrf_token to false.')
1882 'explicitly set the parameter needs_csrf_token to false.')
1889
1883
1890 return wh_form(url, method=method, **attrs)
1884 return wh_form(url, method=method, **attrs)
1891
1885
1892
1886
1893 def secure_form(url, method="POST", multipart=False, **attrs):
1887 def secure_form(url, method="POST", multipart=False, **attrs):
1894 """Start a form tag that points the action to an url. This
1888 """Start a form tag that points the action to an url. This
1895 form tag will also include the hidden field containing
1889 form tag will also include the hidden field containing
1896 the auth token.
1890 the auth token.
1897
1891
1898 The url options should be given either as a string, or as a
1892 The url options should be given either as a string, or as a
1899 ``url()`` function. The method for the form defaults to POST.
1893 ``url()`` function. The method for the form defaults to POST.
1900
1894
1901 Options:
1895 Options:
1902
1896
1903 ``multipart``
1897 ``multipart``
1904 If set to True, the enctype is set to "multipart/form-data".
1898 If set to True, the enctype is set to "multipart/form-data".
1905 ``method``
1899 ``method``
1906 The method to use when submitting the form, usually either
1900 The method to use when submitting the form, usually either
1907 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1901 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1908 hidden input with name _method is added to simulate the verb
1902 hidden input with name _method is added to simulate the verb
1909 over POST.
1903 over POST.
1910
1904
1911 """
1905 """
1912 from webhelpers.pylonslib.secure_form import insecure_form
1906 from webhelpers.pylonslib.secure_form import insecure_form
1913 form = insecure_form(url, method, multipart, **attrs)
1907 form = insecure_form(url, method, multipart, **attrs)
1914 token = csrf_input()
1908 token = csrf_input()
1915 return literal("%s\n%s" % (form, token))
1909 return literal("%s\n%s" % (form, token))
1916
1910
1917 def csrf_input():
1911 def csrf_input():
1918 return literal(
1912 return literal(
1919 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1913 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1920 csrf_token_key, csrf_token_key, get_csrf_token()))
1914 csrf_token_key, csrf_token_key, get_csrf_token()))
1921
1915
1922 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1916 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1923 select_html = select(name, selected, options, **attrs)
1917 select_html = select(name, selected, options, **attrs)
1924 select2 = """
1918 select2 = """
1925 <script>
1919 <script>
1926 $(document).ready(function() {
1920 $(document).ready(function() {
1927 $('#%s').select2({
1921 $('#%s').select2({
1928 containerCssClass: 'drop-menu',
1922 containerCssClass: 'drop-menu',
1929 dropdownCssClass: 'drop-menu-dropdown',
1923 dropdownCssClass: 'drop-menu-dropdown',
1930 dropdownAutoWidth: true%s
1924 dropdownAutoWidth: true%s
1931 });
1925 });
1932 });
1926 });
1933 </script>
1927 </script>
1934 """
1928 """
1935 filter_option = """,
1929 filter_option = """,
1936 minimumResultsForSearch: -1
1930 minimumResultsForSearch: -1
1937 """
1931 """
1938 input_id = attrs.get('id') or name
1932 input_id = attrs.get('id') or name
1939 filter_enabled = "" if enable_filter else filter_option
1933 filter_enabled = "" if enable_filter else filter_option
1940 select_script = literal(select2 % (input_id, filter_enabled))
1934 select_script = literal(select2 % (input_id, filter_enabled))
1941
1935
1942 return literal(select_html+select_script)
1936 return literal(select_html+select_script)
1943
1937
1944
1938
1945 def get_visual_attr(tmpl_context_var, attr_name):
1939 def get_visual_attr(tmpl_context_var, attr_name):
1946 """
1940 """
1947 A safe way to get a variable from visual variable of template context
1941 A safe way to get a variable from visual variable of template context
1948
1942
1949 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1943 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1950 :param attr_name: name of the attribute we fetch from the c.visual
1944 :param attr_name: name of the attribute we fetch from the c.visual
1951 """
1945 """
1952 visual = getattr(tmpl_context_var, 'visual', None)
1946 visual = getattr(tmpl_context_var, 'visual', None)
1953 if not visual:
1947 if not visual:
1954 return
1948 return
1955 else:
1949 else:
1956 return getattr(visual, attr_name, None)
1950 return getattr(visual, attr_name, None)
1957
1951
1958
1952
1959 def get_last_path_part(file_node):
1953 def get_last_path_part(file_node):
1960 if not file_node.path:
1954 if not file_node.path:
1961 return u''
1955 return u''
1962
1956
1963 path = safe_unicode(file_node.path.split('/')[-1])
1957 path = safe_unicode(file_node.path.split('/')[-1])
1964 return u'../' + path
1958 return u'../' + path
1965
1959
1966
1960
1967 def route_path(*args, **kwds):
1961 def route_path(*args, **kwds):
1968 """
1962 """
1969 Wrapper around pyramids `route_path` function. It is used to generate
1963 Wrapper around pyramids `route_path` function. It is used to generate
1970 URLs from within pylons views or templates. This will be removed when
1964 URLs from within pylons views or templates. This will be removed when
1971 pyramid migration if finished.
1965 pyramid migration if finished.
1972 """
1966 """
1973 req = get_current_request()
1967 req = get_current_request()
1974 return req.route_path(*args, **kwds)
1968 return req.route_path(*args, **kwds)
1975
1969
1976
1970
1977 def route_path_or_none(*args, **kwargs):
1971 def route_path_or_none(*args, **kwargs):
1978 try:
1972 try:
1979 return route_path(*args, **kwargs)
1973 return route_path(*args, **kwargs)
1980 except KeyError:
1974 except KeyError:
1981 return None
1975 return None
1982
1976
1983
1977
1984 def static_url(*args, **kwds):
1978 def static_url(*args, **kwds):
1985 """
1979 """
1986 Wrapper around pyramids `route_path` function. It is used to generate
1980 Wrapper around pyramids `route_path` function. It is used to generate
1987 URLs from within pylons views or templates. This will be removed when
1981 URLs from within pylons views or templates. This will be removed when
1988 pyramid migration if finished.
1982 pyramid migration if finished.
1989 """
1983 """
1990 req = get_current_request()
1984 req = get_current_request()
1991 return req.static_url(*args, **kwds)
1985 return req.static_url(*args, **kwds)
1992
1986
1993
1987
1994 def resource_path(*args, **kwds):
1988 def resource_path(*args, **kwds):
1995 """
1989 """
1996 Wrapper around pyramids `route_path` function. It is used to generate
1990 Wrapper around pyramids `route_path` function. It is used to generate
1997 URLs from within pylons views or templates. This will be removed when
1991 URLs from within pylons views or templates. This will be removed when
1998 pyramid migration if finished.
1992 pyramid migration if finished.
1999 """
1993 """
2000 req = get_current_request()
1994 req = get_current_request()
2001 return req.resource_path(*args, **kwds)
1995 return req.resource_path(*args, **kwds)
@@ -1,856 +1,857 b''
1 //
1 //
2 // Variables
2 // Variables
3 // --------------------------------------------------
3 // --------------------------------------------------
4
4
5
5
6 //== Colors
6 //== Colors
7 //
7 //
8 //## Gray and brand colors for use across Bootstrap.
8 //## Gray and brand colors for use across Bootstrap.
9
9
10 @gray-base: #000;
10 @gray-base: #000;
11 @gray-darker: lighten(@gray-base, 13.5%); // #222
11 @gray-darker: lighten(@gray-base, 13.5%); // #222
12 @gray-dark: lighten(@gray-base, 20%); // #333
12 @gray-dark: lighten(@gray-base, 20%); // #333
13 @gray: lighten(@gray-base, 33.5%); // #555
13 @gray: lighten(@gray-base, 33.5%); // #555
14 @gray-light: lighten(@gray-base, 46.7%); // #777
14 @gray-light: lighten(@gray-base, 46.7%); // #777
15 @gray-lighter: lighten(@gray-base, 93.5%); // #eee
15 @gray-lighter: lighten(@gray-base, 93.5%); // #eee
16
16
17 @brand-primary: darken(#428bca, 6.5%);
17 @brand-primary: darken(#428bca, 6.5%);
18 @brand-success: #5cb85c;
18 @brand-success: #5cb85c;
19 @brand-info: #5bc0de;
19 @brand-info: #5bc0de;
20 @brand-warning: #f0ad4e;
20 @brand-warning: #f0ad4e;
21 @brand-danger: #d9534f;
21 @brand-danger: #d9534f;
22
22
23
23
24 //== Scaffolding
24 //== Scaffolding
25 //
25 //
26 //## Settings for some of the most global styles.
26 //## Settings for some of the most global styles.
27
27
28 //** Background color for `<body>`.
28 //** Background color for `<body>`.
29 @body-bg: #fff;
29 @body-bg: #fff;
30 //** Global text color on `<body>`.
30 //** Global text color on `<body>`.
31 @text-color: @gray-dark;
31 @text-color: @gray-dark;
32
32
33 //** Global textual link color.
33 //** Global textual link color.
34 @link-color: @brand-primary;
34 @link-color: @brand-primary;
35 //** Link hover color set via `darken()` function.
35 //** Link hover color set via `darken()` function.
36 @link-hover-color: darken(@link-color, 15%);
36 @link-hover-color: darken(@link-color, 15%);
37 //** Link hover decoration.
37 //** Link hover decoration.
38 @link-hover-decoration: underline;
38 @link-hover-decoration: underline;
39
39
40
40
41 //== Typography
41 //== Typography
42 //
42 //
43 //## Font, line-height, and color for body text, headings, and more.
43 //## Font, line-height, and color for body text, headings, and more.
44
44
45 @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
45 @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
46 @font-family-serif: Georgia, "Times New Roman", Times, serif;
46 @font-family-serif: Georgia, "Times New Roman", Times, serif;
47 //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
47 //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
48 @font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
48 @font-family-monospace: Consolas, "Liberation Mono", Menlo, Monaco, Courier, monospace;
49
49 @font-family-base: @font-family-sans-serif;
50 @font-family-base: @font-family-sans-serif;
50
51
51 @font-size-base: 14px;
52 @font-size-base: 14px;
52 @font-size-large: ceil((@font-size-base * 1.25)); // ~18px
53 @font-size-large: ceil((@font-size-base * 1.25)); // ~18px
53 @font-size-small: ceil((@font-size-base * 0.85)); // ~12px
54 @font-size-small: ceil((@font-size-base * 0.85)); // ~12px
54
55
55 @font-size-h1: floor((@font-size-base * 2.6)); // ~36px
56 @font-size-h1: floor((@font-size-base * 2.6)); // ~36px
56 @font-size-h2: floor((@font-size-base * 2.15)); // ~30px
57 @font-size-h2: floor((@font-size-base * 2.15)); // ~30px
57 @font-size-h3: ceil((@font-size-base * 1.7)); // ~24px
58 @font-size-h3: ceil((@font-size-base * 1.7)); // ~24px
58 @font-size-h4: ceil((@font-size-base * 1.25)); // ~18px
59 @font-size-h4: ceil((@font-size-base * 1.25)); // ~18px
59 @font-size-h5: @font-size-base;
60 @font-size-h5: @font-size-base;
60 @font-size-h6: ceil((@font-size-base * 0.85)); // ~12px
61 @font-size-h6: ceil((@font-size-base * 0.85)); // ~12px
61
62
62 //** Unit-less `line-height` for use in components like buttons.
63 //** Unit-less `line-height` for use in components like buttons.
63 @line-height-base: 1.428571429; // 20/14
64 @line-height-base: 1.428571429; // 20/14
64 //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
65 //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
65 @line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px
66 @line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px
66
67
67 //** By default, this inherits from the `<body>`.
68 //** By default, this inherits from the `<body>`.
68 @headings-font-family: inherit;
69 @headings-font-family: inherit;
69 @headings-font-weight: 500;
70 @headings-font-weight: 500;
70 @headings-line-height: 1.1;
71 @headings-line-height: 1.1;
71 @headings-color: inherit;
72 @headings-color: inherit;
72
73
73
74
74 //== Iconography
75 //== Iconography
75 //
76 //
76 //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
77 //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
77
78
78 //** Load fonts from this directory.
79 //** Load fonts from this directory.
79 @icon-font-path: "../fonts/";
80 @icon-font-path: "../fonts/";
80 //** File name for all font files.
81 //** File name for all font files.
81 @icon-font-name: "glyphicons-halflings-regular";
82 @icon-font-name: "glyphicons-halflings-regular";
82 //** Element ID within SVG icon file.
83 //** Element ID within SVG icon file.
83 @icon-font-svg-id: "glyphicons_halflingsregular";
84 @icon-font-svg-id: "glyphicons_halflingsregular";
84
85
85
86
86 //== Components
87 //== Components
87 //
88 //
88 //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
89 //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
89
90
90 @padding-base-vertical: 6px;
91 @padding-base-vertical: 6px;
91 @padding-base-horizontal: 12px;
92 @padding-base-horizontal: 12px;
92
93
93 @padding-large-vertical: 10px;
94 @padding-large-vertical: 10px;
94 @padding-large-horizontal: 16px;
95 @padding-large-horizontal: 16px;
95
96
96 @padding-small-vertical: 5px;
97 @padding-small-vertical: 5px;
97 @padding-small-horizontal: 10px;
98 @padding-small-horizontal: 10px;
98
99
99 @padding-xs-vertical: 1px;
100 @padding-xs-vertical: 1px;
100 @padding-xs-horizontal: 5px;
101 @padding-xs-horizontal: 5px;
101
102
102 @line-height-large: 1.33;
103 @line-height-large: 1.33;
103 @line-height-small: 1.5;
104 @line-height-small: 1.5;
104
105
105 @border-radius-base: @border-radius;
106 @border-radius-base: @border-radius;
106 @border-radius-large: 6px;
107 @border-radius-large: 6px;
107 @border-radius-small: 3px;
108 @border-radius-small: 3px;
108
109
109 //** Global color for active items (e.g., navs or dropdowns).
110 //** Global color for active items (e.g., navs or dropdowns).
110 @component-active-color: #fff;
111 @component-active-color: #fff;
111 //** Global background color for active items (e.g., navs or dropdowns).
112 //** Global background color for active items (e.g., navs or dropdowns).
112 @component-active-bg: @brand-primary;
113 @component-active-bg: @brand-primary;
113
114
114 //** Width of the `border` for generating carets that indicator dropdowns.
115 //** Width of the `border` for generating carets that indicator dropdowns.
115 @caret-width-base: 4px;
116 @caret-width-base: 4px;
116 //** Carets increase slightly in size for larger components.
117 //** Carets increase slightly in size for larger components.
117 @caret-width-large: 5px;
118 @caret-width-large: 5px;
118
119
119
120
120 //== Tables
121 //== Tables
121 //
122 //
122 //## Customizes the `.table` component with basic values, each used across all table variations.
123 //## Customizes the `.table` component with basic values, each used across all table variations.
123
124
124 //** Padding for `<th>`s and `<td>`s.
125 //** Padding for `<th>`s and `<td>`s.
125 @table-cell-padding: 8px;
126 @table-cell-padding: 8px;
126 //** Padding for cells in `.table-condensed`.
127 //** Padding for cells in `.table-condensed`.
127 @table-condensed-cell-padding: 5px;
128 @table-condensed-cell-padding: 5px;
128
129
129 //** Default background color used for all tables.
130 //** Default background color used for all tables.
130 @table-bg: transparent;
131 @table-bg: transparent;
131 //** Background color used for `.table-striped`.
132 //** Background color used for `.table-striped`.
132 @table-bg-accent: #f9f9f9;
133 @table-bg-accent: #f9f9f9;
133 //** Background color used for `.table-hover`.
134 //** Background color used for `.table-hover`.
134 @table-bg-hover: #f5f5f5;
135 @table-bg-hover: #f5f5f5;
135 @table-bg-active: @table-bg-hover;
136 @table-bg-active: @table-bg-hover;
136
137
137 //** Border color for table and cell borders.
138 //** Border color for table and cell borders.
138 @table-border-color: #ddd;
139 @table-border-color: #ddd;
139
140
140
141
141 //== Buttons
142 //== Buttons
142 //
143 //
143 //## For each of Bootstrap's buttons, define text, background and border color.
144 //## For each of Bootstrap's buttons, define text, background and border color.
144
145
145 @btn-font-weight: normal;
146 @btn-font-weight: normal;
146
147
147 @btn-default-color: #333;
148 @btn-default-color: #333;
148 @btn-default-bg: #fff;
149 @btn-default-bg: #fff;
149 @btn-default-border: #ccc;
150 @btn-default-border: #ccc;
150
151
151 @btn-primary-color: #fff;
152 @btn-primary-color: #fff;
152 @btn-primary-bg: @brand-primary;
153 @btn-primary-bg: @brand-primary;
153 @btn-primary-border: darken(@btn-primary-bg, 5%);
154 @btn-primary-border: darken(@btn-primary-bg, 5%);
154
155
155 @btn-success-color: #fff;
156 @btn-success-color: #fff;
156 @btn-success-bg: @brand-success;
157 @btn-success-bg: @brand-success;
157 @btn-success-border: darken(@btn-success-bg, 5%);
158 @btn-success-border: darken(@btn-success-bg, 5%);
158
159
159 @btn-info-color: #fff;
160 @btn-info-color: #fff;
160 @btn-info-bg: @brand-info;
161 @btn-info-bg: @brand-info;
161 @btn-info-border: darken(@btn-info-bg, 5%);
162 @btn-info-border: darken(@btn-info-bg, 5%);
162
163
163 @btn-warning-color: #fff;
164 @btn-warning-color: #fff;
164 @btn-warning-bg: @brand-warning;
165 @btn-warning-bg: @brand-warning;
165 @btn-warning-border: darken(@btn-warning-bg, 5%);
166 @btn-warning-border: darken(@btn-warning-bg, 5%);
166
167
167 @btn-danger-color: #fff;
168 @btn-danger-color: #fff;
168 @btn-danger-bg: @brand-danger;
169 @btn-danger-bg: @brand-danger;
169 @btn-danger-border: darken(@btn-danger-bg, 5%);
170 @btn-danger-border: darken(@btn-danger-bg, 5%);
170
171
171 @btn-link-disabled-color: @gray-light;
172 @btn-link-disabled-color: @gray-light;
172
173
173
174
174 //== Forms
175 //== Forms
175 //
176 //
176 //##
177 //##
177
178
178 //** `<input>` background color
179 //** `<input>` background color
179 @input-bg: #fff;
180 @input-bg: #fff;
180 //** `<input disabled>` background color
181 //** `<input disabled>` background color
181 @input-bg-disabled: @gray-lighter;
182 @input-bg-disabled: @gray-lighter;
182
183
183 //** Text color for `<input>`s
184 //** Text color for `<input>`s
184 @input-color: @gray;
185 @input-color: @gray;
185 //** `<input>` border color
186 //** `<input>` border color
186 @input-border: #ccc;
187 @input-border: #ccc;
187
188
188 // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
189 // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
189 //** Default `.form-control` border radius
190 //** Default `.form-control` border radius
190 @input-border-radius: @border-radius-base;
191 @input-border-radius: @border-radius-base;
191 //** Large `.form-control` border radius
192 //** Large `.form-control` border radius
192 @input-border-radius-large: @border-radius-large;
193 @input-border-radius-large: @border-radius-large;
193 //** Small `.form-control` border radius
194 //** Small `.form-control` border radius
194 @input-border-radius-small: @border-radius-small;
195 @input-border-radius-small: @border-radius-small;
195
196
196 //** Border color for inputs on focus
197 //** Border color for inputs on focus
197 @input-border-focus: #66afe9;
198 @input-border-focus: #66afe9;
198
199
199 //** Placeholder text color
200 //** Placeholder text color
200 @input-color-placeholder: #999;
201 @input-color-placeholder: #999;
201
202
202 //** Default `.form-control` height
203 //** Default `.form-control` height
203 @input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2);
204 @input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2);
204 //** Large `.form-control` height
205 //** Large `.form-control` height
205 @input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
206 @input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
206 //** Small `.form-control` height
207 //** Small `.form-control` height
207 @input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
208 @input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
208
209
209 @legend-color: @gray-dark;
210 @legend-color: @gray-dark;
210 @legend-border-color: #e5e5e5;
211 @legend-border-color: #e5e5e5;
211
212
212 //** Background color for textual input addons
213 //** Background color for textual input addons
213 @input-group-addon-bg: @gray-lighter;
214 @input-group-addon-bg: @gray-lighter;
214 //** Border color for textual input addons
215 //** Border color for textual input addons
215 @input-group-addon-border-color: @input-border;
216 @input-group-addon-border-color: @input-border;
216
217
217 //** Disabled cursor for form controls and buttons.
218 //** Disabled cursor for form controls and buttons.
218 @cursor-disabled: not-allowed;
219 @cursor-disabled: not-allowed;
219
220
220
221
221 //== Dropdowns
222 //== Dropdowns
222 //
223 //
223 //## Dropdown menu container and contents.
224 //## Dropdown menu container and contents.
224
225
225 //** Background for the dropdown menu.
226 //** Background for the dropdown menu.
226 @dropdown-bg: #fff;
227 @dropdown-bg: #fff;
227 //** Dropdown menu `border-color`.
228 //** Dropdown menu `border-color`.
228 @dropdown-border: rgba(0,0,0,.15);
229 @dropdown-border: rgba(0,0,0,.15);
229 //** Dropdown menu `border-color` **for IE8**.
230 //** Dropdown menu `border-color` **for IE8**.
230 @dropdown-fallback-border: #ccc;
231 @dropdown-fallback-border: #ccc;
231 //** Divider color for between dropdown items.
232 //** Divider color for between dropdown items.
232 @dropdown-divider-bg: #e5e5e5;
233 @dropdown-divider-bg: #e5e5e5;
233
234
234 //** Dropdown link text color.
235 //** Dropdown link text color.
235 @dropdown-link-color: @gray-dark;
236 @dropdown-link-color: @gray-dark;
236 //** Hover color for dropdown links.
237 //** Hover color for dropdown links.
237 @dropdown-link-hover-color: darken(@gray-dark, 5%);
238 @dropdown-link-hover-color: darken(@gray-dark, 5%);
238 //** Hover background for dropdown links.
239 //** Hover background for dropdown links.
239 @dropdown-link-hover-bg: #f5f5f5;
240 @dropdown-link-hover-bg: #f5f5f5;
240
241
241 //** Active dropdown menu item text color.
242 //** Active dropdown menu item text color.
242 @dropdown-link-active-color: @component-active-color;
243 @dropdown-link-active-color: @component-active-color;
243 //** Active dropdown menu item background color.
244 //** Active dropdown menu item background color.
244 @dropdown-link-active-bg: @component-active-bg;
245 @dropdown-link-active-bg: @component-active-bg;
245
246
246 //** Disabled dropdown menu item background color.
247 //** Disabled dropdown menu item background color.
247 @dropdown-link-disabled-color: @gray-light;
248 @dropdown-link-disabled-color: @gray-light;
248
249
249 //** Text color for headers within dropdown menus.
250 //** Text color for headers within dropdown menus.
250 @dropdown-header-color: @gray-light;
251 @dropdown-header-color: @gray-light;
251
252
252 //** Deprecated `@dropdown-caret-color` as of v3.1.0
253 //** Deprecated `@dropdown-caret-color` as of v3.1.0
253 @dropdown-caret-color: #000;
254 @dropdown-caret-color: #000;
254
255
255
256
256 //-- Z-index master list
257 //-- Z-index master list
257 //
258 //
258 // Warning: Avoid customizing these values. They're used for a bird's eye view
259 // Warning: Avoid customizing these values. They're used for a bird's eye view
259 // of components dependent on the z-axis and are designed to all work together.
260 // of components dependent on the z-axis and are designed to all work together.
260 //
261 //
261 // Note: These variables are not generated into the Customizer.
262 // Note: These variables are not generated into the Customizer.
262
263
263 @zindex-navbar: 1000;
264 @zindex-navbar: 1000;
264 @zindex-dropdown: 1000;
265 @zindex-dropdown: 1000;
265 @zindex-popover: 1060;
266 @zindex-popover: 1060;
266 @zindex-tooltip: 1070;
267 @zindex-tooltip: 1070;
267 @zindex-navbar-fixed: 1030;
268 @zindex-navbar-fixed: 1030;
268 @zindex-modal: 1040;
269 @zindex-modal: 1040;
269
270
270
271
271 //== Media queries breakpoints
272 //== Media queries breakpoints
272 //
273 //
273 //## Define the breakpoints at which your layout will change, adapting to different screen sizes.
274 //## Define the breakpoints at which your layout will change, adapting to different screen sizes.
274
275
275 // Extra small screen / phone
276 // Extra small screen / phone
276 //** Deprecated `@screen-xs` as of v3.0.1
277 //** Deprecated `@screen-xs` as of v3.0.1
277 @screen-xs: 480px;
278 @screen-xs: 480px;
278 //** Deprecated `@screen-xs-min` as of v3.2.0
279 //** Deprecated `@screen-xs-min` as of v3.2.0
279 @screen-xs-min: @screen-xs;
280 @screen-xs-min: @screen-xs;
280 //** Deprecated `@screen-phone` as of v3.0.1
281 //** Deprecated `@screen-phone` as of v3.0.1
281 @screen-phone: @screen-xs-min;
282 @screen-phone: @screen-xs-min;
282
283
283 // Small screen / tablet
284 // Small screen / tablet
284 //** Deprecated `@screen-sm` as of v3.0.1
285 //** Deprecated `@screen-sm` as of v3.0.1
285 @screen-sm: 768px;
286 @screen-sm: 768px;
286 @screen-sm-min: @screen-sm;
287 @screen-sm-min: @screen-sm;
287 //** Deprecated `@screen-tablet` as of v3.0.1
288 //** Deprecated `@screen-tablet` as of v3.0.1
288 @screen-tablet: @screen-sm-min;
289 @screen-tablet: @screen-sm-min;
289
290
290 // Medium screen / desktop
291 // Medium screen / desktop
291 //** Deprecated `@screen-md` as of v3.0.1
292 //** Deprecated `@screen-md` as of v3.0.1
292 @screen-md: 992px;
293 @screen-md: 992px;
293 @screen-md-min: @screen-md;
294 @screen-md-min: @screen-md;
294 //** Deprecated `@screen-desktop` as of v3.0.1
295 //** Deprecated `@screen-desktop` as of v3.0.1
295 @screen-desktop: @screen-md-min;
296 @screen-desktop: @screen-md-min;
296
297
297 // Large screen / wide desktop
298 // Large screen / wide desktop
298 //** Deprecated `@screen-lg` as of v3.0.1
299 //** Deprecated `@screen-lg` as of v3.0.1
299 @screen-lg: 1200px;
300 @screen-lg: 1200px;
300 @screen-lg-min: @screen-lg;
301 @screen-lg-min: @screen-lg;
301 //** Deprecated `@screen-lg-desktop` as of v3.0.1
302 //** Deprecated `@screen-lg-desktop` as of v3.0.1
302 @screen-lg-desktop: @screen-lg-min;
303 @screen-lg-desktop: @screen-lg-min;
303
304
304 // So media queries don't overlap when required, provide a maximum
305 // So media queries don't overlap when required, provide a maximum
305 @screen-xs-max: (@screen-sm-min - 1);
306 @screen-xs-max: (@screen-sm-min - 1);
306 @screen-sm-max: (@screen-md-min - 1);
307 @screen-sm-max: (@screen-md-min - 1);
307 @screen-md-max: (@screen-lg-min - 1);
308 @screen-md-max: (@screen-lg-min - 1);
308
309
309
310
310 //== Grid system
311 //== Grid system
311 //
312 //
312 //## Define your custom responsive grid.
313 //## Define your custom responsive grid.
313
314
314 //** Number of columns in the grid.
315 //** Number of columns in the grid.
315 @grid-columns: 12;
316 @grid-columns: 12;
316 //** Padding between columns. Gets divided in half for the left and right.
317 //** Padding between columns. Gets divided in half for the left and right.
317 @grid-gutter-width: 30px;
318 @grid-gutter-width: 30px;
318 // Navbar collapse
319 // Navbar collapse
319 //** Point at which the navbar becomes uncollapsed.
320 //** Point at which the navbar becomes uncollapsed.
320 @grid-float-breakpoint: @screen-sm-min;
321 @grid-float-breakpoint: @screen-sm-min;
321 //** Point at which the navbar begins collapsing.
322 //** Point at which the navbar begins collapsing.
322 @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
323 @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
323
324
324
325
325 //== Container sizes
326 //== Container sizes
326 //
327 //
327 //## Define the maximum width of `.container` for different screen sizes.
328 //## Define the maximum width of `.container` for different screen sizes.
328
329
329 // Small screen / tablet
330 // Small screen / tablet
330 @container-tablet: (720px + @grid-gutter-width);
331 @container-tablet: (720px + @grid-gutter-width);
331 //** For `@screen-sm-min` and up.
332 //** For `@screen-sm-min` and up.
332 @container-sm: @container-tablet;
333 @container-sm: @container-tablet;
333
334
334 // Medium screen / desktop
335 // Medium screen / desktop
335 @container-desktop: (940px + @grid-gutter-width);
336 @container-desktop: (940px + @grid-gutter-width);
336 //** For `@screen-md-min` and up.
337 //** For `@screen-md-min` and up.
337 @container-md: @container-desktop;
338 @container-md: @container-desktop;
338
339
339 // Large screen / wide desktop
340 // Large screen / wide desktop
340 @container-large-desktop: (1140px + @grid-gutter-width);
341 @container-large-desktop: (1140px + @grid-gutter-width);
341 //** For `@screen-lg-min` and up.
342 //** For `@screen-lg-min` and up.
342 @container-lg: @container-large-desktop;
343 @container-lg: @container-large-desktop;
343
344
344
345
345 //== Navbar
346 //== Navbar
346 //
347 //
347 //##
348 //##
348
349
349 // Basics of a navbar
350 // Basics of a navbar
350 @navbar-height: 50px;
351 @navbar-height: 50px;
351 @navbar-margin-bottom: @line-height-computed;
352 @navbar-margin-bottom: @line-height-computed;
352 @navbar-border-radius: @border-radius-base;
353 @navbar-border-radius: @border-radius-base;
353 @navbar-padding-horizontal: floor((@grid-gutter-width / 2));
354 @navbar-padding-horizontal: floor((@grid-gutter-width / 2));
354 @navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2);
355 @navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2);
355 @navbar-collapse-max-height: 340px;
356 @navbar-collapse-max-height: 340px;
356
357
357 @navbar-default-color: #777;
358 @navbar-default-color: #777;
358 @navbar-default-bg: #f8f8f8;
359 @navbar-default-bg: #f8f8f8;
359 @navbar-default-border: darken(@navbar-default-bg, 6.5%);
360 @navbar-default-border: darken(@navbar-default-bg, 6.5%);
360
361
361 // Navbar links
362 // Navbar links
362 @navbar-default-link-color: #777;
363 @navbar-default-link-color: #777;
363 @navbar-default-link-hover-color: #333;
364 @navbar-default-link-hover-color: #333;
364 @navbar-default-link-hover-bg: transparent;
365 @navbar-default-link-hover-bg: transparent;
365 @navbar-default-link-active-color: #555;
366 @navbar-default-link-active-color: #555;
366 @navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%);
367 @navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%);
367 @navbar-default-link-disabled-color: #ccc;
368 @navbar-default-link-disabled-color: #ccc;
368 @navbar-default-link-disabled-bg: transparent;
369 @navbar-default-link-disabled-bg: transparent;
369
370
370 // Navbar brand label
371 // Navbar brand label
371 @navbar-default-brand-color: @navbar-default-link-color;
372 @navbar-default-brand-color: @navbar-default-link-color;
372 @navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%);
373 @navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%);
373 @navbar-default-brand-hover-bg: transparent;
374 @navbar-default-brand-hover-bg: transparent;
374
375
375 // Navbar toggle
376 // Navbar toggle
376 @navbar-default-toggle-hover-bg: #ddd;
377 @navbar-default-toggle-hover-bg: #ddd;
377 @navbar-default-toggle-icon-bar-bg: #888;
378 @navbar-default-toggle-icon-bar-bg: #888;
378 @navbar-default-toggle-border-color: #ddd;
379 @navbar-default-toggle-border-color: #ddd;
379
380
380
381
381 // Inverted navbar
382 // Inverted navbar
382 // Reset inverted navbar basics
383 // Reset inverted navbar basics
383 @navbar-inverse-color: lighten(@gray-light, 15%);
384 @navbar-inverse-color: lighten(@gray-light, 15%);
384 @navbar-inverse-bg: #222;
385 @navbar-inverse-bg: #222;
385 @navbar-inverse-border: darken(@navbar-inverse-bg, 10%);
386 @navbar-inverse-border: darken(@navbar-inverse-bg, 10%);
386
387
387 // Inverted navbar links
388 // Inverted navbar links
388 @navbar-inverse-link-color: lighten(@gray-light, 15%);
389 @navbar-inverse-link-color: lighten(@gray-light, 15%);
389 @navbar-inverse-link-hover-color: #fff;
390 @navbar-inverse-link-hover-color: #fff;
390 @navbar-inverse-link-hover-bg: transparent;
391 @navbar-inverse-link-hover-bg: transparent;
391 @navbar-inverse-link-active-color: @navbar-inverse-link-hover-color;
392 @navbar-inverse-link-active-color: @navbar-inverse-link-hover-color;
392 @navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%);
393 @navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%);
393 @navbar-inverse-link-disabled-color: #444;
394 @navbar-inverse-link-disabled-color: #444;
394 @navbar-inverse-link-disabled-bg: transparent;
395 @navbar-inverse-link-disabled-bg: transparent;
395
396
396 // Inverted navbar brand label
397 // Inverted navbar brand label
397 @navbar-inverse-brand-color: @navbar-inverse-link-color;
398 @navbar-inverse-brand-color: @navbar-inverse-link-color;
398 @navbar-inverse-brand-hover-color: #fff;
399 @navbar-inverse-brand-hover-color: #fff;
399 @navbar-inverse-brand-hover-bg: transparent;
400 @navbar-inverse-brand-hover-bg: transparent;
400
401
401 // Inverted navbar toggle
402 // Inverted navbar toggle
402 @navbar-inverse-toggle-hover-bg: #333;
403 @navbar-inverse-toggle-hover-bg: #333;
403 @navbar-inverse-toggle-icon-bar-bg: #fff;
404 @navbar-inverse-toggle-icon-bar-bg: #fff;
404 @navbar-inverse-toggle-border-color: #333;
405 @navbar-inverse-toggle-border-color: #333;
405
406
406
407
407 //== Navs
408 //== Navs
408 //
409 //
409 //##
410 //##
410
411
411 //=== Shared nav styles
412 //=== Shared nav styles
412 @nav-link-padding: 10px 15px;
413 @nav-link-padding: 10px 15px;
413 @nav-link-hover-bg: @gray-lighter;
414 @nav-link-hover-bg: @gray-lighter;
414
415
415 @nav-disabled-link-color: @gray-light;
416 @nav-disabled-link-color: @gray-light;
416 @nav-disabled-link-hover-color: @gray-light;
417 @nav-disabled-link-hover-color: @gray-light;
417
418
418 //== Tabs
419 //== Tabs
419 @nav-tabs-border-color: #ddd;
420 @nav-tabs-border-color: #ddd;
420
421
421 @nav-tabs-link-hover-border-color: @gray-lighter;
422 @nav-tabs-link-hover-border-color: @gray-lighter;
422
423
423 @nav-tabs-active-link-hover-bg: @body-bg;
424 @nav-tabs-active-link-hover-bg: @body-bg;
424 @nav-tabs-active-link-hover-color: @gray;
425 @nav-tabs-active-link-hover-color: @gray;
425 @nav-tabs-active-link-hover-border-color: #ddd;
426 @nav-tabs-active-link-hover-border-color: #ddd;
426
427
427 @nav-tabs-justified-link-border-color: #ddd;
428 @nav-tabs-justified-link-border-color: #ddd;
428 @nav-tabs-justified-active-link-border-color: @body-bg;
429 @nav-tabs-justified-active-link-border-color: @body-bg;
429
430
430 //== Pills
431 //== Pills
431 @nav-pills-border-radius: @border-radius-base;
432 @nav-pills-border-radius: @border-radius-base;
432 @nav-pills-active-link-hover-bg: @component-active-bg;
433 @nav-pills-active-link-hover-bg: @component-active-bg;
433 @nav-pills-active-link-hover-color: @component-active-color;
434 @nav-pills-active-link-hover-color: @component-active-color;
434
435
435
436
436 //== Pagination
437 //== Pagination
437 //
438 //
438 //##
439 //##
439
440
440 @pagination-color: @link-color;
441 @pagination-color: @link-color;
441 @pagination-bg: #fff;
442 @pagination-bg: #fff;
442 @pagination-border: #ddd;
443 @pagination-border: #ddd;
443
444
444 @pagination-hover-color: @link-hover-color;
445 @pagination-hover-color: @link-hover-color;
445 @pagination-hover-bg: @gray-lighter;
446 @pagination-hover-bg: @gray-lighter;
446 @pagination-hover-border: #ddd;
447 @pagination-hover-border: #ddd;
447
448
448 @pagination-active-color: #fff;
449 @pagination-active-color: #fff;
449 @pagination-active-bg: @brand-primary;
450 @pagination-active-bg: @brand-primary;
450 @pagination-active-border: @brand-primary;
451 @pagination-active-border: @brand-primary;
451
452
452 @pagination-disabled-color: @gray-light;
453 @pagination-disabled-color: @gray-light;
453 @pagination-disabled-bg: #fff;
454 @pagination-disabled-bg: #fff;
454 @pagination-disabled-border: #ddd;
455 @pagination-disabled-border: #ddd;
455
456
456
457
457 //== Pager
458 //== Pager
458 //
459 //
459 //##
460 //##
460
461
461 @pager-bg: @pagination-bg;
462 @pager-bg: @pagination-bg;
462 @pager-border: @pagination-border;
463 @pager-border: @pagination-border;
463 @pager-border-radius: 15px;
464 @pager-border-radius: 15px;
464
465
465 @pager-hover-bg: @pagination-hover-bg;
466 @pager-hover-bg: @pagination-hover-bg;
466
467
467 @pager-active-bg: @pagination-active-bg;
468 @pager-active-bg: @pagination-active-bg;
468 @pager-active-color: @pagination-active-color;
469 @pager-active-color: @pagination-active-color;
469
470
470 @pager-disabled-color: @pagination-disabled-color;
471 @pager-disabled-color: @pagination-disabled-color;
471
472
472
473
473 //== Jumbotron
474 //== Jumbotron
474 //
475 //
475 //##
476 //##
476
477
477 @jumbotron-padding: 30px;
478 @jumbotron-padding: 30px;
478 @jumbotron-color: inherit;
479 @jumbotron-color: inherit;
479 @jumbotron-bg: @gray-lighter;
480 @jumbotron-bg: @gray-lighter;
480 @jumbotron-heading-color: inherit;
481 @jumbotron-heading-color: inherit;
481 @jumbotron-font-size: ceil((@font-size-base * 1.5));
482 @jumbotron-font-size: ceil((@font-size-base * 1.5));
482
483
483
484
484 //== Form states and alerts
485 //== Form states and alerts
485 //
486 //
486 //## Define colors for form feedback states and, by default, alerts.
487 //## Define colors for form feedback states and, by default, alerts.
487
488
488 @state-success-text: #3c763d;
489 @state-success-text: #3c763d;
489 @state-success-bg: #dff0d8;
490 @state-success-bg: #dff0d8;
490 @state-success-border: darken(spin(@state-success-bg, -10), 5%);
491 @state-success-border: darken(spin(@state-success-bg, -10), 5%);
491
492
492 @state-info-text: #31708f;
493 @state-info-text: #31708f;
493 @state-info-bg: #d9edf7;
494 @state-info-bg: #d9edf7;
494 @state-info-border: darken(spin(@state-info-bg, -10), 7%);
495 @state-info-border: darken(spin(@state-info-bg, -10), 7%);
495
496
496 @state-warning-text: #8a6d3b;
497 @state-warning-text: #8a6d3b;
497 @state-warning-bg: #fcf8e3;
498 @state-warning-bg: #fcf8e3;
498 @state-warning-border: darken(spin(@state-warning-bg, -10), 5%);
499 @state-warning-border: darken(spin(@state-warning-bg, -10), 5%);
499
500
500 @state-danger-text: #a94442;
501 @state-danger-text: #a94442;
501 @state-danger-bg: #f2dede;
502 @state-danger-bg: #f2dede;
502 @state-danger-border: darken(spin(@state-danger-bg, -10), 5%);
503 @state-danger-border: darken(spin(@state-danger-bg, -10), 5%);
503
504
504
505
505 //== Tooltips
506 //== Tooltips
506 //
507 //
507 //##
508 //##
508
509
509 //** Tooltip max width
510 //** Tooltip max width
510 @tooltip-max-width: 200px;
511 @tooltip-max-width: 200px;
511 //** Tooltip text color
512 //** Tooltip text color
512 @tooltip-color: #fff;
513 @tooltip-color: #fff;
513 //** Tooltip background color
514 //** Tooltip background color
514 @tooltip-bg: #000;
515 @tooltip-bg: #000;
515 @tooltip-opacity: .9;
516 @tooltip-opacity: .9;
516
517
517 //** Tooltip arrow width
518 //** Tooltip arrow width
518 @tooltip-arrow-width: 5px;
519 @tooltip-arrow-width: 5px;
519 //** Tooltip arrow color
520 //** Tooltip arrow color
520 @tooltip-arrow-color: @tooltip-bg;
521 @tooltip-arrow-color: @tooltip-bg;
521
522
522
523
523 //== Popovers
524 //== Popovers
524 //
525 //
525 //##
526 //##
526
527
527 //** Popover body background color
528 //** Popover body background color
528 @popover-bg: #fff;
529 @popover-bg: #fff;
529 //** Popover maximum width
530 //** Popover maximum width
530 @popover-max-width: 276px;
531 @popover-max-width: 276px;
531 //** Popover border color
532 //** Popover border color
532 @popover-border-color: rgba(0,0,0,.2);
533 @popover-border-color: rgba(0,0,0,.2);
533 //** Popover fallback border color
534 //** Popover fallback border color
534 @popover-fallback-border-color: #ccc;
535 @popover-fallback-border-color: #ccc;
535
536
536 //** Popover title background color
537 //** Popover title background color
537 @popover-title-bg: darken(@popover-bg, 3%);
538 @popover-title-bg: darken(@popover-bg, 3%);
538
539
539 //** Popover arrow width
540 //** Popover arrow width
540 @popover-arrow-width: 10px;
541 @popover-arrow-width: 10px;
541 //** Popover arrow color
542 //** Popover arrow color
542 @popover-arrow-color: @popover-bg;
543 @popover-arrow-color: @popover-bg;
543
544
544 //** Popover outer arrow width
545 //** Popover outer arrow width
545 @popover-arrow-outer-width: (@popover-arrow-width + 1);
546 @popover-arrow-outer-width: (@popover-arrow-width + 1);
546 //** Popover outer arrow color
547 //** Popover outer arrow color
547 @popover-arrow-outer-color: fadein(@popover-border-color, 5%);
548 @popover-arrow-outer-color: fadein(@popover-border-color, 5%);
548 //** Popover outer arrow fallback color
549 //** Popover outer arrow fallback color
549 @popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%);
550 @popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%);
550
551
551
552
552 //== Labels
553 //== Labels
553 //
554 //
554 //##
555 //##
555
556
556 //** Default label background color
557 //** Default label background color
557 @label-default-bg: @gray-light;
558 @label-default-bg: @gray-light;
558 //** Primary label background color
559 //** Primary label background color
559 @label-primary-bg: @brand-primary;
560 @label-primary-bg: @brand-primary;
560 //** Success label background color
561 //** Success label background color
561 @label-success-bg: @brand-success;
562 @label-success-bg: @brand-success;
562 //** Info label background color
563 //** Info label background color
563 @label-info-bg: @brand-info;
564 @label-info-bg: @brand-info;
564 //** Warning label background color
565 //** Warning label background color
565 @label-warning-bg: @brand-warning;
566 @label-warning-bg: @brand-warning;
566 //** Danger label background color
567 //** Danger label background color
567 @label-danger-bg: @brand-danger;
568 @label-danger-bg: @brand-danger;
568
569
569 //** Default label text color
570 //** Default label text color
570 @label-color: #fff;
571 @label-color: #fff;
571 //** Default text color of a linked label
572 //** Default text color of a linked label
572 @label-link-hover-color: #fff;
573 @label-link-hover-color: #fff;
573
574
574
575
575 //== Modals
576 //== Modals
576 //
577 //
577 //##
578 //##
578
579
579 //** Padding applied to the modal body
580 //** Padding applied to the modal body
580 @modal-inner-padding: 15px;
581 @modal-inner-padding: 15px;
581
582
582 //** Padding applied to the modal title
583 //** Padding applied to the modal title
583 @modal-title-padding: 15px;
584 @modal-title-padding: 15px;
584 //** Modal title line-height
585 //** Modal title line-height
585 @modal-title-line-height: @line-height-base;
586 @modal-title-line-height: @line-height-base;
586
587
587 //** Background color of modal content area
588 //** Background color of modal content area
588 @modal-content-bg: #fff;
589 @modal-content-bg: #fff;
589 //** Modal content border color
590 //** Modal content border color
590 @modal-content-border-color: rgba(0,0,0,.2);
591 @modal-content-border-color: rgba(0,0,0,.2);
591 //** Modal content border color **for IE8**
592 //** Modal content border color **for IE8**
592 @modal-content-fallback-border-color: #999;
593 @modal-content-fallback-border-color: #999;
593
594
594 //** Modal backdrop background color
595 //** Modal backdrop background color
595 @modal-backdrop-bg: #000;
596 @modal-backdrop-bg: #000;
596 //** Modal backdrop opacity
597 //** Modal backdrop opacity
597 @modal-backdrop-opacity: .5;
598 @modal-backdrop-opacity: .5;
598 //** Modal header border color
599 //** Modal header border color
599 @modal-header-border-color: #e5e5e5;
600 @modal-header-border-color: #e5e5e5;
600 //** Modal footer border color
601 //** Modal footer border color
601 @modal-footer-border-color: @modal-header-border-color;
602 @modal-footer-border-color: @modal-header-border-color;
602
603
603 @modal-lg: 900px;
604 @modal-lg: 900px;
604 @modal-md: 600px;
605 @modal-md: 600px;
605 @modal-sm: 300px;
606 @modal-sm: 300px;
606
607
607
608
608 //== Alerts
609 //== Alerts
609 //
610 //
610 //## Define alert colors, border radius, and padding.
611 //## Define alert colors, border radius, and padding.
611
612
612 @alert-padding: 15px;
613 @alert-padding: 15px;
613 @alert-border-radius: @border-radius-base;
614 @alert-border-radius: @border-radius-base;
614 @alert-link-font-weight: bold;
615 @alert-link-font-weight: bold;
615
616
616 @alert-success-bg: @state-success-bg;
617 @alert-success-bg: @state-success-bg;
617 @alert-success-text: @state-success-text;
618 @alert-success-text: @state-success-text;
618 @alert-success-border: @state-success-border;
619 @alert-success-border: @state-success-border;
619
620
620 @alert-info-bg: @state-info-bg;
621 @alert-info-bg: @state-info-bg;
621 @alert-info-text: @state-info-text;
622 @alert-info-text: @state-info-text;
622 @alert-info-border: @state-info-border;
623 @alert-info-border: @state-info-border;
623
624
624 @alert-warning-bg: @state-warning-bg;
625 @alert-warning-bg: @state-warning-bg;
625 @alert-warning-text: @state-warning-text;
626 @alert-warning-text: @state-warning-text;
626 @alert-warning-border: @state-warning-border;
627 @alert-warning-border: @state-warning-border;
627
628
628 @alert-danger-bg: @state-danger-bg;
629 @alert-danger-bg: @state-danger-bg;
629 @alert-danger-text: @state-danger-text;
630 @alert-danger-text: @state-danger-text;
630 @alert-danger-border: @state-danger-border;
631 @alert-danger-border: @state-danger-border;
631
632
632
633
633 //== Progress bars
634 //== Progress bars
634 //
635 //
635 //##
636 //##
636
637
637 //** Background color of the whole progress component
638 //** Background color of the whole progress component
638 @progress-bg: #f5f5f5;
639 @progress-bg: #f5f5f5;
639 //** Progress bar text color
640 //** Progress bar text color
640 @progress-bar-color: #fff;
641 @progress-bar-color: #fff;
641 //** Variable for setting rounded corners on progress bar.
642 //** Variable for setting rounded corners on progress bar.
642 @progress-border-radius: @border-radius-base;
643 @progress-border-radius: @border-radius-base;
643
644
644 //** Default progress bar color
645 //** Default progress bar color
645 @progress-bar-bg: @brand-primary;
646 @progress-bar-bg: @brand-primary;
646 //** Success progress bar color
647 //** Success progress bar color
647 @progress-bar-success-bg: @brand-success;
648 @progress-bar-success-bg: @brand-success;
648 //** Warning progress bar color
649 //** Warning progress bar color
649 @progress-bar-warning-bg: @brand-warning;
650 @progress-bar-warning-bg: @brand-warning;
650 //** Danger progress bar color
651 //** Danger progress bar color
651 @progress-bar-danger-bg: @brand-danger;
652 @progress-bar-danger-bg: @brand-danger;
652 //** Info progress bar color
653 //** Info progress bar color
653 @progress-bar-info-bg: @brand-info;
654 @progress-bar-info-bg: @brand-info;
654
655
655
656
656 //== List group
657 //== List group
657 //
658 //
658 //##
659 //##
659
660
660 //** Background color on `.list-group-item`
661 //** Background color on `.list-group-item`
661 @list-group-bg: #fff;
662 @list-group-bg: #fff;
662 //** `.list-group-item` border color
663 //** `.list-group-item` border color
663 @list-group-border: #ddd;
664 @list-group-border: #ddd;
664 //** List group border radius
665 //** List group border radius
665 @list-group-border-radius: @border-radius-base;
666 @list-group-border-radius: @border-radius-base;
666
667
667 //** Background color of single list items on hover
668 //** Background color of single list items on hover
668 @list-group-hover-bg: #f5f5f5;
669 @list-group-hover-bg: #f5f5f5;
669 //** Text color of active list items
670 //** Text color of active list items
670 @list-group-active-color: @component-active-color;
671 @list-group-active-color: @component-active-color;
671 //** Background color of active list items
672 //** Background color of active list items
672 @list-group-active-bg: @component-active-bg;
673 @list-group-active-bg: @component-active-bg;
673 //** Border color of active list elements
674 //** Border color of active list elements
674 @list-group-active-border: @list-group-active-bg;
675 @list-group-active-border: @list-group-active-bg;
675 //** Text color for content within active list items
676 //** Text color for content within active list items
676 @list-group-active-text-color: lighten(@list-group-active-bg, 40%);
677 @list-group-active-text-color: lighten(@list-group-active-bg, 40%);
677
678
678 //** Text color of disabled list items
679 //** Text color of disabled list items
679 @list-group-disabled-color: @gray-light;
680 @list-group-disabled-color: @gray-light;
680 //** Background color of disabled list items
681 //** Background color of disabled list items
681 @list-group-disabled-bg: @gray-lighter;
682 @list-group-disabled-bg: @gray-lighter;
682 //** Text color for content within disabled list items
683 //** Text color for content within disabled list items
683 @list-group-disabled-text-color: @list-group-disabled-color;
684 @list-group-disabled-text-color: @list-group-disabled-color;
684
685
685 @list-group-link-color: #555;
686 @list-group-link-color: #555;
686 @list-group-link-hover-color: @list-group-link-color;
687 @list-group-link-hover-color: @list-group-link-color;
687 @list-group-link-heading-color: #333;
688 @list-group-link-heading-color: #333;
688
689
689
690
690 //== Panels
691 //== Panels
691 //
692 //
692 //##
693 //##
693
694
694 @panel-bg: #fff;
695 @panel-bg: #fff;
695 @panel-body-padding: @padding;
696 @panel-body-padding: @padding;
696 @panel-heading-padding: 10px 15px;
697 @panel-heading-padding: 10px 15px;
697 @panel-footer-padding: @panel-heading-padding;
698 @panel-footer-padding: @panel-heading-padding;
698 @panel-border-radius: @border-radius-base;
699 @panel-border-radius: @border-radius-base;
699
700
700 //** Border color for elements within panels
701 //** Border color for elements within panels
701 @panel-inner-border: #ddd;
702 @panel-inner-border: #ddd;
702 @panel-footer-bg: #fff;
703 @panel-footer-bg: #fff;
703
704
704 @panel-default-text: @text-color;
705 @panel-default-text: @text-color;
705 @panel-default-border: @grey5;
706 @panel-default-border: @grey5;
706 @panel-default-heading-bg: @grey6;
707 @panel-default-heading-bg: @grey6;
707
708
708 @panel-primary-text: #fff;
709 @panel-primary-text: #fff;
709 @panel-primary-border: @brand-primary;
710 @panel-primary-border: @brand-primary;
710 @panel-primary-heading-bg: @brand-primary;
711 @panel-primary-heading-bg: @brand-primary;
711
712
712 @panel-success-text: @state-success-text;
713 @panel-success-text: @state-success-text;
713 @panel-success-border: @state-success-border;
714 @panel-success-border: @state-success-border;
714 @panel-success-heading-bg: @state-success-bg;
715 @panel-success-heading-bg: @state-success-bg;
715
716
716 @panel-info-text: @state-info-text;
717 @panel-info-text: @state-info-text;
717 @panel-info-border: @state-info-border;
718 @panel-info-border: @state-info-border;
718 @panel-info-heading-bg: @state-info-bg;
719 @panel-info-heading-bg: @state-info-bg;
719
720
720 @panel-warning-text: @state-warning-text;
721 @panel-warning-text: @state-warning-text;
721 @panel-warning-border: @state-warning-border;
722 @panel-warning-border: @state-warning-border;
722 @panel-warning-heading-bg: @state-warning-bg;
723 @panel-warning-heading-bg: @state-warning-bg;
723
724
724 @panel-danger-text: @state-danger-text;
725 @panel-danger-text: @state-danger-text;
725 @panel-danger-border: @state-danger-border;
726 @panel-danger-border: @state-danger-border;
726 @panel-danger-heading-bg: @state-danger-bg;
727 @panel-danger-heading-bg: @state-danger-bg;
727
728
728
729
729 //== Thumbnails
730 //== Thumbnails
730 //
731 //
731 //##
732 //##
732
733
733 //** Padding around the thumbnail image
734 //** Padding around the thumbnail image
734 @thumbnail-padding: 4px;
735 @thumbnail-padding: 4px;
735 //** Thumbnail background color
736 //** Thumbnail background color
736 @thumbnail-bg: @body-bg;
737 @thumbnail-bg: @body-bg;
737 //** Thumbnail border color
738 //** Thumbnail border color
738 @thumbnail-border: #ddd;
739 @thumbnail-border: #ddd;
739 //** Thumbnail border radius
740 //** Thumbnail border radius
740 @thumbnail-border-radius: @border-radius-base;
741 @thumbnail-border-radius: @border-radius-base;
741
742
742 //** Custom text color for thumbnail captions
743 //** Custom text color for thumbnail captions
743 @thumbnail-caption-color: @text-color;
744 @thumbnail-caption-color: @text-color;
744 //** Padding around the thumbnail caption
745 //** Padding around the thumbnail caption
745 @thumbnail-caption-padding: 9px;
746 @thumbnail-caption-padding: 9px;
746
747
747
748
748 //== Wells
749 //== Wells
749 //
750 //
750 //##
751 //##
751
752
752 @well-bg: #f5f5f5;
753 @well-bg: #f5f5f5;
753 @well-border: darken(@well-bg, 7%);
754 @well-border: darken(@well-bg, 7%);
754
755
755
756
756 //== Badges
757 //== Badges
757 //
758 //
758 //##
759 //##
759
760
760 @badge-color: #fff;
761 @badge-color: #fff;
761 //** Linked badge text color on hover
762 //** Linked badge text color on hover
762 @badge-link-hover-color: #fff;
763 @badge-link-hover-color: #fff;
763 @badge-bg: @gray-light;
764 @badge-bg: @gray-light;
764
765
765 //** Badge text color in active nav link
766 //** Badge text color in active nav link
766 @badge-active-color: @link-color;
767 @badge-active-color: @link-color;
767 //** Badge background color in active nav link
768 //** Badge background color in active nav link
768 @badge-active-bg: #fff;
769 @badge-active-bg: #fff;
769
770
770 @badge-font-weight: bold;
771 @badge-font-weight: bold;
771 @badge-line-height: 1;
772 @badge-line-height: 1;
772 @badge-border-radius: 10px;
773 @badge-border-radius: 10px;
773
774
774
775
775 //== Breadcrumbs
776 //== Breadcrumbs
776 //
777 //
777 //##
778 //##
778
779
779 @breadcrumb-padding-vertical: 8px;
780 @breadcrumb-padding-vertical: 8px;
780 @breadcrumb-padding-horizontal: 15px;
781 @breadcrumb-padding-horizontal: 15px;
781 //** Breadcrumb background color
782 //** Breadcrumb background color
782 @breadcrumb-bg: #f5f5f5;
783 @breadcrumb-bg: #f5f5f5;
783 //** Breadcrumb text color
784 //** Breadcrumb text color
784 @breadcrumb-color: #ccc;
785 @breadcrumb-color: #ccc;
785 //** Text color of current page in the breadcrumb
786 //** Text color of current page in the breadcrumb
786 @breadcrumb-active-color: @gray-light;
787 @breadcrumb-active-color: @gray-light;
787 //** Textual separator for between breadcrumb elements
788 //** Textual separator for between breadcrumb elements
788 @breadcrumb-separator: "/";
789 @breadcrumb-separator: "/";
789
790
790
791
791 //== Carousel
792 //== Carousel
792 //
793 //
793 //##
794 //##
794
795
795 @carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6);
796 @carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6);
796
797
797 @carousel-control-color: #fff;
798 @carousel-control-color: #fff;
798 @carousel-control-width: 15%;
799 @carousel-control-width: 15%;
799 @carousel-control-opacity: .5;
800 @carousel-control-opacity: .5;
800 @carousel-control-font-size: 20px;
801 @carousel-control-font-size: 20px;
801
802
802 @carousel-indicator-active-bg: #fff;
803 @carousel-indicator-active-bg: #fff;
803 @carousel-indicator-border-color: #fff;
804 @carousel-indicator-border-color: #fff;
804
805
805 @carousel-caption-color: #fff;
806 @carousel-caption-color: #fff;
806
807
807
808
808 //== Close
809 //== Close
809 //
810 //
810 //##
811 //##
811
812
812 @close-font-weight: bold;
813 @close-font-weight: bold;
813 @close-color: #000;
814 @close-color: #000;
814 @close-text-shadow: 0 1px 0 #fff;
815 @close-text-shadow: 0 1px 0 #fff;
815
816
816
817
817 //== Code
818 //== Code
818 //
819 //
819 //##
820 //##
820
821
821 @code-color: #c7254e;
822 @code-color: #c7254e;
822 @code-bg: #f9f2f4;
823 @code-bg: #f9f2f4;
823
824
824 @kbd-color: #fff;
825 @kbd-color: #fff;
825 @kbd-bg: #333;
826 @kbd-bg: #333;
826
827
827 @pre-bg: #f5f5f5;
828 @pre-bg: #f5f5f5;
828 @pre-color: @gray-dark;
829 @pre-color: @gray-dark;
829 @pre-border-color: #ccc;
830 @pre-border-color: #ccc;
830 @pre-scrollable-max-height: 340px;
831 @pre-scrollable-max-height: 340px;
831
832
832
833
833 //== Type
834 //== Type
834 //
835 //
835 //##
836 //##
836
837
837 //** Horizontal offset for forms and lists.
838 //** Horizontal offset for forms and lists.
838 @component-offset-horizontal: 180px;
839 @component-offset-horizontal: 180px;
839 //** Text muted color
840 //** Text muted color
840 @text-muted: @grey4;
841 @text-muted: @grey4;
841 //** Abbreviations and acronyms border color
842 //** Abbreviations and acronyms border color
842 @abbr-border-color: @gray-light;
843 @abbr-border-color: @gray-light;
843 //** Headings small color
844 //** Headings small color
844 @headings-small-color: @gray-light;
845 @headings-small-color: @gray-light;
845 //** Blockquote small color
846 //** Blockquote small color
846 @blockquote-small-color: @gray-light;
847 @blockquote-small-color: @gray-light;
847 //** Blockquote font size
848 //** Blockquote font size
848 @blockquote-font-size: (@font-size-base * 1.25);
849 @blockquote-font-size: (@font-size-base * 1.25);
849 //** Blockquote border color
850 //** Blockquote border color
850 @blockquote-border-color: @gray-lighter;
851 @blockquote-border-color: @gray-lighter;
851 //** Page header border color
852 //** Page header border color
852 @page-header-border-color: @gray-lighter;
853 @page-header-border-color: @gray-lighter;
853 //** Width of horizontal description list titles
854 //** Width of horizontal description list titles
854 @dl-horizontal-offset: @component-offset-horizontal;
855 @dl-horizontal-offset: @component-offset-horizontal;
855 //** Horizontal line color.
856 //** Horizontal line color.
856 @hr-border: @gray-lighter;
857 @hr-border: @gray-lighter;
@@ -1,643 +1,749 b''
1 // Default styles
1 // Default styles
2
2
3 .diff-collapse {
3 .diff-collapse {
4 margin: @padding 0;
4 margin: @padding 0;
5 text-align: right;
5 text-align: right;
6 }
6 }
7
7
8 .diff-container {
8 .diff-container {
9 margin-bottom: @space;
9 margin-bottom: @space;
10
10
11 .diffblock {
11 .diffblock {
12 margin-bottom: @space;
12 margin-bottom: @space;
13 }
13 }
14
14
15 &.hidden {
15 &.hidden {
16 display: none;
16 display: none;
17 overflow: hidden;
17 overflow: hidden;
18 }
18 }
19 }
19 }
20
20
21 .compare_view_files {
21 .compare_view_files {
22
22
23 .diff-container {
23 .diff-container {
24
24
25 .diffblock {
25 .diffblock {
26 margin-bottom: 0;
26 margin-bottom: 0;
27 }
27 }
28 }
28 }
29 }
29 }
30
30
31 div.diffblock .sidebyside {
31 div.diffblock .sidebyside {
32 background: #ffffff;
32 background: #ffffff;
33 }
33 }
34
34
35 div.diffblock {
35 div.diffblock {
36 overflow-x: auto;
36 overflow-x: auto;
37 overflow-y: hidden;
37 overflow-y: hidden;
38 clear: both;
38 clear: both;
39 padding: 0px;
39 padding: 0px;
40 background: @grey6;
40 background: @grey6;
41 border: @border-thickness solid @grey5;
41 border: @border-thickness solid @grey5;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
44
44
45
45
46 .comments-number {
46 .comments-number {
47 float: right;
47 float: right;
48 }
48 }
49
49
50 // BEGIN CODE-HEADER STYLES
50 // BEGIN CODE-HEADER STYLES
51
51
52 .code-header {
52 .code-header {
53 background: @grey6;
53 background: @grey6;
54 padding: 10px 0 10px 0;
54 padding: 10px 0 10px 0;
55 height: auto;
55 height: auto;
56 width: 100%;
56 width: 100%;
57
57
58 .hash {
58 .hash {
59 float: left;
59 float: left;
60 padding: 2px 0 0 2px;
60 padding: 2px 0 0 2px;
61 }
61 }
62
62
63 .date {
63 .date {
64 float: left;
64 float: left;
65 text-transform: uppercase;
65 text-transform: uppercase;
66 padding: 4px 0px 0px 2px;
66 padding: 4px 0px 0px 2px;
67 }
67 }
68
68
69 div {
69 div {
70 margin-left: 4px;
70 margin-left: 4px;
71 }
71 }
72
72
73 div.compare_header {
73 div.compare_header {
74 min-height: 40px;
74 min-height: 40px;
75 margin: 0;
75 margin: 0;
76 padding: 0 @padding;
76 padding: 0 @padding;
77
77
78 .drop-menu {
78 .drop-menu {
79 float:left;
79 float:left;
80 display: block;
80 display: block;
81 margin:0 0 @padding 0;
81 margin:0 0 @padding 0;
82 }
82 }
83
83
84 .compare-label {
84 .compare-label {
85 float: left;
85 float: left;
86 clear: both;
86 clear: both;
87 display: inline-block;
87 display: inline-block;
88 min-width: 5em;
88 min-width: 5em;
89 margin: 0;
89 margin: 0;
90 padding: @button-padding @button-padding @button-padding 0;
90 padding: @button-padding @button-padding @button-padding 0;
91 font-family: @text-semibold;
91 font-family: @text-semibold;
92 }
92 }
93
93
94 .compare-buttons {
94 .compare-buttons {
95 float: left;
95 float: left;
96 margin: 0;
96 margin: 0;
97 padding: 0 0 @padding;
97 padding: 0 0 @padding;
98
98
99 .btn {
99 .btn {
100 margin: 0 @padding 0 0;
100 margin: 0 @padding 0 0;
101 }
101 }
102 }
102 }
103 }
103 }
104
104
105 }
105 }
106
106
107 .parents {
107 .parents {
108 float: left;
108 float: left;
109 width: 100px;
109 width: 100px;
110 font-weight: 400;
110 font-weight: 400;
111 vertical-align: middle;
111 vertical-align: middle;
112 padding: 0px 2px 0px 2px;
112 padding: 0px 2px 0px 2px;
113 background-color: @grey6;
113 background-color: @grey6;
114
114
115 #parent_link {
115 #parent_link {
116 margin: 00px 2px;
116 margin: 00px 2px;
117
117
118 &.double {
118 &.double {
119 margin: 0px 2px;
119 margin: 0px 2px;
120 }
120 }
121
121
122 &.disabled{
122 &.disabled{
123 margin-right: @padding;
123 margin-right: @padding;
124 }
124 }
125 }
125 }
126 }
126 }
127
127
128 .children {
128 .children {
129 float: right;
129 float: right;
130 width: 100px;
130 width: 100px;
131 font-weight: 400;
131 font-weight: 400;
132 vertical-align: middle;
132 vertical-align: middle;
133 text-align: right;
133 text-align: right;
134 padding: 0px 2px 0px 2px;
134 padding: 0px 2px 0px 2px;
135 background-color: @grey6;
135 background-color: @grey6;
136
136
137 #child_link {
137 #child_link {
138 margin: 0px 2px;
138 margin: 0px 2px;
139
139
140 &.double {
140 &.double {
141 margin: 0px 2px;
141 margin: 0px 2px;
142 }
142 }
143
143
144 &.disabled{
144 &.disabled{
145 margin-right: @padding;
145 margin-right: @padding;
146 }
146 }
147 }
147 }
148 }
148 }
149
149
150 .changeset_header {
150 .changeset_header {
151 height: 16px;
151 height: 16px;
152
152
153 & > div{
153 & > div{
154 margin-right: @padding;
154 margin-right: @padding;
155 }
155 }
156 }
156 }
157
157
158 .changeset_file {
158 .changeset_file {
159 text-align: left;
159 text-align: left;
160 float: left;
160 float: left;
161 padding: 0;
161 padding: 0;
162
162
163 a{
163 a{
164 display: inline-block;
164 display: inline-block;
165 margin-right: 0.5em;
165 margin-right: 0.5em;
166 }
166 }
167
167
168 #selected_mode{
168 #selected_mode{
169 margin-left: 0;
169 margin-left: 0;
170 }
170 }
171 }
171 }
172
172
173 .diff-menu-wrapper {
173 .diff-menu-wrapper {
174 float: left;
174 float: left;
175 }
175 }
176
176
177 .diff-menu {
177 .diff-menu {
178 position: absolute;
178 position: absolute;
179 background: none repeat scroll 0 0 #FFFFFF;
179 background: none repeat scroll 0 0 #FFFFFF;
180 border-color: #003367 @grey3 @grey3;
180 border-color: #003367 @grey3 @grey3;
181 border-right: 1px solid @grey3;
181 border-right: 1px solid @grey3;
182 border-style: solid solid solid;
182 border-style: solid solid solid;
183 border-width: @border-thickness;
183 border-width: @border-thickness;
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 margin-top: 5px;
185 margin-top: 5px;
186 margin-left: 1px;
186 margin-left: 1px;
187 }
187 }
188
188
189 .diff-actions, .editor-actions {
189 .diff-actions, .editor-actions {
190 float: left;
190 float: left;
191
191
192 input{
192 input{
193 margin: 0 0.5em 0 0;
193 margin: 0 0.5em 0 0;
194 }
194 }
195 }
195 }
196
196
197 // END CODE-HEADER STYLES
197 // END CODE-HEADER STYLES
198
198
199 // BEGIN CODE-BODY STYLES
199 // BEGIN CODE-BODY STYLES
200
200
201 .code-body {
201 .code-body {
202 background: white;
202 background: white;
203 padding: 0;
203 padding: 0;
204 background-color: #ffffff;
204 background-color: #ffffff;
205 position: relative;
205 position: relative;
206 max-width: none;
206 max-width: none;
207 box-sizing: border-box;
207 box-sizing: border-box;
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 // to have the intended size and to scroll. Should be simplified.
209 // to have the intended size and to scroll. Should be simplified.
210 width: 100%;
210 width: 100%;
211 overflow-x: auto;
211 overflow-x: auto;
212 }
212 }
213
213
214 pre.raw {
214 pre.raw {
215 background: white;
215 background: white;
216 color: @grey1;
216 color: @grey1;
217 }
217 }
218 // END CODE-BODY STYLES
218 // END CODE-BODY STYLES
219
219
220 }
220 }
221
221
222
222
223 table.code-difftable {
223 table.code-difftable {
224 border-collapse: collapse;
224 border-collapse: collapse;
225 width: 99%;
225 width: 99%;
226 border-radius: 0px !important;
226 border-radius: 0px !important;
227
227
228 td {
228 td {
229 padding: 0 !important;
229 padding: 0 !important;
230 background: none !important;
230 background: none !important;
231 border: 0 !important;
231 border: 0 !important;
232 }
232 }
233
233
234 .context {
234 .context {
235 background: none repeat scroll 0 0 #DDE7EF;
235 background: none repeat scroll 0 0 #DDE7EF;
236 }
236 }
237
237
238 .add {
238 .add {
239 background: none repeat scroll 0 0 #DDFFDD;
239 background: none repeat scroll 0 0 #DDFFDD;
240
240
241 ins {
241 ins {
242 background: none repeat scroll 0 0 #AAFFAA;
242 background: none repeat scroll 0 0 #AAFFAA;
243 text-decoration: none;
243 text-decoration: none;
244 }
244 }
245 }
245 }
246
246
247 .del {
247 .del {
248 background: none repeat scroll 0 0 #FFDDDD;
248 background: none repeat scroll 0 0 #FFDDDD;
249
249
250 del {
250 del {
251 background: none repeat scroll 0 0 #FFAAAA;
251 background: none repeat scroll 0 0 #FFAAAA;
252 text-decoration: none;
252 text-decoration: none;
253 }
253 }
254 }
254 }
255
255
256 /** LINE NUMBERS **/
256 /** LINE NUMBERS **/
257 .lineno {
257 .lineno {
258 padding-left: 2px !important;
258 padding-left: 2px !important;
259 padding-right: 2px;
259 padding-right: 2px;
260 text-align: right;
260 text-align: right;
261 width: 32px;
261 width: 32px;
262 -moz-user-select: none;
262 -moz-user-select: none;
263 -webkit-user-select: none;
263 -webkit-user-select: none;
264 border-right: @border-thickness solid @grey5 !important;
264 border-right: @border-thickness solid @grey5 !important;
265 border-left: 0px solid #CCC !important;
265 border-left: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
267 border-bottom: none !important;
267 border-bottom: none !important;
268
268
269 a {
269 a {
270 &:extend(pre);
270 &:extend(pre);
271 text-align: right;
271 text-align: right;
272 padding-right: 2px;
272 padding-right: 2px;
273 cursor: pointer;
273 cursor: pointer;
274 display: block;
274 display: block;
275 width: 32px;
275 width: 32px;
276 }
276 }
277 }
277 }
278
278
279 .context {
279 .context {
280 cursor: auto;
280 cursor: auto;
281 &:extend(pre);
281 &:extend(pre);
282 }
282 }
283
283
284 .lineno-inline {
284 .lineno-inline {
285 background: none repeat scroll 0 0 #FFF !important;
285 background: none repeat scroll 0 0 #FFF !important;
286 padding-left: 2px;
286 padding-left: 2px;
287 padding-right: 2px;
287 padding-right: 2px;
288 text-align: right;
288 text-align: right;
289 width: 30px;
289 width: 30px;
290 -moz-user-select: none;
290 -moz-user-select: none;
291 -webkit-user-select: none;
291 -webkit-user-select: none;
292 }
292 }
293
293
294 /** CODE **/
294 /** CODE **/
295 .code {
295 .code {
296 display: block;
296 display: block;
297 width: 100%;
297 width: 100%;
298
298
299 td {
299 td {
300 margin: 0;
300 margin: 0;
301 padding: 0;
301 padding: 0;
302 }
302 }
303
303
304 pre {
304 pre {
305 margin: 0;
305 margin: 0;
306 padding: 0;
306 padding: 0;
307 margin-left: .5em;
307 margin-left: .5em;
308 }
308 }
309 }
309 }
310 }
310 }
311
311
312
312
313 // Comments
313 // Comments
314
314
315 div.comment:target {
315 div.comment:target {
316 border-left: 6px solid @comment-highlight-color;
316 border-left: 6px solid @comment-highlight-color;
317 padding-left: 3px;
317 padding-left: 3px;
318 margin-left: -9px;
318 margin-left: -9px;
319 }
319 }
320
320
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 //current values that might change. But to make it clear I put as a calculation
322 //current values that might change. But to make it clear I put as a calculation
323 @comment-max-width: 1065px;
323 @comment-max-width: 1065px;
324 @pr-extra-margin: 34px;
324 @pr-extra-margin: 34px;
325 @pr-border-spacing: 4px;
325 @pr-border-spacing: 4px;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327
327
328 // Pull Request
328 // Pull Request
329 .cs_files .code-difftable {
329 .cs_files .code-difftable {
330 border: @border-thickness solid @grey5; //borders only on PRs
330 border: @border-thickness solid @grey5; //borders only on PRs
331
331
332 .comment-inline-form,
332 .comment-inline-form,
333 div.comment {
333 div.comment {
334 width: @pr-comment-width;
334 width: @pr-comment-width;
335 }
335 }
336 }
336 }
337
337
338 // Changeset
338 // Changeset
339 .code-difftable {
339 .code-difftable {
340 .comment-inline-form,
340 .comment-inline-form,
341 div.comment {
341 div.comment {
342 width: @comment-max-width;
342 width: @comment-max-width;
343 }
343 }
344 }
344 }
345
345
346 //Style page
346 //Style page
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 #style-page .code-difftable{
348 #style-page .code-difftable{
349 .comment-inline-form,
349 .comment-inline-form,
350 div.comment {
350 div.comment {
351 width: @comment-max-width - @style-extra-margin;
351 width: @comment-max-width - @style-extra-margin;
352 }
352 }
353 }
353 }
354
354
355 #context-bar > h2 {
355 #context-bar > h2 {
356 font-size: 20px;
356 font-size: 20px;
357 }
357 }
358
358
359 #context-bar > h2> a {
359 #context-bar > h2> a {
360 font-size: 20px;
360 font-size: 20px;
361 }
361 }
362 // end of defaults
362 // end of defaults
363
363
364 .file_diff_buttons {
364 .file_diff_buttons {
365 padding: 0 0 @padding;
365 padding: 0 0 @padding;
366
366
367 .drop-menu {
367 .drop-menu {
368 float: left;
368 float: left;
369 margin: 0 @padding 0 0;
369 margin: 0 @padding 0 0;
370 }
370 }
371 .btn {
371 .btn {
372 margin: 0 @padding 0 0;
372 margin: 0 @padding 0 0;
373 }
373 }
374 }
374 }
375
375
376 .code-body.textarea.editor {
376 .code-body.textarea.editor {
377 max-width: none;
377 max-width: none;
378 padding: 15px;
378 padding: 15px;
379 }
379 }
380
380
381 td.injected_diff{
381 td.injected_diff{
382 max-width: 1178px;
382 max-width: 1178px;
383 overflow-x: auto;
383 overflow-x: auto;
384 overflow-y: hidden;
384 overflow-y: hidden;
385
385
386 div.diff-container,
386 div.diff-container,
387 div.diffblock{
387 div.diffblock{
388 max-width: 100%;
388 max-width: 100%;
389 }
389 }
390
390
391 div.code-body {
391 div.code-body {
392 max-width: 1124px;
392 max-width: 1124px;
393 overflow-x: auto;
393 overflow-x: auto;
394 overflow-y: hidden;
394 overflow-y: hidden;
395 padding: 0;
395 padding: 0;
396 }
396 }
397 div.diffblock {
397 div.diffblock {
398 border: none;
398 border: none;
399 }
399 }
400
400
401 &.inline-form {
401 &.inline-form {
402 width: 99%
402 width: 99%
403 }
403 }
404 }
404 }
405
405
406
406
407 table.code-difftable {
407 table.code-difftable {
408 width: 100%;
408 width: 100%;
409 }
409 }
410
410
411 /** PYGMENTS COLORING **/
411 /** PYGMENTS COLORING **/
412 div.codeblock {
412 div.codeblock {
413
413
414 // TODO: johbo: Added interim to get rid of the margin around
414 // TODO: johbo: Added interim to get rid of the margin around
415 // Select2 widgets. This needs further cleanup.
415 // Select2 widgets. This needs further cleanup.
416 margin-top: @padding;
416 margin-top: @padding;
417
417
418 overflow: auto;
418 overflow: auto;
419 padding: 0px;
419 padding: 0px;
420 border: @border-thickness solid @grey5;
420 border: @border-thickness solid @grey5;
421 background: @grey6;
421 background: @grey6;
422 .border-radius(@border-radius);
422 .border-radius(@border-radius);
423
423
424 #remove_gist {
424 #remove_gist {
425 float: right;
425 float: right;
426 }
426 }
427
427
428 .author {
428 .author {
429 clear: both;
429 clear: both;
430 vertical-align: middle;
430 vertical-align: middle;
431 font-family: @text-bold;
431 font-family: @text-bold;
432 }
432 }
433
433
434 .btn-mini {
434 .btn-mini {
435 float: left;
435 float: left;
436 margin: 0 5px 0 0;
436 margin: 0 5px 0 0;
437 }
437 }
438
438
439 .code-header {
439 .code-header {
440 padding: @padding;
440 padding: @padding;
441 border-bottom: @border-thickness solid @grey5;
441 border-bottom: @border-thickness solid @grey5;
442
442
443 .rc-user {
443 .rc-user {
444 min-width: 0;
444 min-width: 0;
445 margin-right: .5em;
445 margin-right: .5em;
446 }
446 }
447
447
448 .stats {
448 .stats {
449 clear: both;
449 clear: both;
450 margin: 0 0 @padding 0;
450 margin: 0 0 @padding 0;
451 padding: 0;
451 padding: 0;
452 .left {
452 .left {
453 float: left;
453 float: left;
454 clear: left;
454 clear: left;
455 max-width: 75%;
455 max-width: 75%;
456 margin: 0 0 @padding 0;
456 margin: 0 0 @padding 0;
457
457
458 &.item {
458 &.item {
459 margin-right: @padding;
459 margin-right: @padding;
460 &.last { border-right: none; }
460 &.last { border-right: none; }
461 }
461 }
462 }
462 }
463 .buttons { float: right; }
463 .buttons { float: right; }
464 .author {
464 .author {
465 height: 25px; margin-left: 15px; font-weight: bold;
465 height: 25px; margin-left: 15px; font-weight: bold;
466 }
466 }
467 }
467 }
468
468
469 .commit {
469 .commit {
470 margin: 5px 0 0 26px;
470 margin: 5px 0 0 26px;
471 font-weight: normal;
471 font-weight: normal;
472 white-space: pre-wrap;
472 white-space: pre-wrap;
473 }
473 }
474 }
474 }
475
475
476 .message {
476 .message {
477 position: relative;
477 position: relative;
478 margin: @padding;
478 margin: @padding;
479
479
480 .codeblock-label {
480 .codeblock-label {
481 margin: 0 0 1em 0;
481 margin: 0 0 1em 0;
482 }
482 }
483 }
483 }
484
484
485 .code-body {
485 .code-body {
486 padding: @padding;
486 padding: @padding;
487 background-color: #ffffff;
487 background-color: #ffffff;
488 min-width: 100%;
488 min-width: 100%;
489 box-sizing: border-box;
489 box-sizing: border-box;
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 // to have the intended size and to scroll. Should be simplified.
491 // to have the intended size and to scroll. Should be simplified.
492 width: 100%;
492 width: 100%;
493 overflow-x: auto;
493 overflow-x: auto;
494 }
494 }
495 }
495 }
496
496
497 .code-highlighttable,
497 .code-highlighttable,
498 div.codeblock {
498 div.codeblock {
499
499
500 &.readme {
500 &.readme {
501 background-color: white;
501 background-color: white;
502 }
502 }
503
503
504 .markdown-block table {
504 .markdown-block table {
505 border-collapse: collapse;
505 border-collapse: collapse;
506
506
507 th,
507 th,
508 td {
508 td {
509 padding: .5em !important;
509 padding: .5em;
510 border: @border-thickness solid @border-default-color !important;
510 border: @border-thickness solid @border-default-color;
511 }
511 }
512 }
512 }
513
513
514 table {
514 table {
515 width: 0 !important;
515 border: 0px;
516 border: 0px !important;
517 margin: 0;
516 margin: 0;
518 letter-spacing: normal;
517 letter-spacing: normal;
519
518
520
519
521 td {
520 td {
522 border: 0px !important;
521 border: 0px;
523 vertical-align: top;
522 vertical-align: top;
524 }
523 }
525 }
524 }
526 }
525 }
527
526
528 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
529 div.search-code-body {
528 div.search-code-body {
530 background-color: #ffffff; padding: 5px 0 5px 10px;
529 background-color: #ffffff; padding: 5px 0 5px 10px;
531 pre {
530 pre {
532 .match { background-color: #faffa6;}
531 .match { background-color: #faffa6;}
533 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
534 }
533 }
535 .code-highlighttable {
534 .code-highlighttable {
536 border-collapse: collapse;
535 border-collapse: collapse;
537
536
538 tr:hover {
537 tr:hover {
539 background: #fafafa;
538 background: #fafafa;
540 }
539 }
541 td.code {
540 td.code {
542 padding-left: 10px;
541 padding-left: 10px;
543 }
542 }
544 td.line {
543 td.line {
545 border-right: 1px solid #ccc !important;
544 border-right: 1px solid #ccc !important;
546 padding-right: 10px;
545 padding-right: 10px;
547 text-align: right;
546 text-align: right;
548 font-family: "Lucida Console",Monaco,monospace;
547 font-family: "Lucida Console",Monaco,monospace;
549 span {
548 span {
550 white-space: pre-wrap;
549 white-space: pre-wrap;
551 color: #666666;
550 color: #666666;
552 }
551 }
553 }
552 }
554 }
553 }
555 }
554 }
556
555
557 div.annotatediv { margin-left: 2px; margin-right: 4px; }
556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
558 .code-highlight {
557 .code-highlight {
559 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
560 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
561 pre div:target {background-color: @comment-highlight-color !important;}
560 pre div:target {background-color: @comment-highlight-color !important;}
562 }
561 }
563
562
564 .linenos a { text-decoration: none; }
563 .linenos a { text-decoration: none; }
565
564
566 .CodeMirror-selected { background: @rchighlightblue; }
565 .CodeMirror-selected { background: @rchighlightblue; }
567 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
568 .CodeMirror ::selection { background: @rchighlightblue; }
567 .CodeMirror ::selection { background: @rchighlightblue; }
569 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
570
569
571 .code { display: block; border:0px !important; }
570 .code { display: block; border:0px !important; }
572 .code-highlight,
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
573 .codehilite {
572 .codehilite {
574 .hll { background-color: #ffffcc }
573 .hll { background-color: #ffffcc }
575 .c { color: #408080; font-style: italic } /* Comment */
574 .c { color: #408080; font-style: italic } /* Comment */
576 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
577 .k { color: #008000; font-weight: bold } /* Keyword */
576 .k { color: #008000; font-weight: bold } /* Keyword */
578 .o { color: #666666 } /* Operator */
577 .o { color: #666666 } /* Operator */
579 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
580 .cp { color: #BC7A00 } /* Comment.Preproc */
579 .cp { color: #BC7A00 } /* Comment.Preproc */
581 .c1 { color: #408080; font-style: italic } /* Comment.Single */
580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
582 .cs { color: #408080; font-style: italic } /* Comment.Special */
581 .cs { color: #408080; font-style: italic } /* Comment.Special */
583 .gd { color: #A00000 } /* Generic.Deleted */
582 .gd { color: #A00000 } /* Generic.Deleted */
584 .ge { font-style: italic } /* Generic.Emph */
583 .ge { font-style: italic } /* Generic.Emph */
585 .gr { color: #FF0000 } /* Generic.Error */
584 .gr { color: #FF0000 } /* Generic.Error */
586 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
587 .gi { color: #00A000 } /* Generic.Inserted */
586 .gi { color: #00A000 } /* Generic.Inserted */
588 .go { color: #808080 } /* Generic.Output */
587 .go { color: #808080 } /* Generic.Output */
589 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
590 .gs { font-weight: bold } /* Generic.Strong */
589 .gs { font-weight: bold } /* Generic.Strong */
591 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
592 .gt { color: #0040D0 } /* Generic.Traceback */
591 .gt { color: #0040D0 } /* Generic.Traceback */
593 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
594 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
595 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
596 .kp { color: #008000 } /* Keyword.Pseudo */
595 .kp { color: #008000 } /* Keyword.Pseudo */
597 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
598 .kt { color: #B00040 } /* Keyword.Type */
597 .kt { color: #B00040 } /* Keyword.Type */
599 .m { color: #666666 } /* Literal.Number */
598 .m { color: #666666 } /* Literal.Number */
600 .s { color: #BA2121 } /* Literal.String */
599 .s { color: #BA2121 } /* Literal.String */
601 .na { color: #7D9029 } /* Name.Attribute */
600 .na { color: #7D9029 } /* Name.Attribute */
602 .nb { color: #008000 } /* Name.Builtin */
601 .nb { color: #008000 } /* Name.Builtin */
603 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
604 .no { color: #880000 } /* Name.Constant */
603 .no { color: #880000 } /* Name.Constant */
605 .nd { color: #AA22FF } /* Name.Decorator */
604 .nd { color: #AA22FF } /* Name.Decorator */
606 .ni { color: #999999; font-weight: bold } /* Name.Entity */
605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
607 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
608 .nf { color: #0000FF } /* Name.Function */
607 .nf { color: #0000FF } /* Name.Function */
609 .nl { color: #A0A000 } /* Name.Label */
608 .nl { color: #A0A000 } /* Name.Label */
610 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
611 .nt { color: #008000; font-weight: bold } /* Name.Tag */
610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
612 .nv { color: #19177C } /* Name.Variable */
611 .nv { color: #19177C } /* Name.Variable */
613 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
614 .w { color: #bbbbbb } /* Text.Whitespace */
613 .w { color: #bbbbbb } /* Text.Whitespace */
615 .mf { color: #666666 } /* Literal.Number.Float */
614 .mf { color: #666666 } /* Literal.Number.Float */
616 .mh { color: #666666 } /* Literal.Number.Hex */
615 .mh { color: #666666 } /* Literal.Number.Hex */
617 .mi { color: #666666 } /* Literal.Number.Integer */
616 .mi { color: #666666 } /* Literal.Number.Integer */
618 .mo { color: #666666 } /* Literal.Number.Oct */
617 .mo { color: #666666 } /* Literal.Number.Oct */
619 .sb { color: #BA2121 } /* Literal.String.Backtick */
618 .sb { color: #BA2121 } /* Literal.String.Backtick */
620 .sc { color: #BA2121 } /* Literal.String.Char */
619 .sc { color: #BA2121 } /* Literal.String.Char */
621 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
622 .s2 { color: #BA2121 } /* Literal.String.Double */
621 .s2 { color: #BA2121 } /* Literal.String.Double */
623 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
624 .sh { color: #BA2121 } /* Literal.String.Heredoc */
623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
625 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
626 .sx { color: #008000 } /* Literal.String.Other */
625 .sx { color: #008000 } /* Literal.String.Other */
627 .sr { color: #BB6688 } /* Literal.String.Regex */
626 .sr { color: #BB6688 } /* Literal.String.Regex */
628 .s1 { color: #BA2121 } /* Literal.String.Single */
627 .s1 { color: #BA2121 } /* Literal.String.Single */
629 .ss { color: #19177C } /* Literal.String.Symbol */
628 .ss { color: #19177C } /* Literal.String.Symbol */
630 .bp { color: #008000 } /* Name.Builtin.Pseudo */
629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
631 .vc { color: #19177C } /* Name.Variable.Class */
630 .vc { color: #19177C } /* Name.Variable.Class */
632 .vg { color: #19177C } /* Name.Variable.Global */
631 .vg { color: #19177C } /* Name.Variable.Global */
633 .vi { color: #19177C } /* Name.Variable.Instance */
632 .vi { color: #19177C } /* Name.Variable.Instance */
634 .il { color: #666666 } /* Literal.Number.Integer.Long */
633 .il { color: #666666 } /* Literal.Number.Integer.Long */
635 }
634 }
636
635
637 /* customized pre blocks for markdown/rst */
636 /* customized pre blocks for markdown/rst */
638 pre.literal-block, .codehilite pre{
637 pre.literal-block, .codehilite pre{
639 padding: @padding;
638 padding: @padding;
640 border: 1px solid @grey6;
639 border: 1px solid @grey6;
641 .border-radius(@border-radius);
640 .border-radius(@border-radius);
642 background-color: @grey7;
641 background-color: @grey7;
643 }
642 }
643
644
645 /* START NEW CODE BLOCK CSS */
646
647 table.cb {
648 width: 100%;
649 border-collapse: collapse;
650 margin-bottom: 10px;
651
652 * {
653 box-sizing: border-box;
654 }
655
656 /* intentionally general selector since .cb-line-selected must override it
657 and they both use !important since the td itself may have a random color
658 generated by annotation blocks. TLDR: if you change it, make sure
659 annotated block selection and line selection in file view still work */
660 .cb-line-fresh .cb-content {
661 background: white !important;
662 }
663
664 tr.cb-annotate {
665 border-top: 1px solid #eee;
666
667 &+ .cb-line {
668 border-top: 1px solid #eee;
669 }
670
671 &:first-child {
672 border-top: none;
673 &+ .cb-line {
674 border-top: none;
675 }
676 }
677 }
678
679 td {
680 vertical-align: top;
681 padding: 2px 10px;
682
683 &.cb-content {
684 white-space: pre-wrap;
685 font-family: @font-family-monospace;
686 font-size: 12.35px;
687
688 span {
689 word-break: break-word;
690 }
691 }
692
693 &.cb-lineno {
694 padding: 0;
695 height: 1px; /* this allows the <a> link to fill to 100% height of the td */
696 width: 50px;
697 color: rgba(0, 0, 0, 0.3);
698 text-align: right;
699 border-right: 1px solid #eee;
700 font-family: @font-family-monospace;
701
702 a::before {
703 content: attr(data-line-no);
704 }
705 &.cb-line-selected {
706 background: @comment-highlight-color !important;
707 }
708
709 a {
710 display: block;
711 height: 100%;
712 color: rgba(0, 0, 0, 0.3);
713 padding: 0 10px; /* vertical padding is 0 so that height: 100% works */
714 line-height: 18px; /* use this instead of vertical padding */
715 }
716 }
717
718 &.cb-content {
719 &.cb-line-selected {
720 background: @comment-highlight-color !important;
721 }
722 }
723
724 &.cb-annotate-info {
725 width: 320px;
726 min-width: 320px;
727 max-width: 320px;
728 padding: 5px 2px;
729 font-size: 13px;
730
731 strong.cb-annotate-message {
732 padding: 5px 0;
733 white-space: pre-line;
734 display: inline-block;
735 }
736 .rc-user {
737 float: none;
738 padding: 0 6px 0 17px;
739 min-width: auto;
740 min-height: auto;
741 }
742 }
743
744 &.cb-annotate-revision {
745 cursor: pointer;
746 text-align: right;
747 }
748 }
749 }
@@ -1,388 +1,476 b''
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('compare_url', url_data);
61 window.location = pyroutes.url('compare_url', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 if (!container.hasClass('loaded')) {
79 if (!container.hasClass('loaded')) {
80 $.ajax({url: url})
80 $.ajax({url: url})
81 .complete(function (data) {
81 .complete(function (data) {
82 var responseJSON = data.responseJSON;
82 var responseJSON = data.responseJSON;
83 container.addClass('loaded');
83 container.addClass('loaded');
84 container.html(responseJSON.size);
84 container.html(responseJSON.size);
85 callback(responseJSON.code_stats)
85 callback(responseJSON.code_stats)
86 })
86 })
87 .fail(function (data) {
87 .fail(function (data) {
88 console.log('failed to load repo stats');
88 console.log('failed to load repo stats');
89 });
89 });
90 }
90 }
91
91
92 };
92 };
93
93
94 var showRepoStats = function(target, data){
94 var showRepoStats = function(target, data){
95 var container = $('#' + target);
95 var container = $('#' + target);
96
96
97 if (container.hasClass('loaded')) {
97 if (container.hasClass('loaded')) {
98 return
98 return
99 }
99 }
100
100
101 var total = 0;
101 var total = 0;
102 var no_data = true;
102 var no_data = true;
103 var tbl = document.createElement('table');
103 var tbl = document.createElement('table');
104 tbl.setAttribute('class', 'trending_language_tbl');
104 tbl.setAttribute('class', 'trending_language_tbl');
105
105
106 $.each(data, function(key, val){
106 $.each(data, function(key, val){
107 total += val.count;
107 total += val.count;
108 });
108 });
109
109
110 var sortedStats = [];
110 var sortedStats = [];
111 for (var obj in data){
111 for (var obj in data){
112 sortedStats.push([obj, data[obj]])
112 sortedStats.push([obj, data[obj]])
113 }
113 }
114 var sortedData = sortedStats.sort(function (a, b) {
114 var sortedData = sortedStats.sort(function (a, b) {
115 return b[1].count - a[1].count
115 return b[1].count - a[1].count
116 });
116 });
117 var cnt = 0;
117 var cnt = 0;
118 $.each(sortedData, function(idx, val){
118 $.each(sortedData, function(idx, val){
119 cnt += 1;
119 cnt += 1;
120 no_data = false;
120 no_data = false;
121
121
122 var hide = cnt > 2;
122 var hide = cnt > 2;
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124 if (hide) {
124 if (hide) {
125 tr.setAttribute('style', 'display:none');
125 tr.setAttribute('style', 'display:none');
126 tr.setAttribute('class', 'stats_hidden');
126 tr.setAttribute('class', 'stats_hidden');
127 }
127 }
128
128
129 var key = val[0];
129 var key = val[0];
130 var obj = {"desc": val[1].desc, "count": val[1].count};
130 var obj = {"desc": val[1].desc, "count": val[1].count};
131
131
132 var percentage = Math.round((obj.count / total * 100), 2);
132 var percentage = Math.round((obj.count / total * 100), 2);
133
133
134 var td1 = document.createElement('td');
134 var td1 = document.createElement('td');
135 td1.width = 300;
135 td1.width = 300;
136 var trending_language_label = document.createElement('div');
136 var trending_language_label = document.createElement('div');
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 td1.appendChild(trending_language_label);
138 td1.appendChild(trending_language_label);
139
139
140 var td2 = document.createElement('td');
140 var td2 = document.createElement('td');
141 var trending_language = document.createElement('div');
141 var trending_language = document.createElement('div');
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143
143
144 trending_language.title = key + " " + nr_files;
144 trending_language.title = key + " " + nr_files;
145
145
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148
148
149 trending_language.setAttribute("class", 'trending_language');
149 trending_language.setAttribute("class", 'trending_language');
150 $('b', trending_language)[0].style.width = percentage + "%";
150 $('b', trending_language)[0].style.width = percentage + "%";
151 td2.appendChild(trending_language);
151 td2.appendChild(trending_language);
152
152
153 tr.appendChild(td1);
153 tr.appendChild(td1);
154 tr.appendChild(td2);
154 tr.appendChild(td2);
155 tbl.appendChild(tr);
155 tbl.appendChild(tr);
156 if (cnt == 3) {
156 if (cnt == 3) {
157 var show_more = document.createElement('tr');
157 var show_more = document.createElement('tr');
158 var td = document.createElement('td');
158 var td = document.createElement('td');
159 lnk = document.createElement('a');
159 lnk = document.createElement('a');
160
160
161 lnk.href = '#';
161 lnk.href = '#';
162 lnk.innerHTML = _gettext('Show more');
162 lnk.innerHTML = _gettext('Show more');
163 lnk.id = 'code_stats_show_more';
163 lnk.id = 'code_stats_show_more';
164 td.appendChild(lnk);
164 td.appendChild(lnk);
165
165
166 show_more.appendChild(td);
166 show_more.appendChild(td);
167 show_more.appendChild(document.createElement('td'));
167 show_more.appendChild(document.createElement('td'));
168 tbl.appendChild(show_more);
168 tbl.appendChild(show_more);
169 }
169 }
170 });
170 });
171
171
172 $(container).html(tbl);
172 $(container).html(tbl);
173 $(container).addClass('loaded');
173 $(container).addClass('loaded');
174
174
175 $('#code_stats_show_more').on('click', function (e) {
175 $('#code_stats_show_more').on('click', function (e) {
176 e.preventDefault();
176 e.preventDefault();
177 $('.stats_hidden').each(function (idx) {
177 $('.stats_hidden').each(function (idx) {
178 $(this).css("display", "");
178 $(this).css("display", "");
179 });
179 });
180 $('#code_stats_show_more').hide();
180 $('#code_stats_show_more').hide();
181 });
181 });
182
182
183 };
183 };
184
184
185
185
186 // Toggle Collapsable Content
186 // Toggle Collapsable Content
187 function collapsableContent() {
187 function collapsableContent() {
188
188
189 $('.collapsable-content').not('.no-hide').hide();
189 $('.collapsable-content').not('.no-hide').hide();
190
190
191 $('.btn-collapse').unbind(); //in case we've been here before
191 $('.btn-collapse').unbind(); //in case we've been here before
192 $('.btn-collapse').click(function() {
192 $('.btn-collapse').click(function() {
193 var button = $(this);
193 var button = $(this);
194 var togglename = $(this).data("toggle");
194 var togglename = $(this).data("toggle");
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 if ($(this).html()=="Show Less")
196 if ($(this).html()=="Show Less")
197 $(this).html("Show More");
197 $(this).html("Show More");
198 else
198 else
199 $(this).html("Show Less");
199 $(this).html("Show Less");
200 });
200 });
201 };
201 };
202
202
203 var timeagoActivate = function() {
203 var timeagoActivate = function() {
204 $("time.timeago").timeago();
204 $("time.timeago").timeago();
205 };
205 };
206
206
207 // Formatting values in a Select2 dropdown of commit references
207 // Formatting values in a Select2 dropdown of commit references
208 var formatSelect2SelectionRefs = function(commit_ref){
208 var formatSelect2SelectionRefs = function(commit_ref){
209 var tmpl = '';
209 var tmpl = '';
210 if (!commit_ref.text || commit_ref.type === 'sha'){
210 if (!commit_ref.text || commit_ref.type === 'sha'){
211 return commit_ref.text;
211 return commit_ref.text;
212 }
212 }
213 if (commit_ref.type === 'branch'){
213 if (commit_ref.type === 'branch'){
214 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
214 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
215 } else if (commit_ref.type === 'tag'){
215 } else if (commit_ref.type === 'tag'){
216 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
216 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
217 } else if (commit_ref.type === 'book'){
217 } else if (commit_ref.type === 'book'){
218 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
218 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
219 }
219 }
220 return tmpl.concat(commit_ref.text);
220 return tmpl.concat(commit_ref.text);
221 };
221 };
222
222
223 // takes a given html element and scrolls it down offset pixels
223 // takes a given html element and scrolls it down offset pixels
224 function offsetScroll(element, offset){
224 function offsetScroll(element, offset){
225 setTimeout(function(){
225 setTimeout(function(){
226 var location = element.offset().top;
226 var location = element.offset().top;
227 // some browsers use body, some use html
227 // some browsers use body, some use html
228 $('html, body').animate({ scrollTop: (location - offset) });
228 $('html, body').animate({ scrollTop: (location - offset) });
229 }, 100);
229 }, 100);
230 }
230 }
231
231
232 /**
232 /**
233 * global hooks after DOM is loaded
233 * global hooks after DOM is loaded
234 */
234 */
235 $(document).ready(function() {
235 $(document).ready(function() {
236 firefoxAnchorFix();
236 firefoxAnchorFix();
237
237
238 $('.navigation a.menulink').on('click', function(e){
238 $('.navigation a.menulink').on('click', function(e){
239 var menuitem = $(this).parent('li');
239 var menuitem = $(this).parent('li');
240 if (menuitem.hasClass('open')) {
240 if (menuitem.hasClass('open')) {
241 menuitem.removeClass('open');
241 menuitem.removeClass('open');
242 } else {
242 } else {
243 menuitem.addClass('open');
243 menuitem.addClass('open');
244 $(document).on('click', function(event) {
244 $(document).on('click', function(event) {
245 if (!$(event.target).closest(menuitem).length) {
245 if (!$(event.target).closest(menuitem).length) {
246 menuitem.removeClass('open');
246 menuitem.removeClass('open');
247 }
247 }
248 });
248 });
249 }
249 }
250 });
250 });
251 $('.compare_view_files').on(
251 $('.compare_view_files').on(
252 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
252 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
253 if (event.type === "mouseenter") {
253 if (event.type === "mouseenter") {
254 $(this).parents('tr.line').addClass('hover');
254 $(this).parents('tr.line').addClass('hover');
255 } else {
255 } else {
256 $(this).parents('tr.line').removeClass('hover');
256 $(this).parents('tr.line').removeClass('hover');
257 }
257 }
258 });
258 });
259
259
260 $('.compare_view_files').on(
260 $('.compare_view_files').on(
261 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
261 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
262 if (event.type === "mouseenter") {
262 if (event.type === "mouseenter") {
263 $(this).parents('tr.line').addClass('commenting');
263 $(this).parents('tr.line').addClass('commenting');
264 } else {
264 } else {
265 $(this).parents('tr.line').removeClass('commenting');
265 $(this).parents('tr.line').removeClass('commenting');
266 }
266 }
267 });
267 });
268
268
269 $('.compare_view_files').on(
269 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
270 when new diffs are integrated */
271 'click', '.cb-lineno a', function(event) {
272
273 if ($(this).attr('data-line-no') !== ""){
274 $('.cb-line-selected').removeClass('cb-line-selected');
275 var td = $(this).parent();
276 td.addClass('cb-line-selected'); // line number td
277 td.next().addClass('cb-line-selected'); // line content td
278
279 // Replace URL without jumping to it if browser supports.
280 // Default otherwise
281 if (history.pushState) {
282 var new_location = location.href.rstrip('#');
283 if (location.hash) {
284 new_location = new_location.replace(location.hash, "");
285 }
286
287 // Make new anchor url
288 new_location = new_location + $(this).attr('href');
289 history.pushState(true, document.title, new_location);
290
291 return false;
292 }
293 }
294 });
295
296 $('.compare_view_files').on( /* TODO: replace this with .cb function above
297 when new diffs are integrated */
270 'click', 'tr.line .lineno a',function(event) {
298 'click', 'tr.line .lineno a',function(event) {
271 if ($(this).text() != ""){
299 if ($(this).text() != ""){
272 $('tr.line').removeClass('selected');
300 $('tr.line').removeClass('selected');
273 $(this).parents("tr.line").addClass('selected');
301 $(this).parents("tr.line").addClass('selected');
274
302
275 // Replace URL without jumping to it if browser supports.
303 // Replace URL without jumping to it if browser supports.
276 // Default otherwise
304 // Default otherwise
277 if (history.pushState) {
305 if (history.pushState) {
278 var new_location = location.href;
306 var new_location = location.href;
279 if (location.hash){
307 if (location.hash){
280 new_location = new_location.replace(location.hash, "");
308 new_location = new_location.replace(location.hash, "");
281 }
309 }
282
310
283 // Make new anchor url
311 // Make new anchor url
284 var new_location = new_location+$(this).attr('href');
312 var new_location = new_location+$(this).attr('href');
285 history.pushState(true, document.title, new_location);
313 history.pushState(true, document.title, new_location);
286
314
287 return false;
315 return false;
288 }
316 }
289 }
317 }
290 });
318 });
291
319
292 $('.compare_view_files').on(
320 $('.compare_view_files').on(
293 'click', 'tr.line .add-comment-line a',function(event) {
321 'click', 'tr.line .add-comment-line a',function(event) {
294 var tr = $(event.currentTarget).parents('tr.line')[0];
322 var tr = $(event.currentTarget).parents('tr.line')[0];
295 injectInlineForm(tr);
323 injectInlineForm(tr);
296 return false;
324 return false;
297 });
325 });
298
326
299 $('.collapse_file').on('click', function(e) {
327 $('.collapse_file').on('click', function(e) {
300 e.stopPropagation();
328 e.stopPropagation();
301 if ($(e.target).is('a')) { return; }
329 if ($(e.target).is('a')) { return; }
302 var node = $(e.delegateTarget).first();
330 var node = $(e.delegateTarget).first();
303 var icon = $($(node.children().first()).children().first());
331 var icon = $($(node.children().first()).children().first());
304 var id = node.attr('fid');
332 var id = node.attr('fid');
305 var target = $('#'+id);
333 var target = $('#'+id);
306 var tr = $('#tr_'+id);
334 var tr = $('#tr_'+id);
307 var diff = $('#diff_'+id);
335 var diff = $('#diff_'+id);
308 if(node.hasClass('expand_file')){
336 if(node.hasClass('expand_file')){
309 node.removeClass('expand_file');
337 node.removeClass('expand_file');
310 icon.removeClass('expand_file_icon');
338 icon.removeClass('expand_file_icon');
311 node.addClass('collapse_file');
339 node.addClass('collapse_file');
312 icon.addClass('collapse_file_icon');
340 icon.addClass('collapse_file_icon');
313 diff.show();
341 diff.show();
314 tr.show();
342 tr.show();
315 target.show();
343 target.show();
316 } else {
344 } else {
317 node.removeClass('collapse_file');
345 node.removeClass('collapse_file');
318 icon.removeClass('collapse_file_icon');
346 icon.removeClass('collapse_file_icon');
319 node.addClass('expand_file');
347 node.addClass('expand_file');
320 icon.addClass('expand_file_icon');
348 icon.addClass('expand_file_icon');
321 diff.hide();
349 diff.hide();
322 tr.hide();
350 tr.hide();
323 target.hide();
351 target.hide();
324 }
352 }
325 });
353 });
326
354
327 $('#expand_all_files').click(function() {
355 $('#expand_all_files').click(function() {
328 $('.expand_file').each(function() {
356 $('.expand_file').each(function() {
329 var node = $(this);
357 var node = $(this);
330 var icon = $($(node.children().first()).children().first());
358 var icon = $($(node.children().first()).children().first());
331 var id = $(this).attr('fid');
359 var id = $(this).attr('fid');
332 var target = $('#'+id);
360 var target = $('#'+id);
333 var tr = $('#tr_'+id);
361 var tr = $('#tr_'+id);
334 var diff = $('#diff_'+id);
362 var diff = $('#diff_'+id);
335 node.removeClass('expand_file');
363 node.removeClass('expand_file');
336 icon.removeClass('expand_file_icon');
364 icon.removeClass('expand_file_icon');
337 node.addClass('collapse_file');
365 node.addClass('collapse_file');
338 icon.addClass('collapse_file_icon');
366 icon.addClass('collapse_file_icon');
339 diff.show();
367 diff.show();
340 tr.show();
368 tr.show();
341 target.show();
369 target.show();
342 });
370 });
343 });
371 });
344
372
345 $('#collapse_all_files').click(function() {
373 $('#collapse_all_files').click(function() {
346 $('.collapse_file').each(function() {
374 $('.collapse_file').each(function() {
347 var node = $(this);
375 var node = $(this);
348 var icon = $($(node.children().first()).children().first());
376 var icon = $($(node.children().first()).children().first());
349 var id = $(this).attr('fid');
377 var id = $(this).attr('fid');
350 var target = $('#'+id);
378 var target = $('#'+id);
351 var tr = $('#tr_'+id);
379 var tr = $('#tr_'+id);
352 var diff = $('#diff_'+id);
380 var diff = $('#diff_'+id);
353 node.removeClass('collapse_file');
381 node.removeClass('collapse_file');
354 icon.removeClass('collapse_file_icon');
382 icon.removeClass('collapse_file_icon');
355 node.addClass('expand_file');
383 node.addClass('expand_file');
356 icon.addClass('expand_file_icon');
384 icon.addClass('expand_file_icon');
357 diff.hide();
385 diff.hide();
358 tr.hide();
386 tr.hide();
359 target.hide();
387 target.hide();
360 });
388 });
361 });
389 });
362
390
363 // Mouse over behavior for comments and line selection
391 // Mouse over behavior for comments and line selection
364
392
365 // Select the line that comes from the url anchor
393 // Select the line that comes from the url anchor
366 // At the time of development, Chrome didn't seem to support jquery's :target
394 // At the time of development, Chrome didn't seem to support jquery's :target
367 // element, so I had to scroll manually
395 // element, so I had to scroll manually
368 if (location.hash) {
396
397 if (location.hash) { /* TODO: dan: remove this and replace with code block
398 below when new diffs are ready */
369 var result = splitDelimitedHash(location.hash);
399 var result = splitDelimitedHash(location.hash);
370 var loc = result.loc;
400 var loc = result.loc;
371 var remainder = result.remainder;
372 if (loc.length > 1){
401 if (loc.length > 1){
373 var lineno = $(loc+'.lineno');
402 var lineno = $(loc+'.lineno');
374 if (lineno.length > 0){
403 if (lineno.length > 0){
375 var tr = lineno.parents('tr.line');
404 var tr = lineno.parents('tr.line');
376 tr.addClass('selected');
405 tr.addClass('selected');
377
406
378 tr[0].scrollIntoView();
407 tr[0].scrollIntoView();
379
408
380 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
409 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
381 tr:tr,
410 tr: tr,
382 remainder:remainder});
411 remainder: result.remainder});
383 }
412 }
384 }
413 }
385 }
414 }
386
415
416 if (location.hash) { /* TODO: dan: use this to replace the code block above
417 when new diffs are ready */
418 var result = splitDelimitedHash(location.hash);
419 var loc = result.loc;
420 if (loc.length > 1) {
421 var page_highlights = loc.substring(
422 loc.indexOf('#') + 1).split('L');
423
424 if (page_highlights.length > 1) {
425 var highlight_ranges = page_highlights[1].split(",");
426 var h_lines = [];
427 for (var pos in highlight_ranges) {
428 var _range = highlight_ranges[pos].split('-');
429 if (_range.length === 2) {
430 var start = parseInt(_range[0]);
431 var end = parseInt(_range[1]);
432 if (start < end) {
433 for (var i = start; i <= end; i++) {
434 h_lines.push(i);
435 }
436 }
437 }
438 else {
439 h_lines.push(parseInt(highlight_ranges[pos]));
440 }
441 }
442 for (pos in h_lines) {
443 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
444 if (line_td.length) {
445 line_td.addClass('cb-line-selected'); // line number td
446 line_td.next().addClass('cb-line-selected'); // line content
447 }
448 }
449 var first_line_td = $('td.cb-lineno#L' + h_lines[0]);
450 if (first_line_td.length) {
451 var elOffset = first_line_td.offset().top;
452 var elHeight = first_line_td.height();
453 var windowHeight = $(window).height();
454 var offset;
455
456 if (elHeight < windowHeight) {
457 offset = elOffset - ((windowHeight / 4) - (elHeight / 2));
458 }
459 else {
460 offset = elOffset;
461 }
462 $(function() { // let browser scroll to hash first, then
463 // scroll the line to the middle of page
464 setTimeout(function() {
465 $('html, body').animate({ scrollTop: offset });
466 }, 100);
467 });
468 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
469 lineno: first_line_td,
470 remainder: result.remainder});
471 }
472 }
473 }
474 }
387 collapsableContent();
475 collapsableContent();
388 });
476 });
@@ -1,324 +1,287 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title(*args)">
3 <%def name="title(*args)">
4 ${_('%s Files') % c.repo_name}
4 ${_('%s Files') % c.repo_name}
5 %if hasattr(c,'file'):
5 %if hasattr(c,'file'):
6 &middot; ${h.safe_unicode(c.file.path) or '\\'}
6 &middot; ${h.safe_unicode(c.file.path) or '\\'}
7 %endif
7 %endif
8
8
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${_('Files')}
15 ${_('Files')}
16 %if c.file:
16 %if c.file:
17 @ ${h.show_id(c.commit)}
17 @ ${h.show_id(c.commit)}
18 %endif
18 %endif
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_nav()">
21 <%def name="menu_bar_nav()">
22 ${self.menu_items(active='repositories')}
22 ${self.menu_items(active='repositories')}
23 </%def>
23 </%def>
24
24
25 <%def name="menu_bar_subnav()">
25 <%def name="menu_bar_subnav()">
26 ${self.repo_menu(active='files')}
26 ${self.repo_menu(active='files')}
27 </%def>
27 </%def>
28
28
29 <%def name="main()">
29 <%def name="main()">
30 <div class="title">
30 <div class="title">
31 ${self.repo_page_title(c.rhodecode_db_repo)}
31 ${self.repo_page_title(c.rhodecode_db_repo)}
32 </div>
32 </div>
33
33
34 <div id="pjax-container" class="summary">
34 <div id="pjax-container" class="summary">
35 <div id="files_data">
35 <div id="files_data">
36 <%include file='files_pjax.html'/>
36 <%include file='files_pjax.html'/>
37 </div>
37 </div>
38 </div>
38 </div>
39 <script>
39 <script>
40 var curState = {
40 var curState = {
41 commit_id: "${c.commit.raw_id}"
41 commit_id: "${c.commit.raw_id}"
42 };
42 };
43
43
44 var getState = function(context) {
44 var getState = function(context) {
45 var url = $(location).attr('href');
45 var url = $(location).attr('href');
46 var _base_url = '${h.url("files_home",repo_name=c.repo_name,revision='',f_path='')}';
46 var _base_url = '${h.url("files_home",repo_name=c.repo_name,revision='',f_path='')}';
47 var _annotate_url = '${h.url("files_annotate_home",repo_name=c.repo_name,revision='',f_path='')}';
47 var _annotate_url = '${h.url("files_annotate_home",repo_name=c.repo_name,revision='',f_path='')}';
48 _base_url = _base_url.replace('//', '/');
48 _base_url = _base_url.replace('//', '/');
49 _annotate_url = _annotate_url.replace('//', '/');
49 _annotate_url = _annotate_url.replace('//', '/');
50
50
51 //extract f_path from url.
51 //extract f_path from url.
52 var parts = url.split(_base_url);
52 var parts = url.split(_base_url);
53 if (parts.length != 2) {
53 if (parts.length != 2) {
54 parts = url.split(_annotate_url);
54 parts = url.split(_annotate_url);
55 if (parts.length != 2) {
55 if (parts.length != 2) {
56 var rev = "tip";
56 var rev = "tip";
57 var f_path = "";
57 var f_path = "";
58 } else {
58 } else {
59 var parts2 = parts[1].split('/');
59 var parts2 = parts[1].split('/');
60 var rev = parts2.shift(); // pop the first element which is the revision
60 var rev = parts2.shift(); // pop the first element which is the revision
61 var f_path = parts2.join('/');
61 var f_path = parts2.join('/');
62 }
62 }
63
63
64 } else {
64 } else {
65 var parts2 = parts[1].split('/');
65 var parts2 = parts[1].split('/');
66 var rev = parts2.shift(); // pop the first element which is the revision
66 var rev = parts2.shift(); // pop the first element which is the revision
67 var f_path = parts2.join('/');
67 var f_path = parts2.join('/');
68 }
68 }
69
69
70 var _node_list_url = pyroutes.url('files_nodelist_home',
70 var _node_list_url = pyroutes.url('files_nodelist_home',
71 {repo_name: templateContext.repo_name,
71 {repo_name: templateContext.repo_name,
72 revision: rev, f_path: f_path});
72 revision: rev, f_path: f_path});
73 var _url_base = pyroutes.url('files_home',
73 var _url_base = pyroutes.url('files_home',
74 {repo_name: templateContext.repo_name,
74 {repo_name: templateContext.repo_name,
75 revision: rev, f_path:'__FPATH__'});
75 revision: rev, f_path:'__FPATH__'});
76 return {
76 return {
77 url: url,
77 url: url,
78 f_path: f_path,
78 f_path: f_path,
79 rev: rev,
79 rev: rev,
80 commit_id: curState.commit_id,
80 commit_id: curState.commit_id,
81 node_list_url: _node_list_url,
81 node_list_url: _node_list_url,
82 url_base: _url_base
82 url_base: _url_base
83 };
83 };
84 };
84 };
85
85
86 var metadataRequest = null;
86 var metadataRequest = null;
87 var getFilesMetadata = function() {
87 var getFilesMetadata = function() {
88 if (metadataRequest && metadataRequest.readyState != 4) {
88 if (metadataRequest && metadataRequest.readyState != 4) {
89 metadataRequest.abort();
89 metadataRequest.abort();
90 }
90 }
91 if (source_page) {
91 if (source_page) {
92 return false;
92 return false;
93 }
93 }
94
94
95 if ($('#file-tree-wrapper').hasClass('full-load')) {
95 if ($('#file-tree-wrapper').hasClass('full-load')) {
96 // in case our HTML wrapper has full-load class we don't
96 // in case our HTML wrapper has full-load class we don't
97 // trigger the async load of metadata
97 // trigger the async load of metadata
98 return false;
98 return false;
99 }
99 }
100
100
101 var state = getState('metadata');
101 var state = getState('metadata');
102 var url_data = {
102 var url_data = {
103 'repo_name': templateContext.repo_name,
103 'repo_name': templateContext.repo_name,
104 'commit_id': state.commit_id,
104 'commit_id': state.commit_id,
105 'f_path': state.f_path
105 'f_path': state.f_path
106 };
106 };
107
107
108 var url = pyroutes.url('files_nodetree_full', url_data);
108 var url = pyroutes.url('files_nodetree_full', url_data);
109
109
110 metadataRequest = $.ajax({url: url});
110 metadataRequest = $.ajax({url: url});
111
111
112 metadataRequest.done(function(data) {
112 metadataRequest.done(function(data) {
113 $('#file-tree').html(data);
113 $('#file-tree').html(data);
114 timeagoActivate();
114 timeagoActivate();
115 });
115 });
116 metadataRequest.fail(function (data, textStatus, errorThrown) {
116 metadataRequest.fail(function (data, textStatus, errorThrown) {
117 console.log(data);
117 console.log(data);
118 if (data.status != 0) {
118 if (data.status != 0) {
119 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
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 var callbacks = function() {
124 var callbacks = function() {
125 var state = getState('callbacks');
125 var state = getState('callbacks');
126 timeagoActivate();
126 timeagoActivate();
127
127
128 // used for history, and switch to
128 // used for history, and switch to
129 var initialCommitData = {
129 var initialCommitData = {
130 id: null,
130 id: null,
131 text: '${_("Switch To Commit")}',
131 text: '${_("Switch To Commit")}',
132 type: 'sha',
132 type: 'sha',
133 raw_id: null,
133 raw_id: null,
134 files_url: null
134 files_url: null
135 };
135 };
136
136
137 if ($('#trimmed_message_box').height() < 50) {
137 if ($('#trimmed_message_box').height() < 50) {
138 $('#message_expand').hide();
138 $('#message_expand').hide();
139 }
139 }
140
140
141 $('#message_expand').on('click', function(e) {
141 $('#message_expand').on('click', function(e) {
142 $('#trimmed_message_box').css('max-height', 'none');
142 $('#trimmed_message_box').css('max-height', 'none');
143 $(this).hide();
143 $(this).hide();
144 });
144 });
145
145
146
146
147 if (source_page) {
147 if (source_page) {
148 // variants for with source code, not tree view
148 // variants for with source code, not tree view
149
149
150 if (location.href.indexOf('#') != -1) {
151 page_highlights = location.href.substring(location.href.indexOf('#') + 1).split('L');
152 if (page_highlights.length == 2) {
153 highlight_ranges = page_highlights[1].split(",");
154
155 var h_lines = [];
156 for (pos in highlight_ranges) {
157 var _range = highlight_ranges[pos].split('-');
158 if (_range.length == 2) {
159 var start = parseInt(_range[0]);
160 var end = parseInt(_range[1]);
161 if (start < end) {
162 for (var i = start; i <= end; i++) {
163 h_lines.push(i);
164 }
165 }
166 }
167 else {
168 h_lines.push(parseInt(highlight_ranges[pos]));
169 }
170 }
171
172 for (pos in h_lines) {
173 // @comment-highlight-color
174 $('#L' + h_lines[pos]).css('background-color', '#ffd887');
175 }
176
177 var _first_line = $('#L' + h_lines[0]).get(0);
178 if (_first_line) {
179 var line = $('#L' + h_lines[0]);
180 if (line.length > 0){
181 offsetScroll(line, 70);
182 }
183 }
184 }
185 }
186
187 // select code link event
150 // select code link event
188 $("#hlcode").mouseup(getSelectionLink);
151 $("#hlcode").mouseup(getSelectionLink);
189
152
190 // file history select2
153 // file history select2
191 select2FileHistorySwitcher('#diff1', initialCommitData, state);
154 select2FileHistorySwitcher('#diff1', initialCommitData, state);
192 $('#diff1').on('change', function(e) {
155 $('#diff1').on('change', function(e) {
193 $('#diff').removeClass('disabled').removeAttr("disabled");
156 $('#diff').removeClass('disabled').removeAttr("disabled");
194 $('#show_rev').removeClass('disabled').removeAttr("disabled");
157 $('#show_rev').removeClass('disabled').removeAttr("disabled");
195 });
158 });
196
159
197 // show more authors
160 // show more authors
198 $('#show_authors').on('click', function(e) {
161 $('#show_authors').on('click', function(e) {
199 e.preventDefault();
162 e.preventDefault();
200 var url = pyroutes.url('files_authors_home',
163 var url = pyroutes.url('files_authors_home',
201 {'repo_name': templateContext.repo_name,
164 {'repo_name': templateContext.repo_name,
202 'revision': state.rev, 'f_path': state.f_path});
165 'revision': state.rev, 'f_path': state.f_path});
203
166
204 $.pjax({
167 $.pjax({
205 url: url,
168 url: url,
206 data: 'annotate=${"1" if c.annotate else "0"}',
169 data: 'annotate=${"1" if c.annotate else "0"}',
207 container: '#file_authors',
170 container: '#file_authors',
208 push: false,
171 push: false,
209 timeout: pjaxTimeout
172 timeout: pjaxTimeout
210 }).complete(function(){
173 }).complete(function(){
211 $('#show_authors').hide();
174 $('#show_authors').hide();
212 })
175 })
213 });
176 });
214
177
215 // load file short history
178 // load file short history
216 $('#file_history_overview').on('click', function(e) {
179 $('#file_history_overview').on('click', function(e) {
217 e.preventDefault();
180 e.preventDefault();
218 path = state.f_path;
181 path = state.f_path;
219 if (path.indexOf("#") >= 0) {
182 if (path.indexOf("#") >= 0) {
220 path = path.slice(0, path.indexOf("#"));
183 path = path.slice(0, path.indexOf("#"));
221 }
184 }
222 var url = pyroutes.url('changelog_file_home',
185 var url = pyroutes.url('changelog_file_home',
223 {'repo_name': templateContext.repo_name,
186 {'repo_name': templateContext.repo_name,
224 'revision': state.rev, 'f_path': path, 'limit': 6});
187 'revision': state.rev, 'f_path': path, 'limit': 6});
225 $('#file_history_container').show();
188 $('#file_history_container').show();
226 $('#file_history_container').html('<div class="file-history-inner">{0}</div>'.format(_gettext('Loading ...')));
189 $('#file_history_container').html('<div class="file-history-inner">{0}</div>'.format(_gettext('Loading ...')));
227
190
228 $.pjax({
191 $.pjax({
229 url: url,
192 url: url,
230 container: '#file_history_container',
193 container: '#file_history_container',
231 push: false,
194 push: false,
232 timeout: pjaxTimeout
195 timeout: pjaxTimeout
233 })
196 })
234 });
197 });
235
198
236 }
199 }
237 else {
200 else {
238 getFilesMetadata();
201 getFilesMetadata();
239
202
240 // fuzzy file filter
203 // fuzzy file filter
241 fileBrowserListeners(state.node_list_url, state.url_base);
204 fileBrowserListeners(state.node_list_url, state.url_base);
242
205
243 // switch to widget
206 // switch to widget
244 select2RefSwitcher('#refs_filter', initialCommitData);
207 select2RefSwitcher('#refs_filter', initialCommitData);
245 $('#refs_filter').on('change', function(e) {
208 $('#refs_filter').on('change', function(e) {
246 var data = $('#refs_filter').select2('data');
209 var data = $('#refs_filter').select2('data');
247 curState.commit_id = data.raw_id;
210 curState.commit_id = data.raw_id;
248 $.pjax({url: data.files_url, container: '#pjax-container', timeout: pjaxTimeout});
211 $.pjax({url: data.files_url, container: '#pjax-container', timeout: pjaxTimeout});
249 });
212 });
250
213
251 $("#prev_commit_link").on('click', function(e) {
214 $("#prev_commit_link").on('click', function(e) {
252 var data = $(this).data();
215 var data = $(this).data();
253 curState.commit_id = data.commitId;
216 curState.commit_id = data.commitId;
254 });
217 });
255
218
256 $("#next_commit_link").on('click', function(e) {
219 $("#next_commit_link").on('click', function(e) {
257 var data = $(this).data();
220 var data = $(this).data();
258 curState.commit_id = data.commitId;
221 curState.commit_id = data.commitId;
259 });
222 });
260
223
261 $('#at_rev').on("keypress", function(e) {
224 $('#at_rev').on("keypress", function(e) {
262 /* ENTER PRESSED */
225 /* ENTER PRESSED */
263 if (e.keyCode === 13) {
226 if (e.keyCode === 13) {
264 var rev = $('#at_rev').val();
227 var rev = $('#at_rev').val();
265 // explicit reload page here. with pjax entering bad input
228 // explicit reload page here. with pjax entering bad input
266 // produces not so nice results
229 // produces not so nice results
267 window.location = pyroutes.url('files_home',
230 window.location = pyroutes.url('files_home',
268 {'repo_name': templateContext.repo_name,
231 {'repo_name': templateContext.repo_name,
269 'revision': rev, 'f_path': state.f_path});
232 'revision': rev, 'f_path': state.f_path});
270 }
233 }
271 });
234 });
272 }
235 }
273 };
236 };
274
237
275 var pjaxTimeout = 5000;
238 var pjaxTimeout = 5000;
276
239
277 $(document).pjax(".pjax-link", "#pjax-container", {
240 $(document).pjax(".pjax-link", "#pjax-container", {
278 "fragment": "#pjax-content",
241 "fragment": "#pjax-content",
279 "maxCacheLength": 1000,
242 "maxCacheLength": 1000,
280 "timeout": pjaxTimeout
243 "timeout": pjaxTimeout
281 });
244 });
282
245
283 // define global back/forward states
246 // define global back/forward states
284 var isPjaxPopState = false;
247 var isPjaxPopState = false;
285 $(document).on('pjax:popstate', function() {
248 $(document).on('pjax:popstate', function() {
286 isPjaxPopState = true;
249 isPjaxPopState = true;
287 });
250 });
288
251
289 $(document).on('pjax:end', function(xhr, options) {
252 $(document).on('pjax:end', function(xhr, options) {
290 if (isPjaxPopState) {
253 if (isPjaxPopState) {
291 isPjaxPopState = false;
254 isPjaxPopState = false;
292 callbacks();
255 callbacks();
293 _NODEFILTER.resetFilter();
256 _NODEFILTER.resetFilter();
294 }
257 }
295
258
296 // run callback for tracking if defined for google analytics etc.
259 // run callback for tracking if defined for google analytics etc.
297 // this is used to trigger tracking on pjax
260 // this is used to trigger tracking on pjax
298 if (typeof window.rhodecode_statechange_callback !== 'undefined') {
261 if (typeof window.rhodecode_statechange_callback !== 'undefined') {
299 var state = getState('statechange');
262 var state = getState('statechange');
300 rhodecode_statechange_callback(state.url, null)
263 rhodecode_statechange_callback(state.url, null)
301 }
264 }
302 });
265 });
303
266
304 $(document).on('pjax:success', function(event, xhr, options) {
267 $(document).on('pjax:success', function(event, xhr, options) {
305 if (event.target.id == "file_history_container") {
268 if (event.target.id == "file_history_container") {
306 $('#file_history_overview').hide();
269 $('#file_history_overview').hide();
307 $('#file_history_overview_full').show();
270 $('#file_history_overview_full').show();
308 timeagoActivate();
271 timeagoActivate();
309 } else {
272 } else {
310 callbacks();
273 callbacks();
311 }
274 }
312 });
275 });
313
276
314 $(document).ready(function() {
277 $(document).ready(function() {
315 callbacks();
278 callbacks();
316 var search_GET = "${request.GET.get('search','')}";
279 var search_GET = "${request.GET.get('search','')}";
317 if (search_GET == "1") {
280 if (search_GET == "1") {
318 _NODEFILTER.initFilter();
281 _NODEFILTER.initFilter();
319 }
282 }
320 });
283 });
321
284
322 </script>
285 </script>
323
286
324 </%def>
287 </%def>
@@ -1,71 +1,82 b''
1 <%namespace name="sourceblock" file="/codeblocks/source.html"/>
1
2
2 <div id="codeblock" class="codeblock">
3 <div id="codeblock" class="codeblock">
3 <div class="codeblock-header">
4 <div class="codeblock-header">
4 <div class="stats">
5 <div class="stats">
5 <span> <strong>${c.file}</strong></span>
6 <span> <strong>${c.file}</strong></span>
6 <span> | ${c.file.lines()[0]} ${ungettext('line', 'lines', c.file.lines()[0])}</span>
7 <span> | ${c.file.lines()[0]} ${ungettext('line', 'lines', c.file.lines()[0])}</span>
7 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
8 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
8 <span> | ${c.file.mimetype} </span>
9 <span> | ${c.file.mimetype} </span>
9 <span class="item last"> | ${h.get_lexer_for_filenode(c.file).__class__.__name__}</span>
10 <span class="item last"> | ${h.get_lexer_for_filenode(c.file).__class__.__name__}</span>
10 </div>
11 </div>
11 <div class="buttons">
12 <div class="buttons">
12 <a id="file_history_overview" href="#">
13 <a id="file_history_overview" href="#">
13 ${_('History')}
14 ${_('History')}
14 </a>
15 </a>
15 <a id="file_history_overview_full" style="display: none" href="${h.url('changelog_file_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path=c.f_path)}">
16 <a id="file_history_overview_full" style="display: none" href="${h.url('changelog_file_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path=c.f_path)}">
16 ${_('Show Full History')}
17 ${_('Show Full History')}
17 </a> |
18 </a> |
18 %if c.annotate:
19 %if c.annotate:
19 ${h.link_to(_('Source'), h.url('files_home', repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
20 ${h.link_to(_('Source'), h.url('files_home', repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
20 %else:
21 %else:
21 ${h.link_to(_('Annotation'), h.url('files_annotate_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
22 ${h.link_to(_('Annotation'), h.url('files_annotate_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
22 %endif
23 %endif
23 | ${h.link_to(_('Raw'), h.url('files_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
24 | ${h.link_to(_('Raw'), h.url('files_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
24 | <a href="${h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path)}">
25 | <a href="${h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path)}">
25 ${_('Download')}
26 ${_('Download')}
26 </a>
27 </a>
27
28
28 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
29 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
29 |
30 |
30 %if c.on_branch_head and c.branch_or_raw_id and not c.file.is_binary:
31 %if c.on_branch_head and c.branch_or_raw_id and not c.file.is_binary:
31 <a href="${h.url('files_edit_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit')}">
32 <a href="${h.url('files_edit_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit')}">
32 ${_('Edit on Branch:%s') % c.branch_or_raw_id}
33 ${_('Edit on Branch:%s') % c.branch_or_raw_id}
33 </a>
34 </a>
34 | <a class="btn-danger btn-link" href="${h.url('files_delete_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit')}">${_('Delete')}
35 | <a class="btn-danger btn-link" href="${h.url('files_delete_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit')}">${_('Delete')}
35 </a>
36 </a>
36 %elif c.on_branch_head and c.branch_or_raw_id and c.file.is_binary:
37 %elif c.on_branch_head and c.branch_or_raw_id and c.file.is_binary:
37 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing binary files not allowed'))}
38 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing binary files not allowed'))}
38 | ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit'),class_="btn-danger btn-link")}
39 | ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.branch_or_raw_id,f_path=c.f_path, anchor='edit'),class_="btn-danger btn-link")}
39 %else:
40 %else:
40 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
41 ${h.link_to(_('Edit'), '#', class_="btn btn-link disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
41 | ${h.link_to(_('Delete'), '#', class_="btn btn-danger btn-link disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
42 | ${h.link_to(_('Delete'), '#', class_="btn btn-danger btn-link disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
42 %endif
43 %endif
43 %endif
44 %endif
44 </div>
45 </div>
45 </div>
46 </div>
46 <div id="file_history_container"></div>
47 <div id="file_history_container"></div>
47 <div class="code-body">
48 <div class="code-body">
48 %if c.file.is_binary:
49 %if c.file.is_binary:
49 <div>
50 <div>
50 ${_('Binary file (%s)') % c.file.mimetype}
51 ${_('Binary file (%s)') % c.file.mimetype}
51 </div>
52 </div>
52 %else:
53 %else:
53 % if c.file.size < c.cut_off_limit:
54 % if c.file.size < c.cut_off_limit:
54 %if c.annotate:
55 %if c.renderer and not c.annotate:
55 ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
56 %elif c.renderer:
57 ${h.render(c.file.content, renderer=c.renderer)}
56 ${h.render(c.file.content, renderer=c.renderer)}
58 %else:
57 %else:
59 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
58 <table class="cb codehilite">
59 %if c.annotate:
60 <% color_hasher = h.color_hasher() %>
61 %for annotation, lines in c.annotated_lines:
62 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
63 %endfor
64 %else:
65 %for line_num, tokens in c.lines:
66 ${sourceblock.render_line(line_num, tokens)}
67 %endfor
68 %endif
69 </table>
70 </div>
60 %endif
71 %endif
61 %else:
72 %else:
62 ${_('File is too big to display')} ${h.link_to(_('Show as raw'),
73 ${_('File is too big to display')} ${h.link_to(_('Show as raw'),
63 h.url('files_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
74 h.url('files_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id,f_path=c.f_path))}
64 %endif
75 %endif
65 %endif
76 %endif
66 </div>
77 </div>
67 </div>
78 </div>
68
79
69 <script>
80 <script>
70 var source_page = true;
81 var source_page = true;
71 </script>
82 </script>
@@ -1,1078 +1,1078 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.controllers.files import FilesController
26 from rhodecode.controllers.files import FilesController
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
30 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs.backends.base import EmptyCommit
31 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.model.db import Repository
34 from rhodecode.model.db import Repository
35 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.tests import (
36 from rhodecode.tests import (
37 url, TEST_USER_ADMIN_LOGIN, assert_session_flash, assert_not_in_session_flash)
37 url, TEST_USER_ADMIN_LOGIN, assert_session_flash, assert_not_in_session_flash)
38 from rhodecode.tests.fixture import Fixture
38 from rhodecode.tests.fixture import Fixture
39 from rhodecode.tests.utils import AssertResponse
39 from rhodecode.tests.utils import AssertResponse
40
40
41 fixture = Fixture()
41 fixture = Fixture()
42
42
43 NODE_HISTORY = {
43 NODE_HISTORY = {
44 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
45 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
46 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
47 }
47 }
48
48
49
49
50
50
51 def _commit_change(
51 def _commit_change(
52 repo, filename, content, message, vcs_type, parent=None,
52 repo, filename, content, message, vcs_type, parent=None,
53 newfile=False):
53 newfile=False):
54 repo = Repository.get_by_repo_name(repo)
54 repo = Repository.get_by_repo_name(repo)
55 _commit = parent
55 _commit = parent
56 if not parent:
56 if not parent:
57 _commit = EmptyCommit(alias=vcs_type)
57 _commit = EmptyCommit(alias=vcs_type)
58
58
59 if newfile:
59 if newfile:
60 nodes = {
60 nodes = {
61 filename: {
61 filename: {
62 'content': content
62 'content': content
63 }
63 }
64 }
64 }
65 commit = ScmModel().create_nodes(
65 commit = ScmModel().create_nodes(
66 user=TEST_USER_ADMIN_LOGIN, repo=repo,
66 user=TEST_USER_ADMIN_LOGIN, repo=repo,
67 message=message,
67 message=message,
68 nodes=nodes,
68 nodes=nodes,
69 parent_commit=_commit,
69 parent_commit=_commit,
70 author=TEST_USER_ADMIN_LOGIN,
70 author=TEST_USER_ADMIN_LOGIN,
71 )
71 )
72 else:
72 else:
73 commit = ScmModel().commit_change(
73 commit = ScmModel().commit_change(
74 repo=repo.scm_instance(), repo_name=repo.repo_name,
74 repo=repo.scm_instance(), repo_name=repo.repo_name,
75 commit=parent, user=TEST_USER_ADMIN_LOGIN,
75 commit=parent, user=TEST_USER_ADMIN_LOGIN,
76 author=TEST_USER_ADMIN_LOGIN,
76 author=TEST_USER_ADMIN_LOGIN,
77 message=message,
77 message=message,
78 content=content,
78 content=content,
79 f_path=filename
79 f_path=filename
80 )
80 )
81 return commit
81 return commit
82
82
83
83
84
84
85 @pytest.mark.usefixtures("app")
85 @pytest.mark.usefixtures("app")
86 class TestFilesController:
86 class TestFilesController:
87
87
88 def test_index(self, backend):
88 def test_index(self, backend):
89 response = self.app.get(url(
89 response = self.app.get(url(
90 controller='files', action='index',
90 controller='files', action='index',
91 repo_name=backend.repo_name, revision='tip', f_path='/'))
91 repo_name=backend.repo_name, revision='tip', f_path='/'))
92 commit = backend.repo.get_commit()
92 commit = backend.repo.get_commit()
93
93
94 params = {
94 params = {
95 'repo_name': backend.repo_name,
95 'repo_name': backend.repo_name,
96 'commit_id': commit.raw_id,
96 'commit_id': commit.raw_id,
97 'date': commit.date
97 'date': commit.date
98 }
98 }
99 assert_dirs_in_response(response, ['docs', 'vcs'], params)
99 assert_dirs_in_response(response, ['docs', 'vcs'], params)
100 files = [
100 files = [
101 '.gitignore',
101 '.gitignore',
102 '.hgignore',
102 '.hgignore',
103 '.hgtags',
103 '.hgtags',
104 # TODO: missing in Git
104 # TODO: missing in Git
105 # '.travis.yml',
105 # '.travis.yml',
106 'MANIFEST.in',
106 'MANIFEST.in',
107 'README.rst',
107 'README.rst',
108 # TODO: File is missing in svn repository
108 # TODO: File is missing in svn repository
109 # 'run_test_and_report.sh',
109 # 'run_test_and_report.sh',
110 'setup.cfg',
110 'setup.cfg',
111 'setup.py',
111 'setup.py',
112 'test_and_report.sh',
112 'test_and_report.sh',
113 'tox.ini',
113 'tox.ini',
114 ]
114 ]
115 assert_files_in_response(response, files, params)
115 assert_files_in_response(response, files, params)
116 assert_timeago_in_response(response, files, params)
116 assert_timeago_in_response(response, files, params)
117
117
118 def test_index_links_submodules_with_absolute_url(self, backend_hg):
118 def test_index_links_submodules_with_absolute_url(self, backend_hg):
119 repo = backend_hg['subrepos']
119 repo = backend_hg['subrepos']
120 response = self.app.get(url(
120 response = self.app.get(url(
121 controller='files', action='index',
121 controller='files', action='index',
122 repo_name=repo.repo_name, revision='tip', f_path='/'))
122 repo_name=repo.repo_name, revision='tip', f_path='/'))
123 assert_response = AssertResponse(response)
123 assert_response = AssertResponse(response)
124 assert_response.contains_one_link(
124 assert_response.contains_one_link(
125 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
125 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
126
126
127 def test_index_links_submodules_with_absolute_url_subpaths(
127 def test_index_links_submodules_with_absolute_url_subpaths(
128 self, backend_hg):
128 self, backend_hg):
129 repo = backend_hg['subrepos']
129 repo = backend_hg['subrepos']
130 response = self.app.get(url(
130 response = self.app.get(url(
131 controller='files', action='index',
131 controller='files', action='index',
132 repo_name=repo.repo_name, revision='tip', f_path='/'))
132 repo_name=repo.repo_name, revision='tip', f_path='/'))
133 assert_response = AssertResponse(response)
133 assert_response = AssertResponse(response)
134 assert_response.contains_one_link(
134 assert_response.contains_one_link(
135 'subpaths-path @ 000000000000',
135 'subpaths-path @ 000000000000',
136 'http://sub-base.example.com/subpaths-path')
136 'http://sub-base.example.com/subpaths-path')
137
137
138 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
138 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
139 def test_files_menu(self, backend):
139 def test_files_menu(self, backend):
140 new_branch = "temp_branch_name"
140 new_branch = "temp_branch_name"
141 commits = [
141 commits = [
142 {'message': 'a'},
142 {'message': 'a'},
143 {'message': 'b', 'branch': new_branch}
143 {'message': 'b', 'branch': new_branch}
144 ]
144 ]
145 backend.create_repo(commits)
145 backend.create_repo(commits)
146
146
147 backend.repo.landing_rev = "branch:%s" % new_branch
147 backend.repo.landing_rev = "branch:%s" % new_branch
148
148
149 # get response based on tip and not new revision
149 # get response based on tip and not new revision
150 response = self.app.get(url(
150 response = self.app.get(url(
151 controller='files', action='index',
151 controller='files', action='index',
152 repo_name=backend.repo_name, revision='tip', f_path='/'),
152 repo_name=backend.repo_name, revision='tip', f_path='/'),
153 status=200)
153 status=200)
154
154
155 # make sure Files menu url is not tip but new revision
155 # make sure Files menu url is not tip but new revision
156 landing_rev = backend.repo.landing_rev[1]
156 landing_rev = backend.repo.landing_rev[1]
157 files_url = url('files_home', repo_name=backend.repo_name,
157 files_url = url('files_home', repo_name=backend.repo_name,
158 revision=landing_rev)
158 revision=landing_rev)
159
159
160 assert landing_rev != 'tip'
160 assert landing_rev != 'tip'
161 response.mustcontain('<li class="active"><a class="menulink" href="%s">' % files_url)
161 response.mustcontain('<li class="active"><a class="menulink" href="%s">' % files_url)
162
162
163 def test_index_commit(self, backend):
163 def test_index_commit(self, backend):
164 commit = backend.repo.get_commit(commit_idx=32)
164 commit = backend.repo.get_commit(commit_idx=32)
165
165
166 response = self.app.get(url(
166 response = self.app.get(url(
167 controller='files', action='index',
167 controller='files', action='index',
168 repo_name=backend.repo_name,
168 repo_name=backend.repo_name,
169 revision=commit.raw_id,
169 revision=commit.raw_id,
170 f_path='/')
170 f_path='/')
171 )
171 )
172
172
173 dirs = ['docs', 'tests']
173 dirs = ['docs', 'tests']
174 files = ['README.rst']
174 files = ['README.rst']
175 params = {
175 params = {
176 'repo_name': backend.repo_name,
176 'repo_name': backend.repo_name,
177 'commit_id': commit.raw_id,
177 'commit_id': commit.raw_id,
178 }
178 }
179 assert_dirs_in_response(response, dirs, params)
179 assert_dirs_in_response(response, dirs, params)
180 assert_files_in_response(response, files, params)
180 assert_files_in_response(response, files, params)
181
181
182 @pytest.mark.xfail_backends("git", reason="Missing branches in git repo")
182 @pytest.mark.xfail_backends("git", reason="Missing branches in git repo")
183 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
183 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
184 def test_index_different_branch(self, backend):
184 def test_index_different_branch(self, backend):
185 # TODO: Git test repository does not contain branches
185 # TODO: Git test repository does not contain branches
186 # TODO: Branch support in Subversion
186 # TODO: Branch support in Subversion
187
187
188 commit = backend.repo.get_commit(commit_idx=150)
188 commit = backend.repo.get_commit(commit_idx=150)
189 response = self.app.get(url(
189 response = self.app.get(url(
190 controller='files', action='index',
190 controller='files', action='index',
191 repo_name=backend.repo_name,
191 repo_name=backend.repo_name,
192 revision=commit.raw_id,
192 revision=commit.raw_id,
193 f_path='/'))
193 f_path='/'))
194 assert_response = AssertResponse(response)
194 assert_response = AssertResponse(response)
195 assert_response.element_contains(
195 assert_response.element_contains(
196 '.tags .branchtag', 'git')
196 '.tags .branchtag', 'git')
197
197
198 def test_index_paging(self, backend):
198 def test_index_paging(self, backend):
199 repo = backend.repo
199 repo = backend.repo
200 indexes = [73, 92, 109, 1, 0]
200 indexes = [73, 92, 109, 1, 0]
201 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
201 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
202 for rev in indexes]
202 for rev in indexes]
203
203
204 for idx in idx_map:
204 for idx in idx_map:
205 response = self.app.get(url(
205 response = self.app.get(url(
206 controller='files', action='index',
206 controller='files', action='index',
207 repo_name=backend.repo_name,
207 repo_name=backend.repo_name,
208 revision=idx[1],
208 revision=idx[1],
209 f_path='/'))
209 f_path='/'))
210
210
211 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
211 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
212
212
213 def test_file_source(self, backend):
213 def test_file_source(self, backend):
214 commit = backend.repo.get_commit(commit_idx=167)
214 commit = backend.repo.get_commit(commit_idx=167)
215 response = self.app.get(url(
215 response = self.app.get(url(
216 controller='files', action='index',
216 controller='files', action='index',
217 repo_name=backend.repo_name,
217 repo_name=backend.repo_name,
218 revision=commit.raw_id,
218 revision=commit.raw_id,
219 f_path='vcs/nodes.py'))
219 f_path='vcs/nodes.py'))
220
220
221 msgbox = """<div class="commit right-content">%s</div>"""
221 msgbox = """<div class="commit right-content">%s</div>"""
222 response.mustcontain(msgbox % (commit.message, ))
222 response.mustcontain(msgbox % (commit.message, ))
223
223
224 assert_response = AssertResponse(response)
224 assert_response = AssertResponse(response)
225 if commit.branch:
225 if commit.branch:
226 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
226 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
227 if commit.tags:
227 if commit.tags:
228 for tag in commit.tags:
228 for tag in commit.tags:
229 assert_response.element_contains('.tags.tags-main .tagtag', tag)
229 assert_response.element_contains('.tags.tags-main .tagtag', tag)
230
230
231 def test_file_source_history(self, backend):
231 def test_file_source_history(self, backend):
232 response = self.app.get(
232 response = self.app.get(
233 url(
233 url(
234 controller='files', action='history',
234 controller='files', action='history',
235 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
236 revision='tip',
236 revision='tip',
237 f_path='vcs/nodes.py'),
237 f_path='vcs/nodes.py'),
238 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
238 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
239 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
239 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
240
240
241 def test_file_source_history_svn(self, backend_svn):
241 def test_file_source_history_svn(self, backend_svn):
242 simple_repo = backend_svn['svn-simple-layout']
242 simple_repo = backend_svn['svn-simple-layout']
243 response = self.app.get(
243 response = self.app.get(
244 url(
244 url(
245 controller='files', action='history',
245 controller='files', action='history',
246 repo_name=simple_repo.repo_name,
246 repo_name=simple_repo.repo_name,
247 revision='tip',
247 revision='tip',
248 f_path='trunk/example.py'),
248 f_path='trunk/example.py'),
249 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
249 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
250
250
251 expected_data = json.loads(
251 expected_data = json.loads(
252 fixture.load_resource('svn_node_history_branches.json'))
252 fixture.load_resource('svn_node_history_branches.json'))
253 assert expected_data == response.json
253 assert expected_data == response.json
254
254
255 def test_file_annotation_history(self, backend):
255 def test_file_annotation_history(self, backend):
256 response = self.app.get(
256 response = self.app.get(
257 url(
257 url(
258 controller='files', action='history',
258 controller='files', action='history',
259 repo_name=backend.repo_name,
259 repo_name=backend.repo_name,
260 revision='tip',
260 revision='tip',
261 f_path='vcs/nodes.py',
261 f_path='vcs/nodes.py',
262 annotate=True),
262 annotate=True),
263 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
263 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
264 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
264 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
265
265
266 def test_file_annotation(self, backend):
266 def test_file_annotation(self, backend):
267 response = self.app.get(url(
267 response = self.app.get(url(
268 controller='files', action='index',
268 controller='files', action='index',
269 repo_name=backend.repo_name, revision='tip', f_path='vcs/nodes.py',
269 repo_name=backend.repo_name, revision='tip', f_path='vcs/nodes.py',
270 annotate=True))
270 annotate=True))
271
271
272 expected_revisions = {
272 expected_revisions = {
273 'hg': 'r356:25213a5fbb04',
273 'hg': 'r356',
274 'git': 'r345:c994f0de03b2',
274 'git': 'r345',
275 'svn': 'r208:209',
275 'svn': 'r208',
276 }
276 }
277 response.mustcontain(expected_revisions[backend.alias])
277 response.mustcontain(expected_revisions[backend.alias])
278
278
279 def test_file_authors(self, backend):
279 def test_file_authors(self, backend):
280 response = self.app.get(url(
280 response = self.app.get(url(
281 controller='files', action='authors',
281 controller='files', action='authors',
282 repo_name=backend.repo_name,
282 repo_name=backend.repo_name,
283 revision='tip',
283 revision='tip',
284 f_path='vcs/nodes.py',
284 f_path='vcs/nodes.py',
285 annotate=True))
285 annotate=True))
286
286
287 expected_authors = {
287 expected_authors = {
288 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
288 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
289 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
289 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
290 'svn': ('marcin', 'lukasz'),
290 'svn': ('marcin', 'lukasz'),
291 }
291 }
292
292
293 for author in expected_authors[backend.alias]:
293 for author in expected_authors[backend.alias]:
294 response.mustcontain(author)
294 response.mustcontain(author)
295
295
296 def test_tree_search_top_level(self, backend, xhr_header):
296 def test_tree_search_top_level(self, backend, xhr_header):
297 commit = backend.repo.get_commit(commit_idx=173)
297 commit = backend.repo.get_commit(commit_idx=173)
298 response = self.app.get(
298 response = self.app.get(
299 url('files_nodelist_home', repo_name=backend.repo_name,
299 url('files_nodelist_home', repo_name=backend.repo_name,
300 revision=commit.raw_id, f_path='/'),
300 revision=commit.raw_id, f_path='/'),
301 extra_environ=xhr_header)
301 extra_environ=xhr_header)
302 assert 'nodes' in response.json
302 assert 'nodes' in response.json
303 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
303 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
304
304
305 def test_tree_search_at_path(self, backend, xhr_header):
305 def test_tree_search_at_path(self, backend, xhr_header):
306 commit = backend.repo.get_commit(commit_idx=173)
306 commit = backend.repo.get_commit(commit_idx=173)
307 response = self.app.get(
307 response = self.app.get(
308 url('files_nodelist_home', repo_name=backend.repo_name,
308 url('files_nodelist_home', repo_name=backend.repo_name,
309 revision=commit.raw_id, f_path='/docs'),
309 revision=commit.raw_id, f_path='/docs'),
310 extra_environ=xhr_header)
310 extra_environ=xhr_header)
311 assert 'nodes' in response.json
311 assert 'nodes' in response.json
312 nodes = response.json['nodes']
312 nodes = response.json['nodes']
313 assert {'name': 'docs/api', 'type': 'dir'} in nodes
313 assert {'name': 'docs/api', 'type': 'dir'} in nodes
314 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
314 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
315
315
316 def test_tree_search_at_path_missing_xhr(self, backend):
316 def test_tree_search_at_path_missing_xhr(self, backend):
317 self.app.get(
317 self.app.get(
318 url('files_nodelist_home', repo_name=backend.repo_name,
318 url('files_nodelist_home', repo_name=backend.repo_name,
319 revision='tip', f_path=''), status=400)
319 revision='tip', f_path=''), status=400)
320
320
321 def test_tree_view_list(self, backend, xhr_header):
321 def test_tree_view_list(self, backend, xhr_header):
322 commit = backend.repo.get_commit(commit_idx=173)
322 commit = backend.repo.get_commit(commit_idx=173)
323 response = self.app.get(
323 response = self.app.get(
324 url('files_nodelist_home', repo_name=backend.repo_name,
324 url('files_nodelist_home', repo_name=backend.repo_name,
325 f_path='/', revision=commit.raw_id),
325 f_path='/', revision=commit.raw_id),
326 extra_environ=xhr_header,
326 extra_environ=xhr_header,
327 )
327 )
328 response.mustcontain("vcs/web/simplevcs/views/repository.py")
328 response.mustcontain("vcs/web/simplevcs/views/repository.py")
329
329
330 def test_tree_view_list_at_path(self, backend, xhr_header):
330 def test_tree_view_list_at_path(self, backend, xhr_header):
331 commit = backend.repo.get_commit(commit_idx=173)
331 commit = backend.repo.get_commit(commit_idx=173)
332 response = self.app.get(
332 response = self.app.get(
333 url('files_nodelist_home', repo_name=backend.repo_name,
333 url('files_nodelist_home', repo_name=backend.repo_name,
334 f_path='/docs', revision=commit.raw_id),
334 f_path='/docs', revision=commit.raw_id),
335 extra_environ=xhr_header,
335 extra_environ=xhr_header,
336 )
336 )
337 response.mustcontain("docs/index.rst")
337 response.mustcontain("docs/index.rst")
338
338
339 def test_tree_view_list_missing_xhr(self, backend):
339 def test_tree_view_list_missing_xhr(self, backend):
340 self.app.get(
340 self.app.get(
341 url('files_nodelist_home', repo_name=backend.repo_name,
341 url('files_nodelist_home', repo_name=backend.repo_name,
342 f_path='/', revision='tip'), status=400)
342 f_path='/', revision='tip'), status=400)
343
343
344 def test_nodetree_full_success(self, backend, xhr_header):
344 def test_nodetree_full_success(self, backend, xhr_header):
345 commit = backend.repo.get_commit(commit_idx=173)
345 commit = backend.repo.get_commit(commit_idx=173)
346 response = self.app.get(
346 response = self.app.get(
347 url('files_nodetree_full', repo_name=backend.repo_name,
347 url('files_nodetree_full', repo_name=backend.repo_name,
348 f_path='/', commit_id=commit.raw_id),
348 f_path='/', commit_id=commit.raw_id),
349 extra_environ=xhr_header)
349 extra_environ=xhr_header)
350
350
351 assert_response = AssertResponse(response)
351 assert_response = AssertResponse(response)
352
352
353 for attr in ['data-commit-id', 'data-date', 'data-author']:
353 for attr in ['data-commit-id', 'data-date', 'data-author']:
354 elements = assert_response.get_elements('[{}]'.format(attr))
354 elements = assert_response.get_elements('[{}]'.format(attr))
355 assert len(elements) > 1
355 assert len(elements) > 1
356
356
357 for element in elements:
357 for element in elements:
358 assert element.get(attr)
358 assert element.get(attr)
359
359
360 def test_nodetree_full_if_file(self, backend, xhr_header):
360 def test_nodetree_full_if_file(self, backend, xhr_header):
361 commit = backend.repo.get_commit(commit_idx=173)
361 commit = backend.repo.get_commit(commit_idx=173)
362 response = self.app.get(
362 response = self.app.get(
363 url('files_nodetree_full', repo_name=backend.repo_name,
363 url('files_nodetree_full', repo_name=backend.repo_name,
364 f_path='README.rst', commit_id=commit.raw_id),
364 f_path='README.rst', commit_id=commit.raw_id),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 assert response.body == ''
366 assert response.body == ''
367
367
368 def test_tree_metadata_list_missing_xhr(self, backend):
368 def test_tree_metadata_list_missing_xhr(self, backend):
369 self.app.get(
369 self.app.get(
370 url('files_nodetree_full', repo_name=backend.repo_name,
370 url('files_nodetree_full', repo_name=backend.repo_name,
371 f_path='/', commit_id='tip'), status=400)
371 f_path='/', commit_id='tip'), status=400)
372
372
373 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
373 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
374 self, app, backend_stub, autologin_regular_user, user_regular,
374 self, app, backend_stub, autologin_regular_user, user_regular,
375 user_util):
375 user_util):
376 repo = backend_stub.create_repo()
376 repo = backend_stub.create_repo()
377 user_util.grant_user_permission_to_repo(
377 user_util.grant_user_permission_to_repo(
378 repo, user_regular, 'repository.write')
378 repo, user_regular, 'repository.write')
379 response = self.app.get(url(
379 response = self.app.get(url(
380 controller='files', action='index',
380 controller='files', action='index',
381 repo_name=repo.repo_name, revision='tip', f_path='/'))
381 repo_name=repo.repo_name, revision='tip', f_path='/'))
382 assert_session_flash(
382 assert_session_flash(
383 response,
383 response,
384 'There are no files yet. <a class="alert-link" '
384 'There are no files yet. <a class="alert-link" '
385 'href="/%s/add/0/#edit">Click here to add a new file.</a>'
385 'href="/%s/add/0/#edit">Click here to add a new file.</a>'
386 % (repo.repo_name))
386 % (repo.repo_name))
387
387
388 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
388 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
389 self, backend_stub, user_util):
389 self, backend_stub, user_util):
390 repo = backend_stub.create_repo()
390 repo = backend_stub.create_repo()
391 repo_file_url = url(
391 repo_file_url = url(
392 'files_add_home',
392 'files_add_home',
393 repo_name=repo.repo_name,
393 repo_name=repo.repo_name,
394 revision=0, f_path='', anchor='edit')
394 revision=0, f_path='', anchor='edit')
395 response = self.app.get(url(
395 response = self.app.get(url(
396 controller='files', action='index',
396 controller='files', action='index',
397 repo_name=repo.repo_name, revision='tip', f_path='/'))
397 repo_name=repo.repo_name, revision='tip', f_path='/'))
398 assert_not_in_session_flash(response, repo_file_url)
398 assert_not_in_session_flash(response, repo_file_url)
399
399
400
400
401 # TODO: johbo: Think about a better place for these tests. Either controller
401 # TODO: johbo: Think about a better place for these tests. Either controller
402 # specific unit tests or we move down the whole logic further towards the vcs
402 # specific unit tests or we move down the whole logic further towards the vcs
403 # layer
403 # layer
404 class TestAdjustFilePathForSvn:
404 class TestAdjustFilePathForSvn:
405 """SVN specific adjustments of node history in FileController."""
405 """SVN specific adjustments of node history in FileController."""
406
406
407 def test_returns_path_relative_to_matched_reference(self):
407 def test_returns_path_relative_to_matched_reference(self):
408 repo = self._repo(branches=['trunk'])
408 repo = self._repo(branches=['trunk'])
409 self.assert_file_adjustment('trunk/file', 'file', repo)
409 self.assert_file_adjustment('trunk/file', 'file', repo)
410
410
411 def test_does_not_modify_file_if_no_reference_matches(self):
411 def test_does_not_modify_file_if_no_reference_matches(self):
412 repo = self._repo(branches=['trunk'])
412 repo = self._repo(branches=['trunk'])
413 self.assert_file_adjustment('notes/file', 'notes/file', repo)
413 self.assert_file_adjustment('notes/file', 'notes/file', repo)
414
414
415 def test_does_not_adjust_partial_directory_names(self):
415 def test_does_not_adjust_partial_directory_names(self):
416 repo = self._repo(branches=['trun'])
416 repo = self._repo(branches=['trun'])
417 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
417 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
418
418
419 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
419 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
420 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
420 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
421 self.assert_file_adjustment('trunk/new/file', 'file', repo)
421 self.assert_file_adjustment('trunk/new/file', 'file', repo)
422
422
423 def assert_file_adjustment(self, f_path, expected, repo):
423 def assert_file_adjustment(self, f_path, expected, repo):
424 controller = FilesController()
424 controller = FilesController()
425 result = controller._adjust_file_path_for_svn(f_path, repo)
425 result = controller._adjust_file_path_for_svn(f_path, repo)
426 assert result == expected
426 assert result == expected
427
427
428 def _repo(self, branches=None):
428 def _repo(self, branches=None):
429 repo = mock.Mock()
429 repo = mock.Mock()
430 repo.branches = OrderedDict((name, '0') for name in branches or [])
430 repo.branches = OrderedDict((name, '0') for name in branches or [])
431 repo.tags = {}
431 repo.tags = {}
432 return repo
432 return repo
433
433
434
434
435 @pytest.mark.usefixtures("app")
435 @pytest.mark.usefixtures("app")
436 class TestRepositoryArchival:
436 class TestRepositoryArchival:
437
437
438 def test_archival(self, backend):
438 def test_archival(self, backend):
439 backend.enable_downloads()
439 backend.enable_downloads()
440 commit = backend.repo.get_commit(commit_idx=173)
440 commit = backend.repo.get_commit(commit_idx=173)
441 for archive, info in settings.ARCHIVE_SPECS.items():
441 for archive, info in settings.ARCHIVE_SPECS.items():
442 mime_type, arch_ext = info
442 mime_type, arch_ext = info
443 short = commit.short_id + arch_ext
443 short = commit.short_id + arch_ext
444 fname = commit.raw_id + arch_ext
444 fname = commit.raw_id + arch_ext
445 filename = '%s-%s' % (backend.repo_name, short)
445 filename = '%s-%s' % (backend.repo_name, short)
446 response = self.app.get(url(controller='files',
446 response = self.app.get(url(controller='files',
447 action='archivefile',
447 action='archivefile',
448 repo_name=backend.repo_name,
448 repo_name=backend.repo_name,
449 fname=fname))
449 fname=fname))
450
450
451 assert response.status == '200 OK'
451 assert response.status == '200 OK'
452 headers = {
452 headers = {
453 'Pragma': 'no-cache',
453 'Pragma': 'no-cache',
454 'Cache-Control': 'no-cache',
454 'Cache-Control': 'no-cache',
455 'Content-Disposition': 'attachment; filename=%s' % filename,
455 'Content-Disposition': 'attachment; filename=%s' % filename,
456 'Content-Type': '%s; charset=utf-8' % mime_type,
456 'Content-Type': '%s; charset=utf-8' % mime_type,
457 }
457 }
458 if 'Set-Cookie' in response.response.headers:
458 if 'Set-Cookie' in response.response.headers:
459 del response.response.headers['Set-Cookie']
459 del response.response.headers['Set-Cookie']
460 assert response.response.headers == headers
460 assert response.response.headers == headers
461
461
462 def test_archival_wrong_ext(self, backend):
462 def test_archival_wrong_ext(self, backend):
463 backend.enable_downloads()
463 backend.enable_downloads()
464 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
465 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
465 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
466 fname = commit.raw_id + arch_ext
466 fname = commit.raw_id + arch_ext
467
467
468 response = self.app.get(url(controller='files',
468 response = self.app.get(url(controller='files',
469 action='archivefile',
469 action='archivefile',
470 repo_name=backend.repo_name,
470 repo_name=backend.repo_name,
471 fname=fname))
471 fname=fname))
472 response.mustcontain('Unknown archive type')
472 response.mustcontain('Unknown archive type')
473
473
474 def test_archival_wrong_commit_id(self, backend):
474 def test_archival_wrong_commit_id(self, backend):
475 backend.enable_downloads()
475 backend.enable_downloads()
476 for commit_id in ['00x000000', 'tar', 'wrong', '@##$@$42413232',
476 for commit_id in ['00x000000', 'tar', 'wrong', '@##$@$42413232',
477 '232dffcd']:
477 '232dffcd']:
478 fname = '%s.zip' % commit_id
478 fname = '%s.zip' % commit_id
479
479
480 response = self.app.get(url(controller='files',
480 response = self.app.get(url(controller='files',
481 action='archivefile',
481 action='archivefile',
482 repo_name=backend.repo_name,
482 repo_name=backend.repo_name,
483 fname=fname))
483 fname=fname))
484 response.mustcontain('Unknown revision')
484 response.mustcontain('Unknown revision')
485
485
486
486
487 @pytest.mark.usefixtures("app", "autologin_user")
487 @pytest.mark.usefixtures("app", "autologin_user")
488 class TestRawFileHandling:
488 class TestRawFileHandling:
489
489
490 def test_raw_file_ok(self, backend):
490 def test_raw_file_ok(self, backend):
491 commit = backend.repo.get_commit(commit_idx=173)
491 commit = backend.repo.get_commit(commit_idx=173)
492 response = self.app.get(url(controller='files', action='rawfile',
492 response = self.app.get(url(controller='files', action='rawfile',
493 repo_name=backend.repo_name,
493 repo_name=backend.repo_name,
494 revision=commit.raw_id,
494 revision=commit.raw_id,
495 f_path='vcs/nodes.py'))
495 f_path='vcs/nodes.py'))
496
496
497 assert response.content_disposition == "attachment; filename=nodes.py"
497 assert response.content_disposition == "attachment; filename=nodes.py"
498 assert response.content_type == "text/x-python"
498 assert response.content_type == "text/x-python"
499
499
500 def test_raw_file_wrong_cs(self, backend):
500 def test_raw_file_wrong_cs(self, backend):
501 commit_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
501 commit_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
502 f_path = 'vcs/nodes.py'
502 f_path = 'vcs/nodes.py'
503
503
504 response = self.app.get(url(controller='files', action='rawfile',
504 response = self.app.get(url(controller='files', action='rawfile',
505 repo_name=backend.repo_name,
505 repo_name=backend.repo_name,
506 revision=commit_id,
506 revision=commit_id,
507 f_path=f_path), status=404)
507 f_path=f_path), status=404)
508
508
509 msg = """No such commit exists for this repository"""
509 msg = """No such commit exists for this repository"""
510 response.mustcontain(msg)
510 response.mustcontain(msg)
511
511
512 def test_raw_file_wrong_f_path(self, backend):
512 def test_raw_file_wrong_f_path(self, backend):
513 commit = backend.repo.get_commit(commit_idx=173)
513 commit = backend.repo.get_commit(commit_idx=173)
514 f_path = 'vcs/ERRORnodes.py'
514 f_path = 'vcs/ERRORnodes.py'
515 response = self.app.get(url(controller='files', action='rawfile',
515 response = self.app.get(url(controller='files', action='rawfile',
516 repo_name=backend.repo_name,
516 repo_name=backend.repo_name,
517 revision=commit.raw_id,
517 revision=commit.raw_id,
518 f_path=f_path), status=404)
518 f_path=f_path), status=404)
519
519
520 msg = (
520 msg = (
521 "There is no file nor directory at the given path: "
521 "There is no file nor directory at the given path: "
522 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
522 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
523 response.mustcontain(msg)
523 response.mustcontain(msg)
524
524
525 def test_raw_ok(self, backend):
525 def test_raw_ok(self, backend):
526 commit = backend.repo.get_commit(commit_idx=173)
526 commit = backend.repo.get_commit(commit_idx=173)
527 response = self.app.get(url(controller='files', action='raw',
527 response = self.app.get(url(controller='files', action='raw',
528 repo_name=backend.repo_name,
528 repo_name=backend.repo_name,
529 revision=commit.raw_id,
529 revision=commit.raw_id,
530 f_path='vcs/nodes.py'))
530 f_path='vcs/nodes.py'))
531
531
532 assert response.content_type == "text/plain"
532 assert response.content_type == "text/plain"
533
533
534 def test_raw_wrong_cs(self, backend):
534 def test_raw_wrong_cs(self, backend):
535 commit_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
535 commit_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
536 f_path = 'vcs/nodes.py'
536 f_path = 'vcs/nodes.py'
537
537
538 response = self.app.get(url(controller='files', action='raw',
538 response = self.app.get(url(controller='files', action='raw',
539 repo_name=backend.repo_name,
539 repo_name=backend.repo_name,
540 revision=commit_id,
540 revision=commit_id,
541 f_path=f_path), status=404)
541 f_path=f_path), status=404)
542
542
543 msg = """No such commit exists for this repository"""
543 msg = """No such commit exists for this repository"""
544 response.mustcontain(msg)
544 response.mustcontain(msg)
545
545
546 def test_raw_wrong_f_path(self, backend):
546 def test_raw_wrong_f_path(self, backend):
547 commit = backend.repo.get_commit(commit_idx=173)
547 commit = backend.repo.get_commit(commit_idx=173)
548 f_path = 'vcs/ERRORnodes.py'
548 f_path = 'vcs/ERRORnodes.py'
549 response = self.app.get(url(controller='files', action='raw',
549 response = self.app.get(url(controller='files', action='raw',
550 repo_name=backend.repo_name,
550 repo_name=backend.repo_name,
551 revision=commit.raw_id,
551 revision=commit.raw_id,
552 f_path=f_path), status=404)
552 f_path=f_path), status=404)
553 msg = (
553 msg = (
554 "There is no file nor directory at the given path: "
554 "There is no file nor directory at the given path: "
555 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
555 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
556 response.mustcontain(msg)
556 response.mustcontain(msg)
557
557
558 def test_raw_svg_should_not_be_rendered(self, backend):
558 def test_raw_svg_should_not_be_rendered(self, backend):
559 backend.create_repo()
559 backend.create_repo()
560 backend.ensure_file("xss.svg")
560 backend.ensure_file("xss.svg")
561 response = self.app.get(url(controller='files', action='raw',
561 response = self.app.get(url(controller='files', action='raw',
562 repo_name=backend.repo_name,
562 repo_name=backend.repo_name,
563 revision='tip',
563 revision='tip',
564 f_path='xss.svg'))
564 f_path='xss.svg'))
565
565
566 # If the content type is image/svg+xml then it allows to render HTML
566 # If the content type is image/svg+xml then it allows to render HTML
567 # and malicious SVG.
567 # and malicious SVG.
568 assert response.content_type == "text/plain"
568 assert response.content_type == "text/plain"
569
569
570
570
571 @pytest.mark.usefixtures("app")
571 @pytest.mark.usefixtures("app")
572 class TestFilesDiff:
572 class TestFilesDiff:
573
573
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
575 def test_file_full_diff(self, backend, diff):
575 def test_file_full_diff(self, backend, diff):
576 commit1 = backend.repo.get_commit(commit_idx=-1)
576 commit1 = backend.repo.get_commit(commit_idx=-1)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
578 response = self.app.get(
578 response = self.app.get(
579 url(
579 url(
580 controller='files',
580 controller='files',
581 action='diff',
581 action='diff',
582 repo_name=backend.repo_name,
582 repo_name=backend.repo_name,
583 f_path='README'),
583 f_path='README'),
584 params={
584 params={
585 'diff1': commit1.raw_id,
585 'diff1': commit1.raw_id,
586 'diff2': commit2.raw_id,
586 'diff2': commit2.raw_id,
587 'fulldiff': '1',
587 'fulldiff': '1',
588 'diff': diff,
588 'diff': diff,
589 })
589 })
590 response.mustcontain('README.rst')
590 response.mustcontain('README.rst')
591 response.mustcontain('No newline at end of file')
591 response.mustcontain('No newline at end of file')
592
592
593 def test_file_binary_diff(self, backend):
593 def test_file_binary_diff(self, backend):
594 commits = [
594 commits = [
595 {'message': 'First commit'},
595 {'message': 'First commit'},
596 {'message': 'Commit with binary',
596 {'message': 'Commit with binary',
597 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
597 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
598 ]
598 ]
599 repo = backend.create_repo(commits=commits)
599 repo = backend.create_repo(commits=commits)
600
600
601 response = self.app.get(
601 response = self.app.get(
602 url(
602 url(
603 controller='files',
603 controller='files',
604 action='diff',
604 action='diff',
605 repo_name=backend.repo_name,
605 repo_name=backend.repo_name,
606 f_path='file.bin'),
606 f_path='file.bin'),
607 params={
607 params={
608 'diff1': repo.get_commit(commit_idx=0).raw_id,
608 'diff1': repo.get_commit(commit_idx=0).raw_id,
609 'diff2': repo.get_commit(commit_idx=1).raw_id,
609 'diff2': repo.get_commit(commit_idx=1).raw_id,
610 'fulldiff': '1',
610 'fulldiff': '1',
611 'diff': 'diff',
611 'diff': 'diff',
612 })
612 })
613 response.mustcontain('Cannot diff binary files')
613 response.mustcontain('Cannot diff binary files')
614
614
615 def test_diff_2way(self, backend):
615 def test_diff_2way(self, backend):
616 commit1 = backend.repo.get_commit(commit_idx=-1)
616 commit1 = backend.repo.get_commit(commit_idx=-1)
617 commit2 = backend.repo.get_commit(commit_idx=-2)
617 commit2 = backend.repo.get_commit(commit_idx=-2)
618 response = self.app.get(
618 response = self.app.get(
619 url(
619 url(
620 controller='files',
620 controller='files',
621 action='diff_2way',
621 action='diff_2way',
622 repo_name=backend.repo_name,
622 repo_name=backend.repo_name,
623 f_path='README'),
623 f_path='README'),
624 params={
624 params={
625 'diff1': commit1.raw_id,
625 'diff1': commit1.raw_id,
626 'diff2': commit2.raw_id,
626 'diff2': commit2.raw_id,
627 })
627 })
628
628
629 # Expecting links to both variants of the file. Links are used
629 # Expecting links to both variants of the file. Links are used
630 # to load the content dynamically.
630 # to load the content dynamically.
631 response.mustcontain('/%s/README' % commit1.raw_id)
631 response.mustcontain('/%s/README' % commit1.raw_id)
632 response.mustcontain('/%s/README' % commit2.raw_id)
632 response.mustcontain('/%s/README' % commit2.raw_id)
633
633
634 def test_requires_one_commit_id(self, backend, autologin_user):
634 def test_requires_one_commit_id(self, backend, autologin_user):
635 response = self.app.get(
635 response = self.app.get(
636 url(
636 url(
637 controller='files',
637 controller='files',
638 action='diff',
638 action='diff',
639 repo_name=backend.repo_name,
639 repo_name=backend.repo_name,
640 f_path='README.rst'),
640 f_path='README.rst'),
641 status=400)
641 status=400)
642 response.mustcontain(
642 response.mustcontain(
643 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
643 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
644
644
645 def test_returns_not_found_if_file_does_not_exist(self, vcsbackend):
645 def test_returns_not_found_if_file_does_not_exist(self, vcsbackend):
646 repo = vcsbackend.repo
646 repo = vcsbackend.repo
647 self.app.get(
647 self.app.get(
648 url(
648 url(
649 controller='files',
649 controller='files',
650 action='diff',
650 action='diff',
651 repo_name=repo.name,
651 repo_name=repo.name,
652 f_path='does-not-exist-in-any-commit',
652 f_path='does-not-exist-in-any-commit',
653 diff1=repo[0].raw_id,
653 diff1=repo[0].raw_id,
654 diff2=repo[1].raw_id),
654 diff2=repo[1].raw_id),
655 status=404)
655 status=404)
656
656
657 def test_returns_redirect_if_file_not_changed(self, backend):
657 def test_returns_redirect_if_file_not_changed(self, backend):
658 commit = backend.repo.get_commit(commit_idx=-1)
658 commit = backend.repo.get_commit(commit_idx=-1)
659 f_path= 'README'
659 f_path= 'README'
660 response = self.app.get(
660 response = self.app.get(
661 url(
661 url(
662 controller='files',
662 controller='files',
663 action='diff_2way',
663 action='diff_2way',
664 repo_name=backend.repo_name,
664 repo_name=backend.repo_name,
665 f_path=f_path,
665 f_path=f_path,
666 diff1=commit.raw_id,
666 diff1=commit.raw_id,
667 diff2=commit.raw_id,
667 diff2=commit.raw_id,
668 ),
668 ),
669 status=302
669 status=302
670 )
670 )
671 assert response.headers['Location'].endswith(f_path)
671 assert response.headers['Location'].endswith(f_path)
672 redirected = response.follow()
672 redirected = response.follow()
673 redirected.mustcontain('has not changed between')
673 redirected.mustcontain('has not changed between')
674
674
675 def test_supports_diff_to_different_path_svn(self, backend_svn):
675 def test_supports_diff_to_different_path_svn(self, backend_svn):
676 repo = backend_svn['svn-simple-layout'].scm_instance()
676 repo = backend_svn['svn-simple-layout'].scm_instance()
677 commit_id = repo[-1].raw_id
677 commit_id = repo[-1].raw_id
678 response = self.app.get(
678 response = self.app.get(
679 url(
679 url(
680 controller='files',
680 controller='files',
681 action='diff',
681 action='diff',
682 repo_name=repo.name,
682 repo_name=repo.name,
683 f_path='trunk/example.py',
683 f_path='trunk/example.py',
684 diff1='tags/v0.2/example.py@' + commit_id,
684 diff1='tags/v0.2/example.py@' + commit_id,
685 diff2=commit_id),
685 diff2=commit_id),
686 status=200)
686 status=200)
687 response.mustcontain(
687 response.mustcontain(
688 "Will print out a useful message on invocation.")
688 "Will print out a useful message on invocation.")
689
689
690 # Note: Expecting that we indicate the user what's being compared
690 # Note: Expecting that we indicate the user what's being compared
691 response.mustcontain("trunk/example.py")
691 response.mustcontain("trunk/example.py")
692 response.mustcontain("tags/v0.2/example.py")
692 response.mustcontain("tags/v0.2/example.py")
693
693
694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
695 repo = backend_svn['svn-simple-layout'].scm_instance()
695 repo = backend_svn['svn-simple-layout'].scm_instance()
696 commit_id = repo[-1].raw_id
696 commit_id = repo[-1].raw_id
697 response = self.app.get(
697 response = self.app.get(
698 url(
698 url(
699 controller='files',
699 controller='files',
700 action='diff',
700 action='diff',
701 repo_name=repo.name,
701 repo_name=repo.name,
702 f_path='trunk/example.py',
702 f_path='trunk/example.py',
703 diff1='branches/argparse/example.py@' + commit_id,
703 diff1='branches/argparse/example.py@' + commit_id,
704 diff2=commit_id),
704 diff2=commit_id),
705 params={'show_rev': 'Show at Revision'},
705 params={'show_rev': 'Show at Revision'},
706 status=302)
706 status=302)
707 assert response.headers['Location'].endswith(
707 assert response.headers['Location'].endswith(
708 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
708 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
709
709
710 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
710 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
711 repo = backend_svn['svn-simple-layout'].scm_instance()
711 repo = backend_svn['svn-simple-layout'].scm_instance()
712 commit_id = repo[-1].raw_id
712 commit_id = repo[-1].raw_id
713 response = self.app.get(
713 response = self.app.get(
714 url(
714 url(
715 controller='files',
715 controller='files',
716 action='diff',
716 action='diff',
717 repo_name=repo.name,
717 repo_name=repo.name,
718 f_path='trunk/example.py',
718 f_path='trunk/example.py',
719 diff1='branches/argparse/example.py@' + commit_id,
719 diff1='branches/argparse/example.py@' + commit_id,
720 diff2=commit_id),
720 diff2=commit_id),
721 params={
721 params={
722 'show_rev': 'Show at Revision',
722 'show_rev': 'Show at Revision',
723 'annotate': 'true',
723 'annotate': 'true',
724 },
724 },
725 status=302)
725 status=302)
726 assert response.headers['Location'].endswith(
726 assert response.headers['Location'].endswith(
727 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
727 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
728
728
729
729
730 @pytest.mark.usefixtures("app", "autologin_user")
730 @pytest.mark.usefixtures("app", "autologin_user")
731 class TestChangingFiles:
731 class TestChangingFiles:
732
732
733 def test_add_file_view(self, backend):
733 def test_add_file_view(self, backend):
734 self.app.get(url(
734 self.app.get(url(
735 'files_add_home',
735 'files_add_home',
736 repo_name=backend.repo_name,
736 repo_name=backend.repo_name,
737 revision='tip', f_path='/'))
737 revision='tip', f_path='/'))
738
738
739 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
739 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
740 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
740 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
741 repo = backend.create_repo()
741 repo = backend.create_repo()
742 filename = 'init.py'
742 filename = 'init.py'
743 response = self.app.post(
743 response = self.app.post(
744 url(
744 url(
745 'files_add',
745 'files_add',
746 repo_name=repo.repo_name,
746 repo_name=repo.repo_name,
747 revision='tip', f_path='/'),
747 revision='tip', f_path='/'),
748 params={
748 params={
749 'content': "",
749 'content': "",
750 'filename': filename,
750 'filename': filename,
751 'location': "",
751 'location': "",
752 'csrf_token': csrf_token,
752 'csrf_token': csrf_token,
753 },
753 },
754 status=302)
754 status=302)
755 assert_session_flash(
755 assert_session_flash(
756 response, 'Successfully committed to %s'
756 response, 'Successfully committed to %s'
757 % os.path.join(filename))
757 % os.path.join(filename))
758
758
759 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
759 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
760 response = self.app.post(
760 response = self.app.post(
761 url(
761 url(
762 'files_add',
762 'files_add',
763 repo_name=backend.repo_name,
763 repo_name=backend.repo_name,
764 revision='tip', f_path='/'),
764 revision='tip', f_path='/'),
765 params={
765 params={
766 'content': "foo",
766 'content': "foo",
767 'csrf_token': csrf_token,
767 'csrf_token': csrf_token,
768 },
768 },
769 status=302)
769 status=302)
770
770
771 assert_session_flash(response, 'No filename')
771 assert_session_flash(response, 'No filename')
772
772
773 def test_add_file_into_repo_errors_and_no_commits(
773 def test_add_file_into_repo_errors_and_no_commits(
774 self, backend, csrf_token):
774 self, backend, csrf_token):
775 repo = backend.create_repo()
775 repo = backend.create_repo()
776 # Create a file with no filename, it will display an error but
776 # Create a file with no filename, it will display an error but
777 # the repo has no commits yet
777 # the repo has no commits yet
778 response = self.app.post(
778 response = self.app.post(
779 url(
779 url(
780 'files_add',
780 'files_add',
781 repo_name=repo.repo_name,
781 repo_name=repo.repo_name,
782 revision='tip', f_path='/'),
782 revision='tip', f_path='/'),
783 params={
783 params={
784 'content': "foo",
784 'content': "foo",
785 'csrf_token': csrf_token,
785 'csrf_token': csrf_token,
786 },
786 },
787 status=302)
787 status=302)
788
788
789 assert_session_flash(response, 'No filename')
789 assert_session_flash(response, 'No filename')
790
790
791 # Not allowed, redirect to the summary
791 # Not allowed, redirect to the summary
792 redirected = response.follow()
792 redirected = response.follow()
793 summary_url = url('summary_home', repo_name=repo.repo_name)
793 summary_url = url('summary_home', repo_name=repo.repo_name)
794
794
795 # As there are no commits, displays the summary page with the error of
795 # As there are no commits, displays the summary page with the error of
796 # creating a file with no filename
796 # creating a file with no filename
797 assert redirected.req.path == summary_url
797 assert redirected.req.path == summary_url
798
798
799 @pytest.mark.parametrize("location, filename", [
799 @pytest.mark.parametrize("location, filename", [
800 ('/abs', 'foo'),
800 ('/abs', 'foo'),
801 ('../rel', 'foo'),
801 ('../rel', 'foo'),
802 ('file/../foo', 'foo'),
802 ('file/../foo', 'foo'),
803 ])
803 ])
804 def test_add_file_into_repo_bad_filenames(
804 def test_add_file_into_repo_bad_filenames(
805 self, location, filename, backend, csrf_token):
805 self, location, filename, backend, csrf_token):
806 response = self.app.post(
806 response = self.app.post(
807 url(
807 url(
808 'files_add',
808 'files_add',
809 repo_name=backend.repo_name,
809 repo_name=backend.repo_name,
810 revision='tip', f_path='/'),
810 revision='tip', f_path='/'),
811 params={
811 params={
812 'content': "foo",
812 'content': "foo",
813 'filename': filename,
813 'filename': filename,
814 'location': location,
814 'location': location,
815 'csrf_token': csrf_token,
815 'csrf_token': csrf_token,
816 },
816 },
817 status=302)
817 status=302)
818
818
819 assert_session_flash(
819 assert_session_flash(
820 response,
820 response,
821 'The location specified must be a relative path and must not '
821 'The location specified must be a relative path and must not '
822 'contain .. in the path')
822 'contain .. in the path')
823
823
824 @pytest.mark.parametrize("cnt, location, filename", [
824 @pytest.mark.parametrize("cnt, location, filename", [
825 (1, '', 'foo.txt'),
825 (1, '', 'foo.txt'),
826 (2, 'dir', 'foo.rst'),
826 (2, 'dir', 'foo.rst'),
827 (3, 'rel/dir', 'foo.bar'),
827 (3, 'rel/dir', 'foo.bar'),
828 ])
828 ])
829 def test_add_file_into_repo(self, cnt, location, filename, backend,
829 def test_add_file_into_repo(self, cnt, location, filename, backend,
830 csrf_token):
830 csrf_token):
831 repo = backend.create_repo()
831 repo = backend.create_repo()
832 response = self.app.post(
832 response = self.app.post(
833 url(
833 url(
834 'files_add',
834 'files_add',
835 repo_name=repo.repo_name,
835 repo_name=repo.repo_name,
836 revision='tip', f_path='/'),
836 revision='tip', f_path='/'),
837 params={
837 params={
838 'content': "foo",
838 'content': "foo",
839 'filename': filename,
839 'filename': filename,
840 'location': location,
840 'location': location,
841 'csrf_token': csrf_token,
841 'csrf_token': csrf_token,
842 },
842 },
843 status=302)
843 status=302)
844 assert_session_flash(
844 assert_session_flash(
845 response, 'Successfully committed to %s'
845 response, 'Successfully committed to %s'
846 % os.path.join(location, filename))
846 % os.path.join(location, filename))
847
847
848 def test_edit_file_view(self, backend):
848 def test_edit_file_view(self, backend):
849 response = self.app.get(
849 response = self.app.get(
850 url(
850 url(
851 'files_edit_home',
851 'files_edit_home',
852 repo_name=backend.repo_name,
852 repo_name=backend.repo_name,
853 revision=backend.default_head_id,
853 revision=backend.default_head_id,
854 f_path='vcs/nodes.py'),
854 f_path='vcs/nodes.py'),
855 status=200)
855 status=200)
856 response.mustcontain("Module holding everything related to vcs nodes.")
856 response.mustcontain("Module holding everything related to vcs nodes.")
857
857
858 def test_edit_file_view_not_on_branch(self, backend):
858 def test_edit_file_view_not_on_branch(self, backend):
859 repo = backend.create_repo()
859 repo = backend.create_repo()
860 backend.ensure_file("vcs/nodes.py")
860 backend.ensure_file("vcs/nodes.py")
861
861
862 response = self.app.get(
862 response = self.app.get(
863 url(
863 url(
864 'files_edit_home',
864 'files_edit_home',
865 repo_name=repo.repo_name,
865 repo_name=repo.repo_name,
866 revision='tip', f_path='vcs/nodes.py'),
866 revision='tip', f_path='vcs/nodes.py'),
867 status=302)
867 status=302)
868 assert_session_flash(
868 assert_session_flash(
869 response,
869 response,
870 'You can only edit files with revision being a valid branch')
870 'You can only edit files with revision being a valid branch')
871
871
872 def test_edit_file_view_commit_changes(self, backend, csrf_token):
872 def test_edit_file_view_commit_changes(self, backend, csrf_token):
873 repo = backend.create_repo()
873 repo = backend.create_repo()
874 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
874 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
875
875
876 response = self.app.post(
876 response = self.app.post(
877 url(
877 url(
878 'files_edit',
878 'files_edit',
879 repo_name=repo.repo_name,
879 repo_name=repo.repo_name,
880 revision=backend.default_head_id,
880 revision=backend.default_head_id,
881 f_path='vcs/nodes.py'),
881 f_path='vcs/nodes.py'),
882 params={
882 params={
883 'content': "print 'hello world'",
883 'content': "print 'hello world'",
884 'message': 'I committed',
884 'message': 'I committed',
885 'filename': "vcs/nodes.py",
885 'filename': "vcs/nodes.py",
886 'csrf_token': csrf_token,
886 'csrf_token': csrf_token,
887 },
887 },
888 status=302)
888 status=302)
889 assert_session_flash(
889 assert_session_flash(
890 response, 'Successfully committed to vcs/nodes.py')
890 response, 'Successfully committed to vcs/nodes.py')
891 tip = repo.get_commit(commit_idx=-1)
891 tip = repo.get_commit(commit_idx=-1)
892 assert tip.message == 'I committed'
892 assert tip.message == 'I committed'
893
893
894 def test_edit_file_view_commit_changes_default_message(self, backend,
894 def test_edit_file_view_commit_changes_default_message(self, backend,
895 csrf_token):
895 csrf_token):
896 repo = backend.create_repo()
896 repo = backend.create_repo()
897 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
897 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
898
898
899 commit_id = (
899 commit_id = (
900 backend.default_branch_name or
900 backend.default_branch_name or
901 backend.repo.scm_instance().commit_ids[-1])
901 backend.repo.scm_instance().commit_ids[-1])
902
902
903 response = self.app.post(
903 response = self.app.post(
904 url(
904 url(
905 'files_edit',
905 'files_edit',
906 repo_name=repo.repo_name,
906 repo_name=repo.repo_name,
907 revision=commit_id,
907 revision=commit_id,
908 f_path='vcs/nodes.py'),
908 f_path='vcs/nodes.py'),
909 params={
909 params={
910 'content': "print 'hello world'",
910 'content': "print 'hello world'",
911 'message': '',
911 'message': '',
912 'filename': "vcs/nodes.py",
912 'filename': "vcs/nodes.py",
913 'csrf_token': csrf_token,
913 'csrf_token': csrf_token,
914 },
914 },
915 status=302)
915 status=302)
916 assert_session_flash(
916 assert_session_flash(
917 response, 'Successfully committed to vcs/nodes.py')
917 response, 'Successfully committed to vcs/nodes.py')
918 tip = repo.get_commit(commit_idx=-1)
918 tip = repo.get_commit(commit_idx=-1)
919 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
919 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
920
920
921 def test_delete_file_view(self, backend):
921 def test_delete_file_view(self, backend):
922 self.app.get(url(
922 self.app.get(url(
923 'files_delete_home',
923 'files_delete_home',
924 repo_name=backend.repo_name,
924 repo_name=backend.repo_name,
925 revision='tip', f_path='vcs/nodes.py'))
925 revision='tip', f_path='vcs/nodes.py'))
926
926
927 def test_delete_file_view_not_on_branch(self, backend):
927 def test_delete_file_view_not_on_branch(self, backend):
928 repo = backend.create_repo()
928 repo = backend.create_repo()
929 backend.ensure_file('vcs/nodes.py')
929 backend.ensure_file('vcs/nodes.py')
930
930
931 response = self.app.get(
931 response = self.app.get(
932 url(
932 url(
933 'files_delete_home',
933 'files_delete_home',
934 repo_name=repo.repo_name,
934 repo_name=repo.repo_name,
935 revision='tip', f_path='vcs/nodes.py'),
935 revision='tip', f_path='vcs/nodes.py'),
936 status=302)
936 status=302)
937 assert_session_flash(
937 assert_session_flash(
938 response,
938 response,
939 'You can only delete files with revision being a valid branch')
939 'You can only delete files with revision being a valid branch')
940
940
941 def test_delete_file_view_commit_changes(self, backend, csrf_token):
941 def test_delete_file_view_commit_changes(self, backend, csrf_token):
942 repo = backend.create_repo()
942 repo = backend.create_repo()
943 backend.ensure_file("vcs/nodes.py")
943 backend.ensure_file("vcs/nodes.py")
944
944
945 response = self.app.post(
945 response = self.app.post(
946 url(
946 url(
947 'files_delete_home',
947 'files_delete_home',
948 repo_name=repo.repo_name,
948 repo_name=repo.repo_name,
949 revision=backend.default_head_id,
949 revision=backend.default_head_id,
950 f_path='vcs/nodes.py'),
950 f_path='vcs/nodes.py'),
951 params={
951 params={
952 'message': 'i commited',
952 'message': 'i commited',
953 'csrf_token': csrf_token,
953 'csrf_token': csrf_token,
954 },
954 },
955 status=302)
955 status=302)
956 assert_session_flash(
956 assert_session_flash(
957 response, 'Successfully deleted file vcs/nodes.py')
957 response, 'Successfully deleted file vcs/nodes.py')
958
958
959
959
960 def assert_files_in_response(response, files, params):
960 def assert_files_in_response(response, files, params):
961 template = (
961 template = (
962 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
962 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
963 _assert_items_in_response(response, files, template, params)
963 _assert_items_in_response(response, files, template, params)
964
964
965
965
966 def assert_dirs_in_response(response, dirs, params):
966 def assert_dirs_in_response(response, dirs, params):
967 template = (
967 template = (
968 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
968 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
969 _assert_items_in_response(response, dirs, template, params)
969 _assert_items_in_response(response, dirs, template, params)
970
970
971
971
972 def _assert_items_in_response(response, items, template, params):
972 def _assert_items_in_response(response, items, template, params):
973 for item in items:
973 for item in items:
974 item_params = {'name': item}
974 item_params = {'name': item}
975 item_params.update(params)
975 item_params.update(params)
976 response.mustcontain(template % item_params)
976 response.mustcontain(template % item_params)
977
977
978
978
979 def assert_timeago_in_response(response, items, params):
979 def assert_timeago_in_response(response, items, params):
980 for item in items:
980 for item in items:
981 response.mustcontain(h.age_component(params['date']))
981 response.mustcontain(h.age_component(params['date']))
982
982
983
983
984
984
985 @pytest.mark.usefixtures("autologin_user", "app")
985 @pytest.mark.usefixtures("autologin_user", "app")
986 class TestSideBySideDiff:
986 class TestSideBySideDiff:
987
987
988 def test_diff2way(self, app, backend, backend_stub):
988 def test_diff2way(self, app, backend, backend_stub):
989 f_path = 'content'
989 f_path = 'content'
990 commit1_content = 'content-25d7e49c18b159446c'
990 commit1_content = 'content-25d7e49c18b159446c'
991 commit2_content = 'content-603d6c72c46d953420'
991 commit2_content = 'content-603d6c72c46d953420'
992 repo = backend.create_repo()
992 repo = backend.create_repo()
993
993
994 commit1 = _commit_change(
994 commit1 = _commit_change(
995 repo.repo_name, filename=f_path, content=commit1_content,
995 repo.repo_name, filename=f_path, content=commit1_content,
996 message='A', vcs_type=backend.alias, parent=None, newfile=True)
996 message='A', vcs_type=backend.alias, parent=None, newfile=True)
997
997
998 commit2 = _commit_change(
998 commit2 = _commit_change(
999 repo.repo_name, filename=f_path, content=commit2_content,
999 repo.repo_name, filename=f_path, content=commit2_content,
1000 message='B, child of A', vcs_type=backend.alias, parent=commit1)
1000 message='B, child of A', vcs_type=backend.alias, parent=commit1)
1001
1001
1002 response = self.app.get(url(
1002 response = self.app.get(url(
1003 controller='files', action='diff_2way',
1003 controller='files', action='diff_2way',
1004 repo_name=repo.repo_name,
1004 repo_name=repo.repo_name,
1005 diff1=commit1.raw_id,
1005 diff1=commit1.raw_id,
1006 diff2=commit2.raw_id,
1006 diff2=commit2.raw_id,
1007 f_path=f_path))
1007 f_path=f_path))
1008
1008
1009 assert_response = AssertResponse(response)
1009 assert_response = AssertResponse(response)
1010 response.mustcontain(
1010 response.mustcontain(
1011 ('Side-by-side Diff r0:%s ... r1:%s') % ( commit1.short_id, commit2.short_id ))
1011 ('Side-by-side Diff r0:%s ... r1:%s') % ( commit1.short_id, commit2.short_id ))
1012 response.mustcontain('id="compare"')
1012 response.mustcontain('id="compare"')
1013 response.mustcontain((
1013 response.mustcontain((
1014 "var orig1_url = '/%s/raw/%s/%s';\n"
1014 "var orig1_url = '/%s/raw/%s/%s';\n"
1015 "var orig2_url = '/%s/raw/%s/%s';") %
1015 "var orig2_url = '/%s/raw/%s/%s';") %
1016 ( repo.repo_name, commit1.raw_id, f_path,
1016 ( repo.repo_name, commit1.raw_id, f_path,
1017 repo.repo_name, commit2.raw_id, f_path))
1017 repo.repo_name, commit2.raw_id, f_path))
1018
1018
1019
1019
1020 def test_diff2way_with_empty_file(self, app, backend, backend_stub):
1020 def test_diff2way_with_empty_file(self, app, backend, backend_stub):
1021 commits = [
1021 commits = [
1022 {'message': 'First commit'},
1022 {'message': 'First commit'},
1023 {'message': 'Commit with binary',
1023 {'message': 'Commit with binary',
1024 'added': [nodes.FileNode('file.empty', content='')]},
1024 'added': [nodes.FileNode('file.empty', content='')]},
1025 ]
1025 ]
1026 f_path='file.empty'
1026 f_path='file.empty'
1027 repo = backend.create_repo(commits=commits)
1027 repo = backend.create_repo(commits=commits)
1028 commit_id1 = repo.get_commit(commit_idx=0).raw_id
1028 commit_id1 = repo.get_commit(commit_idx=0).raw_id
1029 commit_id2 = repo.get_commit(commit_idx=1).raw_id
1029 commit_id2 = repo.get_commit(commit_idx=1).raw_id
1030
1030
1031 response = self.app.get(url(
1031 response = self.app.get(url(
1032 controller='files', action='diff_2way',
1032 controller='files', action='diff_2way',
1033 repo_name=repo.repo_name,
1033 repo_name=repo.repo_name,
1034 diff1=commit_id1,
1034 diff1=commit_id1,
1035 diff2=commit_id2,
1035 diff2=commit_id2,
1036 f_path=f_path))
1036 f_path=f_path))
1037
1037
1038 assert_response = AssertResponse(response)
1038 assert_response = AssertResponse(response)
1039 if backend.alias == 'svn':
1039 if backend.alias == 'svn':
1040 assert_session_flash( response,
1040 assert_session_flash( response,
1041 ('%(file_path)s has not changed') % { 'file_path': 'file.empty' })
1041 ('%(file_path)s has not changed') % { 'file_path': 'file.empty' })
1042 else:
1042 else:
1043 response.mustcontain(
1043 response.mustcontain(
1044 ('Side-by-side Diff r0:%s ... r1:%s') % ( repo.get_commit(commit_idx=0).short_id, repo.get_commit(commit_idx=1).short_id ))
1044 ('Side-by-side Diff r0:%s ... r1:%s') % ( repo.get_commit(commit_idx=0).short_id, repo.get_commit(commit_idx=1).short_id ))
1045 response.mustcontain('id="compare"')
1045 response.mustcontain('id="compare"')
1046 response.mustcontain((
1046 response.mustcontain((
1047 "var orig1_url = '/%s/raw/%s/%s';\n"
1047 "var orig1_url = '/%s/raw/%s/%s';\n"
1048 "var orig2_url = '/%s/raw/%s/%s';") %
1048 "var orig2_url = '/%s/raw/%s/%s';") %
1049 ( repo.repo_name, commit_id1, f_path,
1049 ( repo.repo_name, commit_id1, f_path,
1050 repo.repo_name, commit_id2, f_path))
1050 repo.repo_name, commit_id2, f_path))
1051
1051
1052
1052
1053 def test_empty_diff_2way_redirect_to_summary_with_alert(self, app, backend):
1053 def test_empty_diff_2way_redirect_to_summary_with_alert(self, app, backend):
1054 commit_id_range = {
1054 commit_id_range = {
1055 'hg': (
1055 'hg': (
1056 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
1056 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
1057 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
1057 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
1058 'git': (
1058 'git': (
1059 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
1059 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
1060 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
1060 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
1061 'svn': (
1061 'svn': (
1062 '335',
1062 '335',
1063 '337'),
1063 '337'),
1064 }
1064 }
1065 f_path = 'setup.py'
1065 f_path = 'setup.py'
1066
1066
1067 commit_ids = commit_id_range[backend.alias]
1067 commit_ids = commit_id_range[backend.alias]
1068
1068
1069 response = self.app.get(url(
1069 response = self.app.get(url(
1070 controller='files', action='diff_2way',
1070 controller='files', action='diff_2way',
1071 repo_name=backend.repo_name,
1071 repo_name=backend.repo_name,
1072 diff2=commit_ids[0],
1072 diff2=commit_ids[0],
1073 diff1=commit_ids[1],
1073 diff1=commit_ids[1],
1074 f_path=f_path))
1074 f_path=f_path))
1075
1075
1076 assert_response = AssertResponse(response)
1076 assert_response = AssertResponse(response)
1077 assert_session_flash( response,
1077 assert_session_flash( response,
1078 ('%(file_path)s has not changed') % { 'file_path': f_path })
1078 ('%(file_path)s has not changed') % { 'file_path': f_path })
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now