##// END OF EJS Templates
files: use ref names in the url, and make usage of default landing refs....
marcink -
r4372:7db766f9 stable
parent child Browse files
Show More
@@ -1,1070 +1,1070 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.compat import OrderedDict
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
32
32
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
36 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
37
37
38 fixture = Fixture()
38 fixture = Fixture()
39
39
40
40
41 def get_node_history(backend_type):
41 def get_node_history(backend_type):
42 return {
42 return {
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 }[backend_type]
46 }[backend_type]
47
47
48
48
49 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
50 import urllib
50 import urllib
51
51
52 base_url = {
52 base_url = {
53 'repo_summary': '/{repo_name}',
53 'repo_summary': '/{repo_name}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:default_commit': '/{repo_name}/files',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 }[name].format(**kwargs)
76 }[name].format(**kwargs)
77
77
78 if params:
78 if params:
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
80 return base_url
80 return base_url
81
81
82
82
83 def assert_files_in_response(response, files, params):
83 def assert_files_in_response(response, files, params):
84 template = (
84 template = (
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 _assert_items_in_response(response, files, template, params)
86 _assert_items_in_response(response, files, template, params)
87
87
88
88
89 def assert_dirs_in_response(response, dirs, params):
89 def assert_dirs_in_response(response, dirs, params):
90 template = (
90 template = (
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 _assert_items_in_response(response, dirs, template, params)
92 _assert_items_in_response(response, dirs, template, params)
93
93
94
94
95 def _assert_items_in_response(response, items, template, params):
95 def _assert_items_in_response(response, items, template, params):
96 for item in items:
96 for item in items:
97 item_params = {'name': item}
97 item_params = {'name': item}
98 item_params.update(params)
98 item_params.update(params)
99 response.mustcontain(template % item_params)
99 response.mustcontain(template % item_params)
100
100
101
101
102 def assert_timeago_in_response(response, items, params):
102 def assert_timeago_in_response(response, items, params):
103 for item in items:
103 for item in items:
104 response.mustcontain(h.age_component(params['date']))
104 response.mustcontain(h.age_component(params['date']))
105
105
106
106
107 @pytest.mark.usefixtures("app")
107 @pytest.mark.usefixtures("app")
108 class TestFilesViews(object):
108 class TestFilesViews(object):
109
109
110 def test_show_files(self, backend):
110 def test_show_files(self, backend):
111 response = self.app.get(
111 response = self.app.get(
112 route_path('repo_files',
112 route_path('repo_files',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
115 commit = backend.repo.get_commit()
115 commit = backend.repo.get_commit()
116
116
117 params = {
117 params = {
118 'repo_name': backend.repo_name,
118 'repo_name': backend.repo_name,
119 'commit_id': commit.raw_id,
119 'commit_id': commit.raw_id,
120 'date': commit.date
120 'date': commit.date
121 }
121 }
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 files = [
123 files = [
124 '.gitignore',
124 '.gitignore',
125 '.hgignore',
125 '.hgignore',
126 '.hgtags',
126 '.hgtags',
127 # TODO: missing in Git
127 # TODO: missing in Git
128 # '.travis.yml',
128 # '.travis.yml',
129 'MANIFEST.in',
129 'MANIFEST.in',
130 'README.rst',
130 'README.rst',
131 # TODO: File is missing in svn repository
131 # TODO: File is missing in svn repository
132 # 'run_test_and_report.sh',
132 # 'run_test_and_report.sh',
133 'setup.cfg',
133 'setup.cfg',
134 'setup.py',
134 'setup.py',
135 'test_and_report.sh',
135 'test_and_report.sh',
136 'tox.ini',
136 'tox.ini',
137 ]
137 ]
138 assert_files_in_response(response, files, params)
138 assert_files_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
140
140
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 repo = backend_hg['subrepos']
142 repo = backend_hg['subrepos']
143 response = self.app.get(
143 response = self.app.get(
144 route_path('repo_files',
144 route_path('repo_files',
145 repo_name=repo.repo_name,
145 repo_name=repo.repo_name,
146 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 assert_response.contains_one_link(
148 assert_response.contains_one_link(
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150
150
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 self, backend_hg):
152 self, backend_hg):
153 repo = backend_hg['subrepos']
153 repo = backend_hg['subrepos']
154 response = self.app.get(
154 response = self.app.get(
155 route_path('repo_files',
155 route_path('repo_files',
156 repo_name=repo.repo_name,
156 repo_name=repo.repo_name,
157 commit_id='tip', f_path='/'))
157 commit_id='tip', f_path='/'))
158 assert_response = response.assert_response()
158 assert_response = response.assert_response()
159 assert_response.contains_one_link(
159 assert_response.contains_one_link(
160 'subpaths-path @ 000000000000',
160 'subpaths-path @ 000000000000',
161 'http://sub-base.example.com/subpaths-path')
161 'http://sub-base.example.com/subpaths-path')
162
162
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 def test_files_menu(self, backend):
164 def test_files_menu(self, backend):
165 new_branch = "temp_branch_name"
165 new_branch = "temp_branch_name"
166 commits = [
166 commits = [
167 {'message': 'a'},
167 {'message': 'a'},
168 {'message': 'b', 'branch': new_branch}
168 {'message': 'b', 'branch': new_branch}
169 ]
169 ]
170 backend.create_repo(commits)
170 backend.create_repo(commits)
171 backend.repo.landing_rev = "branch:%s" % new_branch
171 backend.repo.landing_rev = "branch:%s" % new_branch
172 Session().commit()
172 Session().commit()
173
173
174 # get response based on tip and not new commit
174 # get response based on tip and not new commit
175 response = self.app.get(
175 response = self.app.get(
176 route_path('repo_files',
176 route_path('repo_files',
177 repo_name=backend.repo_name,
177 repo_name=backend.repo_name,
178 commit_id='tip', f_path='/'))
178 commit_id='tip', f_path='/'))
179
179
180 # make sure Files menu url is not tip but new commit
180 # make sure Files menu url is not tip but new commit
181 landing_rev = backend.repo.landing_ref_name
181 landing_rev = backend.repo.landing_ref_name
182 files_url = route_path('repo_files:default_path',
182 files_url = route_path('repo_files:default_path',
183 repo_name=backend.repo_name,
183 repo_name=backend.repo_name,
184 commit_id=landing_rev)
184 commit_id=landing_rev, params={'at': landing_rev})
185
185
186 assert landing_rev != 'tip'
186 assert landing_rev != 'tip'
187 response.mustcontain(
187 response.mustcontain(
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189
189
190 def test_show_files_commit(self, backend):
190 def test_show_files_commit(self, backend):
191 commit = backend.repo.get_commit(commit_idx=32)
191 commit = backend.repo.get_commit(commit_idx=32)
192
192
193 response = self.app.get(
193 response = self.app.get(
194 route_path('repo_files',
194 route_path('repo_files',
195 repo_name=backend.repo_name,
195 repo_name=backend.repo_name,
196 commit_id=commit.raw_id, f_path='/'))
196 commit_id=commit.raw_id, f_path='/'))
197
197
198 dirs = ['docs', 'tests']
198 dirs = ['docs', 'tests']
199 files = ['README.rst']
199 files = ['README.rst']
200 params = {
200 params = {
201 'repo_name': backend.repo_name,
201 'repo_name': backend.repo_name,
202 'commit_id': commit.raw_id,
202 'commit_id': commit.raw_id,
203 }
203 }
204 assert_dirs_in_response(response, dirs, params)
204 assert_dirs_in_response(response, dirs, params)
205 assert_files_in_response(response, files, params)
205 assert_files_in_response(response, files, params)
206
206
207 def test_show_files_different_branch(self, backend):
207 def test_show_files_different_branch(self, backend):
208 branches = dict(
208 branches = dict(
209 hg=(150, ['git']),
209 hg=(150, ['git']),
210 # TODO: Git test repository does not contain other branches
210 # TODO: Git test repository does not contain other branches
211 git=(633, ['master']),
211 git=(633, ['master']),
212 # TODO: Branch support in Subversion
212 # TODO: Branch support in Subversion
213 svn=(150, [])
213 svn=(150, [])
214 )
214 )
215 idx, branches = branches[backend.alias]
215 idx, branches = branches[backend.alias]
216 commit = backend.repo.get_commit(commit_idx=idx)
216 commit = backend.repo.get_commit(commit_idx=idx)
217 response = self.app.get(
217 response = self.app.get(
218 route_path('repo_files',
218 route_path('repo_files',
219 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
220 commit_id=commit.raw_id, f_path='/'))
220 commit_id=commit.raw_id, f_path='/'))
221
221
222 assert_response = response.assert_response()
222 assert_response = response.assert_response()
223 for branch in branches:
223 for branch in branches:
224 assert_response.element_contains('.tags .branchtag', branch)
224 assert_response.element_contains('.tags .branchtag', branch)
225
225
226 def test_show_files_paging(self, backend):
226 def test_show_files_paging(self, backend):
227 repo = backend.repo
227 repo = backend.repo
228 indexes = [73, 92, 109, 1, 0]
228 indexes = [73, 92, 109, 1, 0]
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 for rev in indexes]
230 for rev in indexes]
231
231
232 for idx in idx_map:
232 for idx in idx_map:
233 response = self.app.get(
233 response = self.app.get(
234 route_path('repo_files',
234 route_path('repo_files',
235 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
236 commit_id=idx[1], f_path='/'))
236 commit_id=idx[1], f_path='/'))
237
237
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239
239
240 def test_file_source(self, backend):
240 def test_file_source(self, backend):
241 commit = backend.repo.get_commit(commit_idx=167)
241 commit = backend.repo.get_commit(commit_idx=167)
242 response = self.app.get(
242 response = self.app.get(
243 route_path('repo_files',
243 route_path('repo_files',
244 repo_name=backend.repo_name,
244 repo_name=backend.repo_name,
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246
246
247 msgbox = """<div class="commit">%s</div>"""
247 msgbox = """<div class="commit">%s</div>"""
248 response.mustcontain(msgbox % (commit.message, ))
248 response.mustcontain(msgbox % (commit.message, ))
249
249
250 assert_response = response.assert_response()
250 assert_response = response.assert_response()
251 if commit.branch:
251 if commit.branch:
252 assert_response.element_contains(
252 assert_response.element_contains(
253 '.tags.tags-main .branchtag', commit.branch)
253 '.tags.tags-main .branchtag', commit.branch)
254 if commit.tags:
254 if commit.tags:
255 for tag in commit.tags:
255 for tag in commit.tags:
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257
257
258 def test_file_source_annotated(self, backend):
258 def test_file_source_annotated(self, backend):
259 response = self.app.get(
259 response = self.app.get(
260 route_path('repo_files:annotated',
260 route_path('repo_files:annotated',
261 repo_name=backend.repo_name,
261 repo_name=backend.repo_name,
262 commit_id='tip', f_path='vcs/nodes.py'))
262 commit_id='tip', f_path='vcs/nodes.py'))
263 expected_commits = {
263 expected_commits = {
264 'hg': 'r356',
264 'hg': 'r356',
265 'git': 'r345',
265 'git': 'r345',
266 'svn': 'r208',
266 'svn': 'r208',
267 }
267 }
268 response.mustcontain(expected_commits[backend.alias])
268 response.mustcontain(expected_commits[backend.alias])
269
269
270 def test_file_source_authors(self, backend):
270 def test_file_source_authors(self, backend):
271 response = self.app.get(
271 response = self.app.get(
272 route_path('repo_file_authors',
272 route_path('repo_file_authors',
273 repo_name=backend.repo_name,
273 repo_name=backend.repo_name,
274 commit_id='tip', f_path='vcs/nodes.py'))
274 commit_id='tip', f_path='vcs/nodes.py'))
275 expected_authors = {
275 expected_authors = {
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 'svn': ('marcin', 'lukasz'),
278 'svn': ('marcin', 'lukasz'),
279 }
279 }
280
280
281 for author in expected_authors[backend.alias]:
281 for author in expected_authors[backend.alias]:
282 response.mustcontain(author)
282 response.mustcontain(author)
283
283
284 def test_file_source_authors_with_annotation(self, backend):
284 def test_file_source_authors_with_annotation(self, backend):
285 response = self.app.get(
285 response = self.app.get(
286 route_path('repo_file_authors',
286 route_path('repo_file_authors',
287 repo_name=backend.repo_name,
287 repo_name=backend.repo_name,
288 commit_id='tip', f_path='vcs/nodes.py',
288 commit_id='tip', f_path='vcs/nodes.py',
289 params=dict(annotate=1)))
289 params=dict(annotate=1)))
290 expected_authors = {
290 expected_authors = {
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 'svn': ('marcin', 'lukasz'),
293 'svn': ('marcin', 'lukasz'),
294 }
294 }
295
295
296 for author in expected_authors[backend.alias]:
296 for author in expected_authors[backend.alias]:
297 response.mustcontain(author)
297 response.mustcontain(author)
298
298
299 def test_file_source_history(self, backend, xhr_header):
299 def test_file_source_history(self, backend, xhr_header):
300 response = self.app.get(
300 response = self.app.get(
301 route_path('repo_file_history',
301 route_path('repo_file_history',
302 repo_name=backend.repo_name,
302 repo_name=backend.repo_name,
303 commit_id='tip', f_path='vcs/nodes.py'),
303 commit_id='tip', f_path='vcs/nodes.py'),
304 extra_environ=xhr_header)
304 extra_environ=xhr_header)
305 assert get_node_history(backend.alias) == json.loads(response.body)
305 assert get_node_history(backend.alias) == json.loads(response.body)
306
306
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 simple_repo = backend_svn['svn-simple-layout']
308 simple_repo = backend_svn['svn-simple-layout']
309 response = self.app.get(
309 response = self.app.get(
310 route_path('repo_file_history',
310 route_path('repo_file_history',
311 repo_name=simple_repo.repo_name,
311 repo_name=simple_repo.repo_name,
312 commit_id='tip', f_path='trunk/example.py'),
312 commit_id='tip', f_path='trunk/example.py'),
313 extra_environ=xhr_header)
313 extra_environ=xhr_header)
314
314
315 expected_data = json.loads(
315 expected_data = json.loads(
316 fixture.load_resource('svn_node_history_branches.json'))
316 fixture.load_resource('svn_node_history_branches.json'))
317
317
318 assert expected_data == response.json
318 assert expected_data == response.json
319
319
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 response = self.app.get(
321 response = self.app.get(
322 route_path('repo_file_history',
322 route_path('repo_file_history',
323 repo_name=backend.repo_name,
323 repo_name=backend.repo_name,
324 commit_id='tip', f_path='vcs/nodes.py',
324 commit_id='tip', f_path='vcs/nodes.py',
325 params=dict(annotate=1)),
325 params=dict(annotate=1)),
326
326
327 extra_environ=xhr_header)
327 extra_environ=xhr_header)
328 assert get_node_history(backend.alias) == json.loads(response.body)
328 assert get_node_history(backend.alias) == json.loads(response.body)
329
329
330 def test_tree_search_top_level(self, backend, xhr_header):
330 def test_tree_search_top_level(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 route_path('repo_files_nodelist',
333 route_path('repo_files_nodelist',
334 repo_name=backend.repo_name,
334 repo_name=backend.repo_name,
335 commit_id=commit.raw_id, f_path='/'),
335 commit_id=commit.raw_id, f_path='/'),
336 extra_environ=xhr_header)
336 extra_environ=xhr_header)
337 assert 'nodes' in response.json
337 assert 'nodes' in response.json
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339
339
340 def test_tree_search_missing_xhr(self, backend):
340 def test_tree_search_missing_xhr(self, backend):
341 self.app.get(
341 self.app.get(
342 route_path('repo_files_nodelist',
342 route_path('repo_files_nodelist',
343 repo_name=backend.repo_name,
343 repo_name=backend.repo_name,
344 commit_id='tip', f_path='/'),
344 commit_id='tip', f_path='/'),
345 status=404)
345 status=404)
346
346
347 def test_tree_search_at_path(self, backend, xhr_header):
347 def test_tree_search_at_path(self, backend, xhr_header):
348 commit = backend.repo.get_commit(commit_idx=173)
348 commit = backend.repo.get_commit(commit_idx=173)
349 response = self.app.get(
349 response = self.app.get(
350 route_path('repo_files_nodelist',
350 route_path('repo_files_nodelist',
351 repo_name=backend.repo_name,
351 repo_name=backend.repo_name,
352 commit_id=commit.raw_id, f_path='/docs'),
352 commit_id=commit.raw_id, f_path='/docs'),
353 extra_environ=xhr_header)
353 extra_environ=xhr_header)
354 assert 'nodes' in response.json
354 assert 'nodes' in response.json
355 nodes = response.json['nodes']
355 nodes = response.json['nodes']
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358
358
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 commit = backend.repo.get_commit(commit_idx=173)
360 commit = backend.repo.get_commit(commit_idx=173)
361 response = self.app.get(
361 response = self.app.get(
362 route_path('repo_files_nodelist',
362 route_path('repo_files_nodelist',
363 repo_name=backend.repo_name,
363 repo_name=backend.repo_name,
364 commit_id=commit.raw_id, f_path='/docs/api'),
364 commit_id=commit.raw_id, f_path='/docs/api'),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 assert 'nodes' in response.json
366 assert 'nodes' in response.json
367 nodes = response.json['nodes']
367 nodes = response.json['nodes']
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369
369
370 def test_tree_search_at_path_missing_xhr(self, backend):
370 def test_tree_search_at_path_missing_xhr(self, backend):
371 self.app.get(
371 self.app.get(
372 route_path('repo_files_nodelist',
372 route_path('repo_files_nodelist',
373 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
374 commit_id='tip', f_path='/docs'),
374 commit_id='tip', f_path='/docs'),
375 status=404)
375 status=404)
376
376
377 def test_nodetree(self, backend, xhr_header):
377 def test_nodetree(self, backend, xhr_header):
378 commit = backend.repo.get_commit(commit_idx=173)
378 commit = backend.repo.get_commit(commit_idx=173)
379 response = self.app.get(
379 response = self.app.get(
380 route_path('repo_nodetree_full',
380 route_path('repo_nodetree_full',
381 repo_name=backend.repo_name,
381 repo_name=backend.repo_name,
382 commit_id=commit.raw_id, f_path='/'),
382 commit_id=commit.raw_id, f_path='/'),
383 extra_environ=xhr_header)
383 extra_environ=xhr_header)
384
384
385 assert_response = response.assert_response()
385 assert_response = response.assert_response()
386
386
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 elements = assert_response.get_elements('[{}]'.format(attr))
388 elements = assert_response.get_elements('[{}]'.format(attr))
389 assert len(elements) > 1
389 assert len(elements) > 1
390
390
391 for element in elements:
391 for element in elements:
392 assert element.get(attr)
392 assert element.get(attr)
393
393
394 def test_nodetree_if_file(self, backend, xhr_header):
394 def test_nodetree_if_file(self, backend, xhr_header):
395 commit = backend.repo.get_commit(commit_idx=173)
395 commit = backend.repo.get_commit(commit_idx=173)
396 response = self.app.get(
396 response = self.app.get(
397 route_path('repo_nodetree_full',
397 route_path('repo_nodetree_full',
398 repo_name=backend.repo_name,
398 repo_name=backend.repo_name,
399 commit_id=commit.raw_id, f_path='README.rst'),
399 commit_id=commit.raw_id, f_path='README.rst'),
400 extra_environ=xhr_header)
400 extra_environ=xhr_header)
401 assert response.body == ''
401 assert response.body == ''
402
402
403 def test_nodetree_wrong_path(self, backend, xhr_header):
403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 commit = backend.repo.get_commit(commit_idx=173)
404 commit = backend.repo.get_commit(commit_idx=173)
405 response = self.app.get(
405 response = self.app.get(
406 route_path('repo_nodetree_full',
406 route_path('repo_nodetree_full',
407 repo_name=backend.repo_name,
407 repo_name=backend.repo_name,
408 commit_id=commit.raw_id, f_path='/dont-exist'),
408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 extra_environ=xhr_header)
409 extra_environ=xhr_header)
410
410
411 err = 'error: There is no file nor ' \
411 err = 'error: There is no file nor ' \
412 'directory at the given path'
412 'directory at the given path'
413 assert err in response.body
413 assert err in response.body
414
414
415 def test_nodetree_missing_xhr(self, backend):
415 def test_nodetree_missing_xhr(self, backend):
416 self.app.get(
416 self.app.get(
417 route_path('repo_nodetree_full',
417 route_path('repo_nodetree_full',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id='tip', f_path='/'),
419 commit_id='tip', f_path='/'),
420 status=404)
420 status=404)
421
421
422
422
423 @pytest.mark.usefixtures("app", "autologin_user")
423 @pytest.mark.usefixtures("app", "autologin_user")
424 class TestRawFileHandling(object):
424 class TestRawFileHandling(object):
425
425
426 def test_download_file(self, backend):
426 def test_download_file(self, backend):
427 commit = backend.repo.get_commit(commit_idx=173)
427 commit = backend.repo.get_commit(commit_idx=173)
428 response = self.app.get(
428 response = self.app.get(
429 route_path('repo_file_download',
429 route_path('repo_file_download',
430 repo_name=backend.repo_name,
430 repo_name=backend.repo_name,
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432
432
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 assert response.content_type == "text/x-python"
434 assert response.content_type == "text/x-python"
435
435
436 def test_download_file_wrong_cs(self, backend):
436 def test_download_file_wrong_cs(self, backend):
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438
438
439 response = self.app.get(
439 response = self.app.get(
440 route_path('repo_file_download',
440 route_path('repo_file_download',
441 repo_name=backend.repo_name,
441 repo_name=backend.repo_name,
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 status=404)
443 status=404)
444
444
445 msg = """No such commit exists for this repository"""
445 msg = """No such commit exists for this repository"""
446 response.mustcontain(msg)
446 response.mustcontain(msg)
447
447
448 def test_download_file_wrong_f_path(self, backend):
448 def test_download_file_wrong_f_path(self, backend):
449 commit = backend.repo.get_commit(commit_idx=173)
449 commit = backend.repo.get_commit(commit_idx=173)
450 f_path = 'vcs/ERRORnodes.py'
450 f_path = 'vcs/ERRORnodes.py'
451
451
452 response = self.app.get(
452 response = self.app.get(
453 route_path('repo_file_download',
453 route_path('repo_file_download',
454 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
455 commit_id=commit.raw_id, f_path=f_path),
455 commit_id=commit.raw_id, f_path=f_path),
456 status=404)
456 status=404)
457
457
458 msg = (
458 msg = (
459 "There is no file nor directory at the given path: "
459 "There is no file nor directory at the given path: "
460 "`%s` at commit %s" % (f_path, commit.short_id))
460 "`%s` at commit %s" % (f_path, commit.short_id))
461 response.mustcontain(msg)
461 response.mustcontain(msg)
462
462
463 def test_file_raw(self, backend):
463 def test_file_raw(self, backend):
464 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
465 response = self.app.get(
465 response = self.app.get(
466 route_path('repo_file_raw',
466 route_path('repo_file_raw',
467 repo_name=backend.repo_name,
467 repo_name=backend.repo_name,
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469
469
470 assert response.content_type == "text/plain"
470 assert response.content_type == "text/plain"
471
471
472 def test_file_raw_binary(self, backend):
472 def test_file_raw_binary(self, backend):
473 commit = backend.repo.get_commit()
473 commit = backend.repo.get_commit()
474 response = self.app.get(
474 response = self.app.get(
475 route_path('repo_file_raw',
475 route_path('repo_file_raw',
476 repo_name=backend.repo_name,
476 repo_name=backend.repo_name,
477 commit_id=commit.raw_id,
477 commit_id=commit.raw_id,
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479
479
480 assert response.content_disposition == 'inline'
480 assert response.content_disposition == 'inline'
481
481
482 def test_raw_file_wrong_cs(self, backend):
482 def test_raw_file_wrong_cs(self, backend):
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484
484
485 response = self.app.get(
485 response = self.app.get(
486 route_path('repo_file_raw',
486 route_path('repo_file_raw',
487 repo_name=backend.repo_name,
487 repo_name=backend.repo_name,
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 status=404)
489 status=404)
490
490
491 msg = """No such commit exists for this repository"""
491 msg = """No such commit exists for this repository"""
492 response.mustcontain(msg)
492 response.mustcontain(msg)
493
493
494 def test_raw_wrong_f_path(self, backend):
494 def test_raw_wrong_f_path(self, backend):
495 commit = backend.repo.get_commit(commit_idx=173)
495 commit = backend.repo.get_commit(commit_idx=173)
496 f_path = 'vcs/ERRORnodes.py'
496 f_path = 'vcs/ERRORnodes.py'
497 response = self.app.get(
497 response = self.app.get(
498 route_path('repo_file_raw',
498 route_path('repo_file_raw',
499 repo_name=backend.repo_name,
499 repo_name=backend.repo_name,
500 commit_id=commit.raw_id, f_path=f_path),
500 commit_id=commit.raw_id, f_path=f_path),
501 status=404)
501 status=404)
502
502
503 msg = (
503 msg = (
504 "There is no file nor directory at the given path: "
504 "There is no file nor directory at the given path: "
505 "`%s` at commit %s" % (f_path, commit.short_id))
505 "`%s` at commit %s" % (f_path, commit.short_id))
506 response.mustcontain(msg)
506 response.mustcontain(msg)
507
507
508 def test_raw_svg_should_not_be_rendered(self, backend):
508 def test_raw_svg_should_not_be_rendered(self, backend):
509 backend.create_repo()
509 backend.create_repo()
510 backend.ensure_file("xss.svg")
510 backend.ensure_file("xss.svg")
511 response = self.app.get(
511 response = self.app.get(
512 route_path('repo_file_raw',
512 route_path('repo_file_raw',
513 repo_name=backend.repo_name,
513 repo_name=backend.repo_name,
514 commit_id='tip', f_path='xss.svg'),)
514 commit_id='tip', f_path='xss.svg'),)
515 # If the content type is image/svg+xml then it allows to render HTML
515 # If the content type is image/svg+xml then it allows to render HTML
516 # and malicious SVG.
516 # and malicious SVG.
517 assert response.content_type == "text/plain"
517 assert response.content_type == "text/plain"
518
518
519
519
520 @pytest.mark.usefixtures("app")
520 @pytest.mark.usefixtures("app")
521 class TestRepositoryArchival(object):
521 class TestRepositoryArchival(object):
522
522
523 def test_archival(self, backend):
523 def test_archival(self, backend):
524 backend.enable_downloads()
524 backend.enable_downloads()
525 commit = backend.repo.get_commit(commit_idx=173)
525 commit = backend.repo.get_commit(commit_idx=173)
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527
527
528 short = commit.short_id + extension
528 short = commit.short_id + extension
529 fname = commit.raw_id + extension
529 fname = commit.raw_id + extension
530 filename = '%s-%s' % (backend.repo_name, short)
530 filename = '%s-%s' % (backend.repo_name, short)
531 response = self.app.get(
531 response = self.app.get(
532 route_path('repo_archivefile',
532 route_path('repo_archivefile',
533 repo_name=backend.repo_name,
533 repo_name=backend.repo_name,
534 fname=fname))
534 fname=fname))
535
535
536 assert response.status == '200 OK'
536 assert response.status == '200 OK'
537 headers = [
537 headers = [
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 ('Content-Type', '%s' % content_type),
539 ('Content-Type', '%s' % content_type),
540 ]
540 ]
541
541
542 for header in headers:
542 for header in headers:
543 assert header in response.headers.items()
543 assert header in response.headers.items()
544
544
545 @pytest.mark.parametrize('arch_ext',[
545 @pytest.mark.parametrize('arch_ext',[
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
547 def test_archival_wrong_ext(self, backend, arch_ext):
547 def test_archival_wrong_ext(self, backend, arch_ext):
548 backend.enable_downloads()
548 backend.enable_downloads()
549 commit = backend.repo.get_commit(commit_idx=173)
549 commit = backend.repo.get_commit(commit_idx=173)
550
550
551 fname = commit.raw_id + '.' + arch_ext
551 fname = commit.raw_id + '.' + arch_ext
552
552
553 response = self.app.get(
553 response = self.app.get(
554 route_path('repo_archivefile',
554 route_path('repo_archivefile',
555 repo_name=backend.repo_name,
555 repo_name=backend.repo_name,
556 fname=fname))
556 fname=fname))
557 response.mustcontain(
557 response.mustcontain(
558 'Unknown archive type for: `{}`'.format(fname))
558 'Unknown archive type for: `{}`'.format(fname))
559
559
560 @pytest.mark.parametrize('commit_id', [
560 @pytest.mark.parametrize('commit_id', [
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
562 def test_archival_wrong_commit_id(self, backend, commit_id):
562 def test_archival_wrong_commit_id(self, backend, commit_id):
563 backend.enable_downloads()
563 backend.enable_downloads()
564 fname = '%s.zip' % commit_id
564 fname = '%s.zip' % commit_id
565
565
566 response = self.app.get(
566 response = self.app.get(
567 route_path('repo_archivefile',
567 route_path('repo_archivefile',
568 repo_name=backend.repo_name,
568 repo_name=backend.repo_name,
569 fname=fname))
569 fname=fname))
570 response.mustcontain('Unknown commit_id')
570 response.mustcontain('Unknown commit_id')
571
571
572
572
573 @pytest.mark.usefixtures("app")
573 @pytest.mark.usefixtures("app")
574 class TestFilesDiff(object):
574 class TestFilesDiff(object):
575
575
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
577 def test_file_full_diff(self, backend, diff):
577 def test_file_full_diff(self, backend, diff):
578 commit1 = backend.repo.get_commit(commit_idx=-1)
578 commit1 = backend.repo.get_commit(commit_idx=-1)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
580
580
581 response = self.app.get(
581 response = self.app.get(
582 route_path('repo_files_diff',
582 route_path('repo_files_diff',
583 repo_name=backend.repo_name,
583 repo_name=backend.repo_name,
584 f_path='README'),
584 f_path='README'),
585 params={
585 params={
586 'diff1': commit2.raw_id,
586 'diff1': commit2.raw_id,
587 'diff2': commit1.raw_id,
587 'diff2': commit1.raw_id,
588 'fulldiff': '1',
588 'fulldiff': '1',
589 'diff': diff,
589 'diff': diff,
590 })
590 })
591
591
592 if diff == 'diff':
592 if diff == 'diff':
593 # use redirect since this is OLD view redirecting to compare page
593 # use redirect since this is OLD view redirecting to compare page
594 response = response.follow()
594 response = response.follow()
595
595
596 # It's a symlink to README.rst
596 # It's a symlink to README.rst
597 response.mustcontain('README.rst')
597 response.mustcontain('README.rst')
598 response.mustcontain('No newline at end of file')
598 response.mustcontain('No newline at end of file')
599
599
600 def test_file_binary_diff(self, backend):
600 def test_file_binary_diff(self, backend):
601 commits = [
601 commits = [
602 {'message': 'First commit'},
602 {'message': 'First commit'},
603 {'message': 'Commit with binary',
603 {'message': 'Commit with binary',
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
605 ]
605 ]
606 repo = backend.create_repo(commits=commits)
606 repo = backend.create_repo(commits=commits)
607
607
608 response = self.app.get(
608 response = self.app.get(
609 route_path('repo_files_diff',
609 route_path('repo_files_diff',
610 repo_name=backend.repo_name,
610 repo_name=backend.repo_name,
611 f_path='file.bin'),
611 f_path='file.bin'),
612 params={
612 params={
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
615 'fulldiff': '1',
615 'fulldiff': '1',
616 'diff': 'diff',
616 'diff': 'diff',
617 })
617 })
618 # use redirect since this is OLD view redirecting to compare page
618 # use redirect since this is OLD view redirecting to compare page
619 response = response.follow()
619 response = response.follow()
620 response.mustcontain('Collapse 1 commit')
620 response.mustcontain('Collapse 1 commit')
621 file_changes = (1, 0, 0)
621 file_changes = (1, 0, 0)
622
622
623 compare_page = ComparePage(response)
623 compare_page = ComparePage(response)
624 compare_page.contains_change_summary(*file_changes)
624 compare_page.contains_change_summary(*file_changes)
625
625
626 if backend.alias == 'svn':
626 if backend.alias == 'svn':
627 response.mustcontain('new file 10644')
627 response.mustcontain('new file 10644')
628 # TODO(marcink): SVN doesn't yet detect binary changes
628 # TODO(marcink): SVN doesn't yet detect binary changes
629 else:
629 else:
630 response.mustcontain('new file 100644')
630 response.mustcontain('new file 100644')
631 response.mustcontain('binary diff hidden')
631 response.mustcontain('binary diff hidden')
632
632
633 def test_diff_2way(self, backend):
633 def test_diff_2way(self, backend):
634 commit1 = backend.repo.get_commit(commit_idx=-1)
634 commit1 = backend.repo.get_commit(commit_idx=-1)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
636 response = self.app.get(
636 response = self.app.get(
637 route_path('repo_files_diff_2way_redirect',
637 route_path('repo_files_diff_2way_redirect',
638 repo_name=backend.repo_name,
638 repo_name=backend.repo_name,
639 f_path='README'),
639 f_path='README'),
640 params={
640 params={
641 'diff1': commit2.raw_id,
641 'diff1': commit2.raw_id,
642 'diff2': commit1.raw_id,
642 'diff2': commit1.raw_id,
643 })
643 })
644 # use redirect since this is OLD view redirecting to compare page
644 # use redirect since this is OLD view redirecting to compare page
645 response = response.follow()
645 response = response.follow()
646
646
647 # It's a symlink to README.rst
647 # It's a symlink to README.rst
648 response.mustcontain('README.rst')
648 response.mustcontain('README.rst')
649 response.mustcontain('No newline at end of file')
649 response.mustcontain('No newline at end of file')
650
650
651 def test_requires_one_commit_id(self, backend, autologin_user):
651 def test_requires_one_commit_id(self, backend, autologin_user):
652 response = self.app.get(
652 response = self.app.get(
653 route_path('repo_files_diff',
653 route_path('repo_files_diff',
654 repo_name=backend.repo_name,
654 repo_name=backend.repo_name,
655 f_path='README.rst'),
655 f_path='README.rst'),
656 status=400)
656 status=400)
657 response.mustcontain(
657 response.mustcontain(
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
659
659
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
661 repo = vcsbackend.repo
661 repo = vcsbackend.repo
662 response = self.app.get(
662 response = self.app.get(
663 route_path('repo_files_diff',
663 route_path('repo_files_diff',
664 repo_name=repo.name,
664 repo_name=repo.name,
665 f_path='does-not-exist-in-any-commit'),
665 f_path='does-not-exist-in-any-commit'),
666 params={
666 params={
667 'diff1': repo[0].raw_id,
667 'diff1': repo[0].raw_id,
668 'diff2': repo[1].raw_id
668 'diff2': repo[1].raw_id
669 })
669 })
670
670
671 response = response.follow()
671 response = response.follow()
672 response.mustcontain('No files')
672 response.mustcontain('No files')
673
673
674 def test_returns_redirect_if_file_not_changed(self, backend):
674 def test_returns_redirect_if_file_not_changed(self, backend):
675 commit = backend.repo.get_commit(commit_idx=-1)
675 commit = backend.repo.get_commit(commit_idx=-1)
676 response = self.app.get(
676 response = self.app.get(
677 route_path('repo_files_diff_2way_redirect',
677 route_path('repo_files_diff_2way_redirect',
678 repo_name=backend.repo_name,
678 repo_name=backend.repo_name,
679 f_path='README'),
679 f_path='README'),
680 params={
680 params={
681 'diff1': commit.raw_id,
681 'diff1': commit.raw_id,
682 'diff2': commit.raw_id,
682 'diff2': commit.raw_id,
683 })
683 })
684
684
685 response = response.follow()
685 response = response.follow()
686 response.mustcontain('No files')
686 response.mustcontain('No files')
687 response.mustcontain('No commits in this compare')
687 response.mustcontain('No commits in this compare')
688
688
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
690 #TODO: check this case
690 #TODO: check this case
691 return
691 return
692
692
693 repo = backend_svn['svn-simple-layout'].scm_instance()
693 repo = backend_svn['svn-simple-layout'].scm_instance()
694 commit_id_1 = '24'
694 commit_id_1 = '24'
695 commit_id_2 = '26'
695 commit_id_2 = '26'
696
696
697 response = self.app.get(
697 response = self.app.get(
698 route_path('repo_files_diff',
698 route_path('repo_files_diff',
699 repo_name=backend_svn.repo_name,
699 repo_name=backend_svn.repo_name,
700 f_path='trunk/example.py'),
700 f_path='trunk/example.py'),
701 params={
701 params={
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
703 'diff2': commit_id_2,
703 'diff2': commit_id_2,
704 })
704 })
705
705
706 response = response.follow()
706 response = response.follow()
707 response.mustcontain(
707 response.mustcontain(
708 # diff contains this
708 # diff contains this
709 "Will print out a useful message on invocation.")
709 "Will print out a useful message on invocation.")
710
710
711 # Note: Expecting that we indicate the user what's being compared
711 # Note: Expecting that we indicate the user what's being compared
712 response.mustcontain("trunk/example.py")
712 response.mustcontain("trunk/example.py")
713 response.mustcontain("tags/v0.2/example.py")
713 response.mustcontain("tags/v0.2/example.py")
714
714
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
716 #TODO: check this case
716 #TODO: check this case
717 return
717 return
718
718
719 repo = backend_svn['svn-simple-layout'].scm_instance()
719 repo = backend_svn['svn-simple-layout'].scm_instance()
720 commit_id = repo[-1].raw_id
720 commit_id = repo[-1].raw_id
721
721
722 response = self.app.get(
722 response = self.app.get(
723 route_path('repo_files_diff',
723 route_path('repo_files_diff',
724 repo_name=backend_svn.repo_name,
724 repo_name=backend_svn.repo_name,
725 f_path='trunk/example.py'),
725 f_path='trunk/example.py'),
726 params={
726 params={
727 'diff1': 'branches/argparse/example.py@' + commit_id,
727 'diff1': 'branches/argparse/example.py@' + commit_id,
728 'diff2': commit_id,
728 'diff2': commit_id,
729 },
729 },
730 status=302)
730 status=302)
731 response = response.follow()
731 response = response.follow()
732 assert response.headers['Location'].endswith(
732 assert response.headers['Location'].endswith(
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
734
734
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
736 #TODO: check this case
736 #TODO: check this case
737 return
737 return
738
738
739 repo = backend_svn['svn-simple-layout'].scm_instance()
739 repo = backend_svn['svn-simple-layout'].scm_instance()
740 commit_id = repo[-1].raw_id
740 commit_id = repo[-1].raw_id
741 response = self.app.get(
741 response = self.app.get(
742 route_path('repo_files_diff',
742 route_path('repo_files_diff',
743 repo_name=backend_svn.repo_name,
743 repo_name=backend_svn.repo_name,
744 f_path='trunk/example.py'),
744 f_path='trunk/example.py'),
745 params={
745 params={
746 'diff1': 'branches/argparse/example.py@' + commit_id,
746 'diff1': 'branches/argparse/example.py@' + commit_id,
747 'diff2': commit_id,
747 'diff2': commit_id,
748 'show_rev': 'Show at Revision',
748 'show_rev': 'Show at Revision',
749 'annotate': 'true',
749 'annotate': 'true',
750 },
750 },
751 status=302)
751 status=302)
752 response = response.follow()
752 response = response.follow()
753 assert response.headers['Location'].endswith(
753 assert response.headers['Location'].endswith(
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
755
755
756
756
757 @pytest.mark.usefixtures("app", "autologin_user")
757 @pytest.mark.usefixtures("app", "autologin_user")
758 class TestModifyFilesWithWebInterface(object):
758 class TestModifyFilesWithWebInterface(object):
759
759
760 def test_add_file_view(self, backend):
760 def test_add_file_view(self, backend):
761 self.app.get(
761 self.app.get(
762 route_path('repo_files_add_file',
762 route_path('repo_files_add_file',
763 repo_name=backend.repo_name,
763 repo_name=backend.repo_name,
764 commit_id='tip', f_path='/')
764 commit_id='tip', f_path='/')
765 )
765 )
766
766
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
769 backend.create_repo()
769 backend.create_repo()
770 filename = 'init.py'
770 filename = 'init.py'
771 response = self.app.post(
771 response = self.app.post(
772 route_path('repo_files_create_file',
772 route_path('repo_files_create_file',
773 repo_name=backend.repo_name,
773 repo_name=backend.repo_name,
774 commit_id='tip', f_path='/'),
774 commit_id='tip', f_path='/'),
775 params={
775 params={
776 'content': "",
776 'content': "",
777 'filename': filename,
777 'filename': filename,
778 'csrf_token': csrf_token,
778 'csrf_token': csrf_token,
779 },
779 },
780 status=302)
780 status=302)
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
782 assert_session_flash(response, expected_msg)
782 assert_session_flash(response, expected_msg)
783
783
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
785 commit_id = backend.repo.get_commit().raw_id
785 commit_id = backend.repo.get_commit().raw_id
786 response = self.app.post(
786 response = self.app.post(
787 route_path('repo_files_create_file',
787 route_path('repo_files_create_file',
788 repo_name=backend.repo_name,
788 repo_name=backend.repo_name,
789 commit_id=commit_id, f_path='/'),
789 commit_id=commit_id, f_path='/'),
790 params={
790 params={
791 'content': "foo",
791 'content': "foo",
792 'csrf_token': csrf_token,
792 'csrf_token': csrf_token,
793 },
793 },
794 status=302)
794 status=302)
795
795
796 assert_session_flash(response, 'No filename specified')
796 assert_session_flash(response, 'No filename specified')
797
797
798 def test_add_file_into_repo_errors_and_no_commits(
798 def test_add_file_into_repo_errors_and_no_commits(
799 self, backend, csrf_token):
799 self, backend, csrf_token):
800 repo = backend.create_repo()
800 repo = backend.create_repo()
801 # Create a file with no filename, it will display an error but
801 # Create a file with no filename, it will display an error but
802 # the repo has no commits yet
802 # the repo has no commits yet
803 response = self.app.post(
803 response = self.app.post(
804 route_path('repo_files_create_file',
804 route_path('repo_files_create_file',
805 repo_name=repo.repo_name,
805 repo_name=repo.repo_name,
806 commit_id='tip', f_path='/'),
806 commit_id='tip', f_path='/'),
807 params={
807 params={
808 'content': "foo",
808 'content': "foo",
809 'csrf_token': csrf_token,
809 'csrf_token': csrf_token,
810 },
810 },
811 status=302)
811 status=302)
812
812
813 assert_session_flash(response, 'No filename specified')
813 assert_session_flash(response, 'No filename specified')
814
814
815 # Not allowed, redirect to the summary
815 # Not allowed, redirect to the summary
816 redirected = response.follow()
816 redirected = response.follow()
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
818
818
819 # As there are no commits, displays the summary page with the error of
819 # As there are no commits, displays the summary page with the error of
820 # creating a file with no filename
820 # creating a file with no filename
821
821
822 assert redirected.request.path == summary_url
822 assert redirected.request.path == summary_url
823
823
824 @pytest.mark.parametrize("filename, clean_filename", [
824 @pytest.mark.parametrize("filename, clean_filename", [
825 ('/abs/foo', 'abs/foo'),
825 ('/abs/foo', 'abs/foo'),
826 ('../rel/foo', 'rel/foo'),
826 ('../rel/foo', 'rel/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
828 ])
828 ])
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
830 repo = backend.create_repo()
830 repo = backend.create_repo()
831 commit_id = repo.get_commit().raw_id
831 commit_id = repo.get_commit().raw_id
832
832
833 response = self.app.post(
833 response = self.app.post(
834 route_path('repo_files_create_file',
834 route_path('repo_files_create_file',
835 repo_name=repo.repo_name,
835 repo_name=repo.repo_name,
836 commit_id=commit_id, f_path='/'),
836 commit_id=commit_id, f_path='/'),
837 params={
837 params={
838 'content': "foo",
838 'content': "foo",
839 'filename': filename,
839 'filename': filename,
840 'csrf_token': csrf_token,
840 'csrf_token': csrf_token,
841 },
841 },
842 status=302)
842 status=302)
843
843
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
845 assert_session_flash(response, expected_msg)
845 assert_session_flash(response, expected_msg)
846
846
847 @pytest.mark.parametrize("cnt, filename, content", [
847 @pytest.mark.parametrize("cnt, filename, content", [
848 (1, 'foo.txt', "Content"),
848 (1, 'foo.txt', "Content"),
849 (2, 'dir/foo.rst', "Content"),
849 (2, 'dir/foo.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
852 ])
852 ])
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
854 repo = backend.create_repo()
854 repo = backend.create_repo()
855 commit_id = repo.get_commit().raw_id
855 commit_id = repo.get_commit().raw_id
856 response = self.app.post(
856 response = self.app.post(
857 route_path('repo_files_create_file',
857 route_path('repo_files_create_file',
858 repo_name=repo.repo_name,
858 repo_name=repo.repo_name,
859 commit_id=commit_id, f_path='/'),
859 commit_id=commit_id, f_path='/'),
860 params={
860 params={
861 'content': content,
861 'content': content,
862 'filename': filename,
862 'filename': filename,
863 'csrf_token': csrf_token,
863 'csrf_token': csrf_token,
864 },
864 },
865 status=302)
865 status=302)
866
866
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
868 assert_session_flash(response, expected_msg)
868 assert_session_flash(response, expected_msg)
869
869
870 def test_edit_file_view(self, backend):
870 def test_edit_file_view(self, backend):
871 response = self.app.get(
871 response = self.app.get(
872 route_path('repo_files_edit_file',
872 route_path('repo_files_edit_file',
873 repo_name=backend.repo_name,
873 repo_name=backend.repo_name,
874 commit_id=backend.default_head_id,
874 commit_id=backend.default_head_id,
875 f_path='vcs/nodes.py'),
875 f_path='vcs/nodes.py'),
876 status=200)
876 status=200)
877 response.mustcontain("Module holding everything related to vcs nodes.")
877 response.mustcontain("Module holding everything related to vcs nodes.")
878
878
879 def test_edit_file_view_not_on_branch(self, backend):
879 def test_edit_file_view_not_on_branch(self, backend):
880 repo = backend.create_repo()
880 repo = backend.create_repo()
881 backend.ensure_file("vcs/nodes.py")
881 backend.ensure_file("vcs/nodes.py")
882
882
883 response = self.app.get(
883 response = self.app.get(
884 route_path('repo_files_edit_file',
884 route_path('repo_files_edit_file',
885 repo_name=repo.repo_name,
885 repo_name=repo.repo_name,
886 commit_id='tip',
886 commit_id='tip',
887 f_path='vcs/nodes.py'),
887 f_path='vcs/nodes.py'),
888 status=302)
888 status=302)
889 assert_session_flash(
889 assert_session_flash(
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
891
891
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
893 repo = backend.create_repo()
893 repo = backend.create_repo()
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
895
895
896 response = self.app.post(
896 response = self.app.post(
897 route_path('repo_files_update_file',
897 route_path('repo_files_update_file',
898 repo_name=repo.repo_name,
898 repo_name=repo.repo_name,
899 commit_id=backend.default_head_id,
899 commit_id=backend.default_head_id,
900 f_path='vcs/nodes.py'),
900 f_path='vcs/nodes.py'),
901 params={
901 params={
902 'content': "print 'hello world'",
902 'content': "print 'hello world'",
903 'message': 'I committed',
903 'message': 'I committed',
904 'filename': "vcs/nodes.py",
904 'filename': "vcs/nodes.py",
905 'csrf_token': csrf_token,
905 'csrf_token': csrf_token,
906 },
906 },
907 status=302)
907 status=302)
908 assert_session_flash(
908 assert_session_flash(
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
910 tip = repo.get_commit(commit_idx=-1)
910 tip = repo.get_commit(commit_idx=-1)
911 assert tip.message == 'I committed'
911 assert tip.message == 'I committed'
912
912
913 def test_edit_file_view_commit_changes_default_message(self, backend,
913 def test_edit_file_view_commit_changes_default_message(self, backend,
914 csrf_token):
914 csrf_token):
915 repo = backend.create_repo()
915 repo = backend.create_repo()
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917
917
918 commit_id = (
918 commit_id = (
919 backend.default_branch_name or
919 backend.default_branch_name or
920 backend.repo.scm_instance().commit_ids[-1])
920 backend.repo.scm_instance().commit_ids[-1])
921
921
922 response = self.app.post(
922 response = self.app.post(
923 route_path('repo_files_update_file',
923 route_path('repo_files_update_file',
924 repo_name=repo.repo_name,
924 repo_name=repo.repo_name,
925 commit_id=commit_id,
925 commit_id=commit_id,
926 f_path='vcs/nodes.py'),
926 f_path='vcs/nodes.py'),
927 params={
927 params={
928 'content': "print 'hello world'",
928 'content': "print 'hello world'",
929 'message': '',
929 'message': '',
930 'filename': "vcs/nodes.py",
930 'filename': "vcs/nodes.py",
931 'csrf_token': csrf_token,
931 'csrf_token': csrf_token,
932 },
932 },
933 status=302)
933 status=302)
934 assert_session_flash(
934 assert_session_flash(
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
936 tip = repo.get_commit(commit_idx=-1)
936 tip = repo.get_commit(commit_idx=-1)
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
938
938
939 def test_delete_file_view(self, backend):
939 def test_delete_file_view(self, backend):
940 self.app.get(
940 self.app.get(
941 route_path('repo_files_remove_file',
941 route_path('repo_files_remove_file',
942 repo_name=backend.repo_name,
942 repo_name=backend.repo_name,
943 commit_id=backend.default_head_id,
943 commit_id=backend.default_head_id,
944 f_path='vcs/nodes.py'),
944 f_path='vcs/nodes.py'),
945 status=200)
945 status=200)
946
946
947 def test_delete_file_view_not_on_branch(self, backend):
947 def test_delete_file_view_not_on_branch(self, backend):
948 repo = backend.create_repo()
948 repo = backend.create_repo()
949 backend.ensure_file('vcs/nodes.py')
949 backend.ensure_file('vcs/nodes.py')
950
950
951 response = self.app.get(
951 response = self.app.get(
952 route_path('repo_files_remove_file',
952 route_path('repo_files_remove_file',
953 repo_name=repo.repo_name,
953 repo_name=repo.repo_name,
954 commit_id='tip',
954 commit_id='tip',
955 f_path='vcs/nodes.py'),
955 f_path='vcs/nodes.py'),
956 status=302)
956 status=302)
957 assert_session_flash(
957 assert_session_flash(
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
959
959
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
961 repo = backend.create_repo()
961 repo = backend.create_repo()
962 backend.ensure_file("vcs/nodes.py")
962 backend.ensure_file("vcs/nodes.py")
963
963
964 response = self.app.post(
964 response = self.app.post(
965 route_path('repo_files_delete_file',
965 route_path('repo_files_delete_file',
966 repo_name=repo.repo_name,
966 repo_name=repo.repo_name,
967 commit_id=backend.default_head_id,
967 commit_id=backend.default_head_id,
968 f_path='vcs/nodes.py'),
968 f_path='vcs/nodes.py'),
969 params={
969 params={
970 'message': 'i commited',
970 'message': 'i commited',
971 'csrf_token': csrf_token,
971 'csrf_token': csrf_token,
972 },
972 },
973 status=302)
973 status=302)
974 assert_session_flash(
974 assert_session_flash(
975 response, 'Successfully deleted file `vcs/nodes.py`')
975 response, 'Successfully deleted file `vcs/nodes.py`')
976
976
977
977
978 @pytest.mark.usefixtures("app")
978 @pytest.mark.usefixtures("app")
979 class TestFilesViewOtherCases(object):
979 class TestFilesViewOtherCases(object):
980
980
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
982 self, backend_stub, autologin_regular_user, user_regular,
982 self, backend_stub, autologin_regular_user, user_regular,
983 user_util):
983 user_util):
984
984
985 repo = backend_stub.create_repo()
985 repo = backend_stub.create_repo()
986 user_util.grant_user_permission_to_repo(
986 user_util.grant_user_permission_to_repo(
987 repo, user_regular, 'repository.write')
987 repo, user_regular, 'repository.write')
988 response = self.app.get(
988 response = self.app.get(
989 route_path('repo_files',
989 route_path('repo_files',
990 repo_name=repo.repo_name,
990 repo_name=repo.repo_name,
991 commit_id='tip', f_path='/'))
991 commit_id='tip', f_path='/'))
992
992
993 repo_file_add_url = route_path(
993 repo_file_add_url = route_path(
994 'repo_files_add_file',
994 'repo_files_add_file',
995 repo_name=repo.repo_name,
995 repo_name=repo.repo_name,
996 commit_id=0, f_path='')
996 commit_id=0, f_path='')
997
997
998 assert_session_flash(
998 assert_session_flash(
999 response,
999 response,
1000 'There are no files yet. <a class="alert-link" '
1000 'There are no files yet. <a class="alert-link" '
1001 'href="{}">Click here to add a new file.</a>'
1001 'href="{}">Click here to add a new file.</a>'
1002 .format(repo_file_add_url))
1002 .format(repo_file_add_url))
1003
1003
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1005 self, backend_stub, autologin_regular_user):
1005 self, backend_stub, autologin_regular_user):
1006 repo = backend_stub.create_repo()
1006 repo = backend_stub.create_repo()
1007 # init session for anon user
1007 # init session for anon user
1008 route_path('repo_summary', repo_name=repo.repo_name)
1008 route_path('repo_summary', repo_name=repo.repo_name)
1009
1009
1010 repo_file_add_url = route_path(
1010 repo_file_add_url = route_path(
1011 'repo_files_add_file',
1011 'repo_files_add_file',
1012 repo_name=repo.repo_name,
1012 repo_name=repo.repo_name,
1013 commit_id=0, f_path='')
1013 commit_id=0, f_path='')
1014
1014
1015 response = self.app.get(
1015 response = self.app.get(
1016 route_path('repo_files',
1016 route_path('repo_files',
1017 repo_name=repo.repo_name,
1017 repo_name=repo.repo_name,
1018 commit_id='tip', f_path='/'))
1018 commit_id='tip', f_path='/'))
1019
1019
1020 assert_session_flash(response, no_=repo_file_add_url)
1020 assert_session_flash(response, no_=repo_file_add_url)
1021
1021
1022 @pytest.mark.parametrize('file_node', [
1022 @pytest.mark.parametrize('file_node', [
1023 'archive/file.zip',
1023 'archive/file.zip',
1024 'diff/my-file.txt',
1024 'diff/my-file.txt',
1025 'render.py',
1025 'render.py',
1026 'render',
1026 'render',
1027 'remove_file',
1027 'remove_file',
1028 'remove_file/to-delete.txt',
1028 'remove_file/to-delete.txt',
1029 ])
1029 ])
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1031 backend.create_repo()
1031 backend.create_repo()
1032 backend.ensure_file(file_node)
1032 backend.ensure_file(file_node)
1033
1033
1034 self.app.get(
1034 self.app.get(
1035 route_path('repo_files',
1035 route_path('repo_files',
1036 repo_name=backend.repo_name,
1036 repo_name=backend.repo_name,
1037 commit_id='tip', f_path=file_node),
1037 commit_id='tip', f_path=file_node),
1038 status=200)
1038 status=200)
1039
1039
1040
1040
1041 class TestAdjustFilePathForSvn(object):
1041 class TestAdjustFilePathForSvn(object):
1042 """
1042 """
1043 SVN specific adjustments of node history in RepoFilesView.
1043 SVN specific adjustments of node history in RepoFilesView.
1044 """
1044 """
1045
1045
1046 def test_returns_path_relative_to_matched_reference(self):
1046 def test_returns_path_relative_to_matched_reference(self):
1047 repo = self._repo(branches=['trunk'])
1047 repo = self._repo(branches=['trunk'])
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1049
1049
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1051 repo = self._repo(branches=['trunk'])
1051 repo = self._repo(branches=['trunk'])
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1053
1053
1054 def test_does_not_adjust_partial_directory_names(self):
1054 def test_does_not_adjust_partial_directory_names(self):
1055 repo = self._repo(branches=['trun'])
1055 repo = self._repo(branches=['trun'])
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1057
1057
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1061
1061
1062 def assert_file_adjustment(self, f_path, expected, repo):
1062 def assert_file_adjustment(self, f_path, expected, repo):
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1064 assert result == expected
1064 assert result == expected
1065
1065
1066 def _repo(self, branches=None):
1066 def _repo(self, branches=None):
1067 repo = mock.Mock()
1067 repo = mock.Mock()
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1069 repo.tags = {}
1069 repo.tags = {}
1070 return repo
1070 return repo
@@ -1,1603 +1,1614 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 itertools
21 import itertools
22 import logging
22 import logging
23 import os
23 import os
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27 import urllib
27 import urllib
28 import pathlib2
28 import pathlib2
29
29
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 from pyramid.view import view_config
31 from pyramid.view import view_config
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34
34
35 import rhodecode
35 import rhodecode
36 from rhodecode.apps._base import RepoAppView
36 from rhodecode.apps._base import RepoAppView
37
37
38
38
39 from rhodecode.lib import diffs, helpers as h, rc_cache
39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 from rhodecode.lib import audit_logger
40 from rhodecode.lib import audit_logger
41 from rhodecode.lib.view_utils import parse_path_ref
41 from rhodecode.lib.view_utils import parse_path_ref
42 from rhodecode.lib.exceptions import NonRelativePathError
42 from rhodecode.lib.exceptions import NonRelativePathError
43 from rhodecode.lib.codeblocks import (
43 from rhodecode.lib.codeblocks import (
44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 from rhodecode.lib.utils2 import (
45 from rhodecode.lib.utils2 import (
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 from rhodecode.lib.auth import (
47 from rhodecode.lib.auth import (
48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 from rhodecode.lib.vcs import path as vcspath
49 from rhodecode.lib.vcs import path as vcspath
50 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 from rhodecode.lib.vcs.conf import settings
51 from rhodecode.lib.vcs.conf import settings
52 from rhodecode.lib.vcs.nodes import FileNode
52 from rhodecode.lib.vcs.nodes import FileNode
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 NodeDoesNotExistError, CommitError, NodeError)
56 NodeDoesNotExistError, CommitError, NodeError)
57
57
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.db import Repository
59 from rhodecode.model.db import Repository
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 class RepoFilesView(RepoAppView):
64 class RepoFilesView(RepoAppView):
65
65
66 @staticmethod
66 @staticmethod
67 def adjust_file_path_for_svn(f_path, repo):
67 def adjust_file_path_for_svn(f_path, repo):
68 """
68 """
69 Computes the relative path of `f_path`.
69 Computes the relative path of `f_path`.
70
70
71 This is mainly based on prefix matching of the recognized tags and
71 This is mainly based on prefix matching of the recognized tags and
72 branches in the underlying repository.
72 branches in the underlying repository.
73 """
73 """
74 tags_and_branches = itertools.chain(
74 tags_and_branches = itertools.chain(
75 repo.branches.iterkeys(),
75 repo.branches.iterkeys(),
76 repo.tags.iterkeys())
76 repo.tags.iterkeys())
77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78
78
79 for name in tags_and_branches:
79 for name in tags_and_branches:
80 if f_path.startswith('{}/'.format(name)):
80 if f_path.startswith('{}/'.format(name)):
81 f_path = vcspath.relpath(f_path, name)
81 f_path = vcspath.relpath(f_path, name)
82 break
82 break
83 return f_path
83 return f_path
84
84
85 def load_default_context(self):
85 def load_default_context(self):
86 c = self._get_local_tmpl_context(include_app_defaults=True)
86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 c.rhodecode_repo = self.rhodecode_vcs_repo
87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 c.enable_downloads = self.db_repo.enable_downloads
88 c.enable_downloads = self.db_repo.enable_downloads
89 return c
89 return c
90
90
91 def _ensure_not_locked(self, commit_id='tip'):
91 def _ensure_not_locked(self, commit_id='tip'):
92 _ = self.request.translate
92 _ = self.request.translate
93
93
94 repo = self.db_repo
94 repo = self.db_repo
95 if repo.enable_locking and repo.locked[0]:
95 if repo.enable_locking and repo.locked[0]:
96 h.flash(_('This repository has been locked by %s on %s')
96 h.flash(_('This repository has been locked by %s on %s')
97 % (h.person_by_id(repo.locked[0]),
97 % (h.person_by_id(repo.locked[0]),
98 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 'warning')
99 'warning')
100 files_url = h.route_path(
100 files_url = h.route_path(
101 'repo_files:default_path',
101 'repo_files:default_path',
102 repo_name=self.db_repo_name, commit_id=commit_id)
102 repo_name=self.db_repo_name, commit_id=commit_id)
103 raise HTTPFound(files_url)
103 raise HTTPFound(files_url)
104
104
105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 _ = self.request.translate
106 _ = self.request.translate
107
107
108 if not is_head:
108 if not is_head:
109 message = _('Cannot modify file. '
109 message = _('Cannot modify file. '
110 'Given commit `{}` is not head of a branch.').format(commit_id)
110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 h.flash(message, category='warning')
111 h.flash(message, category='warning')
112
112
113 if json_mode:
113 if json_mode:
114 return message
114 return message
115
115
116 files_url = h.route_path(
116 files_url = h.route_path(
117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 f_path=f_path)
118 f_path=f_path)
119 raise HTTPFound(files_url)
119 raise HTTPFound(files_url)
120
120
121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 _ = self.request.translate
122 _ = self.request.translate
123
123
124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 self.db_repo_name, branch_name)
125 self.db_repo_name, branch_name)
126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 message = _('Branch `{}` changes forbidden by rule {}.').format(
127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 branch_name, rule)
128 branch_name, rule)
129 h.flash(message, 'warning')
129 h.flash(message, 'warning')
130
130
131 if json_mode:
131 if json_mode:
132 return message
132 return message
133
133
134 files_url = h.route_path(
134 files_url = h.route_path(
135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136
136
137 raise HTTPFound(files_url)
137 raise HTTPFound(files_url)
138
138
139 def _get_commit_and_path(self):
139 def _get_commit_and_path(self):
140 default_commit_id = self.db_repo.landing_ref_name
140 default_commit_id = self.db_repo.landing_ref_name
141 default_f_path = '/'
141 default_f_path = '/'
142
142
143 commit_id = self.request.matchdict.get(
143 commit_id = self.request.matchdict.get(
144 'commit_id', default_commit_id)
144 'commit_id', default_commit_id)
145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 return commit_id, f_path
146 return commit_id, f_path
147
147
148 def _get_default_encoding(self, c):
148 def _get_default_encoding(self, c):
149 enc_list = getattr(c, 'default_encodings', [])
149 enc_list = getattr(c, 'default_encodings', [])
150 return enc_list[0] if enc_list else 'UTF-8'
150 return enc_list[0] if enc_list else 'UTF-8'
151
151
152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 """
153 """
154 This is a safe way to get commit. If an error occurs it redirects to
154 This is a safe way to get commit. If an error occurs it redirects to
155 tip with proper message
155 tip with proper message
156
156
157 :param commit_id: id of commit to fetch
157 :param commit_id: id of commit to fetch
158 :param redirect_after: toggle redirection
158 :param redirect_after: toggle redirection
159 """
159 """
160 _ = self.request.translate
160 _ = self.request.translate
161
161
162 try:
162 try:
163 return self.rhodecode_vcs_repo.get_commit(commit_id)
163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 except EmptyRepositoryError:
164 except EmptyRepositoryError:
165 if not redirect_after:
165 if not redirect_after:
166 return None
166 return None
167
167
168 _url = h.route_path(
168 _url = h.route_path(
169 'repo_files_add_file',
169 'repo_files_add_file',
170 repo_name=self.db_repo_name, commit_id=0, f_path='')
170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171
171
172 if h.HasRepoPermissionAny(
172 if h.HasRepoPermissionAny(
173 'repository.write', 'repository.admin')(self.db_repo_name):
173 'repository.write', 'repository.admin')(self.db_repo_name):
174 add_new = h.link_to(
174 add_new = h.link_to(
175 _('Click here to add a new file.'), _url, class_="alert-link")
175 _('Click here to add a new file.'), _url, class_="alert-link")
176 else:
176 else:
177 add_new = ""
177 add_new = ""
178
178
179 h.flash(h.literal(
179 h.flash(h.literal(
180 _('There are no files yet. %s') % add_new), category='warning')
180 _('There are no files yet. %s') % add_new), category='warning')
181 raise HTTPFound(
181 raise HTTPFound(
182 h.route_path('repo_summary', repo_name=self.db_repo_name))
182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183
183
184 except (CommitDoesNotExistError, LookupError) as e:
184 except (CommitDoesNotExistError, LookupError) as e:
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 h.flash(msg, category='error')
186 h.flash(msg, category='error')
187 raise HTTPNotFound()
187 raise HTTPNotFound()
188 except RepositoryError as e:
188 except RepositoryError as e:
189 h.flash(safe_str(h.escape(e)), category='error')
189 h.flash(safe_str(h.escape(e)), category='error')
190 raise HTTPNotFound()
190 raise HTTPNotFound()
191
191
192 def _get_filenode_or_redirect(self, commit_obj, path):
192 def _get_filenode_or_redirect(self, commit_obj, path):
193 """
193 """
194 Returns file_node, if error occurs or given path is directory,
194 Returns file_node, if error occurs or given path is directory,
195 it'll redirect to top level path
195 it'll redirect to top level path
196 """
196 """
197 _ = self.request.translate
197 _ = self.request.translate
198
198
199 try:
199 try:
200 file_node = commit_obj.get_node(path)
200 file_node = commit_obj.get_node(path)
201 if file_node.is_dir():
201 if file_node.is_dir():
202 raise RepositoryError('The given path is a directory')
202 raise RepositoryError('The given path is a directory')
203 except CommitDoesNotExistError:
203 except CommitDoesNotExistError:
204 log.exception('No such commit exists for this repository')
204 log.exception('No such commit exists for this repository')
205 h.flash(_('No such commit exists for this repository'), category='error')
205 h.flash(_('No such commit exists for this repository'), category='error')
206 raise HTTPNotFound()
206 raise HTTPNotFound()
207 except RepositoryError as e:
207 except RepositoryError as e:
208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 h.flash(safe_str(h.escape(e)), category='error')
209 h.flash(safe_str(h.escape(e)), category='error')
210 raise HTTPNotFound()
210 raise HTTPNotFound()
211
211
212 return file_node
212 return file_node
213
213
214 def _is_valid_head(self, commit_id, repo):
214 def _is_valid_head(self, commit_id, repo):
215 branch_name = sha_commit_id = ''
215 branch_name = sha_commit_id = ''
216 is_head = False
216 is_head = False
217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218
218
219 for _branch_name, branch_commit_id in repo.branches.items():
219 for _branch_name, branch_commit_id in repo.branches.items():
220 # simple case we pass in branch name, it's a HEAD
220 # simple case we pass in branch name, it's a HEAD
221 if commit_id == _branch_name:
221 if commit_id == _branch_name:
222 is_head = True
222 is_head = True
223 branch_name = _branch_name
223 branch_name = _branch_name
224 sha_commit_id = branch_commit_id
224 sha_commit_id = branch_commit_id
225 break
225 break
226 # case when we pass in full sha commit_id, which is a head
226 # case when we pass in full sha commit_id, which is a head
227 elif commit_id == branch_commit_id:
227 elif commit_id == branch_commit_id:
228 is_head = True
228 is_head = True
229 branch_name = _branch_name
229 branch_name = _branch_name
230 sha_commit_id = branch_commit_id
230 sha_commit_id = branch_commit_id
231 break
231 break
232
232
233 if h.is_svn(repo) and not repo.is_empty():
233 if h.is_svn(repo) and not repo.is_empty():
234 # Note: Subversion only has one head.
234 # Note: Subversion only has one head.
235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 is_head = True
236 is_head = True
237 return branch_name, sha_commit_id, is_head
237 return branch_name, sha_commit_id, is_head
238
238
239 # checked branches, means we only need to try to get the branch/commit_sha
239 # checked branches, means we only need to try to get the branch/commit_sha
240 if not repo.is_empty():
240 if not repo.is_empty():
241 commit = repo.get_commit(commit_id=commit_id)
241 commit = repo.get_commit(commit_id=commit_id)
242 if commit:
242 if commit:
243 branch_name = commit.branch
243 branch_name = commit.branch
244 sha_commit_id = commit.raw_id
244 sha_commit_id = commit.raw_id
245
245
246 return branch_name, sha_commit_id, is_head
246 return branch_name, sha_commit_id, is_head
247
247
248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249
249
250 repo_id = self.db_repo.repo_id
250 repo_id = self.db_repo.repo_id
251 force_recache = self.get_recache_flag()
251 force_recache = self.get_recache_flag()
252
252
253 cache_seconds = safe_int(
253 cache_seconds = safe_int(
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 cache_on = not force_recache and cache_seconds > 0
255 cache_on = not force_recache and cache_seconds > 0
256 log.debug(
256 log.debug(
257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 'with caching: %s[TTL: %ss]' % (
258 'with caching: %s[TTL: %ss]' % (
259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260
260
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263
263
264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
265 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
265 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 ver, _repo_id, _commit_id, _f_path)
267 ver, _repo_id, _commit_id, _f_path)
268
268
269 c.full_load = _full_load
269 c.full_load = _full_load
270 return render(
270 return render(
271 'rhodecode:templates/files/files_browser_tree.mako',
271 'rhodecode:templates/files/files_browser_tree.mako',
272 self._get_template_context(c), self.request, _at_rev)
272 self._get_template_context(c), self.request, _at_rev)
273
273
274 return compute_file_tree(
274 return compute_file_tree(
275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277
277
278 def _get_archive_spec(self, fname):
278 def _get_archive_spec(self, fname):
279 log.debug('Detecting archive spec for: `%s`', fname)
279 log.debug('Detecting archive spec for: `%s`', fname)
280
280
281 fileformat = None
281 fileformat = None
282 ext = None
282 ext = None
283 content_type = None
283 content_type = None
284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285
285
286 if fname.endswith(extension):
286 if fname.endswith(extension):
287 fileformat = a_type
287 fileformat = a_type
288 log.debug('archive is of type: %s', fileformat)
288 log.debug('archive is of type: %s', fileformat)
289 ext = extension
289 ext = extension
290 break
290 break
291
291
292 if not fileformat:
292 if not fileformat:
293 raise ValueError()
293 raise ValueError()
294
294
295 # left over part of whole fname is the commit
295 # left over part of whole fname is the commit
296 commit_id = fname[:-len(ext)]
296 commit_id = fname[:-len(ext)]
297
297
298 return commit_id, ext, fileformat, content_type
298 return commit_id, ext, fileformat, content_type
299
299
300 def create_pure_path(self, *parts):
300 def create_pure_path(self, *parts):
301 # Split paths and sanitize them, removing any ../ etc
301 # Split paths and sanitize them, removing any ../ etc
302 sanitized_path = [
302 sanitized_path = [
303 x for x in pathlib2.PurePath(*parts).parts
303 x for x in pathlib2.PurePath(*parts).parts
304 if x not in ['.', '..']]
304 if x not in ['.', '..']]
305
305
306 pure_path = pathlib2.PurePath(*sanitized_path)
306 pure_path = pathlib2.PurePath(*sanitized_path)
307 return pure_path
307 return pure_path
308
308
309 def _is_lf_enabled(self, target_repo):
309 def _is_lf_enabled(self, target_repo):
310 lf_enabled = False
310 lf_enabled = False
311
311
312 lf_key_for_vcs_map = {
312 lf_key_for_vcs_map = {
313 'hg': 'extensions_largefiles',
313 'hg': 'extensions_largefiles',
314 'git': 'vcs_git_lfs_enabled'
314 'git': 'vcs_git_lfs_enabled'
315 }
315 }
316
316
317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318
318
319 if lf_key_for_vcs:
319 if lf_key_for_vcs:
320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321
321
322 return lf_enabled
322 return lf_enabled
323
323
324 @LoginRequired()
324 @LoginRequired()
325 @HasRepoPermissionAnyDecorator(
325 @HasRepoPermissionAnyDecorator(
326 'repository.read', 'repository.write', 'repository.admin')
326 'repository.read', 'repository.write', 'repository.admin')
327 @view_config(
327 @view_config(
328 route_name='repo_archivefile', request_method='GET',
328 route_name='repo_archivefile', request_method='GET',
329 renderer=None)
329 renderer=None)
330 def repo_archivefile(self):
330 def repo_archivefile(self):
331 # archive cache config
331 # archive cache config
332 from rhodecode import CONFIG
332 from rhodecode import CONFIG
333 _ = self.request.translate
333 _ = self.request.translate
334 self.load_default_context()
334 self.load_default_context()
335 default_at_path = '/'
335 default_at_path = '/'
336 fname = self.request.matchdict['fname']
336 fname = self.request.matchdict['fname']
337 subrepos = self.request.GET.get('subrepos') == 'true'
337 subrepos = self.request.GET.get('subrepos') == 'true'
338 at_path = self.request.GET.get('at_path') or default_at_path
338 at_path = self.request.GET.get('at_path') or default_at_path
339
339
340 if not self.db_repo.enable_downloads:
340 if not self.db_repo.enable_downloads:
341 return Response(_('Downloads disabled'))
341 return Response(_('Downloads disabled'))
342
342
343 try:
343 try:
344 commit_id, ext, fileformat, content_type = \
344 commit_id, ext, fileformat, content_type = \
345 self._get_archive_spec(fname)
345 self._get_archive_spec(fname)
346 except ValueError:
346 except ValueError:
347 return Response(_('Unknown archive type for: `{}`').format(
347 return Response(_('Unknown archive type for: `{}`').format(
348 h.escape(fname)))
348 h.escape(fname)))
349
349
350 try:
350 try:
351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 except CommitDoesNotExistError:
352 except CommitDoesNotExistError:
353 return Response(_('Unknown commit_id {}').format(
353 return Response(_('Unknown commit_id {}').format(
354 h.escape(commit_id)))
354 h.escape(commit_id)))
355 except EmptyRepositoryError:
355 except EmptyRepositoryError:
356 return Response(_('Empty repository'))
356 return Response(_('Empty repository'))
357
357
358 try:
358 try:
359 at_path = commit.get_node(at_path).path or default_at_path
359 at_path = commit.get_node(at_path).path or default_at_path
360 except Exception:
360 except Exception:
361 return Response(_('No node at path {} for this repository').format(at_path))
361 return Response(_('No node at path {} for this repository').format(at_path))
362
362
363 path_sha = sha1(at_path)[:8]
363 path_sha = sha1(at_path)[:8]
364
364
365 # original backward compat name of archive
365 # original backward compat name of archive
366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 short_sha = safe_str(commit.short_id)
367 short_sha = safe_str(commit.short_id)
368
368
369 if at_path == default_at_path:
369 if at_path == default_at_path:
370 archive_name = '{}-{}{}{}'.format(
370 archive_name = '{}-{}{}{}'.format(
371 clean_name,
371 clean_name,
372 '-sub' if subrepos else '',
372 '-sub' if subrepos else '',
373 short_sha,
373 short_sha,
374 ext)
374 ext)
375 # custom path and new name
375 # custom path and new name
376 else:
376 else:
377 archive_name = '{}-{}{}-{}{}'.format(
377 archive_name = '{}-{}{}-{}{}'.format(
378 clean_name,
378 clean_name,
379 '-sub' if subrepos else '',
379 '-sub' if subrepos else '',
380 short_sha,
380 short_sha,
381 path_sha,
381 path_sha,
382 ext)
382 ext)
383
383
384 use_cached_archive = False
384 use_cached_archive = False
385 archive_cache_enabled = CONFIG.get(
385 archive_cache_enabled = CONFIG.get(
386 'archive_cache_dir') and not self.request.GET.get('no_cache')
386 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 cached_archive_path = None
387 cached_archive_path = None
388
388
389 if archive_cache_enabled:
389 if archive_cache_enabled:
390 # check if we it's ok to write
390 # check if we it's ok to write
391 if not os.path.isdir(CONFIG['archive_cache_dir']):
391 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 os.makedirs(CONFIG['archive_cache_dir'])
392 os.makedirs(CONFIG['archive_cache_dir'])
393 cached_archive_path = os.path.join(
393 cached_archive_path = os.path.join(
394 CONFIG['archive_cache_dir'], archive_name)
394 CONFIG['archive_cache_dir'], archive_name)
395 if os.path.isfile(cached_archive_path):
395 if os.path.isfile(cached_archive_path):
396 log.debug('Found cached archive in %s', cached_archive_path)
396 log.debug('Found cached archive in %s', cached_archive_path)
397 fd, archive = None, cached_archive_path
397 fd, archive = None, cached_archive_path
398 use_cached_archive = True
398 use_cached_archive = True
399 else:
399 else:
400 log.debug('Archive %s is not yet cached', archive_name)
400 log.debug('Archive %s is not yet cached', archive_name)
401
401
402 if not use_cached_archive:
402 if not use_cached_archive:
403 # generate new archive
403 # generate new archive
404 fd, archive = tempfile.mkstemp()
404 fd, archive = tempfile.mkstemp()
405 log.debug('Creating new temp archive in %s', archive)
405 log.debug('Creating new temp archive in %s', archive)
406 try:
406 try:
407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 archive_at_path=at_path)
408 archive_at_path=at_path)
409 except ImproperArchiveTypeError:
409 except ImproperArchiveTypeError:
410 return _('Unknown archive type')
410 return _('Unknown archive type')
411 if archive_cache_enabled:
411 if archive_cache_enabled:
412 # if we generated the archive and we have cache enabled
412 # if we generated the archive and we have cache enabled
413 # let's use this for future
413 # let's use this for future
414 log.debug('Storing new archive in %s', cached_archive_path)
414 log.debug('Storing new archive in %s', cached_archive_path)
415 shutil.move(archive, cached_archive_path)
415 shutil.move(archive, cached_archive_path)
416 archive = cached_archive_path
416 archive = cached_archive_path
417
417
418 # store download action
418 # store download action
419 audit_logger.store_web(
419 audit_logger.store_web(
420 'repo.archive.download', action_data={
420 'repo.archive.download', action_data={
421 'user_agent': self.request.user_agent,
421 'user_agent': self.request.user_agent,
422 'archive_name': archive_name,
422 'archive_name': archive_name,
423 'archive_spec': fname,
423 'archive_spec': fname,
424 'archive_cached': use_cached_archive},
424 'archive_cached': use_cached_archive},
425 user=self._rhodecode_user,
425 user=self._rhodecode_user,
426 repo=self.db_repo,
426 repo=self.db_repo,
427 commit=True
427 commit=True
428 )
428 )
429
429
430 def get_chunked_archive(archive_path):
430 def get_chunked_archive(archive_path):
431 with open(archive_path, 'rb') as stream:
431 with open(archive_path, 'rb') as stream:
432 while True:
432 while True:
433 data = stream.read(16 * 1024)
433 data = stream.read(16 * 1024)
434 if not data:
434 if not data:
435 if fd: # fd means we used temporary file
435 if fd: # fd means we used temporary file
436 os.close(fd)
436 os.close(fd)
437 if not archive_cache_enabled:
437 if not archive_cache_enabled:
438 log.debug('Destroying temp archive %s', archive_path)
438 log.debug('Destroying temp archive %s', archive_path)
439 os.remove(archive_path)
439 os.remove(archive_path)
440 break
440 break
441 yield data
441 yield data
442
442
443 response = Response(app_iter=get_chunked_archive(archive))
443 response = Response(app_iter=get_chunked_archive(archive))
444 response.content_disposition = str(
444 response.content_disposition = str(
445 'attachment; filename=%s' % archive_name)
445 'attachment; filename=%s' % archive_name)
446 response.content_type = str(content_type)
446 response.content_type = str(content_type)
447
447
448 return response
448 return response
449
449
450 def _get_file_node(self, commit_id, f_path):
450 def _get_file_node(self, commit_id, f_path):
451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 try:
453 try:
454 node = commit.get_node(f_path)
454 node = commit.get_node(f_path)
455 if node.is_dir():
455 if node.is_dir():
456 raise NodeError('%s path is a %s not a file'
456 raise NodeError('%s path is a %s not a file'
457 % (node, type(node)))
457 % (node, type(node)))
458 except NodeDoesNotExistError:
458 except NodeDoesNotExistError:
459 commit = EmptyCommit(
459 commit = EmptyCommit(
460 commit_id=commit_id,
460 commit_id=commit_id,
461 idx=commit.idx,
461 idx=commit.idx,
462 repo=commit.repository,
462 repo=commit.repository,
463 alias=commit.repository.alias,
463 alias=commit.repository.alias,
464 message=commit.message,
464 message=commit.message,
465 author=commit.author,
465 author=commit.author,
466 date=commit.date)
466 date=commit.date)
467 node = FileNode(f_path, '', commit=commit)
467 node = FileNode(f_path, '', commit=commit)
468 else:
468 else:
469 commit = EmptyCommit(
469 commit = EmptyCommit(
470 repo=self.rhodecode_vcs_repo,
470 repo=self.rhodecode_vcs_repo,
471 alias=self.rhodecode_vcs_repo.alias)
471 alias=self.rhodecode_vcs_repo.alias)
472 node = FileNode(f_path, '', commit=commit)
472 node = FileNode(f_path, '', commit=commit)
473 return node
473 return node
474
474
475 @LoginRequired()
475 @LoginRequired()
476 @HasRepoPermissionAnyDecorator(
476 @HasRepoPermissionAnyDecorator(
477 'repository.read', 'repository.write', 'repository.admin')
477 'repository.read', 'repository.write', 'repository.admin')
478 @view_config(
478 @view_config(
479 route_name='repo_files_diff', request_method='GET',
479 route_name='repo_files_diff', request_method='GET',
480 renderer=None)
480 renderer=None)
481 def repo_files_diff(self):
481 def repo_files_diff(self):
482 c = self.load_default_context()
482 c = self.load_default_context()
483 f_path = self._get_f_path(self.request.matchdict)
483 f_path = self._get_f_path(self.request.matchdict)
484 diff1 = self.request.GET.get('diff1', '')
484 diff1 = self.request.GET.get('diff1', '')
485 diff2 = self.request.GET.get('diff2', '')
485 diff2 = self.request.GET.get('diff2', '')
486
486
487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488
488
489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 line_context = self.request.GET.get('context', 3)
490 line_context = self.request.GET.get('context', 3)
491
491
492 if not any((diff1, diff2)):
492 if not any((diff1, diff2)):
493 h.flash(
493 h.flash(
494 'Need query parameter "diff1" or "diff2" to generate a diff.',
494 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 category='error')
495 category='error')
496 raise HTTPBadRequest()
496 raise HTTPBadRequest()
497
497
498 c.action = self.request.GET.get('diff')
498 c.action = self.request.GET.get('diff')
499 if c.action not in ['download', 'raw']:
499 if c.action not in ['download', 'raw']:
500 compare_url = h.route_path(
500 compare_url = h.route_path(
501 'repo_compare',
501 'repo_compare',
502 repo_name=self.db_repo_name,
502 repo_name=self.db_repo_name,
503 source_ref_type='rev',
503 source_ref_type='rev',
504 source_ref=diff1,
504 source_ref=diff1,
505 target_repo=self.db_repo_name,
505 target_repo=self.db_repo_name,
506 target_ref_type='rev',
506 target_ref_type='rev',
507 target_ref=diff2,
507 target_ref=diff2,
508 _query=dict(f_path=f_path))
508 _query=dict(f_path=f_path))
509 # redirect to new view if we render diff
509 # redirect to new view if we render diff
510 raise HTTPFound(compare_url)
510 raise HTTPFound(compare_url)
511
511
512 try:
512 try:
513 node1 = self._get_file_node(diff1, path1)
513 node1 = self._get_file_node(diff1, path1)
514 node2 = self._get_file_node(diff2, f_path)
514 node2 = self._get_file_node(diff2, f_path)
515 except (RepositoryError, NodeError):
515 except (RepositoryError, NodeError):
516 log.exception("Exception while trying to get node from repository")
516 log.exception("Exception while trying to get node from repository")
517 raise HTTPFound(
517 raise HTTPFound(
518 h.route_path('repo_files', repo_name=self.db_repo_name,
518 h.route_path('repo_files', repo_name=self.db_repo_name,
519 commit_id='tip', f_path=f_path))
519 commit_id='tip', f_path=f_path))
520
520
521 if all(isinstance(node.commit, EmptyCommit)
521 if all(isinstance(node.commit, EmptyCommit)
522 for node in (node1, node2)):
522 for node in (node1, node2)):
523 raise HTTPNotFound()
523 raise HTTPNotFound()
524
524
525 c.commit_1 = node1.commit
525 c.commit_1 = node1.commit
526 c.commit_2 = node2.commit
526 c.commit_2 = node2.commit
527
527
528 if c.action == 'download':
528 if c.action == 'download':
529 _diff = diffs.get_gitdiff(node1, node2,
529 _diff = diffs.get_gitdiff(node1, node2,
530 ignore_whitespace=ignore_whitespace,
530 ignore_whitespace=ignore_whitespace,
531 context=line_context)
531 context=line_context)
532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533
533
534 response = Response(self.path_filter.get_raw_patch(diff))
534 response = Response(self.path_filter.get_raw_patch(diff))
535 response.content_type = 'text/plain'
535 response.content_type = 'text/plain'
536 response.content_disposition = (
536 response.content_disposition = (
537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 )
538 )
539 charset = self._get_default_encoding(c)
539 charset = self._get_default_encoding(c)
540 if charset:
540 if charset:
541 response.charset = charset
541 response.charset = charset
542 return response
542 return response
543
543
544 elif c.action == 'raw':
544 elif c.action == 'raw':
545 _diff = diffs.get_gitdiff(node1, node2,
545 _diff = diffs.get_gitdiff(node1, node2,
546 ignore_whitespace=ignore_whitespace,
546 ignore_whitespace=ignore_whitespace,
547 context=line_context)
547 context=line_context)
548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549
549
550 response = Response(self.path_filter.get_raw_patch(diff))
550 response = Response(self.path_filter.get_raw_patch(diff))
551 response.content_type = 'text/plain'
551 response.content_type = 'text/plain'
552 charset = self._get_default_encoding(c)
552 charset = self._get_default_encoding(c)
553 if charset:
553 if charset:
554 response.charset = charset
554 response.charset = charset
555 return response
555 return response
556
556
557 # in case we ever end up here
557 # in case we ever end up here
558 raise HTTPNotFound()
558 raise HTTPNotFound()
559
559
560 @LoginRequired()
560 @LoginRequired()
561 @HasRepoPermissionAnyDecorator(
561 @HasRepoPermissionAnyDecorator(
562 'repository.read', 'repository.write', 'repository.admin')
562 'repository.read', 'repository.write', 'repository.admin')
563 @view_config(
563 @view_config(
564 route_name='repo_files_diff_2way_redirect', request_method='GET',
564 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 renderer=None)
565 renderer=None)
566 def repo_files_diff_2way_redirect(self):
566 def repo_files_diff_2way_redirect(self):
567 """
567 """
568 Kept only to make OLD links work
568 Kept only to make OLD links work
569 """
569 """
570 f_path = self._get_f_path_unchecked(self.request.matchdict)
570 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 diff1 = self.request.GET.get('diff1', '')
571 diff1 = self.request.GET.get('diff1', '')
572 diff2 = self.request.GET.get('diff2', '')
572 diff2 = self.request.GET.get('diff2', '')
573
573
574 if not any((diff1, diff2)):
574 if not any((diff1, diff2)):
575 h.flash(
575 h.flash(
576 'Need query parameter "diff1" or "diff2" to generate a diff.',
576 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 category='error')
577 category='error')
578 raise HTTPBadRequest()
578 raise HTTPBadRequest()
579
579
580 compare_url = h.route_path(
580 compare_url = h.route_path(
581 'repo_compare',
581 'repo_compare',
582 repo_name=self.db_repo_name,
582 repo_name=self.db_repo_name,
583 source_ref_type='rev',
583 source_ref_type='rev',
584 source_ref=diff1,
584 source_ref=diff1,
585 target_ref_type='rev',
585 target_ref_type='rev',
586 target_ref=diff2,
586 target_ref=diff2,
587 _query=dict(f_path=f_path, diffmode='sideside',
587 _query=dict(f_path=f_path, diffmode='sideside',
588 target_repo=self.db_repo_name,))
588 target_repo=self.db_repo_name,))
589 raise HTTPFound(compare_url)
589 raise HTTPFound(compare_url)
590
590
591 @LoginRequired()
591 @LoginRequired()
592 @view_config(
593 route_name='repo_files:default_commit', request_method='GET',
594 renderer=None)
595 def repo_files_default(self):
596 c = self.load_default_context()
597
598 landing_url = h.route_path(
599 'repo_files', repo_name=c.repo_name,
600 commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='',
601 _query={'at': c.rhodecode_db_repo.landing_ref_name})
602
603 raise HTTPFound(landing_url)
604
605 @LoginRequired()
592 @HasRepoPermissionAnyDecorator(
606 @HasRepoPermissionAnyDecorator(
593 'repository.read', 'repository.write', 'repository.admin')
607 'repository.read', 'repository.write', 'repository.admin')
594 @view_config(
608 @view_config(
595 route_name='repo_files', request_method='GET',
609 route_name='repo_files', request_method='GET',
596 renderer=None)
610 renderer=None)
597 @view_config(
611 @view_config(
598 route_name='repo_files:default_path', request_method='GET',
612 route_name='repo_files:default_path', request_method='GET',
599 renderer=None)
613 renderer=None)
600 @view_config(
614 @view_config(
601 route_name='repo_files:default_commit', request_method='GET',
602 renderer=None)
603 @view_config(
604 route_name='repo_files:rendered', request_method='GET',
615 route_name='repo_files:rendered', request_method='GET',
605 renderer=None)
616 renderer=None)
606 @view_config(
617 @view_config(
607 route_name='repo_files:annotated', request_method='GET',
618 route_name='repo_files:annotated', request_method='GET',
608 renderer=None)
619 renderer=None)
609 def repo_files(self):
620 def repo_files(self):
610 c = self.load_default_context()
621 c = self.load_default_context()
611
622
612 view_name = getattr(self.request.matched_route, 'name', None)
623 view_name = getattr(self.request.matched_route, 'name', None)
613
624
614 c.annotate = view_name == 'repo_files:annotated'
625 c.annotate = view_name == 'repo_files:annotated'
615 # default is false, but .rst/.md files later are auto rendered, we can
626 # default is false, but .rst/.md files later are auto rendered, we can
616 # overwrite auto rendering by setting this GET flag
627 # overwrite auto rendering by setting this GET flag
617 c.renderer = view_name == 'repo_files:rendered' or \
628 c.renderer = view_name == 'repo_files:rendered' or \
618 not self.request.GET.get('no-render', False)
629 not self.request.GET.get('no-render', False)
619
630
620 commit_id, f_path = self._get_commit_and_path()
631 commit_id, f_path = self._get_commit_and_path()
621
632
622 c.commit = self._get_commit_or_redirect(commit_id)
633 c.commit = self._get_commit_or_redirect(commit_id)
623 c.branch = self.request.GET.get('branch', None)
634 c.branch = self.request.GET.get('branch', None)
624 c.f_path = f_path
635 c.f_path = f_path
625 at_rev = self.request.GET.get('at')
636 at_rev = self.request.GET.get('at')
626
637
627 # prev link
638 # prev link
628 try:
639 try:
629 prev_commit = c.commit.prev(c.branch)
640 prev_commit = c.commit.prev(c.branch)
630 c.prev_commit = prev_commit
641 c.prev_commit = prev_commit
631 c.url_prev = h.route_path(
642 c.url_prev = h.route_path(
632 'repo_files', repo_name=self.db_repo_name,
643 'repo_files', repo_name=self.db_repo_name,
633 commit_id=prev_commit.raw_id, f_path=f_path)
644 commit_id=prev_commit.raw_id, f_path=f_path)
634 if c.branch:
645 if c.branch:
635 c.url_prev += '?branch=%s' % c.branch
646 c.url_prev += '?branch=%s' % c.branch
636 except (CommitDoesNotExistError, VCSError):
647 except (CommitDoesNotExistError, VCSError):
637 c.url_prev = '#'
648 c.url_prev = '#'
638 c.prev_commit = EmptyCommit()
649 c.prev_commit = EmptyCommit()
639
650
640 # next link
651 # next link
641 try:
652 try:
642 next_commit = c.commit.next(c.branch)
653 next_commit = c.commit.next(c.branch)
643 c.next_commit = next_commit
654 c.next_commit = next_commit
644 c.url_next = h.route_path(
655 c.url_next = h.route_path(
645 'repo_files', repo_name=self.db_repo_name,
656 'repo_files', repo_name=self.db_repo_name,
646 commit_id=next_commit.raw_id, f_path=f_path)
657 commit_id=next_commit.raw_id, f_path=f_path)
647 if c.branch:
658 if c.branch:
648 c.url_next += '?branch=%s' % c.branch
659 c.url_next += '?branch=%s' % c.branch
649 except (CommitDoesNotExistError, VCSError):
660 except (CommitDoesNotExistError, VCSError):
650 c.url_next = '#'
661 c.url_next = '#'
651 c.next_commit = EmptyCommit()
662 c.next_commit = EmptyCommit()
652
663
653 # files or dirs
664 # files or dirs
654 try:
665 try:
655 c.file = c.commit.get_node(f_path)
666 c.file = c.commit.get_node(f_path)
656 c.file_author = True
667 c.file_author = True
657 c.file_tree = ''
668 c.file_tree = ''
658
669
659 # load file content
670 # load file content
660 if c.file.is_file():
671 if c.file.is_file():
661 c.lf_node = {}
672 c.lf_node = {}
662
673
663 has_lf_enabled = self._is_lf_enabled(self.db_repo)
674 has_lf_enabled = self._is_lf_enabled(self.db_repo)
664 if has_lf_enabled:
675 if has_lf_enabled:
665 c.lf_node = c.file.get_largefile_node()
676 c.lf_node = c.file.get_largefile_node()
666
677
667 c.file_source_page = 'true'
678 c.file_source_page = 'true'
668 c.file_last_commit = c.file.last_commit
679 c.file_last_commit = c.file.last_commit
669
680
670 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
681 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
671
682
672 if not (c.file_size_too_big or c.file.is_binary):
683 if not (c.file_size_too_big or c.file.is_binary):
673 if c.annotate: # annotation has precedence over renderer
684 if c.annotate: # annotation has precedence over renderer
674 c.annotated_lines = filenode_as_annotated_lines_tokens(
685 c.annotated_lines = filenode_as_annotated_lines_tokens(
675 c.file
686 c.file
676 )
687 )
677 else:
688 else:
678 c.renderer = (
689 c.renderer = (
679 c.renderer and h.renderer_from_filename(c.file.path)
690 c.renderer and h.renderer_from_filename(c.file.path)
680 )
691 )
681 if not c.renderer:
692 if not c.renderer:
682 c.lines = filenode_as_lines_tokens(c.file)
693 c.lines = filenode_as_lines_tokens(c.file)
683
694
684 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
695 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
685 commit_id, self.rhodecode_vcs_repo)
696 commit_id, self.rhodecode_vcs_repo)
686 c.on_branch_head = is_head
697 c.on_branch_head = is_head
687
698
688 branch = c.commit.branch if (
699 branch = c.commit.branch if (
689 c.commit.branch and '/' not in c.commit.branch) else None
700 c.commit.branch and '/' not in c.commit.branch) else None
690 c.branch_or_raw_id = branch or c.commit.raw_id
701 c.branch_or_raw_id = branch or c.commit.raw_id
691 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
702 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
692
703
693 author = c.file_last_commit.author
704 author = c.file_last_commit.author
694 c.authors = [[
705 c.authors = [[
695 h.email(author),
706 h.email(author),
696 h.person(author, 'username_or_name_or_email'),
707 h.person(author, 'username_or_name_or_email'),
697 1
708 1
698 ]]
709 ]]
699
710
700 else: # load tree content at path
711 else: # load tree content at path
701 c.file_source_page = 'false'
712 c.file_source_page = 'false'
702 c.authors = []
713 c.authors = []
703 # this loads a simple tree without metadata to speed things up
714 # this loads a simple tree without metadata to speed things up
704 # later via ajax we call repo_nodetree_full and fetch whole
715 # later via ajax we call repo_nodetree_full and fetch whole
705 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
716 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
706
717
707 c.readme_data, c.readme_file = \
718 c.readme_data, c.readme_file = \
708 self._get_readme_data(self.db_repo, c.visual.default_renderer,
719 self._get_readme_data(self.db_repo, c.visual.default_renderer,
709 c.commit.raw_id, f_path)
720 c.commit.raw_id, f_path)
710
721
711 except RepositoryError as e:
722 except RepositoryError as e:
712 h.flash(safe_str(h.escape(e)), category='error')
723 h.flash(safe_str(h.escape(e)), category='error')
713 raise HTTPNotFound()
724 raise HTTPNotFound()
714
725
715 if self.request.environ.get('HTTP_X_PJAX'):
726 if self.request.environ.get('HTTP_X_PJAX'):
716 html = render('rhodecode:templates/files/files_pjax.mako',
727 html = render('rhodecode:templates/files/files_pjax.mako',
717 self._get_template_context(c), self.request)
728 self._get_template_context(c), self.request)
718 else:
729 else:
719 html = render('rhodecode:templates/files/files.mako',
730 html = render('rhodecode:templates/files/files.mako',
720 self._get_template_context(c), self.request)
731 self._get_template_context(c), self.request)
721 return Response(html)
732 return Response(html)
722
733
723 @HasRepoPermissionAnyDecorator(
734 @HasRepoPermissionAnyDecorator(
724 'repository.read', 'repository.write', 'repository.admin')
735 'repository.read', 'repository.write', 'repository.admin')
725 @view_config(
736 @view_config(
726 route_name='repo_files:annotated_previous', request_method='GET',
737 route_name='repo_files:annotated_previous', request_method='GET',
727 renderer=None)
738 renderer=None)
728 def repo_files_annotated_previous(self):
739 def repo_files_annotated_previous(self):
729 self.load_default_context()
740 self.load_default_context()
730
741
731 commit_id, f_path = self._get_commit_and_path()
742 commit_id, f_path = self._get_commit_and_path()
732 commit = self._get_commit_or_redirect(commit_id)
743 commit = self._get_commit_or_redirect(commit_id)
733 prev_commit_id = commit.raw_id
744 prev_commit_id = commit.raw_id
734 line_anchor = self.request.GET.get('line_anchor')
745 line_anchor = self.request.GET.get('line_anchor')
735 is_file = False
746 is_file = False
736 try:
747 try:
737 _file = commit.get_node(f_path)
748 _file = commit.get_node(f_path)
738 is_file = _file.is_file()
749 is_file = _file.is_file()
739 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
750 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
740 pass
751 pass
741
752
742 if is_file:
753 if is_file:
743 history = commit.get_path_history(f_path)
754 history = commit.get_path_history(f_path)
744 prev_commit_id = history[1].raw_id \
755 prev_commit_id = history[1].raw_id \
745 if len(history) > 1 else prev_commit_id
756 if len(history) > 1 else prev_commit_id
746 prev_url = h.route_path(
757 prev_url = h.route_path(
747 'repo_files:annotated', repo_name=self.db_repo_name,
758 'repo_files:annotated', repo_name=self.db_repo_name,
748 commit_id=prev_commit_id, f_path=f_path,
759 commit_id=prev_commit_id, f_path=f_path,
749 _anchor='L{}'.format(line_anchor))
760 _anchor='L{}'.format(line_anchor))
750
761
751 raise HTTPFound(prev_url)
762 raise HTTPFound(prev_url)
752
763
753 @LoginRequired()
764 @LoginRequired()
754 @HasRepoPermissionAnyDecorator(
765 @HasRepoPermissionAnyDecorator(
755 'repository.read', 'repository.write', 'repository.admin')
766 'repository.read', 'repository.write', 'repository.admin')
756 @view_config(
767 @view_config(
757 route_name='repo_nodetree_full', request_method='GET',
768 route_name='repo_nodetree_full', request_method='GET',
758 renderer=None, xhr=True)
769 renderer=None, xhr=True)
759 @view_config(
770 @view_config(
760 route_name='repo_nodetree_full:default_path', request_method='GET',
771 route_name='repo_nodetree_full:default_path', request_method='GET',
761 renderer=None, xhr=True)
772 renderer=None, xhr=True)
762 def repo_nodetree_full(self):
773 def repo_nodetree_full(self):
763 """
774 """
764 Returns rendered html of file tree that contains commit date,
775 Returns rendered html of file tree that contains commit date,
765 author, commit_id for the specified combination of
776 author, commit_id for the specified combination of
766 repo, commit_id and file path
777 repo, commit_id and file path
767 """
778 """
768 c = self.load_default_context()
779 c = self.load_default_context()
769
780
770 commit_id, f_path = self._get_commit_and_path()
781 commit_id, f_path = self._get_commit_and_path()
771 commit = self._get_commit_or_redirect(commit_id)
782 commit = self._get_commit_or_redirect(commit_id)
772 try:
783 try:
773 dir_node = commit.get_node(f_path)
784 dir_node = commit.get_node(f_path)
774 except RepositoryError as e:
785 except RepositoryError as e:
775 return Response('error: {}'.format(h.escape(safe_str(e))))
786 return Response('error: {}'.format(h.escape(safe_str(e))))
776
787
777 if dir_node.is_file():
788 if dir_node.is_file():
778 return Response('')
789 return Response('')
779
790
780 c.file = dir_node
791 c.file = dir_node
781 c.commit = commit
792 c.commit = commit
782 at_rev = self.request.GET.get('at')
793 at_rev = self.request.GET.get('at')
783
794
784 html = self._get_tree_at_commit(
795 html = self._get_tree_at_commit(
785 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
796 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
786
797
787 return Response(html)
798 return Response(html)
788
799
789 def _get_attachement_headers(self, f_path):
800 def _get_attachement_headers(self, f_path):
790 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
801 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
791 safe_path = f_name.replace('"', '\\"')
802 safe_path = f_name.replace('"', '\\"')
792 encoded_path = urllib.quote(f_name)
803 encoded_path = urllib.quote(f_name)
793
804
794 return "attachment; " \
805 return "attachment; " \
795 "filename=\"{}\"; " \
806 "filename=\"{}\"; " \
796 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
807 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
797
808
798 @LoginRequired()
809 @LoginRequired()
799 @HasRepoPermissionAnyDecorator(
810 @HasRepoPermissionAnyDecorator(
800 'repository.read', 'repository.write', 'repository.admin')
811 'repository.read', 'repository.write', 'repository.admin')
801 @view_config(
812 @view_config(
802 route_name='repo_file_raw', request_method='GET',
813 route_name='repo_file_raw', request_method='GET',
803 renderer=None)
814 renderer=None)
804 def repo_file_raw(self):
815 def repo_file_raw(self):
805 """
816 """
806 Action for show as raw, some mimetypes are "rendered",
817 Action for show as raw, some mimetypes are "rendered",
807 those include images, icons.
818 those include images, icons.
808 """
819 """
809 c = self.load_default_context()
820 c = self.load_default_context()
810
821
811 commit_id, f_path = self._get_commit_and_path()
822 commit_id, f_path = self._get_commit_and_path()
812 commit = self._get_commit_or_redirect(commit_id)
823 commit = self._get_commit_or_redirect(commit_id)
813 file_node = self._get_filenode_or_redirect(commit, f_path)
824 file_node = self._get_filenode_or_redirect(commit, f_path)
814
825
815 raw_mimetype_mapping = {
826 raw_mimetype_mapping = {
816 # map original mimetype to a mimetype used for "show as raw"
827 # map original mimetype to a mimetype used for "show as raw"
817 # you can also provide a content-disposition to override the
828 # you can also provide a content-disposition to override the
818 # default "attachment" disposition.
829 # default "attachment" disposition.
819 # orig_type: (new_type, new_dispo)
830 # orig_type: (new_type, new_dispo)
820
831
821 # show images inline:
832 # show images inline:
822 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
833 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
823 # for example render an SVG with javascript inside or even render
834 # for example render an SVG with javascript inside or even render
824 # HTML.
835 # HTML.
825 'image/x-icon': ('image/x-icon', 'inline'),
836 'image/x-icon': ('image/x-icon', 'inline'),
826 'image/png': ('image/png', 'inline'),
837 'image/png': ('image/png', 'inline'),
827 'image/gif': ('image/gif', 'inline'),
838 'image/gif': ('image/gif', 'inline'),
828 'image/jpeg': ('image/jpeg', 'inline'),
839 'image/jpeg': ('image/jpeg', 'inline'),
829 'application/pdf': ('application/pdf', 'inline'),
840 'application/pdf': ('application/pdf', 'inline'),
830 }
841 }
831
842
832 mimetype = file_node.mimetype
843 mimetype = file_node.mimetype
833 try:
844 try:
834 mimetype, disposition = raw_mimetype_mapping[mimetype]
845 mimetype, disposition = raw_mimetype_mapping[mimetype]
835 except KeyError:
846 except KeyError:
836 # we don't know anything special about this, handle it safely
847 # we don't know anything special about this, handle it safely
837 if file_node.is_binary:
848 if file_node.is_binary:
838 # do same as download raw for binary files
849 # do same as download raw for binary files
839 mimetype, disposition = 'application/octet-stream', 'attachment'
850 mimetype, disposition = 'application/octet-stream', 'attachment'
840 else:
851 else:
841 # do not just use the original mimetype, but force text/plain,
852 # do not just use the original mimetype, but force text/plain,
842 # otherwise it would serve text/html and that might be unsafe.
853 # otherwise it would serve text/html and that might be unsafe.
843 # Note: underlying vcs library fakes text/plain mimetype if the
854 # Note: underlying vcs library fakes text/plain mimetype if the
844 # mimetype can not be determined and it thinks it is not
855 # mimetype can not be determined and it thinks it is not
845 # binary.This might lead to erroneous text display in some
856 # binary.This might lead to erroneous text display in some
846 # cases, but helps in other cases, like with text files
857 # cases, but helps in other cases, like with text files
847 # without extension.
858 # without extension.
848 mimetype, disposition = 'text/plain', 'inline'
859 mimetype, disposition = 'text/plain', 'inline'
849
860
850 if disposition == 'attachment':
861 if disposition == 'attachment':
851 disposition = self._get_attachement_headers(f_path)
862 disposition = self._get_attachement_headers(f_path)
852
863
853 stream_content = file_node.stream_bytes()
864 stream_content = file_node.stream_bytes()
854
865
855 response = Response(app_iter=stream_content)
866 response = Response(app_iter=stream_content)
856 response.content_disposition = disposition
867 response.content_disposition = disposition
857 response.content_type = mimetype
868 response.content_type = mimetype
858
869
859 charset = self._get_default_encoding(c)
870 charset = self._get_default_encoding(c)
860 if charset:
871 if charset:
861 response.charset = charset
872 response.charset = charset
862
873
863 return response
874 return response
864
875
865 @LoginRequired()
876 @LoginRequired()
866 @HasRepoPermissionAnyDecorator(
877 @HasRepoPermissionAnyDecorator(
867 'repository.read', 'repository.write', 'repository.admin')
878 'repository.read', 'repository.write', 'repository.admin')
868 @view_config(
879 @view_config(
869 route_name='repo_file_download', request_method='GET',
880 route_name='repo_file_download', request_method='GET',
870 renderer=None)
881 renderer=None)
871 @view_config(
882 @view_config(
872 route_name='repo_file_download:legacy', request_method='GET',
883 route_name='repo_file_download:legacy', request_method='GET',
873 renderer=None)
884 renderer=None)
874 def repo_file_download(self):
885 def repo_file_download(self):
875 c = self.load_default_context()
886 c = self.load_default_context()
876
887
877 commit_id, f_path = self._get_commit_and_path()
888 commit_id, f_path = self._get_commit_and_path()
878 commit = self._get_commit_or_redirect(commit_id)
889 commit = self._get_commit_or_redirect(commit_id)
879 file_node = self._get_filenode_or_redirect(commit, f_path)
890 file_node = self._get_filenode_or_redirect(commit, f_path)
880
891
881 if self.request.GET.get('lf'):
892 if self.request.GET.get('lf'):
882 # only if lf get flag is passed, we download this file
893 # only if lf get flag is passed, we download this file
883 # as LFS/Largefile
894 # as LFS/Largefile
884 lf_node = file_node.get_largefile_node()
895 lf_node = file_node.get_largefile_node()
885 if lf_node:
896 if lf_node:
886 # overwrite our pointer with the REAL large-file
897 # overwrite our pointer with the REAL large-file
887 file_node = lf_node
898 file_node = lf_node
888
899
889 disposition = self._get_attachement_headers(f_path)
900 disposition = self._get_attachement_headers(f_path)
890
901
891 stream_content = file_node.stream_bytes()
902 stream_content = file_node.stream_bytes()
892
903
893 response = Response(app_iter=stream_content)
904 response = Response(app_iter=stream_content)
894 response.content_disposition = disposition
905 response.content_disposition = disposition
895 response.content_type = file_node.mimetype
906 response.content_type = file_node.mimetype
896
907
897 charset = self._get_default_encoding(c)
908 charset = self._get_default_encoding(c)
898 if charset:
909 if charset:
899 response.charset = charset
910 response.charset = charset
900
911
901 return response
912 return response
902
913
903 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
914 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
904
915
905 cache_seconds = safe_int(
916 cache_seconds = safe_int(
906 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
917 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
907 cache_on = cache_seconds > 0
918 cache_on = cache_seconds > 0
908 log.debug(
919 log.debug(
909 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
920 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
910 'with caching: %s[TTL: %ss]' % (
921 'with caching: %s[TTL: %ss]' % (
911 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
922 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
912
923
913 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
924 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
914 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
925 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
915
926
916 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
927 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
917 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
928 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
918 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
929 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
919 _repo_id, commit_id, f_path)
930 _repo_id, commit_id, f_path)
920 try:
931 try:
921 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
932 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
922 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
933 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
923 log.exception(safe_str(e))
934 log.exception(safe_str(e))
924 h.flash(safe_str(h.escape(e)), category='error')
935 h.flash(safe_str(h.escape(e)), category='error')
925 raise HTTPFound(h.route_path(
936 raise HTTPFound(h.route_path(
926 'repo_files', repo_name=self.db_repo_name,
937 'repo_files', repo_name=self.db_repo_name,
927 commit_id='tip', f_path='/'))
938 commit_id='tip', f_path='/'))
928
939
929 return _d + _f
940 return _d + _f
930
941
931 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
942 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
932 commit_id, f_path)
943 commit_id, f_path)
933 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
944 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
934
945
935 @LoginRequired()
946 @LoginRequired()
936 @HasRepoPermissionAnyDecorator(
947 @HasRepoPermissionAnyDecorator(
937 'repository.read', 'repository.write', 'repository.admin')
948 'repository.read', 'repository.write', 'repository.admin')
938 @view_config(
949 @view_config(
939 route_name='repo_files_nodelist', request_method='GET',
950 route_name='repo_files_nodelist', request_method='GET',
940 renderer='json_ext', xhr=True)
951 renderer='json_ext', xhr=True)
941 def repo_nodelist(self):
952 def repo_nodelist(self):
942 self.load_default_context()
953 self.load_default_context()
943
954
944 commit_id, f_path = self._get_commit_and_path()
955 commit_id, f_path = self._get_commit_and_path()
945 commit = self._get_commit_or_redirect(commit_id)
956 commit = self._get_commit_or_redirect(commit_id)
946
957
947 metadata = self._get_nodelist_at_commit(
958 metadata = self._get_nodelist_at_commit(
948 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
959 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
949 return {'nodes': metadata}
960 return {'nodes': metadata}
950
961
951 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
962 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
952 items = []
963 items = []
953 for name, commit_id in branches_or_tags.items():
964 for name, commit_id in branches_or_tags.items():
954 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
965 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
955 items.append((sym_ref, name, ref_type))
966 items.append((sym_ref, name, ref_type))
956 return items
967 return items
957
968
958 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
969 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
959 return commit_id
970 return commit_id
960
971
961 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
972 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
962 return commit_id
973 return commit_id
963
974
964 # NOTE(dan): old code we used in "diff" mode compare
975 # NOTE(dan): old code we used in "diff" mode compare
965 new_f_path = vcspath.join(name, f_path)
976 new_f_path = vcspath.join(name, f_path)
966 return u'%s@%s' % (new_f_path, commit_id)
977 return u'%s@%s' % (new_f_path, commit_id)
967
978
968 def _get_node_history(self, commit_obj, f_path, commits=None):
979 def _get_node_history(self, commit_obj, f_path, commits=None):
969 """
980 """
970 get commit history for given node
981 get commit history for given node
971
982
972 :param commit_obj: commit to calculate history
983 :param commit_obj: commit to calculate history
973 :param f_path: path for node to calculate history for
984 :param f_path: path for node to calculate history for
974 :param commits: if passed don't calculate history and take
985 :param commits: if passed don't calculate history and take
975 commits defined in this list
986 commits defined in this list
976 """
987 """
977 _ = self.request.translate
988 _ = self.request.translate
978
989
979 # calculate history based on tip
990 # calculate history based on tip
980 tip = self.rhodecode_vcs_repo.get_commit()
991 tip = self.rhodecode_vcs_repo.get_commit()
981 if commits is None:
992 if commits is None:
982 pre_load = ["author", "branch"]
993 pre_load = ["author", "branch"]
983 try:
994 try:
984 commits = tip.get_path_history(f_path, pre_load=pre_load)
995 commits = tip.get_path_history(f_path, pre_load=pre_load)
985 except (NodeDoesNotExistError, CommitError):
996 except (NodeDoesNotExistError, CommitError):
986 # this node is not present at tip!
997 # this node is not present at tip!
987 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
998 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
988
999
989 history = []
1000 history = []
990 commits_group = ([], _("Changesets"))
1001 commits_group = ([], _("Changesets"))
991 for commit in commits:
1002 for commit in commits:
992 branch = ' (%s)' % commit.branch if commit.branch else ''
1003 branch = ' (%s)' % commit.branch if commit.branch else ''
993 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1004 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
994 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1005 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
995 history.append(commits_group)
1006 history.append(commits_group)
996
1007
997 symbolic_reference = self._symbolic_reference
1008 symbolic_reference = self._symbolic_reference
998
1009
999 if self.rhodecode_vcs_repo.alias == 'svn':
1010 if self.rhodecode_vcs_repo.alias == 'svn':
1000 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1011 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1001 f_path, self.rhodecode_vcs_repo)
1012 f_path, self.rhodecode_vcs_repo)
1002 if adjusted_f_path != f_path:
1013 if adjusted_f_path != f_path:
1003 log.debug(
1014 log.debug(
1004 'Recognized svn tag or branch in file "%s", using svn '
1015 'Recognized svn tag or branch in file "%s", using svn '
1005 'specific symbolic references', f_path)
1016 'specific symbolic references', f_path)
1006 f_path = adjusted_f_path
1017 f_path = adjusted_f_path
1007 symbolic_reference = self._symbolic_reference_svn
1018 symbolic_reference = self._symbolic_reference_svn
1008
1019
1009 branches = self._create_references(
1020 branches = self._create_references(
1010 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1021 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1011 branches_group = (branches, _("Branches"))
1022 branches_group = (branches, _("Branches"))
1012
1023
1013 tags = self._create_references(
1024 tags = self._create_references(
1014 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1025 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1015 tags_group = (tags, _("Tags"))
1026 tags_group = (tags, _("Tags"))
1016
1027
1017 history.append(branches_group)
1028 history.append(branches_group)
1018 history.append(tags_group)
1029 history.append(tags_group)
1019
1030
1020 return history, commits
1031 return history, commits
1021
1032
1022 @LoginRequired()
1033 @LoginRequired()
1023 @HasRepoPermissionAnyDecorator(
1034 @HasRepoPermissionAnyDecorator(
1024 'repository.read', 'repository.write', 'repository.admin')
1035 'repository.read', 'repository.write', 'repository.admin')
1025 @view_config(
1036 @view_config(
1026 route_name='repo_file_history', request_method='GET',
1037 route_name='repo_file_history', request_method='GET',
1027 renderer='json_ext')
1038 renderer='json_ext')
1028 def repo_file_history(self):
1039 def repo_file_history(self):
1029 self.load_default_context()
1040 self.load_default_context()
1030
1041
1031 commit_id, f_path = self._get_commit_and_path()
1042 commit_id, f_path = self._get_commit_and_path()
1032 commit = self._get_commit_or_redirect(commit_id)
1043 commit = self._get_commit_or_redirect(commit_id)
1033 file_node = self._get_filenode_or_redirect(commit, f_path)
1044 file_node = self._get_filenode_or_redirect(commit, f_path)
1034
1045
1035 if file_node.is_file():
1046 if file_node.is_file():
1036 file_history, _hist = self._get_node_history(commit, f_path)
1047 file_history, _hist = self._get_node_history(commit, f_path)
1037
1048
1038 res = []
1049 res = []
1039 for section_items, section in file_history:
1050 for section_items, section in file_history:
1040 items = []
1051 items = []
1041 for obj_id, obj_text, obj_type in section_items:
1052 for obj_id, obj_text, obj_type in section_items:
1042 at_rev = ''
1053 at_rev = ''
1043 if obj_type in ['branch', 'bookmark', 'tag']:
1054 if obj_type in ['branch', 'bookmark', 'tag']:
1044 at_rev = obj_text
1055 at_rev = obj_text
1045 entry = {
1056 entry = {
1046 'id': obj_id,
1057 'id': obj_id,
1047 'text': obj_text,
1058 'text': obj_text,
1048 'type': obj_type,
1059 'type': obj_type,
1049 'at_rev': at_rev
1060 'at_rev': at_rev
1050 }
1061 }
1051
1062
1052 items.append(entry)
1063 items.append(entry)
1053
1064
1054 res.append({
1065 res.append({
1055 'text': section,
1066 'text': section,
1056 'children': items
1067 'children': items
1057 })
1068 })
1058
1069
1059 data = {
1070 data = {
1060 'more': False,
1071 'more': False,
1061 'results': res
1072 'results': res
1062 }
1073 }
1063 return data
1074 return data
1064
1075
1065 log.warning('Cannot fetch history for directory')
1076 log.warning('Cannot fetch history for directory')
1066 raise HTTPBadRequest()
1077 raise HTTPBadRequest()
1067
1078
1068 @LoginRequired()
1079 @LoginRequired()
1069 @HasRepoPermissionAnyDecorator(
1080 @HasRepoPermissionAnyDecorator(
1070 'repository.read', 'repository.write', 'repository.admin')
1081 'repository.read', 'repository.write', 'repository.admin')
1071 @view_config(
1082 @view_config(
1072 route_name='repo_file_authors', request_method='GET',
1083 route_name='repo_file_authors', request_method='GET',
1073 renderer='rhodecode:templates/files/file_authors_box.mako')
1084 renderer='rhodecode:templates/files/file_authors_box.mako')
1074 def repo_file_authors(self):
1085 def repo_file_authors(self):
1075 c = self.load_default_context()
1086 c = self.load_default_context()
1076
1087
1077 commit_id, f_path = self._get_commit_and_path()
1088 commit_id, f_path = self._get_commit_and_path()
1078 commit = self._get_commit_or_redirect(commit_id)
1089 commit = self._get_commit_or_redirect(commit_id)
1079 file_node = self._get_filenode_or_redirect(commit, f_path)
1090 file_node = self._get_filenode_or_redirect(commit, f_path)
1080
1091
1081 if not file_node.is_file():
1092 if not file_node.is_file():
1082 raise HTTPBadRequest()
1093 raise HTTPBadRequest()
1083
1094
1084 c.file_last_commit = file_node.last_commit
1095 c.file_last_commit = file_node.last_commit
1085 if self.request.GET.get('annotate') == '1':
1096 if self.request.GET.get('annotate') == '1':
1086 # use _hist from annotation if annotation mode is on
1097 # use _hist from annotation if annotation mode is on
1087 commit_ids = set(x[1] for x in file_node.annotate)
1098 commit_ids = set(x[1] for x in file_node.annotate)
1088 _hist = (
1099 _hist = (
1089 self.rhodecode_vcs_repo.get_commit(commit_id)
1100 self.rhodecode_vcs_repo.get_commit(commit_id)
1090 for commit_id in commit_ids)
1101 for commit_id in commit_ids)
1091 else:
1102 else:
1092 _f_history, _hist = self._get_node_history(commit, f_path)
1103 _f_history, _hist = self._get_node_history(commit, f_path)
1093 c.file_author = False
1104 c.file_author = False
1094
1105
1095 unique = collections.OrderedDict()
1106 unique = collections.OrderedDict()
1096 for commit in _hist:
1107 for commit in _hist:
1097 author = commit.author
1108 author = commit.author
1098 if author not in unique:
1109 if author not in unique:
1099 unique[commit.author] = [
1110 unique[commit.author] = [
1100 h.email(author),
1111 h.email(author),
1101 h.person(author, 'username_or_name_or_email'),
1112 h.person(author, 'username_or_name_or_email'),
1102 1 # counter
1113 1 # counter
1103 ]
1114 ]
1104
1115
1105 else:
1116 else:
1106 # increase counter
1117 # increase counter
1107 unique[commit.author][2] += 1
1118 unique[commit.author][2] += 1
1108
1119
1109 c.authors = [val for val in unique.values()]
1120 c.authors = [val for val in unique.values()]
1110
1121
1111 return self._get_template_context(c)
1122 return self._get_template_context(c)
1112
1123
1113 @LoginRequired()
1124 @LoginRequired()
1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1125 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1115 @view_config(
1126 @view_config(
1116 route_name='repo_files_check_head', request_method='POST',
1127 route_name='repo_files_check_head', request_method='POST',
1117 renderer='json_ext', xhr=True)
1128 renderer='json_ext', xhr=True)
1118 def repo_files_check_head(self):
1129 def repo_files_check_head(self):
1119 self.load_default_context()
1130 self.load_default_context()
1120
1131
1121 commit_id, f_path = self._get_commit_and_path()
1132 commit_id, f_path = self._get_commit_and_path()
1122 _branch_name, _sha_commit_id, is_head = \
1133 _branch_name, _sha_commit_id, is_head = \
1123 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1134 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1124
1135
1125 new_path = self.request.POST.get('path')
1136 new_path = self.request.POST.get('path')
1126 operation = self.request.POST.get('operation')
1137 operation = self.request.POST.get('operation')
1127 path_exist = ''
1138 path_exist = ''
1128
1139
1129 if new_path and operation in ['create', 'upload']:
1140 if new_path and operation in ['create', 'upload']:
1130 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1141 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1131 try:
1142 try:
1132 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1143 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1133 # NOTE(dan): construct whole path without leading /
1144 # NOTE(dan): construct whole path without leading /
1134 file_node = commit_obj.get_node(new_f_path)
1145 file_node = commit_obj.get_node(new_f_path)
1135 if file_node is not None:
1146 if file_node is not None:
1136 path_exist = new_f_path
1147 path_exist = new_f_path
1137 except EmptyRepositoryError:
1148 except EmptyRepositoryError:
1138 pass
1149 pass
1139 except Exception:
1150 except Exception:
1140 pass
1151 pass
1141
1152
1142 return {
1153 return {
1143 'branch': _branch_name,
1154 'branch': _branch_name,
1144 'sha': _sha_commit_id,
1155 'sha': _sha_commit_id,
1145 'is_head': is_head,
1156 'is_head': is_head,
1146 'path_exists': path_exist
1157 'path_exists': path_exist
1147 }
1158 }
1148
1159
1149 @LoginRequired()
1160 @LoginRequired()
1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1161 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 @view_config(
1162 @view_config(
1152 route_name='repo_files_remove_file', request_method='GET',
1163 route_name='repo_files_remove_file', request_method='GET',
1153 renderer='rhodecode:templates/files/files_delete.mako')
1164 renderer='rhodecode:templates/files/files_delete.mako')
1154 def repo_files_remove_file(self):
1165 def repo_files_remove_file(self):
1155 _ = self.request.translate
1166 _ = self.request.translate
1156 c = self.load_default_context()
1167 c = self.load_default_context()
1157 commit_id, f_path = self._get_commit_and_path()
1168 commit_id, f_path = self._get_commit_and_path()
1158
1169
1159 self._ensure_not_locked()
1170 self._ensure_not_locked()
1160 _branch_name, _sha_commit_id, is_head = \
1171 _branch_name, _sha_commit_id, is_head = \
1161 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1172 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1162
1173
1163 self.forbid_non_head(is_head, f_path)
1174 self.forbid_non_head(is_head, f_path)
1164 self.check_branch_permission(_branch_name)
1175 self.check_branch_permission(_branch_name)
1165
1176
1166 c.commit = self._get_commit_or_redirect(commit_id)
1177 c.commit = self._get_commit_or_redirect(commit_id)
1167 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1178 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1168
1179
1169 c.default_message = _(
1180 c.default_message = _(
1170 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1181 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1171 c.f_path = f_path
1182 c.f_path = f_path
1172
1183
1173 return self._get_template_context(c)
1184 return self._get_template_context(c)
1174
1185
1175 @LoginRequired()
1186 @LoginRequired()
1176 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1177 @CSRFRequired()
1188 @CSRFRequired()
1178 @view_config(
1189 @view_config(
1179 route_name='repo_files_delete_file', request_method='POST',
1190 route_name='repo_files_delete_file', request_method='POST',
1180 renderer=None)
1191 renderer=None)
1181 def repo_files_delete_file(self):
1192 def repo_files_delete_file(self):
1182 _ = self.request.translate
1193 _ = self.request.translate
1183
1194
1184 c = self.load_default_context()
1195 c = self.load_default_context()
1185 commit_id, f_path = self._get_commit_and_path()
1196 commit_id, f_path = self._get_commit_and_path()
1186
1197
1187 self._ensure_not_locked()
1198 self._ensure_not_locked()
1188 _branch_name, _sha_commit_id, is_head = \
1199 _branch_name, _sha_commit_id, is_head = \
1189 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1200 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1190
1201
1191 self.forbid_non_head(is_head, f_path)
1202 self.forbid_non_head(is_head, f_path)
1192 self.check_branch_permission(_branch_name)
1203 self.check_branch_permission(_branch_name)
1193
1204
1194 c.commit = self._get_commit_or_redirect(commit_id)
1205 c.commit = self._get_commit_or_redirect(commit_id)
1195 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1206 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1196
1207
1197 c.default_message = _(
1208 c.default_message = _(
1198 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1209 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1199 c.f_path = f_path
1210 c.f_path = f_path
1200 node_path = f_path
1211 node_path = f_path
1201 author = self._rhodecode_db_user.full_contact
1212 author = self._rhodecode_db_user.full_contact
1202 message = self.request.POST.get('message') or c.default_message
1213 message = self.request.POST.get('message') or c.default_message
1203 try:
1214 try:
1204 nodes = {
1215 nodes = {
1205 node_path: {
1216 node_path: {
1206 'content': ''
1217 'content': ''
1207 }
1218 }
1208 }
1219 }
1209 ScmModel().delete_nodes(
1220 ScmModel().delete_nodes(
1210 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1221 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1211 message=message,
1222 message=message,
1212 nodes=nodes,
1223 nodes=nodes,
1213 parent_commit=c.commit,
1224 parent_commit=c.commit,
1214 author=author,
1225 author=author,
1215 )
1226 )
1216
1227
1217 h.flash(
1228 h.flash(
1218 _('Successfully deleted file `{}`').format(
1229 _('Successfully deleted file `{}`').format(
1219 h.escape(f_path)), category='success')
1230 h.escape(f_path)), category='success')
1220 except Exception:
1231 except Exception:
1221 log.exception('Error during commit operation')
1232 log.exception('Error during commit operation')
1222 h.flash(_('Error occurred during commit'), category='error')
1233 h.flash(_('Error occurred during commit'), category='error')
1223 raise HTTPFound(
1234 raise HTTPFound(
1224 h.route_path('repo_commit', repo_name=self.db_repo_name,
1235 h.route_path('repo_commit', repo_name=self.db_repo_name,
1225 commit_id='tip'))
1236 commit_id='tip'))
1226
1237
1227 @LoginRequired()
1238 @LoginRequired()
1228 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1239 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1229 @view_config(
1240 @view_config(
1230 route_name='repo_files_edit_file', request_method='GET',
1241 route_name='repo_files_edit_file', request_method='GET',
1231 renderer='rhodecode:templates/files/files_edit.mako')
1242 renderer='rhodecode:templates/files/files_edit.mako')
1232 def repo_files_edit_file(self):
1243 def repo_files_edit_file(self):
1233 _ = self.request.translate
1244 _ = self.request.translate
1234 c = self.load_default_context()
1245 c = self.load_default_context()
1235 commit_id, f_path = self._get_commit_and_path()
1246 commit_id, f_path = self._get_commit_and_path()
1236
1247
1237 self._ensure_not_locked()
1248 self._ensure_not_locked()
1238 _branch_name, _sha_commit_id, is_head = \
1249 _branch_name, _sha_commit_id, is_head = \
1239 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1250 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1240
1251
1241 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1252 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1242 self.check_branch_permission(_branch_name, commit_id=commit_id)
1253 self.check_branch_permission(_branch_name, commit_id=commit_id)
1243
1254
1244 c.commit = self._get_commit_or_redirect(commit_id)
1255 c.commit = self._get_commit_or_redirect(commit_id)
1245 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1256 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1246
1257
1247 if c.file.is_binary:
1258 if c.file.is_binary:
1248 files_url = h.route_path(
1259 files_url = h.route_path(
1249 'repo_files',
1260 'repo_files',
1250 repo_name=self.db_repo_name,
1261 repo_name=self.db_repo_name,
1251 commit_id=c.commit.raw_id, f_path=f_path)
1262 commit_id=c.commit.raw_id, f_path=f_path)
1252 raise HTTPFound(files_url)
1263 raise HTTPFound(files_url)
1253
1264
1254 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1265 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1255 c.f_path = f_path
1266 c.f_path = f_path
1256
1267
1257 return self._get_template_context(c)
1268 return self._get_template_context(c)
1258
1269
1259 @LoginRequired()
1270 @LoginRequired()
1260 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1271 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1261 @CSRFRequired()
1272 @CSRFRequired()
1262 @view_config(
1273 @view_config(
1263 route_name='repo_files_update_file', request_method='POST',
1274 route_name='repo_files_update_file', request_method='POST',
1264 renderer=None)
1275 renderer=None)
1265 def repo_files_update_file(self):
1276 def repo_files_update_file(self):
1266 _ = self.request.translate
1277 _ = self.request.translate
1267 c = self.load_default_context()
1278 c = self.load_default_context()
1268 commit_id, f_path = self._get_commit_and_path()
1279 commit_id, f_path = self._get_commit_and_path()
1269
1280
1270 self._ensure_not_locked()
1281 self._ensure_not_locked()
1271
1282
1272 c.commit = self._get_commit_or_redirect(commit_id)
1283 c.commit = self._get_commit_or_redirect(commit_id)
1273 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1284 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1274
1285
1275 if c.file.is_binary:
1286 if c.file.is_binary:
1276 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1287 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1277 commit_id=c.commit.raw_id, f_path=f_path))
1288 commit_id=c.commit.raw_id, f_path=f_path))
1278
1289
1279 _branch_name, _sha_commit_id, is_head = \
1290 _branch_name, _sha_commit_id, is_head = \
1280 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1291 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1281
1292
1282 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1293 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1283 self.check_branch_permission(_branch_name, commit_id=commit_id)
1294 self.check_branch_permission(_branch_name, commit_id=commit_id)
1284
1295
1285 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1296 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1286 c.f_path = f_path
1297 c.f_path = f_path
1287
1298
1288 old_content = c.file.content
1299 old_content = c.file.content
1289 sl = old_content.splitlines(1)
1300 sl = old_content.splitlines(1)
1290 first_line = sl[0] if sl else ''
1301 first_line = sl[0] if sl else ''
1291
1302
1292 r_post = self.request.POST
1303 r_post = self.request.POST
1293 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1304 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1294 line_ending_mode = detect_mode(first_line, 0)
1305 line_ending_mode = detect_mode(first_line, 0)
1295 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1306 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1296
1307
1297 message = r_post.get('message') or c.default_message
1308 message = r_post.get('message') or c.default_message
1298 org_node_path = c.file.unicode_path
1309 org_node_path = c.file.unicode_path
1299 filename = r_post['filename']
1310 filename = r_post['filename']
1300
1311
1301 root_path = c.file.dir_path
1312 root_path = c.file.dir_path
1302 pure_path = self.create_pure_path(root_path, filename)
1313 pure_path = self.create_pure_path(root_path, filename)
1303 node_path = safe_unicode(bytes(pure_path))
1314 node_path = safe_unicode(bytes(pure_path))
1304
1315
1305 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1316 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1306 commit_id=commit_id)
1317 commit_id=commit_id)
1307 if content == old_content and node_path == org_node_path:
1318 if content == old_content and node_path == org_node_path:
1308 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1319 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1309 category='warning')
1320 category='warning')
1310 raise HTTPFound(default_redirect_url)
1321 raise HTTPFound(default_redirect_url)
1311
1322
1312 try:
1323 try:
1313 mapping = {
1324 mapping = {
1314 org_node_path: {
1325 org_node_path: {
1315 'org_filename': org_node_path,
1326 'org_filename': org_node_path,
1316 'filename': node_path,
1327 'filename': node_path,
1317 'content': content,
1328 'content': content,
1318 'lexer': '',
1329 'lexer': '',
1319 'op': 'mod',
1330 'op': 'mod',
1320 'mode': c.file.mode
1331 'mode': c.file.mode
1321 }
1332 }
1322 }
1333 }
1323
1334
1324 commit = ScmModel().update_nodes(
1335 commit = ScmModel().update_nodes(
1325 user=self._rhodecode_db_user.user_id,
1336 user=self._rhodecode_db_user.user_id,
1326 repo=self.db_repo,
1337 repo=self.db_repo,
1327 message=message,
1338 message=message,
1328 nodes=mapping,
1339 nodes=mapping,
1329 parent_commit=c.commit,
1340 parent_commit=c.commit,
1330 )
1341 )
1331
1342
1332 h.flash(_('Successfully committed changes to file `{}`').format(
1343 h.flash(_('Successfully committed changes to file `{}`').format(
1333 h.escape(f_path)), category='success')
1344 h.escape(f_path)), category='success')
1334 default_redirect_url = h.route_path(
1345 default_redirect_url = h.route_path(
1335 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1346 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1336
1347
1337 except Exception:
1348 except Exception:
1338 log.exception('Error occurred during commit')
1349 log.exception('Error occurred during commit')
1339 h.flash(_('Error occurred during commit'), category='error')
1350 h.flash(_('Error occurred during commit'), category='error')
1340
1351
1341 raise HTTPFound(default_redirect_url)
1352 raise HTTPFound(default_redirect_url)
1342
1353
1343 @LoginRequired()
1354 @LoginRequired()
1344 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1355 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1345 @view_config(
1356 @view_config(
1346 route_name='repo_files_add_file', request_method='GET',
1357 route_name='repo_files_add_file', request_method='GET',
1347 renderer='rhodecode:templates/files/files_add.mako')
1358 renderer='rhodecode:templates/files/files_add.mako')
1348 @view_config(
1359 @view_config(
1349 route_name='repo_files_upload_file', request_method='GET',
1360 route_name='repo_files_upload_file', request_method='GET',
1350 renderer='rhodecode:templates/files/files_upload.mako')
1361 renderer='rhodecode:templates/files/files_upload.mako')
1351 def repo_files_add_file(self):
1362 def repo_files_add_file(self):
1352 _ = self.request.translate
1363 _ = self.request.translate
1353 c = self.load_default_context()
1364 c = self.load_default_context()
1354 commit_id, f_path = self._get_commit_and_path()
1365 commit_id, f_path = self._get_commit_and_path()
1355
1366
1356 self._ensure_not_locked()
1367 self._ensure_not_locked()
1357
1368
1358 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1369 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1359 if c.commit is None:
1370 if c.commit is None:
1360 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1371 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1361
1372
1362 if self.rhodecode_vcs_repo.is_empty():
1373 if self.rhodecode_vcs_repo.is_empty():
1363 # for empty repository we cannot check for current branch, we rely on
1374 # for empty repository we cannot check for current branch, we rely on
1364 # c.commit.branch instead
1375 # c.commit.branch instead
1365 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1376 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1366 else:
1377 else:
1367 _branch_name, _sha_commit_id, is_head = \
1378 _branch_name, _sha_commit_id, is_head = \
1368 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1379 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1369
1380
1370 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1381 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1371 self.check_branch_permission(_branch_name, commit_id=commit_id)
1382 self.check_branch_permission(_branch_name, commit_id=commit_id)
1372
1383
1373 c.default_message = (_('Added file via RhodeCode Enterprise'))
1384 c.default_message = (_('Added file via RhodeCode Enterprise'))
1374 c.f_path = f_path.lstrip('/') # ensure not relative path
1385 c.f_path = f_path.lstrip('/') # ensure not relative path
1375
1386
1376 return self._get_template_context(c)
1387 return self._get_template_context(c)
1377
1388
1378 @LoginRequired()
1389 @LoginRequired()
1379 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1390 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1380 @CSRFRequired()
1391 @CSRFRequired()
1381 @view_config(
1392 @view_config(
1382 route_name='repo_files_create_file', request_method='POST',
1393 route_name='repo_files_create_file', request_method='POST',
1383 renderer=None)
1394 renderer=None)
1384 def repo_files_create_file(self):
1395 def repo_files_create_file(self):
1385 _ = self.request.translate
1396 _ = self.request.translate
1386 c = self.load_default_context()
1397 c = self.load_default_context()
1387 commit_id, f_path = self._get_commit_and_path()
1398 commit_id, f_path = self._get_commit_and_path()
1388
1399
1389 self._ensure_not_locked()
1400 self._ensure_not_locked()
1390
1401
1391 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1402 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1392 if c.commit is None:
1403 if c.commit is None:
1393 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1404 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1394
1405
1395 # calculate redirect URL
1406 # calculate redirect URL
1396 if self.rhodecode_vcs_repo.is_empty():
1407 if self.rhodecode_vcs_repo.is_empty():
1397 default_redirect_url = h.route_path(
1408 default_redirect_url = h.route_path(
1398 'repo_summary', repo_name=self.db_repo_name)
1409 'repo_summary', repo_name=self.db_repo_name)
1399 else:
1410 else:
1400 default_redirect_url = h.route_path(
1411 default_redirect_url = h.route_path(
1401 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1412 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1402
1413
1403 if self.rhodecode_vcs_repo.is_empty():
1414 if self.rhodecode_vcs_repo.is_empty():
1404 # for empty repository we cannot check for current branch, we rely on
1415 # for empty repository we cannot check for current branch, we rely on
1405 # c.commit.branch instead
1416 # c.commit.branch instead
1406 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1417 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1407 else:
1418 else:
1408 _branch_name, _sha_commit_id, is_head = \
1419 _branch_name, _sha_commit_id, is_head = \
1409 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1420 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1410
1421
1411 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1422 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1412 self.check_branch_permission(_branch_name, commit_id=commit_id)
1423 self.check_branch_permission(_branch_name, commit_id=commit_id)
1413
1424
1414 c.default_message = (_('Added file via RhodeCode Enterprise'))
1425 c.default_message = (_('Added file via RhodeCode Enterprise'))
1415 c.f_path = f_path
1426 c.f_path = f_path
1416
1427
1417 r_post = self.request.POST
1428 r_post = self.request.POST
1418 message = r_post.get('message') or c.default_message
1429 message = r_post.get('message') or c.default_message
1419 filename = r_post.get('filename')
1430 filename = r_post.get('filename')
1420 unix_mode = 0
1431 unix_mode = 0
1421 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1432 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1422
1433
1423 if not filename:
1434 if not filename:
1424 # If there's no commit, redirect to repo summary
1435 # If there's no commit, redirect to repo summary
1425 if type(c.commit) is EmptyCommit:
1436 if type(c.commit) is EmptyCommit:
1426 redirect_url = h.route_path(
1437 redirect_url = h.route_path(
1427 'repo_summary', repo_name=self.db_repo_name)
1438 'repo_summary', repo_name=self.db_repo_name)
1428 else:
1439 else:
1429 redirect_url = default_redirect_url
1440 redirect_url = default_redirect_url
1430 h.flash(_('No filename specified'), category='warning')
1441 h.flash(_('No filename specified'), category='warning')
1431 raise HTTPFound(redirect_url)
1442 raise HTTPFound(redirect_url)
1432
1443
1433 root_path = f_path
1444 root_path = f_path
1434 pure_path = self.create_pure_path(root_path, filename)
1445 pure_path = self.create_pure_path(root_path, filename)
1435 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1446 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1436
1447
1437 author = self._rhodecode_db_user.full_contact
1448 author = self._rhodecode_db_user.full_contact
1438 nodes = {
1449 nodes = {
1439 node_path: {
1450 node_path: {
1440 'content': content
1451 'content': content
1441 }
1452 }
1442 }
1453 }
1443
1454
1444 try:
1455 try:
1445
1456
1446 commit = ScmModel().create_nodes(
1457 commit = ScmModel().create_nodes(
1447 user=self._rhodecode_db_user.user_id,
1458 user=self._rhodecode_db_user.user_id,
1448 repo=self.db_repo,
1459 repo=self.db_repo,
1449 message=message,
1460 message=message,
1450 nodes=nodes,
1461 nodes=nodes,
1451 parent_commit=c.commit,
1462 parent_commit=c.commit,
1452 author=author,
1463 author=author,
1453 )
1464 )
1454
1465
1455 h.flash(_('Successfully committed new file `{}`').format(
1466 h.flash(_('Successfully committed new file `{}`').format(
1456 h.escape(node_path)), category='success')
1467 h.escape(node_path)), category='success')
1457
1468
1458 default_redirect_url = h.route_path(
1469 default_redirect_url = h.route_path(
1459 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1470 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1460
1471
1461 except NonRelativePathError:
1472 except NonRelativePathError:
1462 log.exception('Non Relative path found')
1473 log.exception('Non Relative path found')
1463 h.flash(_('The location specified must be a relative path and must not '
1474 h.flash(_('The location specified must be a relative path and must not '
1464 'contain .. in the path'), category='warning')
1475 'contain .. in the path'), category='warning')
1465 raise HTTPFound(default_redirect_url)
1476 raise HTTPFound(default_redirect_url)
1466 except (NodeError, NodeAlreadyExistsError) as e:
1477 except (NodeError, NodeAlreadyExistsError) as e:
1467 h.flash(_(h.escape(e)), category='error')
1478 h.flash(_(h.escape(e)), category='error')
1468 except Exception:
1479 except Exception:
1469 log.exception('Error occurred during commit')
1480 log.exception('Error occurred during commit')
1470 h.flash(_('Error occurred during commit'), category='error')
1481 h.flash(_('Error occurred during commit'), category='error')
1471
1482
1472 raise HTTPFound(default_redirect_url)
1483 raise HTTPFound(default_redirect_url)
1473
1484
1474 @LoginRequired()
1485 @LoginRequired()
1475 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1486 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1476 @CSRFRequired()
1487 @CSRFRequired()
1477 @view_config(
1488 @view_config(
1478 route_name='repo_files_upload_file', request_method='POST',
1489 route_name='repo_files_upload_file', request_method='POST',
1479 renderer='json_ext')
1490 renderer='json_ext')
1480 def repo_files_upload_file(self):
1491 def repo_files_upload_file(self):
1481 _ = self.request.translate
1492 _ = self.request.translate
1482 c = self.load_default_context()
1493 c = self.load_default_context()
1483 commit_id, f_path = self._get_commit_and_path()
1494 commit_id, f_path = self._get_commit_and_path()
1484
1495
1485 self._ensure_not_locked()
1496 self._ensure_not_locked()
1486
1497
1487 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1498 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1488 if c.commit is None:
1499 if c.commit is None:
1489 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1500 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1490
1501
1491 # calculate redirect URL
1502 # calculate redirect URL
1492 if self.rhodecode_vcs_repo.is_empty():
1503 if self.rhodecode_vcs_repo.is_empty():
1493 default_redirect_url = h.route_path(
1504 default_redirect_url = h.route_path(
1494 'repo_summary', repo_name=self.db_repo_name)
1505 'repo_summary', repo_name=self.db_repo_name)
1495 else:
1506 else:
1496 default_redirect_url = h.route_path(
1507 default_redirect_url = h.route_path(
1497 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1508 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1498
1509
1499 if self.rhodecode_vcs_repo.is_empty():
1510 if self.rhodecode_vcs_repo.is_empty():
1500 # for empty repository we cannot check for current branch, we rely on
1511 # for empty repository we cannot check for current branch, we rely on
1501 # c.commit.branch instead
1512 # c.commit.branch instead
1502 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1513 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1503 else:
1514 else:
1504 _branch_name, _sha_commit_id, is_head = \
1515 _branch_name, _sha_commit_id, is_head = \
1505 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1516 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1506
1517
1507 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1518 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1508 if error:
1519 if error:
1509 return {
1520 return {
1510 'error': error,
1521 'error': error,
1511 'redirect_url': default_redirect_url
1522 'redirect_url': default_redirect_url
1512 }
1523 }
1513 error = self.check_branch_permission(_branch_name, json_mode=True)
1524 error = self.check_branch_permission(_branch_name, json_mode=True)
1514 if error:
1525 if error:
1515 return {
1526 return {
1516 'error': error,
1527 'error': error,
1517 'redirect_url': default_redirect_url
1528 'redirect_url': default_redirect_url
1518 }
1529 }
1519
1530
1520 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1531 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1521 c.f_path = f_path
1532 c.f_path = f_path
1522
1533
1523 r_post = self.request.POST
1534 r_post = self.request.POST
1524
1535
1525 message = c.default_message
1536 message = c.default_message
1526 user_message = r_post.getall('message')
1537 user_message = r_post.getall('message')
1527 if isinstance(user_message, list) and user_message:
1538 if isinstance(user_message, list) and user_message:
1528 # we take the first from duplicated results if it's not empty
1539 # we take the first from duplicated results if it's not empty
1529 message = user_message[0] if user_message[0] else message
1540 message = user_message[0] if user_message[0] else message
1530
1541
1531 nodes = {}
1542 nodes = {}
1532
1543
1533 for file_obj in r_post.getall('files_upload') or []:
1544 for file_obj in r_post.getall('files_upload') or []:
1534 content = file_obj.file
1545 content = file_obj.file
1535 filename = file_obj.filename
1546 filename = file_obj.filename
1536
1547
1537 root_path = f_path
1548 root_path = f_path
1538 pure_path = self.create_pure_path(root_path, filename)
1549 pure_path = self.create_pure_path(root_path, filename)
1539 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1550 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1540
1551
1541 nodes[node_path] = {
1552 nodes[node_path] = {
1542 'content': content
1553 'content': content
1543 }
1554 }
1544
1555
1545 if not nodes:
1556 if not nodes:
1546 error = 'missing files'
1557 error = 'missing files'
1547 return {
1558 return {
1548 'error': error,
1559 'error': error,
1549 'redirect_url': default_redirect_url
1560 'redirect_url': default_redirect_url
1550 }
1561 }
1551
1562
1552 author = self._rhodecode_db_user.full_contact
1563 author = self._rhodecode_db_user.full_contact
1553
1564
1554 try:
1565 try:
1555 commit = ScmModel().create_nodes(
1566 commit = ScmModel().create_nodes(
1556 user=self._rhodecode_db_user.user_id,
1567 user=self._rhodecode_db_user.user_id,
1557 repo=self.db_repo,
1568 repo=self.db_repo,
1558 message=message,
1569 message=message,
1559 nodes=nodes,
1570 nodes=nodes,
1560 parent_commit=c.commit,
1571 parent_commit=c.commit,
1561 author=author,
1572 author=author,
1562 )
1573 )
1563 if len(nodes) == 1:
1574 if len(nodes) == 1:
1564 flash_message = _('Successfully committed {} new files').format(len(nodes))
1575 flash_message = _('Successfully committed {} new files').format(len(nodes))
1565 else:
1576 else:
1566 flash_message = _('Successfully committed 1 new file')
1577 flash_message = _('Successfully committed 1 new file')
1567
1578
1568 h.flash(flash_message, category='success')
1579 h.flash(flash_message, category='success')
1569
1580
1570 default_redirect_url = h.route_path(
1581 default_redirect_url = h.route_path(
1571 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1582 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1572
1583
1573 except NonRelativePathError:
1584 except NonRelativePathError:
1574 log.exception('Non Relative path found')
1585 log.exception('Non Relative path found')
1575 error = _('The location specified must be a relative path and must not '
1586 error = _('The location specified must be a relative path and must not '
1576 'contain .. in the path')
1587 'contain .. in the path')
1577 h.flash(error, category='warning')
1588 h.flash(error, category='warning')
1578
1589
1579 return {
1590 return {
1580 'error': error,
1591 'error': error,
1581 'redirect_url': default_redirect_url
1592 'redirect_url': default_redirect_url
1582 }
1593 }
1583 except (NodeError, NodeAlreadyExistsError) as e:
1594 except (NodeError, NodeAlreadyExistsError) as e:
1584 error = h.escape(e)
1595 error = h.escape(e)
1585 h.flash(error, category='error')
1596 h.flash(error, category='error')
1586
1597
1587 return {
1598 return {
1588 'error': error,
1599 'error': error,
1589 'redirect_url': default_redirect_url
1600 'redirect_url': default_redirect_url
1590 }
1601 }
1591 except Exception:
1602 except Exception:
1592 log.exception('Error occurred during commit')
1603 log.exception('Error occurred during commit')
1593 error = _('Error occurred during commit')
1604 error = _('Error occurred during commit')
1594 h.flash(error, category='error')
1605 h.flash(error, category='error')
1595 return {
1606 return {
1596 'error': error,
1607 'error': error,
1597 'redirect_url': default_redirect_url
1608 'redirect_url': default_redirect_url
1598 }
1609 }
1599
1610
1600 return {
1611 return {
1601 'error': None,
1612 'error': None,
1602 'redirect_url': default_redirect_url
1613 'redirect_url': default_redirect_url
1603 }
1614 }
@@ -1,1960 +1,1974 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 os
28 import os
29 import random
29 import random
30 import hashlib
30 import hashlib
31 import StringIO
31 import StringIO
32 import textwrap
32 import textwrap
33 import urllib
33 import urllib
34 import math
34 import math
35 import logging
35 import logging
36 import re
36 import re
37 import time
37 import time
38 import string
38 import string
39 import hashlib
39 import hashlib
40 from collections import OrderedDict
40 from collections import OrderedDict
41
41
42 import pygments
42 import pygments
43 import itertools
43 import itertools
44 import fnmatch
44 import fnmatch
45 import bleach
45 import bleach
46
46
47 from pyramid import compat
47 from pyramid import compat
48 from datetime import datetime
48 from datetime import datetime
49 from functools import partial
49 from functools import partial
50 from pygments.formatters.html import HtmlFormatter
50 from pygments.formatters.html import HtmlFormatter
51 from pygments.lexers import (
51 from pygments.lexers import (
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53
53
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55
55
56 from webhelpers2.html import literal, HTML, escape
56 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html._autolink import _auto_link_urls
57 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html.tools import (
58 from webhelpers2.html.tools import (
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60
60
61 from webhelpers2.text import (
61 from webhelpers2.text import (
62 chop_at, collapse, convert_accented_entities,
62 chop_at, collapse, convert_accented_entities,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 from webhelpers2.date import time_ago_in_words
65 from webhelpers2.date import time_ago_in_words
66
66
67 from webhelpers2.html.tags import (
67 from webhelpers2.html.tags import (
68 _input, NotGiven, _make_safe_id_component as safeid,
68 _input, NotGiven, _make_safe_id_component as safeid,
69 form as insecure_form,
69 form as insecure_form,
70 auto_discovery_link, checkbox, end_form, file,
70 auto_discovery_link, checkbox, end_form, file,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 ul, radio, Options)
73 ul, radio, Options)
74
74
75 from webhelpers2.number import format_byte_size
75 from webhelpers2.number import format_byte_size
76
76
77 from rhodecode.lib.action_parser import action_parser
77 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.ext_json import json
79 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils2 import (
81 from rhodecode.lib.utils2 import (
82 str2bool, safe_unicode, safe_str,
82 str2bool, safe_unicode, safe_str,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 from rhodecode.model.changeset_status import ChangesetStatusModel
90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
91 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.repo_group import RepoGroupModel
92 from rhodecode.model.repo_group import RepoGroupModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
94
94
95
95
96 log = logging.getLogger(__name__)
96 log = logging.getLogger(__name__)
97
97
98
98
99 DEFAULT_USER = User.DEFAULT_USER
99 DEFAULT_USER = User.DEFAULT_USER
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101
101
102
102
103 def asset(path, ver=None, **kwargs):
103 def asset(path, ver=None, **kwargs):
104 """
104 """
105 Helper to generate a static asset file path for rhodecode assets
105 Helper to generate a static asset file path for rhodecode assets
106
106
107 eg. h.asset('images/image.png', ver='3923')
107 eg. h.asset('images/image.png', ver='3923')
108
108
109 :param path: path of asset
109 :param path: path of asset
110 :param ver: optional version query param to append as ?ver=
110 :param ver: optional version query param to append as ?ver=
111 """
111 """
112 request = get_current_request()
112 request = get_current_request()
113 query = {}
113 query = {}
114 query.update(kwargs)
114 query.update(kwargs)
115 if ver:
115 if ver:
116 query = {'ver': ver}
116 query = {'ver': ver}
117 return request.static_path(
117 return request.static_path(
118 'rhodecode:public/{}'.format(path), _query=query)
118 'rhodecode:public/{}'.format(path), _query=query)
119
119
120
120
121 default_html_escape_table = {
121 default_html_escape_table = {
122 ord('&'): u'&amp;',
122 ord('&'): u'&amp;',
123 ord('<'): u'&lt;',
123 ord('<'): u'&lt;',
124 ord('>'): u'&gt;',
124 ord('>'): u'&gt;',
125 ord('"'): u'&quot;',
125 ord('"'): u'&quot;',
126 ord("'"): u'&#39;',
126 ord("'"): u'&#39;',
127 }
127 }
128
128
129
129
130 def html_escape(text, html_escape_table=default_html_escape_table):
130 def html_escape(text, html_escape_table=default_html_escape_table):
131 """Produce entities within text."""
131 """Produce entities within text."""
132 return text.translate(html_escape_table)
132 return text.translate(html_escape_table)
133
133
134
134
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 """
136 """
137 Truncate string ``s`` at the first occurrence of ``sub``.
137 Truncate string ``s`` at the first occurrence of ``sub``.
138
138
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 """
140 """
141 suffix_if_chopped = suffix_if_chopped or ''
141 suffix_if_chopped = suffix_if_chopped or ''
142 pos = s.find(sub)
142 pos = s.find(sub)
143 if pos == -1:
143 if pos == -1:
144 return s
144 return s
145
145
146 if inclusive:
146 if inclusive:
147 pos += len(sub)
147 pos += len(sub)
148
148
149 chopped = s[:pos]
149 chopped = s[:pos]
150 left = s[pos:].strip()
150 left = s[pos:].strip()
151
151
152 if left and suffix_if_chopped:
152 if left and suffix_if_chopped:
153 chopped += suffix_if_chopped
153 chopped += suffix_if_chopped
154
154
155 return chopped
155 return chopped
156
156
157
157
158 def shorter(text, size=20, prefix=False):
158 def shorter(text, size=20, prefix=False):
159 postfix = '...'
159 postfix = '...'
160 if len(text) > size:
160 if len(text) > size:
161 if prefix:
161 if prefix:
162 # shorten in front
162 # shorten in front
163 return postfix + text[-(size - len(postfix)):]
163 return postfix + text[-(size - len(postfix)):]
164 else:
164 else:
165 return text[:size - len(postfix)] + postfix
165 return text[:size - len(postfix)] + postfix
166 return text
166 return text
167
167
168
168
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 """
170 """
171 Reset button
171 Reset button
172 """
172 """
173 return _input(type, name, value, id, attrs)
173 return _input(type, name, value, id, attrs)
174
174
175
175
176 def select(name, selected_values, options, id=NotGiven, **attrs):
176 def select(name, selected_values, options, id=NotGiven, **attrs):
177
177
178 if isinstance(options, (list, tuple)):
178 if isinstance(options, (list, tuple)):
179 options_iter = options
179 options_iter = options
180 # Handle old value,label lists ... where value also can be value,label lists
180 # Handle old value,label lists ... where value also can be value,label lists
181 options = Options()
181 options = Options()
182 for opt in options_iter:
182 for opt in options_iter:
183 if isinstance(opt, tuple) and len(opt) == 2:
183 if isinstance(opt, tuple) and len(opt) == 2:
184 value, label = opt
184 value, label = opt
185 elif isinstance(opt, basestring):
185 elif isinstance(opt, basestring):
186 value = label = opt
186 value = label = opt
187 else:
187 else:
188 raise ValueError('invalid select option type %r' % type(opt))
188 raise ValueError('invalid select option type %r' % type(opt))
189
189
190 if isinstance(value, (list, tuple)):
190 if isinstance(value, (list, tuple)):
191 option_group = options.add_optgroup(label)
191 option_group = options.add_optgroup(label)
192 for opt2 in value:
192 for opt2 in value:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 group_value, group_label = opt2
194 group_value, group_label = opt2
195 elif isinstance(opt2, basestring):
195 elif isinstance(opt2, basestring):
196 group_value = group_label = opt2
196 group_value = group_label = opt2
197 else:
197 else:
198 raise ValueError('invalid select option type %r' % type(opt2))
198 raise ValueError('invalid select option type %r' % type(opt2))
199
199
200 option_group.add_option(group_label, group_value)
200 option_group.add_option(group_label, group_value)
201 else:
201 else:
202 options.add_option(label, value)
202 options.add_option(label, value)
203
203
204 return raw_select(name, selected_values, options, id=id, **attrs)
204 return raw_select(name, selected_values, options, id=id, **attrs)
205
205
206
206
207 def branding(name, length=40):
207 def branding(name, length=40):
208 return truncate(name, length, indicator="")
208 return truncate(name, length, indicator="")
209
209
210
210
211 def FID(raw_id, path):
211 def FID(raw_id, path):
212 """
212 """
213 Creates a unique ID for filenode based on it's hash of path and commit
213 Creates a unique ID for filenode based on it's hash of path and commit
214 it's safe to use in urls
214 it's safe to use in urls
215
215
216 :param raw_id:
216 :param raw_id:
217 :param path:
217 :param path:
218 """
218 """
219
219
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221
221
222
222
223 class _GetError(object):
223 class _GetError(object):
224 """Get error from form_errors, and represent it as span wrapped error
224 """Get error from form_errors, and represent it as span wrapped error
225 message
225 message
226
226
227 :param field_name: field to fetch errors for
227 :param field_name: field to fetch errors for
228 :param form_errors: form errors dict
228 :param form_errors: form errors dict
229 """
229 """
230
230
231 def __call__(self, field_name, form_errors):
231 def __call__(self, field_name, form_errors):
232 tmpl = """<span class="error_msg">%s</span>"""
232 tmpl = """<span class="error_msg">%s</span>"""
233 if form_errors and field_name in form_errors:
233 if form_errors and field_name in form_errors:
234 return literal(tmpl % form_errors.get(field_name))
234 return literal(tmpl % form_errors.get(field_name))
235
235
236
236
237 get_error = _GetError()
237 get_error = _GetError()
238
238
239
239
240 class _ToolTip(object):
240 class _ToolTip(object):
241
241
242 def __call__(self, tooltip_title, trim_at=50):
242 def __call__(self, tooltip_title, trim_at=50):
243 """
243 """
244 Special function just to wrap our text into nice formatted
244 Special function just to wrap our text into nice formatted
245 autowrapped text
245 autowrapped text
246
246
247 :param tooltip_title:
247 :param tooltip_title:
248 """
248 """
249 tooltip_title = escape(tooltip_title)
249 tooltip_title = escape(tooltip_title)
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 return tooltip_title
251 return tooltip_title
252
252
253
253
254 tooltip = _ToolTip()
254 tooltip = _ToolTip()
255
255
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
257
257
258
258
259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
259 def files_breadcrumbs(repo_name, commit_id, file_path, landing_ref_name=None, at_ref=None,
260 limit_items=False, linkify_last_item=False, hide_last_item=False, copy_path_icon=True):
260 if isinstance(file_path, str):
261 if isinstance(file_path, str):
261 file_path = safe_unicode(file_path)
262 file_path = safe_unicode(file_path)
262
263
263 route_qry = {'at': at_ref} if at_ref else None
264 if at_ref:
265 route_qry = {'at': at_ref}
266 default_commit_id = at_ref or landing_ref_name or commit_id
267 else:
268 route_qry = None
269 default_commit_id = commit_id
264
270
265 # first segment is a `..` link to repo files
271 # first segment is a `..` link to repo files
266 root_name = literal(u'<i class="icon-home"></i>')
272 root_name = literal(u'<i class="icon-home"></i>')
267 url_segments = [
273 url_segments = [
268 link_to(
274 link_to(
269 root_name,
275 root_name,
270 route_path(
276 route_path(
271 'repo_files',
277 'repo_files',
272 repo_name=repo_name,
278 repo_name=repo_name,
273 commit_id=commit_id,
279 commit_id=default_commit_id,
274 f_path='',
280 f_path='',
275 _query=route_qry),
281 _query=route_qry),
276 )]
282 )]
277
283
278 path_segments = file_path.split('/')
284 path_segments = file_path.split('/')
279 last_cnt = len(path_segments) - 1
285 last_cnt = len(path_segments) - 1
280 for cnt, segment in enumerate(path_segments):
286 for cnt, segment in enumerate(path_segments):
281 if not segment:
287 if not segment:
282 continue
288 continue
283 segment_html = escape(segment)
289 segment_html = escape(segment)
284
290
285 last_item = cnt == last_cnt
291 last_item = cnt == last_cnt
286
292
293 if last_item and hide_last_item:
294 # iterate over and hide last element
295 continue
296
287 if last_item and linkify_last_item is False:
297 if last_item and linkify_last_item is False:
288 # plain version
298 # plain version
289 url_segments.append(segment_html)
299 url_segments.append(segment_html)
290 else:
300 else:
291 url_segments.append(
301 url_segments.append(
292 link_to(
302 link_to(
293 segment_html,
303 segment_html,
294 route_path(
304 route_path(
295 'repo_files',
305 'repo_files',
296 repo_name=repo_name,
306 repo_name=repo_name,
297 commit_id=commit_id,
307 commit_id=default_commit_id,
298 f_path='/'.join(path_segments[:cnt + 1]),
308 f_path='/'.join(path_segments[:cnt + 1]),
299 _query=route_qry),
309 _query=route_qry),
300 ))
310 ))
301
311
302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
312 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
303 if limit_items and len(limited_url_segments) < len(url_segments):
313 if limit_items and len(limited_url_segments) < len(url_segments):
304 url_segments = limited_url_segments
314 url_segments = limited_url_segments
305
315
306 full_path = file_path
316 full_path = file_path
307 icon = files_icon.format(escape(full_path))
317 if copy_path_icon:
318 icon = files_icon.format(escape(full_path))
319 else:
320 icon = ''
321
308 if file_path == '':
322 if file_path == '':
309 return root_name
323 return root_name
310 else:
324 else:
311 return literal(' / '.join(url_segments) + icon)
325 return literal(' / '.join(url_segments) + icon)
312
326
313
327
314 def files_url_data(request):
328 def files_url_data(request):
315 matchdict = request.matchdict
329 matchdict = request.matchdict
316
330
317 if 'f_path' not in matchdict:
331 if 'f_path' not in matchdict:
318 matchdict['f_path'] = ''
332 matchdict['f_path'] = ''
319
333
320 if 'commit_id' not in matchdict:
334 if 'commit_id' not in matchdict:
321 matchdict['commit_id'] = 'tip'
335 matchdict['commit_id'] = 'tip'
322
336
323 return json.dumps(matchdict)
337 return json.dumps(matchdict)
324
338
325
339
326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
340 def code_highlight(code, lexer, formatter, use_hl_filter=False):
327 """
341 """
328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
342 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
329
343
330 If ``outfile`` is given and a valid file object (an object
344 If ``outfile`` is given and a valid file object (an object
331 with a ``write`` method), the result will be written to it, otherwise
345 with a ``write`` method), the result will be written to it, otherwise
332 it is returned as a string.
346 it is returned as a string.
333 """
347 """
334 if use_hl_filter:
348 if use_hl_filter:
335 # add HL filter
349 # add HL filter
336 from rhodecode.lib.index import search_utils
350 from rhodecode.lib.index import search_utils
337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
351 lexer.add_filter(search_utils.ElasticSearchHLFilter())
338 return pygments.format(pygments.lex(code, lexer), formatter)
352 return pygments.format(pygments.lex(code, lexer), formatter)
339
353
340
354
341 class CodeHtmlFormatter(HtmlFormatter):
355 class CodeHtmlFormatter(HtmlFormatter):
342 """
356 """
343 My code Html Formatter for source codes
357 My code Html Formatter for source codes
344 """
358 """
345
359
346 def wrap(self, source, outfile):
360 def wrap(self, source, outfile):
347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
361 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
348
362
349 def _wrap_code(self, source):
363 def _wrap_code(self, source):
350 for cnt, it in enumerate(source):
364 for cnt, it in enumerate(source):
351 i, t = it
365 i, t = it
352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
366 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
353 yield i, t
367 yield i, t
354
368
355 def _wrap_tablelinenos(self, inner):
369 def _wrap_tablelinenos(self, inner):
356 dummyoutfile = StringIO.StringIO()
370 dummyoutfile = StringIO.StringIO()
357 lncount = 0
371 lncount = 0
358 for t, line in inner:
372 for t, line in inner:
359 if t:
373 if t:
360 lncount += 1
374 lncount += 1
361 dummyoutfile.write(line)
375 dummyoutfile.write(line)
362
376
363 fl = self.linenostart
377 fl = self.linenostart
364 mw = len(str(lncount + fl - 1))
378 mw = len(str(lncount + fl - 1))
365 sp = self.linenospecial
379 sp = self.linenospecial
366 st = self.linenostep
380 st = self.linenostep
367 la = self.lineanchors
381 la = self.lineanchors
368 aln = self.anchorlinenos
382 aln = self.anchorlinenos
369 nocls = self.noclasses
383 nocls = self.noclasses
370 if sp:
384 if sp:
371 lines = []
385 lines = []
372
386
373 for i in range(fl, fl + lncount):
387 for i in range(fl, fl + lncount):
374 if i % st == 0:
388 if i % st == 0:
375 if i % sp == 0:
389 if i % sp == 0:
376 if aln:
390 if aln:
377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
391 lines.append('<a href="#%s%d" class="special">%*d</a>' %
378 (la, i, mw, i))
392 (la, i, mw, i))
379 else:
393 else:
380 lines.append('<span class="special">%*d</span>' % (mw, i))
394 lines.append('<span class="special">%*d</span>' % (mw, i))
381 else:
395 else:
382 if aln:
396 if aln:
383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
397 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
384 else:
398 else:
385 lines.append('%*d' % (mw, i))
399 lines.append('%*d' % (mw, i))
386 else:
400 else:
387 lines.append('')
401 lines.append('')
388 ls = '\n'.join(lines)
402 ls = '\n'.join(lines)
389 else:
403 else:
390 lines = []
404 lines = []
391 for i in range(fl, fl + lncount):
405 for i in range(fl, fl + lncount):
392 if i % st == 0:
406 if i % st == 0:
393 if aln:
407 if aln:
394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
408 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
395 else:
409 else:
396 lines.append('%*d' % (mw, i))
410 lines.append('%*d' % (mw, i))
397 else:
411 else:
398 lines.append('')
412 lines.append('')
399 ls = '\n'.join(lines)
413 ls = '\n'.join(lines)
400
414
401 # in case you wonder about the seemingly redundant <div> here: since the
415 # in case you wonder about the seemingly redundant <div> here: since the
402 # content in the other cell also is wrapped in a div, some browsers in
416 # content in the other cell also is wrapped in a div, some browsers in
403 # some configurations seem to mess up the formatting...
417 # some configurations seem to mess up the formatting...
404 if nocls:
418 if nocls:
405 yield 0, ('<table class="%stable">' % self.cssclass +
419 yield 0, ('<table class="%stable">' % self.cssclass +
406 '<tr><td><div class="linenodiv" '
420 '<tr><td><div class="linenodiv" '
407 'style="background-color: #f0f0f0; padding-right: 10px">'
421 'style="background-color: #f0f0f0; padding-right: 10px">'
408 '<pre style="line-height: 125%">' +
422 '<pre style="line-height: 125%">' +
409 ls + '</pre></div></td><td id="hlcode" class="code">')
423 ls + '</pre></div></td><td id="hlcode" class="code">')
410 else:
424 else:
411 yield 0, ('<table class="%stable">' % self.cssclass +
425 yield 0, ('<table class="%stable">' % self.cssclass +
412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
426 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
413 ls + '</pre></div></td><td id="hlcode" class="code">')
427 ls + '</pre></div></td><td id="hlcode" class="code">')
414 yield 0, dummyoutfile.getvalue()
428 yield 0, dummyoutfile.getvalue()
415 yield 0, '</td></tr></table>'
429 yield 0, '</td></tr></table>'
416
430
417
431
418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
432 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
419 def __init__(self, **kw):
433 def __init__(self, **kw):
420 # only show these line numbers if set
434 # only show these line numbers if set
421 self.only_lines = kw.pop('only_line_numbers', [])
435 self.only_lines = kw.pop('only_line_numbers', [])
422 self.query_terms = kw.pop('query_terms', [])
436 self.query_terms = kw.pop('query_terms', [])
423 self.max_lines = kw.pop('max_lines', 5)
437 self.max_lines = kw.pop('max_lines', 5)
424 self.line_context = kw.pop('line_context', 3)
438 self.line_context = kw.pop('line_context', 3)
425 self.url = kw.pop('url', None)
439 self.url = kw.pop('url', None)
426
440
427 super(CodeHtmlFormatter, self).__init__(**kw)
441 super(CodeHtmlFormatter, self).__init__(**kw)
428
442
429 def _wrap_code(self, source):
443 def _wrap_code(self, source):
430 for cnt, it in enumerate(source):
444 for cnt, it in enumerate(source):
431 i, t = it
445 i, t = it
432 t = '<pre>%s</pre>' % t
446 t = '<pre>%s</pre>' % t
433 yield i, t
447 yield i, t
434
448
435 def _wrap_tablelinenos(self, inner):
449 def _wrap_tablelinenos(self, inner):
436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
450 yield 0, '<table class="code-highlight %stable">' % self.cssclass
437
451
438 last_shown_line_number = 0
452 last_shown_line_number = 0
439 current_line_number = 1
453 current_line_number = 1
440
454
441 for t, line in inner:
455 for t, line in inner:
442 if not t:
456 if not t:
443 yield t, line
457 yield t, line
444 continue
458 continue
445
459
446 if current_line_number in self.only_lines:
460 if current_line_number in self.only_lines:
447 if last_shown_line_number + 1 != current_line_number:
461 if last_shown_line_number + 1 != current_line_number:
448 yield 0, '<tr>'
462 yield 0, '<tr>'
449 yield 0, '<td class="line">...</td>'
463 yield 0, '<td class="line">...</td>'
450 yield 0, '<td id="hlcode" class="code"></td>'
464 yield 0, '<td id="hlcode" class="code"></td>'
451 yield 0, '</tr>'
465 yield 0, '</tr>'
452
466
453 yield 0, '<tr>'
467 yield 0, '<tr>'
454 if self.url:
468 if self.url:
455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
469 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
456 self.url, current_line_number, current_line_number)
470 self.url, current_line_number, current_line_number)
457 else:
471 else:
458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
472 yield 0, '<td class="line"><a href="">%i</a></td>' % (
459 current_line_number)
473 current_line_number)
460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
474 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
461 yield 0, '</tr>'
475 yield 0, '</tr>'
462
476
463 last_shown_line_number = current_line_number
477 last_shown_line_number = current_line_number
464
478
465 current_line_number += 1
479 current_line_number += 1
466
480
467 yield 0, '</table>'
481 yield 0, '</table>'
468
482
469
483
470 def hsv_to_rgb(h, s, v):
484 def hsv_to_rgb(h, s, v):
471 """ Convert hsv color values to rgb """
485 """ Convert hsv color values to rgb """
472
486
473 if s == 0.0:
487 if s == 0.0:
474 return v, v, v
488 return v, v, v
475 i = int(h * 6.0) # XXX assume int() truncates!
489 i = int(h * 6.0) # XXX assume int() truncates!
476 f = (h * 6.0) - i
490 f = (h * 6.0) - i
477 p = v * (1.0 - s)
491 p = v * (1.0 - s)
478 q = v * (1.0 - s * f)
492 q = v * (1.0 - s * f)
479 t = v * (1.0 - s * (1.0 - f))
493 t = v * (1.0 - s * (1.0 - f))
480 i = i % 6
494 i = i % 6
481 if i == 0:
495 if i == 0:
482 return v, t, p
496 return v, t, p
483 if i == 1:
497 if i == 1:
484 return q, v, p
498 return q, v, p
485 if i == 2:
499 if i == 2:
486 return p, v, t
500 return p, v, t
487 if i == 3:
501 if i == 3:
488 return p, q, v
502 return p, q, v
489 if i == 4:
503 if i == 4:
490 return t, p, v
504 return t, p, v
491 if i == 5:
505 if i == 5:
492 return v, p, q
506 return v, p, q
493
507
494
508
495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
509 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
496 """
510 """
497 Generator for getting n of evenly distributed colors using
511 Generator for getting n of evenly distributed colors using
498 hsv color and golden ratio. It always return same order of colors
512 hsv color and golden ratio. It always return same order of colors
499
513
500 :param n: number of colors to generate
514 :param n: number of colors to generate
501 :param saturation: saturation of returned colors
515 :param saturation: saturation of returned colors
502 :param lightness: lightness of returned colors
516 :param lightness: lightness of returned colors
503 :returns: RGB tuple
517 :returns: RGB tuple
504 """
518 """
505
519
506 golden_ratio = 0.618033988749895
520 golden_ratio = 0.618033988749895
507 h = 0.22717784590367374
521 h = 0.22717784590367374
508
522
509 for _ in xrange(n):
523 for _ in xrange(n):
510 h += golden_ratio
524 h += golden_ratio
511 h %= 1
525 h %= 1
512 HSV_tuple = [h, saturation, lightness]
526 HSV_tuple = [h, saturation, lightness]
513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
527 RGB_tuple = hsv_to_rgb(*HSV_tuple)
514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
528 yield map(lambda x: str(int(x * 256)), RGB_tuple)
515
529
516
530
517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
531 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
518 """
532 """
519 Returns a function which when called with an argument returns a unique
533 Returns a function which when called with an argument returns a unique
520 color for that argument, eg.
534 color for that argument, eg.
521
535
522 :param n: number of colors to generate
536 :param n: number of colors to generate
523 :param saturation: saturation of returned colors
537 :param saturation: saturation of returned colors
524 :param lightness: lightness of returned colors
538 :param lightness: lightness of returned colors
525 :returns: css RGB string
539 :returns: css RGB string
526
540
527 >>> color_hash = color_hasher()
541 >>> color_hash = color_hasher()
528 >>> color_hash('hello')
542 >>> color_hash('hello')
529 'rgb(34, 12, 59)'
543 'rgb(34, 12, 59)'
530 >>> color_hash('hello')
544 >>> color_hash('hello')
531 'rgb(34, 12, 59)'
545 'rgb(34, 12, 59)'
532 >>> color_hash('other')
546 >>> color_hash('other')
533 'rgb(90, 224, 159)'
547 'rgb(90, 224, 159)'
534 """
548 """
535
549
536 color_dict = {}
550 color_dict = {}
537 cgenerator = unique_color_generator(
551 cgenerator = unique_color_generator(
538 saturation=saturation, lightness=lightness)
552 saturation=saturation, lightness=lightness)
539
553
540 def get_color_string(thing):
554 def get_color_string(thing):
541 if thing in color_dict:
555 if thing in color_dict:
542 col = color_dict[thing]
556 col = color_dict[thing]
543 else:
557 else:
544 col = color_dict[thing] = cgenerator.next()
558 col = color_dict[thing] = cgenerator.next()
545 return "rgb(%s)" % (', '.join(col))
559 return "rgb(%s)" % (', '.join(col))
546
560
547 return get_color_string
561 return get_color_string
548
562
549
563
550 def get_lexer_safe(mimetype=None, filepath=None):
564 def get_lexer_safe(mimetype=None, filepath=None):
551 """
565 """
552 Tries to return a relevant pygments lexer using mimetype/filepath name,
566 Tries to return a relevant pygments lexer using mimetype/filepath name,
553 defaulting to plain text if none could be found
567 defaulting to plain text if none could be found
554 """
568 """
555 lexer = None
569 lexer = None
556 try:
570 try:
557 if mimetype:
571 if mimetype:
558 lexer = get_lexer_for_mimetype(mimetype)
572 lexer = get_lexer_for_mimetype(mimetype)
559 if not lexer:
573 if not lexer:
560 lexer = get_lexer_for_filename(filepath)
574 lexer = get_lexer_for_filename(filepath)
561 except pygments.util.ClassNotFound:
575 except pygments.util.ClassNotFound:
562 pass
576 pass
563
577
564 if not lexer:
578 if not lexer:
565 lexer = get_lexer_by_name('text')
579 lexer = get_lexer_by_name('text')
566
580
567 return lexer
581 return lexer
568
582
569
583
570 def get_lexer_for_filenode(filenode):
584 def get_lexer_for_filenode(filenode):
571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
585 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
572 return lexer
586 return lexer
573
587
574
588
575 def pygmentize(filenode, **kwargs):
589 def pygmentize(filenode, **kwargs):
576 """
590 """
577 pygmentize function using pygments
591 pygmentize function using pygments
578
592
579 :param filenode:
593 :param filenode:
580 """
594 """
581 lexer = get_lexer_for_filenode(filenode)
595 lexer = get_lexer_for_filenode(filenode)
582 return literal(code_highlight(filenode.content, lexer,
596 return literal(code_highlight(filenode.content, lexer,
583 CodeHtmlFormatter(**kwargs)))
597 CodeHtmlFormatter(**kwargs)))
584
598
585
599
586 def is_following_repo(repo_name, user_id):
600 def is_following_repo(repo_name, user_id):
587 from rhodecode.model.scm import ScmModel
601 from rhodecode.model.scm import ScmModel
588 return ScmModel().is_following_repo(repo_name, user_id)
602 return ScmModel().is_following_repo(repo_name, user_id)
589
603
590
604
591 class _Message(object):
605 class _Message(object):
592 """A message returned by ``Flash.pop_messages()``.
606 """A message returned by ``Flash.pop_messages()``.
593
607
594 Converting the message to a string returns the message text. Instances
608 Converting the message to a string returns the message text. Instances
595 also have the following attributes:
609 also have the following attributes:
596
610
597 * ``message``: the message text.
611 * ``message``: the message text.
598 * ``category``: the category specified when the message was created.
612 * ``category``: the category specified when the message was created.
599 """
613 """
600
614
601 def __init__(self, category, message, sub_data=None):
615 def __init__(self, category, message, sub_data=None):
602 self.category = category
616 self.category = category
603 self.message = message
617 self.message = message
604 self.sub_data = sub_data or {}
618 self.sub_data = sub_data or {}
605
619
606 def __str__(self):
620 def __str__(self):
607 return self.message
621 return self.message
608
622
609 __unicode__ = __str__
623 __unicode__ = __str__
610
624
611 def __html__(self):
625 def __html__(self):
612 return escape(safe_unicode(self.message))
626 return escape(safe_unicode(self.message))
613
627
614
628
615 class Flash(object):
629 class Flash(object):
616 # List of allowed categories. If None, allow any category.
630 # List of allowed categories. If None, allow any category.
617 categories = ["warning", "notice", "error", "success"]
631 categories = ["warning", "notice", "error", "success"]
618
632
619 # Default category if none is specified.
633 # Default category if none is specified.
620 default_category = "notice"
634 default_category = "notice"
621
635
622 def __init__(self, session_key="flash", categories=None,
636 def __init__(self, session_key="flash", categories=None,
623 default_category=None):
637 default_category=None):
624 """
638 """
625 Instantiate a ``Flash`` object.
639 Instantiate a ``Flash`` object.
626
640
627 ``session_key`` is the key to save the messages under in the user's
641 ``session_key`` is the key to save the messages under in the user's
628 session.
642 session.
629
643
630 ``categories`` is an optional list which overrides the default list
644 ``categories`` is an optional list which overrides the default list
631 of categories.
645 of categories.
632
646
633 ``default_category`` overrides the default category used for messages
647 ``default_category`` overrides the default category used for messages
634 when none is specified.
648 when none is specified.
635 """
649 """
636 self.session_key = session_key
650 self.session_key = session_key
637 if categories is not None:
651 if categories is not None:
638 self.categories = categories
652 self.categories = categories
639 if default_category is not None:
653 if default_category is not None:
640 self.default_category = default_category
654 self.default_category = default_category
641 if self.categories and self.default_category not in self.categories:
655 if self.categories and self.default_category not in self.categories:
642 raise ValueError(
656 raise ValueError(
643 "unrecognized default category %r" % (self.default_category,))
657 "unrecognized default category %r" % (self.default_category,))
644
658
645 def pop_messages(self, session=None, request=None):
659 def pop_messages(self, session=None, request=None):
646 """
660 """
647 Return all accumulated messages and delete them from the session.
661 Return all accumulated messages and delete them from the session.
648
662
649 The return value is a list of ``Message`` objects.
663 The return value is a list of ``Message`` objects.
650 """
664 """
651 messages = []
665 messages = []
652
666
653 if not session:
667 if not session:
654 if not request:
668 if not request:
655 request = get_current_request()
669 request = get_current_request()
656 session = request.session
670 session = request.session
657
671
658 # Pop the 'old' pylons flash messages. They are tuples of the form
672 # Pop the 'old' pylons flash messages. They are tuples of the form
659 # (category, message)
673 # (category, message)
660 for cat, msg in session.pop(self.session_key, []):
674 for cat, msg in session.pop(self.session_key, []):
661 messages.append(_Message(cat, msg))
675 messages.append(_Message(cat, msg))
662
676
663 # Pop the 'new' pyramid flash messages for each category as list
677 # Pop the 'new' pyramid flash messages for each category as list
664 # of strings.
678 # of strings.
665 for cat in self.categories:
679 for cat in self.categories:
666 for msg in session.pop_flash(queue=cat):
680 for msg in session.pop_flash(queue=cat):
667 sub_data = {}
681 sub_data = {}
668 if hasattr(msg, 'rsplit'):
682 if hasattr(msg, 'rsplit'):
669 flash_data = msg.rsplit('|DELIM|', 1)
683 flash_data = msg.rsplit('|DELIM|', 1)
670 org_message = flash_data[0]
684 org_message = flash_data[0]
671 if len(flash_data) > 1:
685 if len(flash_data) > 1:
672 sub_data = json.loads(flash_data[1])
686 sub_data = json.loads(flash_data[1])
673 else:
687 else:
674 org_message = msg
688 org_message = msg
675
689
676 messages.append(_Message(cat, org_message, sub_data=sub_data))
690 messages.append(_Message(cat, org_message, sub_data=sub_data))
677
691
678 # Map messages from the default queue to the 'notice' category.
692 # Map messages from the default queue to the 'notice' category.
679 for msg in session.pop_flash():
693 for msg in session.pop_flash():
680 messages.append(_Message('notice', msg))
694 messages.append(_Message('notice', msg))
681
695
682 session.save()
696 session.save()
683 return messages
697 return messages
684
698
685 def json_alerts(self, session=None, request=None):
699 def json_alerts(self, session=None, request=None):
686 payloads = []
700 payloads = []
687 messages = flash.pop_messages(session=session, request=request) or []
701 messages = flash.pop_messages(session=session, request=request) or []
688 for message in messages:
702 for message in messages:
689 payloads.append({
703 payloads.append({
690 'message': {
704 'message': {
691 'message': u'{}'.format(message.message),
705 'message': u'{}'.format(message.message),
692 'level': message.category,
706 'level': message.category,
693 'force': True,
707 'force': True,
694 'subdata': message.sub_data
708 'subdata': message.sub_data
695 }
709 }
696 })
710 })
697 return json.dumps(payloads)
711 return json.dumps(payloads)
698
712
699 def __call__(self, message, category=None, ignore_duplicate=True,
713 def __call__(self, message, category=None, ignore_duplicate=True,
700 session=None, request=None):
714 session=None, request=None):
701
715
702 if not session:
716 if not session:
703 if not request:
717 if not request:
704 request = get_current_request()
718 request = get_current_request()
705 session = request.session
719 session = request.session
706
720
707 session.flash(
721 session.flash(
708 message, queue=category, allow_duplicate=not ignore_duplicate)
722 message, queue=category, allow_duplicate=not ignore_duplicate)
709
723
710
724
711 flash = Flash()
725 flash = Flash()
712
726
713 #==============================================================================
727 #==============================================================================
714 # SCM FILTERS available via h.
728 # SCM FILTERS available via h.
715 #==============================================================================
729 #==============================================================================
716 from rhodecode.lib.vcs.utils import author_name, author_email
730 from rhodecode.lib.vcs.utils import author_name, author_email
717 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
731 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
718 from rhodecode.model.db import User, ChangesetStatus
732 from rhodecode.model.db import User, ChangesetStatus
719
733
720 capitalize = lambda x: x.capitalize()
734 capitalize = lambda x: x.capitalize()
721 email = author_email
735 email = author_email
722 short_id = lambda x: x[:12]
736 short_id = lambda x: x[:12]
723 hide_credentials = lambda x: ''.join(credentials_filter(x))
737 hide_credentials = lambda x: ''.join(credentials_filter(x))
724
738
725
739
726 import pytz
740 import pytz
727 import tzlocal
741 import tzlocal
728 local_timezone = tzlocal.get_localzone()
742 local_timezone = tzlocal.get_localzone()
729
743
730
744
731 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
745 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
732 title = value or format_date(datetime_iso)
746 title = value or format_date(datetime_iso)
733 tzinfo = '+00:00'
747 tzinfo = '+00:00'
734
748
735 # detect if we have a timezone info, otherwise, add it
749 # detect if we have a timezone info, otherwise, add it
736 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
750 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
737 force_timezone = os.environ.get('RC_TIMEZONE', '')
751 force_timezone = os.environ.get('RC_TIMEZONE', '')
738 if force_timezone:
752 if force_timezone:
739 force_timezone = pytz.timezone(force_timezone)
753 force_timezone = pytz.timezone(force_timezone)
740 timezone = force_timezone or local_timezone
754 timezone = force_timezone or local_timezone
741 offset = timezone.localize(datetime_iso).strftime('%z')
755 offset = timezone.localize(datetime_iso).strftime('%z')
742 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
756 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
743
757
744 return literal(
758 return literal(
745 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
759 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
746 cls='tooltip' if tooltip else '',
760 cls='tooltip' if tooltip else '',
747 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
761 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
748 title=title, dt=datetime_iso, tzinfo=tzinfo
762 title=title, dt=datetime_iso, tzinfo=tzinfo
749 ))
763 ))
750
764
751
765
752 def _shorten_commit_id(commit_id, commit_len=None):
766 def _shorten_commit_id(commit_id, commit_len=None):
753 if commit_len is None:
767 if commit_len is None:
754 request = get_current_request()
768 request = get_current_request()
755 commit_len = request.call_context.visual.show_sha_length
769 commit_len = request.call_context.visual.show_sha_length
756 return commit_id[:commit_len]
770 return commit_id[:commit_len]
757
771
758
772
759 def show_id(commit, show_idx=None, commit_len=None):
773 def show_id(commit, show_idx=None, commit_len=None):
760 """
774 """
761 Configurable function that shows ID
775 Configurable function that shows ID
762 by default it's r123:fffeeefffeee
776 by default it's r123:fffeeefffeee
763
777
764 :param commit: commit instance
778 :param commit: commit instance
765 """
779 """
766 if show_idx is None:
780 if show_idx is None:
767 request = get_current_request()
781 request = get_current_request()
768 show_idx = request.call_context.visual.show_revision_number
782 show_idx = request.call_context.visual.show_revision_number
769
783
770 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
784 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
771 if show_idx:
785 if show_idx:
772 return 'r%s:%s' % (commit.idx, raw_id)
786 return 'r%s:%s' % (commit.idx, raw_id)
773 else:
787 else:
774 return '%s' % (raw_id, )
788 return '%s' % (raw_id, )
775
789
776
790
777 def format_date(date):
791 def format_date(date):
778 """
792 """
779 use a standardized formatting for dates used in RhodeCode
793 use a standardized formatting for dates used in RhodeCode
780
794
781 :param date: date/datetime object
795 :param date: date/datetime object
782 :return: formatted date
796 :return: formatted date
783 """
797 """
784
798
785 if date:
799 if date:
786 _fmt = "%a, %d %b %Y %H:%M:%S"
800 _fmt = "%a, %d %b %Y %H:%M:%S"
787 return safe_unicode(date.strftime(_fmt))
801 return safe_unicode(date.strftime(_fmt))
788
802
789 return u""
803 return u""
790
804
791
805
792 class _RepoChecker(object):
806 class _RepoChecker(object):
793
807
794 def __init__(self, backend_alias):
808 def __init__(self, backend_alias):
795 self._backend_alias = backend_alias
809 self._backend_alias = backend_alias
796
810
797 def __call__(self, repository):
811 def __call__(self, repository):
798 if hasattr(repository, 'alias'):
812 if hasattr(repository, 'alias'):
799 _type = repository.alias
813 _type = repository.alias
800 elif hasattr(repository, 'repo_type'):
814 elif hasattr(repository, 'repo_type'):
801 _type = repository.repo_type
815 _type = repository.repo_type
802 else:
816 else:
803 _type = repository
817 _type = repository
804 return _type == self._backend_alias
818 return _type == self._backend_alias
805
819
806
820
807 is_git = _RepoChecker('git')
821 is_git = _RepoChecker('git')
808 is_hg = _RepoChecker('hg')
822 is_hg = _RepoChecker('hg')
809 is_svn = _RepoChecker('svn')
823 is_svn = _RepoChecker('svn')
810
824
811
825
812 def get_repo_type_by_name(repo_name):
826 def get_repo_type_by_name(repo_name):
813 repo = Repository.get_by_repo_name(repo_name)
827 repo = Repository.get_by_repo_name(repo_name)
814 if repo:
828 if repo:
815 return repo.repo_type
829 return repo.repo_type
816
830
817
831
818 def is_svn_without_proxy(repository):
832 def is_svn_without_proxy(repository):
819 if is_svn(repository):
833 if is_svn(repository):
820 from rhodecode.model.settings import VcsSettingsModel
834 from rhodecode.model.settings import VcsSettingsModel
821 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
835 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
822 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
836 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
823 return False
837 return False
824
838
825
839
826 def discover_user(author):
840 def discover_user(author):
827 """
841 """
828 Tries to discover RhodeCode User based on the author string. Author string
842 Tries to discover RhodeCode User based on the author string. Author string
829 is typically `FirstName LastName <email@address.com>`
843 is typically `FirstName LastName <email@address.com>`
830 """
844 """
831
845
832 # if author is already an instance use it for extraction
846 # if author is already an instance use it for extraction
833 if isinstance(author, User):
847 if isinstance(author, User):
834 return author
848 return author
835
849
836 # Valid email in the attribute passed, see if they're in the system
850 # Valid email in the attribute passed, see if they're in the system
837 _email = author_email(author)
851 _email = author_email(author)
838 if _email != '':
852 if _email != '':
839 user = User.get_by_email(_email, case_insensitive=True, cache=True)
853 user = User.get_by_email(_email, case_insensitive=True, cache=True)
840 if user is not None:
854 if user is not None:
841 return user
855 return user
842
856
843 # Maybe it's a username, we try to extract it and fetch by username ?
857 # Maybe it's a username, we try to extract it and fetch by username ?
844 _author = author_name(author)
858 _author = author_name(author)
845 user = User.get_by_username(_author, case_insensitive=True, cache=True)
859 user = User.get_by_username(_author, case_insensitive=True, cache=True)
846 if user is not None:
860 if user is not None:
847 return user
861 return user
848
862
849 return None
863 return None
850
864
851
865
852 def email_or_none(author):
866 def email_or_none(author):
853 # extract email from the commit string
867 # extract email from the commit string
854 _email = author_email(author)
868 _email = author_email(author)
855
869
856 # If we have an email, use it, otherwise
870 # If we have an email, use it, otherwise
857 # see if it contains a username we can get an email from
871 # see if it contains a username we can get an email from
858 if _email != '':
872 if _email != '':
859 return _email
873 return _email
860 else:
874 else:
861 user = User.get_by_username(
875 user = User.get_by_username(
862 author_name(author), case_insensitive=True, cache=True)
876 author_name(author), case_insensitive=True, cache=True)
863
877
864 if user is not None:
878 if user is not None:
865 return user.email
879 return user.email
866
880
867 # No valid email, not a valid user in the system, none!
881 # No valid email, not a valid user in the system, none!
868 return None
882 return None
869
883
870
884
871 def link_to_user(author, length=0, **kwargs):
885 def link_to_user(author, length=0, **kwargs):
872 user = discover_user(author)
886 user = discover_user(author)
873 # user can be None, but if we have it already it means we can re-use it
887 # user can be None, but if we have it already it means we can re-use it
874 # in the person() function, so we save 1 intensive-query
888 # in the person() function, so we save 1 intensive-query
875 if user:
889 if user:
876 author = user
890 author = user
877
891
878 display_person = person(author, 'username_or_name_or_email')
892 display_person = person(author, 'username_or_name_or_email')
879 if length:
893 if length:
880 display_person = shorter(display_person, length)
894 display_person = shorter(display_person, length)
881
895
882 if user:
896 if user:
883 return link_to(
897 return link_to(
884 escape(display_person),
898 escape(display_person),
885 route_path('user_profile', username=user.username),
899 route_path('user_profile', username=user.username),
886 **kwargs)
900 **kwargs)
887 else:
901 else:
888 return escape(display_person)
902 return escape(display_person)
889
903
890
904
891 def link_to_group(users_group_name, **kwargs):
905 def link_to_group(users_group_name, **kwargs):
892 return link_to(
906 return link_to(
893 escape(users_group_name),
907 escape(users_group_name),
894 route_path('user_group_profile', user_group_name=users_group_name),
908 route_path('user_group_profile', user_group_name=users_group_name),
895 **kwargs)
909 **kwargs)
896
910
897
911
898 def person(author, show_attr="username_and_name"):
912 def person(author, show_attr="username_and_name"):
899 user = discover_user(author)
913 user = discover_user(author)
900 if user:
914 if user:
901 return getattr(user, show_attr)
915 return getattr(user, show_attr)
902 else:
916 else:
903 _author = author_name(author)
917 _author = author_name(author)
904 _email = email(author)
918 _email = email(author)
905 return _author or _email
919 return _author or _email
906
920
907
921
908 def author_string(email):
922 def author_string(email):
909 if email:
923 if email:
910 user = User.get_by_email(email, case_insensitive=True, cache=True)
924 user = User.get_by_email(email, case_insensitive=True, cache=True)
911 if user:
925 if user:
912 if user.first_name or user.last_name:
926 if user.first_name or user.last_name:
913 return '%s %s &lt;%s&gt;' % (
927 return '%s %s &lt;%s&gt;' % (
914 user.first_name, user.last_name, email)
928 user.first_name, user.last_name, email)
915 else:
929 else:
916 return email
930 return email
917 else:
931 else:
918 return email
932 return email
919 else:
933 else:
920 return None
934 return None
921
935
922
936
923 def person_by_id(id_, show_attr="username_and_name"):
937 def person_by_id(id_, show_attr="username_and_name"):
924 # attr to return from fetched user
938 # attr to return from fetched user
925 person_getter = lambda usr: getattr(usr, show_attr)
939 person_getter = lambda usr: getattr(usr, show_attr)
926
940
927 #maybe it's an ID ?
941 #maybe it's an ID ?
928 if str(id_).isdigit() or isinstance(id_, int):
942 if str(id_).isdigit() or isinstance(id_, int):
929 id_ = int(id_)
943 id_ = int(id_)
930 user = User.get(id_)
944 user = User.get(id_)
931 if user is not None:
945 if user is not None:
932 return person_getter(user)
946 return person_getter(user)
933 return id_
947 return id_
934
948
935
949
936 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
950 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
937 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
951 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
938 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
952 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
939
953
940
954
941 tags_paterns = OrderedDict((
955 tags_paterns = OrderedDict((
942 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
956 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
943 '<div class="metatag" tag="lang">\\2</div>')),
957 '<div class="metatag" tag="lang">\\2</div>')),
944
958
945 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
959 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
946 '<div class="metatag" tag="see">see: \\1 </div>')),
960 '<div class="metatag" tag="see">see: \\1 </div>')),
947
961
948 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
962 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
949 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
963 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
950
964
951 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
965 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
952 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
966 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
953
967
954 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
968 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
955 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
969 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
956
970
957 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
971 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
958 '<div class="metatag" tag="state \\1">\\1</div>')),
972 '<div class="metatag" tag="state \\1">\\1</div>')),
959
973
960 # label in grey
974 # label in grey
961 ('label', (re.compile(r'\[([a-z]+)\]'),
975 ('label', (re.compile(r'\[([a-z]+)\]'),
962 '<div class="metatag" tag="label">\\1</div>')),
976 '<div class="metatag" tag="label">\\1</div>')),
963
977
964 # generic catch all in grey
978 # generic catch all in grey
965 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
979 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
966 '<div class="metatag" tag="generic">\\1</div>')),
980 '<div class="metatag" tag="generic">\\1</div>')),
967 ))
981 ))
968
982
969
983
970 def extract_metatags(value):
984 def extract_metatags(value):
971 """
985 """
972 Extract supported meta-tags from given text value
986 Extract supported meta-tags from given text value
973 """
987 """
974 tags = []
988 tags = []
975 if not value:
989 if not value:
976 return tags, ''
990 return tags, ''
977
991
978 for key, val in tags_paterns.items():
992 for key, val in tags_paterns.items():
979 pat, replace_html = val
993 pat, replace_html = val
980 tags.extend([(key, x.group()) for x in pat.finditer(value)])
994 tags.extend([(key, x.group()) for x in pat.finditer(value)])
981 value = pat.sub('', value)
995 value = pat.sub('', value)
982
996
983 return tags, value
997 return tags, value
984
998
985
999
986 def style_metatag(tag_type, value):
1000 def style_metatag(tag_type, value):
987 """
1001 """
988 converts tags from value into html equivalent
1002 converts tags from value into html equivalent
989 """
1003 """
990 if not value:
1004 if not value:
991 return ''
1005 return ''
992
1006
993 html_value = value
1007 html_value = value
994 tag_data = tags_paterns.get(tag_type)
1008 tag_data = tags_paterns.get(tag_type)
995 if tag_data:
1009 if tag_data:
996 pat, replace_html = tag_data
1010 pat, replace_html = tag_data
997 # convert to plain `unicode` instead of a markup tag to be used in
1011 # convert to plain `unicode` instead of a markup tag to be used in
998 # regex expressions. safe_unicode doesn't work here
1012 # regex expressions. safe_unicode doesn't work here
999 html_value = pat.sub(replace_html, unicode(value))
1013 html_value = pat.sub(replace_html, unicode(value))
1000
1014
1001 return html_value
1015 return html_value
1002
1016
1003
1017
1004 def bool2icon(value, show_at_false=True):
1018 def bool2icon(value, show_at_false=True):
1005 """
1019 """
1006 Returns boolean value of a given value, represented as html element with
1020 Returns boolean value of a given value, represented as html element with
1007 classes that will represent icons
1021 classes that will represent icons
1008
1022
1009 :param value: given value to convert to html node
1023 :param value: given value to convert to html node
1010 """
1024 """
1011
1025
1012 if value: # does bool conversion
1026 if value: # does bool conversion
1013 return HTML.tag('i', class_="icon-true", title='True')
1027 return HTML.tag('i', class_="icon-true", title='True')
1014 else: # not true as bool
1028 else: # not true as bool
1015 if show_at_false:
1029 if show_at_false:
1016 return HTML.tag('i', class_="icon-false", title='False')
1030 return HTML.tag('i', class_="icon-false", title='False')
1017 return HTML.tag('i')
1031 return HTML.tag('i')
1018
1032
1019 #==============================================================================
1033 #==============================================================================
1020 # PERMS
1034 # PERMS
1021 #==============================================================================
1035 #==============================================================================
1022 from rhodecode.lib.auth import (
1036 from rhodecode.lib.auth import (
1023 HasPermissionAny, HasPermissionAll,
1037 HasPermissionAny, HasPermissionAll,
1024 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1038 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1025 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1039 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1026 csrf_token_key, AuthUser)
1040 csrf_token_key, AuthUser)
1027
1041
1028
1042
1029 #==============================================================================
1043 #==============================================================================
1030 # GRAVATAR URL
1044 # GRAVATAR URL
1031 #==============================================================================
1045 #==============================================================================
1032 class InitialsGravatar(object):
1046 class InitialsGravatar(object):
1033 def __init__(self, email_address, first_name, last_name, size=30,
1047 def __init__(self, email_address, first_name, last_name, size=30,
1034 background=None, text_color='#fff'):
1048 background=None, text_color='#fff'):
1035 self.size = size
1049 self.size = size
1036 self.first_name = first_name
1050 self.first_name = first_name
1037 self.last_name = last_name
1051 self.last_name = last_name
1038 self.email_address = email_address
1052 self.email_address = email_address
1039 self.background = background or self.str2color(email_address)
1053 self.background = background or self.str2color(email_address)
1040 self.text_color = text_color
1054 self.text_color = text_color
1041
1055
1042 def get_color_bank(self):
1056 def get_color_bank(self):
1043 """
1057 """
1044 returns a predefined list of colors that gravatars can use.
1058 returns a predefined list of colors that gravatars can use.
1045 Those are randomized distinct colors that guarantee readability and
1059 Those are randomized distinct colors that guarantee readability and
1046 uniqueness.
1060 uniqueness.
1047
1061
1048 generated with: http://phrogz.net/css/distinct-colors.html
1062 generated with: http://phrogz.net/css/distinct-colors.html
1049 """
1063 """
1050 return [
1064 return [
1051 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1065 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1052 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1066 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1053 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1067 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1054 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1068 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1055 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1069 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1056 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1070 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1057 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1071 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1058 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1072 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1059 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1073 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1060 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1074 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1061 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1075 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1062 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1076 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1063 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1077 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1064 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1078 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1065 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1079 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1066 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1080 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1067 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1081 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1068 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1082 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1069 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1083 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1070 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1084 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1071 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1085 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1072 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1086 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1073 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1087 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1074 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1088 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1075 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1089 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1076 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1090 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1077 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1091 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1078 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1092 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1079 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1093 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1080 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1094 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1081 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1095 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1082 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1096 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1083 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1097 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1084 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1098 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1085 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1099 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1086 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1100 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1087 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1101 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1088 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1102 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1089 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1103 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1090 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1104 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1091 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1105 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1092 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1106 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1093 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1107 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1094 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1108 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1095 '#4f8c46', '#368dd9', '#5c0073'
1109 '#4f8c46', '#368dd9', '#5c0073'
1096 ]
1110 ]
1097
1111
1098 def rgb_to_hex_color(self, rgb_tuple):
1112 def rgb_to_hex_color(self, rgb_tuple):
1099 """
1113 """
1100 Converts an rgb_tuple passed to an hex color.
1114 Converts an rgb_tuple passed to an hex color.
1101
1115
1102 :param rgb_tuple: tuple with 3 ints represents rgb color space
1116 :param rgb_tuple: tuple with 3 ints represents rgb color space
1103 """
1117 """
1104 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1118 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1105
1119
1106 def email_to_int_list(self, email_str):
1120 def email_to_int_list(self, email_str):
1107 """
1121 """
1108 Get every byte of the hex digest value of email and turn it to integer.
1122 Get every byte of the hex digest value of email and turn it to integer.
1109 It's going to be always between 0-255
1123 It's going to be always between 0-255
1110 """
1124 """
1111 digest = md5_safe(email_str.lower())
1125 digest = md5_safe(email_str.lower())
1112 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1126 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1113
1127
1114 def pick_color_bank_index(self, email_str, color_bank):
1128 def pick_color_bank_index(self, email_str, color_bank):
1115 return self.email_to_int_list(email_str)[0] % len(color_bank)
1129 return self.email_to_int_list(email_str)[0] % len(color_bank)
1116
1130
1117 def str2color(self, email_str):
1131 def str2color(self, email_str):
1118 """
1132 """
1119 Tries to map in a stable algorithm an email to color
1133 Tries to map in a stable algorithm an email to color
1120
1134
1121 :param email_str:
1135 :param email_str:
1122 """
1136 """
1123 color_bank = self.get_color_bank()
1137 color_bank = self.get_color_bank()
1124 # pick position (module it's length so we always find it in the
1138 # pick position (module it's length so we always find it in the
1125 # bank even if it's smaller than 256 values
1139 # bank even if it's smaller than 256 values
1126 pos = self.pick_color_bank_index(email_str, color_bank)
1140 pos = self.pick_color_bank_index(email_str, color_bank)
1127 return color_bank[pos]
1141 return color_bank[pos]
1128
1142
1129 def normalize_email(self, email_address):
1143 def normalize_email(self, email_address):
1130 import unicodedata
1144 import unicodedata
1131 # default host used to fill in the fake/missing email
1145 # default host used to fill in the fake/missing email
1132 default_host = u'localhost'
1146 default_host = u'localhost'
1133
1147
1134 if not email_address:
1148 if not email_address:
1135 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1149 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1136
1150
1137 email_address = safe_unicode(email_address)
1151 email_address = safe_unicode(email_address)
1138
1152
1139 if u'@' not in email_address:
1153 if u'@' not in email_address:
1140 email_address = u'%s@%s' % (email_address, default_host)
1154 email_address = u'%s@%s' % (email_address, default_host)
1141
1155
1142 if email_address.endswith(u'@'):
1156 if email_address.endswith(u'@'):
1143 email_address = u'%s%s' % (email_address, default_host)
1157 email_address = u'%s%s' % (email_address, default_host)
1144
1158
1145 email_address = unicodedata.normalize('NFKD', email_address)\
1159 email_address = unicodedata.normalize('NFKD', email_address)\
1146 .encode('ascii', 'ignore')
1160 .encode('ascii', 'ignore')
1147 return email_address
1161 return email_address
1148
1162
1149 def get_initials(self):
1163 def get_initials(self):
1150 """
1164 """
1151 Returns 2 letter initials calculated based on the input.
1165 Returns 2 letter initials calculated based on the input.
1152 The algorithm picks first given email address, and takes first letter
1166 The algorithm picks first given email address, and takes first letter
1153 of part before @, and then the first letter of server name. In case
1167 of part before @, and then the first letter of server name. In case
1154 the part before @ is in a format of `somestring.somestring2` it replaces
1168 the part before @ is in a format of `somestring.somestring2` it replaces
1155 the server letter with first letter of somestring2
1169 the server letter with first letter of somestring2
1156
1170
1157 In case function was initialized with both first and lastname, this
1171 In case function was initialized with both first and lastname, this
1158 overrides the extraction from email by first letter of the first and
1172 overrides the extraction from email by first letter of the first and
1159 last name. We add special logic to that functionality, In case Full name
1173 last name. We add special logic to that functionality, In case Full name
1160 is compound, like Guido Von Rossum, we use last part of the last name
1174 is compound, like Guido Von Rossum, we use last part of the last name
1161 (Von Rossum) picking `R`.
1175 (Von Rossum) picking `R`.
1162
1176
1163 Function also normalizes the non-ascii characters to they ascii
1177 Function also normalizes the non-ascii characters to they ascii
1164 representation, eg Δ„ => A
1178 representation, eg Δ„ => A
1165 """
1179 """
1166 import unicodedata
1180 import unicodedata
1167 # replace non-ascii to ascii
1181 # replace non-ascii to ascii
1168 first_name = unicodedata.normalize(
1182 first_name = unicodedata.normalize(
1169 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1183 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1170 last_name = unicodedata.normalize(
1184 last_name = unicodedata.normalize(
1171 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1185 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1172
1186
1173 # do NFKD encoding, and also make sure email has proper format
1187 # do NFKD encoding, and also make sure email has proper format
1174 email_address = self.normalize_email(self.email_address)
1188 email_address = self.normalize_email(self.email_address)
1175
1189
1176 # first push the email initials
1190 # first push the email initials
1177 prefix, server = email_address.split('@', 1)
1191 prefix, server = email_address.split('@', 1)
1178
1192
1179 # check if prefix is maybe a 'first_name.last_name' syntax
1193 # check if prefix is maybe a 'first_name.last_name' syntax
1180 _dot_split = prefix.rsplit('.', 1)
1194 _dot_split = prefix.rsplit('.', 1)
1181 if len(_dot_split) == 2 and _dot_split[1]:
1195 if len(_dot_split) == 2 and _dot_split[1]:
1182 initials = [_dot_split[0][0], _dot_split[1][0]]
1196 initials = [_dot_split[0][0], _dot_split[1][0]]
1183 else:
1197 else:
1184 initials = [prefix[0], server[0]]
1198 initials = [prefix[0], server[0]]
1185
1199
1186 # then try to replace either first_name or last_name
1200 # then try to replace either first_name or last_name
1187 fn_letter = (first_name or " ")[0].strip()
1201 fn_letter = (first_name or " ")[0].strip()
1188 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1202 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1189
1203
1190 if fn_letter:
1204 if fn_letter:
1191 initials[0] = fn_letter
1205 initials[0] = fn_letter
1192
1206
1193 if ln_letter:
1207 if ln_letter:
1194 initials[1] = ln_letter
1208 initials[1] = ln_letter
1195
1209
1196 return ''.join(initials).upper()
1210 return ''.join(initials).upper()
1197
1211
1198 def get_img_data_by_type(self, font_family, img_type):
1212 def get_img_data_by_type(self, font_family, img_type):
1199 default_user = """
1213 default_user = """
1200 <svg xmlns="http://www.w3.org/2000/svg"
1214 <svg xmlns="http://www.w3.org/2000/svg"
1201 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1215 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1202 viewBox="-15 -10 439.165 429.164"
1216 viewBox="-15 -10 439.165 429.164"
1203
1217
1204 xml:space="preserve"
1218 xml:space="preserve"
1205 style="background:{background};" >
1219 style="background:{background};" >
1206
1220
1207 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1221 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1208 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1222 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1209 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1223 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1210 168.596,153.916,216.671,
1224 168.596,153.916,216.671,
1211 204.583,216.671z" fill="{text_color}"/>
1225 204.583,216.671z" fill="{text_color}"/>
1212 <path d="M407.164,374.717L360.88,
1226 <path d="M407.164,374.717L360.88,
1213 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1227 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1214 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1228 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1215 15.366-44.203,23.488-69.076,23.488c-24.877,
1229 15.366-44.203,23.488-69.076,23.488c-24.877,
1216 0-48.762-8.122-69.078-23.488
1230 0-48.762-8.122-69.078-23.488
1217 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1231 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1218 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1232 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1219 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1233 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1220 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1234 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1221 19.402-10.527 C409.699,390.129,
1235 19.402-10.527 C409.699,390.129,
1222 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1236 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1223 </svg>""".format(
1237 </svg>""".format(
1224 size=self.size,
1238 size=self.size,
1225 background='#979797', # @grey4
1239 background='#979797', # @grey4
1226 text_color=self.text_color,
1240 text_color=self.text_color,
1227 font_family=font_family)
1241 font_family=font_family)
1228
1242
1229 return {
1243 return {
1230 "default_user": default_user
1244 "default_user": default_user
1231 }[img_type]
1245 }[img_type]
1232
1246
1233 def get_img_data(self, svg_type=None):
1247 def get_img_data(self, svg_type=None):
1234 """
1248 """
1235 generates the svg metadata for image
1249 generates the svg metadata for image
1236 """
1250 """
1237 fonts = [
1251 fonts = [
1238 '-apple-system',
1252 '-apple-system',
1239 'BlinkMacSystemFont',
1253 'BlinkMacSystemFont',
1240 'Segoe UI',
1254 'Segoe UI',
1241 'Roboto',
1255 'Roboto',
1242 'Oxygen-Sans',
1256 'Oxygen-Sans',
1243 'Ubuntu',
1257 'Ubuntu',
1244 'Cantarell',
1258 'Cantarell',
1245 'Helvetica Neue',
1259 'Helvetica Neue',
1246 'sans-serif'
1260 'sans-serif'
1247 ]
1261 ]
1248 font_family = ','.join(fonts)
1262 font_family = ','.join(fonts)
1249 if svg_type:
1263 if svg_type:
1250 return self.get_img_data_by_type(font_family, svg_type)
1264 return self.get_img_data_by_type(font_family, svg_type)
1251
1265
1252 initials = self.get_initials()
1266 initials = self.get_initials()
1253 img_data = """
1267 img_data = """
1254 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1268 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1255 width="{size}" height="{size}"
1269 width="{size}" height="{size}"
1256 style="width: 100%; height: 100%; background-color: {background}"
1270 style="width: 100%; height: 100%; background-color: {background}"
1257 viewBox="0 0 {size} {size}">
1271 viewBox="0 0 {size} {size}">
1258 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1272 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1259 pointer-events="auto" fill="{text_color}"
1273 pointer-events="auto" fill="{text_color}"
1260 font-family="{font_family}"
1274 font-family="{font_family}"
1261 style="font-weight: 400; font-size: {f_size}px;">{text}
1275 style="font-weight: 400; font-size: {f_size}px;">{text}
1262 </text>
1276 </text>
1263 </svg>""".format(
1277 </svg>""".format(
1264 size=self.size,
1278 size=self.size,
1265 f_size=self.size/2.05, # scale the text inside the box nicely
1279 f_size=self.size/2.05, # scale the text inside the box nicely
1266 background=self.background,
1280 background=self.background,
1267 text_color=self.text_color,
1281 text_color=self.text_color,
1268 text=initials.upper(),
1282 text=initials.upper(),
1269 font_family=font_family)
1283 font_family=font_family)
1270
1284
1271 return img_data
1285 return img_data
1272
1286
1273 def generate_svg(self, svg_type=None):
1287 def generate_svg(self, svg_type=None):
1274 img_data = self.get_img_data(svg_type)
1288 img_data = self.get_img_data(svg_type)
1275 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1289 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1276
1290
1277
1291
1278 def initials_gravatar(email_address, first_name, last_name, size=30):
1292 def initials_gravatar(email_address, first_name, last_name, size=30):
1279 svg_type = None
1293 svg_type = None
1280 if email_address == User.DEFAULT_USER_EMAIL:
1294 if email_address == User.DEFAULT_USER_EMAIL:
1281 svg_type = 'default_user'
1295 svg_type = 'default_user'
1282 klass = InitialsGravatar(email_address, first_name, last_name, size)
1296 klass = InitialsGravatar(email_address, first_name, last_name, size)
1283 return klass.generate_svg(svg_type=svg_type)
1297 return klass.generate_svg(svg_type=svg_type)
1284
1298
1285
1299
1286 def gravatar_url(email_address, size=30, request=None):
1300 def gravatar_url(email_address, size=30, request=None):
1287 request = get_current_request()
1301 request = get_current_request()
1288 _use_gravatar = request.call_context.visual.use_gravatar
1302 _use_gravatar = request.call_context.visual.use_gravatar
1289 _gravatar_url = request.call_context.visual.gravatar_url
1303 _gravatar_url = request.call_context.visual.gravatar_url
1290
1304
1291 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1305 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1292
1306
1293 email_address = email_address or User.DEFAULT_USER_EMAIL
1307 email_address = email_address or User.DEFAULT_USER_EMAIL
1294 if isinstance(email_address, unicode):
1308 if isinstance(email_address, unicode):
1295 # hashlib crashes on unicode items
1309 # hashlib crashes on unicode items
1296 email_address = safe_str(email_address)
1310 email_address = safe_str(email_address)
1297
1311
1298 # empty email or default user
1312 # empty email or default user
1299 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1313 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1300 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1314 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1301
1315
1302 if _use_gravatar:
1316 if _use_gravatar:
1303 # TODO: Disuse pyramid thread locals. Think about another solution to
1317 # TODO: Disuse pyramid thread locals. Think about another solution to
1304 # get the host and schema here.
1318 # get the host and schema here.
1305 request = get_current_request()
1319 request = get_current_request()
1306 tmpl = safe_str(_gravatar_url)
1320 tmpl = safe_str(_gravatar_url)
1307 tmpl = tmpl.replace('{email}', email_address)\
1321 tmpl = tmpl.replace('{email}', email_address)\
1308 .replace('{md5email}', md5_safe(email_address.lower())) \
1322 .replace('{md5email}', md5_safe(email_address.lower())) \
1309 .replace('{netloc}', request.host)\
1323 .replace('{netloc}', request.host)\
1310 .replace('{scheme}', request.scheme)\
1324 .replace('{scheme}', request.scheme)\
1311 .replace('{size}', safe_str(size))
1325 .replace('{size}', safe_str(size))
1312 return tmpl
1326 return tmpl
1313 else:
1327 else:
1314 return initials_gravatar(email_address, '', '', size=size)
1328 return initials_gravatar(email_address, '', '', size=size)
1315
1329
1316
1330
1317 def breadcrumb_repo_link(repo):
1331 def breadcrumb_repo_link(repo):
1318 """
1332 """
1319 Makes a breadcrumbs path link to repo
1333 Makes a breadcrumbs path link to repo
1320
1334
1321 ex::
1335 ex::
1322 group >> subgroup >> repo
1336 group >> subgroup >> repo
1323
1337
1324 :param repo: a Repository instance
1338 :param repo: a Repository instance
1325 """
1339 """
1326
1340
1327 path = [
1341 path = [
1328 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1342 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1329 title='last change:{}'.format(format_date(group.last_commit_change)))
1343 title='last change:{}'.format(format_date(group.last_commit_change)))
1330 for group in repo.groups_with_parents
1344 for group in repo.groups_with_parents
1331 ] + [
1345 ] + [
1332 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1346 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1333 title='last change:{}'.format(format_date(repo.last_commit_change)))
1347 title='last change:{}'.format(format_date(repo.last_commit_change)))
1334 ]
1348 ]
1335
1349
1336 return literal(' &raquo; '.join(path))
1350 return literal(' &raquo; '.join(path))
1337
1351
1338
1352
1339 def breadcrumb_repo_group_link(repo_group):
1353 def breadcrumb_repo_group_link(repo_group):
1340 """
1354 """
1341 Makes a breadcrumbs path link to repo
1355 Makes a breadcrumbs path link to repo
1342
1356
1343 ex::
1357 ex::
1344 group >> subgroup
1358 group >> subgroup
1345
1359
1346 :param repo_group: a Repository Group instance
1360 :param repo_group: a Repository Group instance
1347 """
1361 """
1348
1362
1349 path = [
1363 path = [
1350 link_to(group.name,
1364 link_to(group.name,
1351 route_path('repo_group_home', repo_group_name=group.group_name),
1365 route_path('repo_group_home', repo_group_name=group.group_name),
1352 title='last change:{}'.format(format_date(group.last_commit_change)))
1366 title='last change:{}'.format(format_date(group.last_commit_change)))
1353 for group in repo_group.parents
1367 for group in repo_group.parents
1354 ] + [
1368 ] + [
1355 link_to(repo_group.name,
1369 link_to(repo_group.name,
1356 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1370 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1357 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1371 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1358 ]
1372 ]
1359
1373
1360 return literal(' &raquo; '.join(path))
1374 return literal(' &raquo; '.join(path))
1361
1375
1362
1376
1363 def format_byte_size_binary(file_size):
1377 def format_byte_size_binary(file_size):
1364 """
1378 """
1365 Formats file/folder sizes to standard.
1379 Formats file/folder sizes to standard.
1366 """
1380 """
1367 if file_size is None:
1381 if file_size is None:
1368 file_size = 0
1382 file_size = 0
1369
1383
1370 formatted_size = format_byte_size(file_size, binary=True)
1384 formatted_size = format_byte_size(file_size, binary=True)
1371 return formatted_size
1385 return formatted_size
1372
1386
1373
1387
1374 def urlify_text(text_, safe=True, **href_attrs):
1388 def urlify_text(text_, safe=True, **href_attrs):
1375 """
1389 """
1376 Extract urls from text and make html links out of them
1390 Extract urls from text and make html links out of them
1377 """
1391 """
1378
1392
1379 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1393 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1380 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1394 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1381
1395
1382 def url_func(match_obj):
1396 def url_func(match_obj):
1383 url_full = match_obj.groups()[0]
1397 url_full = match_obj.groups()[0]
1384 a_options = dict(href_attrs)
1398 a_options = dict(href_attrs)
1385 a_options['href'] = url_full
1399 a_options['href'] = url_full
1386 a_text = url_full
1400 a_text = url_full
1387 return HTML.tag("a", a_text, **a_options)
1401 return HTML.tag("a", a_text, **a_options)
1388
1402
1389 _new_text = url_pat.sub(url_func, text_)
1403 _new_text = url_pat.sub(url_func, text_)
1390
1404
1391 if safe:
1405 if safe:
1392 return literal(_new_text)
1406 return literal(_new_text)
1393 return _new_text
1407 return _new_text
1394
1408
1395
1409
1396 def urlify_commits(text_, repo_name):
1410 def urlify_commits(text_, repo_name):
1397 """
1411 """
1398 Extract commit ids from text and make link from them
1412 Extract commit ids from text and make link from them
1399
1413
1400 :param text_:
1414 :param text_:
1401 :param repo_name: repo name to build the URL with
1415 :param repo_name: repo name to build the URL with
1402 """
1416 """
1403
1417
1404 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1418 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1405
1419
1406 def url_func(match_obj):
1420 def url_func(match_obj):
1407 commit_id = match_obj.groups()[1]
1421 commit_id = match_obj.groups()[1]
1408 pref = match_obj.groups()[0]
1422 pref = match_obj.groups()[0]
1409 suf = match_obj.groups()[2]
1423 suf = match_obj.groups()[2]
1410
1424
1411 tmpl = (
1425 tmpl = (
1412 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1426 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1413 '%(commit_id)s</a>%(suf)s'
1427 '%(commit_id)s</a>%(suf)s'
1414 )
1428 )
1415 return tmpl % {
1429 return tmpl % {
1416 'pref': pref,
1430 'pref': pref,
1417 'cls': 'revision-link',
1431 'cls': 'revision-link',
1418 'url': route_url(
1432 'url': route_url(
1419 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1433 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1420 'commit_id': commit_id,
1434 'commit_id': commit_id,
1421 'suf': suf,
1435 'suf': suf,
1422 'hovercard_alt': 'Commit: {}'.format(commit_id),
1436 'hovercard_alt': 'Commit: {}'.format(commit_id),
1423 'hovercard_url': route_url(
1437 'hovercard_url': route_url(
1424 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1438 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1425 }
1439 }
1426
1440
1427 new_text = url_pat.sub(url_func, text_)
1441 new_text = url_pat.sub(url_func, text_)
1428
1442
1429 return new_text
1443 return new_text
1430
1444
1431
1445
1432 def _process_url_func(match_obj, repo_name, uid, entry,
1446 def _process_url_func(match_obj, repo_name, uid, entry,
1433 return_raw_data=False, link_format='html'):
1447 return_raw_data=False, link_format='html'):
1434 pref = ''
1448 pref = ''
1435 if match_obj.group().startswith(' '):
1449 if match_obj.group().startswith(' '):
1436 pref = ' '
1450 pref = ' '
1437
1451
1438 issue_id = ''.join(match_obj.groups())
1452 issue_id = ''.join(match_obj.groups())
1439
1453
1440 if link_format == 'html':
1454 if link_format == 'html':
1441 tmpl = (
1455 tmpl = (
1442 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1456 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1443 '%(issue-prefix)s%(id-repr)s'
1457 '%(issue-prefix)s%(id-repr)s'
1444 '</a>')
1458 '</a>')
1445 elif link_format == 'html+hovercard':
1459 elif link_format == 'html+hovercard':
1446 tmpl = (
1460 tmpl = (
1447 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1461 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1448 '%(issue-prefix)s%(id-repr)s'
1462 '%(issue-prefix)s%(id-repr)s'
1449 '</a>')
1463 '</a>')
1450 elif link_format in ['rst', 'rst+hovercard']:
1464 elif link_format in ['rst', 'rst+hovercard']:
1451 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1465 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1452 elif link_format in ['markdown', 'markdown+hovercard']:
1466 elif link_format in ['markdown', 'markdown+hovercard']:
1453 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1467 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1454 else:
1468 else:
1455 raise ValueError('Bad link_format:{}'.format(link_format))
1469 raise ValueError('Bad link_format:{}'.format(link_format))
1456
1470
1457 (repo_name_cleaned,
1471 (repo_name_cleaned,
1458 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1472 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1459
1473
1460 # variables replacement
1474 # variables replacement
1461 named_vars = {
1475 named_vars = {
1462 'id': issue_id,
1476 'id': issue_id,
1463 'repo': repo_name,
1477 'repo': repo_name,
1464 'repo_name': repo_name_cleaned,
1478 'repo_name': repo_name_cleaned,
1465 'group_name': parent_group_name,
1479 'group_name': parent_group_name,
1466 # set dummy keys so we always have them
1480 # set dummy keys so we always have them
1467 'hostname': '',
1481 'hostname': '',
1468 'netloc': '',
1482 'netloc': '',
1469 'scheme': ''
1483 'scheme': ''
1470 }
1484 }
1471
1485
1472 request = get_current_request()
1486 request = get_current_request()
1473 if request:
1487 if request:
1474 # exposes, hostname, netloc, scheme
1488 # exposes, hostname, netloc, scheme
1475 host_data = get_host_info(request)
1489 host_data = get_host_info(request)
1476 named_vars.update(host_data)
1490 named_vars.update(host_data)
1477
1491
1478 # named regex variables
1492 # named regex variables
1479 named_vars.update(match_obj.groupdict())
1493 named_vars.update(match_obj.groupdict())
1480 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1494 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1481 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1495 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1482 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1496 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1483
1497
1484 def quote_cleaner(input_str):
1498 def quote_cleaner(input_str):
1485 """Remove quotes as it's HTML"""
1499 """Remove quotes as it's HTML"""
1486 return input_str.replace('"', '')
1500 return input_str.replace('"', '')
1487
1501
1488 data = {
1502 data = {
1489 'pref': pref,
1503 'pref': pref,
1490 'cls': quote_cleaner('issue-tracker-link'),
1504 'cls': quote_cleaner('issue-tracker-link'),
1491 'url': quote_cleaner(_url),
1505 'url': quote_cleaner(_url),
1492 'id-repr': issue_id,
1506 'id-repr': issue_id,
1493 'issue-prefix': entry['pref'],
1507 'issue-prefix': entry['pref'],
1494 'serv': entry['url'],
1508 'serv': entry['url'],
1495 'title': bleach.clean(desc, strip=True),
1509 'title': bleach.clean(desc, strip=True),
1496 'hovercard_url': hovercard_url
1510 'hovercard_url': hovercard_url
1497 }
1511 }
1498
1512
1499 if return_raw_data:
1513 if return_raw_data:
1500 return {
1514 return {
1501 'id': issue_id,
1515 'id': issue_id,
1502 'url': _url
1516 'url': _url
1503 }
1517 }
1504 return tmpl % data
1518 return tmpl % data
1505
1519
1506
1520
1507 def get_active_pattern_entries(repo_name):
1521 def get_active_pattern_entries(repo_name):
1508 repo = None
1522 repo = None
1509 if repo_name:
1523 if repo_name:
1510 # Retrieving repo_name to avoid invalid repo_name to explode on
1524 # Retrieving repo_name to avoid invalid repo_name to explode on
1511 # IssueTrackerSettingsModel but still passing invalid name further down
1525 # IssueTrackerSettingsModel but still passing invalid name further down
1512 repo = Repository.get_by_repo_name(repo_name, cache=True)
1526 repo = Repository.get_by_repo_name(repo_name, cache=True)
1513
1527
1514 settings_model = IssueTrackerSettingsModel(repo=repo)
1528 settings_model = IssueTrackerSettingsModel(repo=repo)
1515 active_entries = settings_model.get_settings(cache=True)
1529 active_entries = settings_model.get_settings(cache=True)
1516 return active_entries
1530 return active_entries
1517
1531
1518
1532
1519 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1533 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1520
1534
1521
1535
1522 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1536 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1523
1537
1524 allowed_formats = ['html', 'rst', 'markdown',
1538 allowed_formats = ['html', 'rst', 'markdown',
1525 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1539 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1526 if link_format not in allowed_formats:
1540 if link_format not in allowed_formats:
1527 raise ValueError('Link format can be only one of:{} got {}'.format(
1541 raise ValueError('Link format can be only one of:{} got {}'.format(
1528 allowed_formats, link_format))
1542 allowed_formats, link_format))
1529
1543
1530 if active_entries is None:
1544 if active_entries is None:
1531 log.debug('Fetch active patterns for repo: %s', repo_name)
1545 log.debug('Fetch active patterns for repo: %s', repo_name)
1532 active_entries = get_active_pattern_entries(repo_name)
1546 active_entries = get_active_pattern_entries(repo_name)
1533
1547
1534 issues_data = []
1548 issues_data = []
1535 new_text = text_string
1549 new_text = text_string
1536
1550
1537 log.debug('Got %s entries to process', len(active_entries))
1551 log.debug('Got %s entries to process', len(active_entries))
1538 for uid, entry in active_entries.items():
1552 for uid, entry in active_entries.items():
1539 log.debug('found issue tracker entry with uid %s', uid)
1553 log.debug('found issue tracker entry with uid %s', uid)
1540
1554
1541 if not (entry['pat'] and entry['url']):
1555 if not (entry['pat'] and entry['url']):
1542 log.debug('skipping due to missing data')
1556 log.debug('skipping due to missing data')
1543 continue
1557 continue
1544
1558
1545 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1559 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1546 uid, entry['pat'], entry['url'], entry['pref'])
1560 uid, entry['pat'], entry['url'], entry['pref'])
1547
1561
1548 if entry.get('pat_compiled'):
1562 if entry.get('pat_compiled'):
1549 pattern = entry['pat_compiled']
1563 pattern = entry['pat_compiled']
1550 else:
1564 else:
1551 try:
1565 try:
1552 pattern = re.compile(r'%s' % entry['pat'])
1566 pattern = re.compile(r'%s' % entry['pat'])
1553 except re.error:
1567 except re.error:
1554 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1568 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1555 continue
1569 continue
1556
1570
1557 data_func = partial(
1571 data_func = partial(
1558 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1572 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1559 return_raw_data=True)
1573 return_raw_data=True)
1560
1574
1561 for match_obj in pattern.finditer(text_string):
1575 for match_obj in pattern.finditer(text_string):
1562 issues_data.append(data_func(match_obj))
1576 issues_data.append(data_func(match_obj))
1563
1577
1564 url_func = partial(
1578 url_func = partial(
1565 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1579 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1566 link_format=link_format)
1580 link_format=link_format)
1567
1581
1568 new_text = pattern.sub(url_func, new_text)
1582 new_text = pattern.sub(url_func, new_text)
1569 log.debug('processed prefix:uid `%s`', uid)
1583 log.debug('processed prefix:uid `%s`', uid)
1570
1584
1571 # finally use global replace, eg !123 -> pr-link, those will not catch
1585 # finally use global replace, eg !123 -> pr-link, those will not catch
1572 # if already similar pattern exists
1586 # if already similar pattern exists
1573 server_url = '${scheme}://${netloc}'
1587 server_url = '${scheme}://${netloc}'
1574 pr_entry = {
1588 pr_entry = {
1575 'pref': '!',
1589 'pref': '!',
1576 'url': server_url + '/_admin/pull-requests/${id}',
1590 'url': server_url + '/_admin/pull-requests/${id}',
1577 'desc': 'Pull Request !${id}',
1591 'desc': 'Pull Request !${id}',
1578 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1592 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1579 }
1593 }
1580 pr_url_func = partial(
1594 pr_url_func = partial(
1581 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1595 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1582 link_format=link_format+'+hovercard')
1596 link_format=link_format+'+hovercard')
1583 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1597 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1584 log.debug('processed !pr pattern')
1598 log.debug('processed !pr pattern')
1585
1599
1586 return new_text, issues_data
1600 return new_text, issues_data
1587
1601
1588
1602
1589 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1603 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1590 """
1604 """
1591 Parses given text message and makes proper links.
1605 Parses given text message and makes proper links.
1592 issues are linked to given issue-server, and rest is a commit link
1606 issues are linked to given issue-server, and rest is a commit link
1593 """
1607 """
1594
1608
1595 def escaper(_text):
1609 def escaper(_text):
1596 return _text.replace('<', '&lt;').replace('>', '&gt;')
1610 return _text.replace('<', '&lt;').replace('>', '&gt;')
1597
1611
1598 new_text = escaper(commit_text)
1612 new_text = escaper(commit_text)
1599
1613
1600 # extract http/https links and make them real urls
1614 # extract http/https links and make them real urls
1601 new_text = urlify_text(new_text, safe=False)
1615 new_text = urlify_text(new_text, safe=False)
1602
1616
1603 # urlify commits - extract commit ids and make link out of them, if we have
1617 # urlify commits - extract commit ids and make link out of them, if we have
1604 # the scope of repository present.
1618 # the scope of repository present.
1605 if repository:
1619 if repository:
1606 new_text = urlify_commits(new_text, repository)
1620 new_text = urlify_commits(new_text, repository)
1607
1621
1608 # process issue tracker patterns
1622 # process issue tracker patterns
1609 new_text, issues = process_patterns(new_text, repository or '',
1623 new_text, issues = process_patterns(new_text, repository or '',
1610 active_entries=active_pattern_entries)
1624 active_entries=active_pattern_entries)
1611
1625
1612 return literal(new_text)
1626 return literal(new_text)
1613
1627
1614
1628
1615 def render_binary(repo_name, file_obj):
1629 def render_binary(repo_name, file_obj):
1616 """
1630 """
1617 Choose how to render a binary file
1631 Choose how to render a binary file
1618 """
1632 """
1619
1633
1620 # unicode
1634 # unicode
1621 filename = file_obj.name
1635 filename = file_obj.name
1622
1636
1623 # images
1637 # images
1624 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1638 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1625 if fnmatch.fnmatch(filename, pat=ext):
1639 if fnmatch.fnmatch(filename, pat=ext):
1626 src = route_path(
1640 src = route_path(
1627 'repo_file_raw', repo_name=repo_name,
1641 'repo_file_raw', repo_name=repo_name,
1628 commit_id=file_obj.commit.raw_id,
1642 commit_id=file_obj.commit.raw_id,
1629 f_path=file_obj.path)
1643 f_path=file_obj.path)
1630
1644
1631 return literal(
1645 return literal(
1632 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1646 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1633
1647
1634
1648
1635 def renderer_from_filename(filename, exclude=None):
1649 def renderer_from_filename(filename, exclude=None):
1636 """
1650 """
1637 choose a renderer based on filename, this works only for text based files
1651 choose a renderer based on filename, this works only for text based files
1638 """
1652 """
1639
1653
1640 # ipython
1654 # ipython
1641 for ext in ['*.ipynb']:
1655 for ext in ['*.ipynb']:
1642 if fnmatch.fnmatch(filename, pat=ext):
1656 if fnmatch.fnmatch(filename, pat=ext):
1643 return 'jupyter'
1657 return 'jupyter'
1644
1658
1645 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1659 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1646 if is_markup:
1660 if is_markup:
1647 return is_markup
1661 return is_markup
1648 return None
1662 return None
1649
1663
1650
1664
1651 def render(source, renderer='rst', mentions=False, relative_urls=None,
1665 def render(source, renderer='rst', mentions=False, relative_urls=None,
1652 repo_name=None, active_pattern_entries=None):
1666 repo_name=None, active_pattern_entries=None):
1653
1667
1654 def maybe_convert_relative_links(html_source):
1668 def maybe_convert_relative_links(html_source):
1655 if relative_urls:
1669 if relative_urls:
1656 return relative_links(html_source, relative_urls)
1670 return relative_links(html_source, relative_urls)
1657 return html_source
1671 return html_source
1658
1672
1659 if renderer == 'plain':
1673 if renderer == 'plain':
1660 return literal(
1674 return literal(
1661 MarkupRenderer.plain(source, leading_newline=False))
1675 MarkupRenderer.plain(source, leading_newline=False))
1662
1676
1663 elif renderer == 'rst':
1677 elif renderer == 'rst':
1664 if repo_name:
1678 if repo_name:
1665 # process patterns on comments if we pass in repo name
1679 # process patterns on comments if we pass in repo name
1666 source, issues = process_patterns(
1680 source, issues = process_patterns(
1667 source, repo_name, link_format='rst',
1681 source, repo_name, link_format='rst',
1668 active_entries=active_pattern_entries)
1682 active_entries=active_pattern_entries)
1669
1683
1670 return literal(
1684 return literal(
1671 '<div class="rst-block">%s</div>' %
1685 '<div class="rst-block">%s</div>' %
1672 maybe_convert_relative_links(
1686 maybe_convert_relative_links(
1673 MarkupRenderer.rst(source, mentions=mentions)))
1687 MarkupRenderer.rst(source, mentions=mentions)))
1674
1688
1675 elif renderer == 'markdown':
1689 elif renderer == 'markdown':
1676 if repo_name:
1690 if repo_name:
1677 # process patterns on comments if we pass in repo name
1691 # process patterns on comments if we pass in repo name
1678 source, issues = process_patterns(
1692 source, issues = process_patterns(
1679 source, repo_name, link_format='markdown',
1693 source, repo_name, link_format='markdown',
1680 active_entries=active_pattern_entries)
1694 active_entries=active_pattern_entries)
1681
1695
1682 return literal(
1696 return literal(
1683 '<div class="markdown-block">%s</div>' %
1697 '<div class="markdown-block">%s</div>' %
1684 maybe_convert_relative_links(
1698 maybe_convert_relative_links(
1685 MarkupRenderer.markdown(source, flavored=True,
1699 MarkupRenderer.markdown(source, flavored=True,
1686 mentions=mentions)))
1700 mentions=mentions)))
1687
1701
1688 elif renderer == 'jupyter':
1702 elif renderer == 'jupyter':
1689 return literal(
1703 return literal(
1690 '<div class="ipynb">%s</div>' %
1704 '<div class="ipynb">%s</div>' %
1691 maybe_convert_relative_links(
1705 maybe_convert_relative_links(
1692 MarkupRenderer.jupyter(source)))
1706 MarkupRenderer.jupyter(source)))
1693
1707
1694 # None means just show the file-source
1708 # None means just show the file-source
1695 return None
1709 return None
1696
1710
1697
1711
1698 def commit_status(repo, commit_id):
1712 def commit_status(repo, commit_id):
1699 return ChangesetStatusModel().get_status(repo, commit_id)
1713 return ChangesetStatusModel().get_status(repo, commit_id)
1700
1714
1701
1715
1702 def commit_status_lbl(commit_status):
1716 def commit_status_lbl(commit_status):
1703 return dict(ChangesetStatus.STATUSES).get(commit_status)
1717 return dict(ChangesetStatus.STATUSES).get(commit_status)
1704
1718
1705
1719
1706 def commit_time(repo_name, commit_id):
1720 def commit_time(repo_name, commit_id):
1707 repo = Repository.get_by_repo_name(repo_name)
1721 repo = Repository.get_by_repo_name(repo_name)
1708 commit = repo.get_commit(commit_id=commit_id)
1722 commit = repo.get_commit(commit_id=commit_id)
1709 return commit.date
1723 return commit.date
1710
1724
1711
1725
1712 def get_permission_name(key):
1726 def get_permission_name(key):
1713 return dict(Permission.PERMS).get(key)
1727 return dict(Permission.PERMS).get(key)
1714
1728
1715
1729
1716 def journal_filter_help(request):
1730 def journal_filter_help(request):
1717 _ = request.translate
1731 _ = request.translate
1718 from rhodecode.lib.audit_logger import ACTIONS
1732 from rhodecode.lib.audit_logger import ACTIONS
1719 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1733 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1720
1734
1721 return _(
1735 return _(
1722 'Example filter terms:\n' +
1736 'Example filter terms:\n' +
1723 ' repository:vcs\n' +
1737 ' repository:vcs\n' +
1724 ' username:marcin\n' +
1738 ' username:marcin\n' +
1725 ' username:(NOT marcin)\n' +
1739 ' username:(NOT marcin)\n' +
1726 ' action:*push*\n' +
1740 ' action:*push*\n' +
1727 ' ip:127.0.0.1\n' +
1741 ' ip:127.0.0.1\n' +
1728 ' date:20120101\n' +
1742 ' date:20120101\n' +
1729 ' date:[20120101100000 TO 20120102]\n' +
1743 ' date:[20120101100000 TO 20120102]\n' +
1730 '\n' +
1744 '\n' +
1731 'Actions: {actions}\n' +
1745 'Actions: {actions}\n' +
1732 '\n' +
1746 '\n' +
1733 'Generate wildcards using \'*\' character:\n' +
1747 'Generate wildcards using \'*\' character:\n' +
1734 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1748 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1735 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1749 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1736 '\n' +
1750 '\n' +
1737 'Optional AND / OR operators in queries\n' +
1751 'Optional AND / OR operators in queries\n' +
1738 ' "repository:vcs OR repository:test"\n' +
1752 ' "repository:vcs OR repository:test"\n' +
1739 ' "username:test AND repository:test*"\n'
1753 ' "username:test AND repository:test*"\n'
1740 ).format(actions=actions)
1754 ).format(actions=actions)
1741
1755
1742
1756
1743 def not_mapped_error(repo_name):
1757 def not_mapped_error(repo_name):
1744 from rhodecode.translation import _
1758 from rhodecode.translation import _
1745 flash(_('%s repository is not mapped to db perhaps'
1759 flash(_('%s repository is not mapped to db perhaps'
1746 ' it was created or renamed from the filesystem'
1760 ' it was created or renamed from the filesystem'
1747 ' please run the application again'
1761 ' please run the application again'
1748 ' in order to rescan repositories') % repo_name, category='error')
1762 ' in order to rescan repositories') % repo_name, category='error')
1749
1763
1750
1764
1751 def ip_range(ip_addr):
1765 def ip_range(ip_addr):
1752 from rhodecode.model.db import UserIpMap
1766 from rhodecode.model.db import UserIpMap
1753 s, e = UserIpMap._get_ip_range(ip_addr)
1767 s, e = UserIpMap._get_ip_range(ip_addr)
1754 return '%s - %s' % (s, e)
1768 return '%s - %s' % (s, e)
1755
1769
1756
1770
1757 def form(url, method='post', needs_csrf_token=True, **attrs):
1771 def form(url, method='post', needs_csrf_token=True, **attrs):
1758 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1772 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1759 if method.lower() != 'get' and needs_csrf_token:
1773 if method.lower() != 'get' and needs_csrf_token:
1760 raise Exception(
1774 raise Exception(
1761 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1775 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1762 'CSRF token. If the endpoint does not require such token you can ' +
1776 'CSRF token. If the endpoint does not require such token you can ' +
1763 'explicitly set the parameter needs_csrf_token to false.')
1777 'explicitly set the parameter needs_csrf_token to false.')
1764
1778
1765 return insecure_form(url, method=method, **attrs)
1779 return insecure_form(url, method=method, **attrs)
1766
1780
1767
1781
1768 def secure_form(form_url, method="POST", multipart=False, **attrs):
1782 def secure_form(form_url, method="POST", multipart=False, **attrs):
1769 """Start a form tag that points the action to an url. This
1783 """Start a form tag that points the action to an url. This
1770 form tag will also include the hidden field containing
1784 form tag will also include the hidden field containing
1771 the auth token.
1785 the auth token.
1772
1786
1773 The url options should be given either as a string, or as a
1787 The url options should be given either as a string, or as a
1774 ``url()`` function. The method for the form defaults to POST.
1788 ``url()`` function. The method for the form defaults to POST.
1775
1789
1776 Options:
1790 Options:
1777
1791
1778 ``multipart``
1792 ``multipart``
1779 If set to True, the enctype is set to "multipart/form-data".
1793 If set to True, the enctype is set to "multipart/form-data".
1780 ``method``
1794 ``method``
1781 The method to use when submitting the form, usually either
1795 The method to use when submitting the form, usually either
1782 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1796 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1783 hidden input with name _method is added to simulate the verb
1797 hidden input with name _method is added to simulate the verb
1784 over POST.
1798 over POST.
1785
1799
1786 """
1800 """
1787
1801
1788 if 'request' in attrs:
1802 if 'request' in attrs:
1789 session = attrs['request'].session
1803 session = attrs['request'].session
1790 del attrs['request']
1804 del attrs['request']
1791 else:
1805 else:
1792 raise ValueError(
1806 raise ValueError(
1793 'Calling this form requires request= to be passed as argument')
1807 'Calling this form requires request= to be passed as argument')
1794
1808
1795 _form = insecure_form(form_url, method, multipart, **attrs)
1809 _form = insecure_form(form_url, method, multipart, **attrs)
1796 token = literal(
1810 token = literal(
1797 '<input type="hidden" name="{}" value="{}">'.format(
1811 '<input type="hidden" name="{}" value="{}">'.format(
1798 csrf_token_key, get_csrf_token(session)))
1812 csrf_token_key, get_csrf_token(session)))
1799
1813
1800 return literal("%s\n%s" % (_form, token))
1814 return literal("%s\n%s" % (_form, token))
1801
1815
1802
1816
1803 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1817 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1804 select_html = select(name, selected, options, **attrs)
1818 select_html = select(name, selected, options, **attrs)
1805
1819
1806 select2 = """
1820 select2 = """
1807 <script>
1821 <script>
1808 $(document).ready(function() {
1822 $(document).ready(function() {
1809 $('#%s').select2({
1823 $('#%s').select2({
1810 containerCssClass: 'drop-menu %s',
1824 containerCssClass: 'drop-menu %s',
1811 dropdownCssClass: 'drop-menu-dropdown',
1825 dropdownCssClass: 'drop-menu-dropdown',
1812 dropdownAutoWidth: true%s
1826 dropdownAutoWidth: true%s
1813 });
1827 });
1814 });
1828 });
1815 </script>
1829 </script>
1816 """
1830 """
1817
1831
1818 filter_option = """,
1832 filter_option = """,
1819 minimumResultsForSearch: -1
1833 minimumResultsForSearch: -1
1820 """
1834 """
1821 input_id = attrs.get('id') or name
1835 input_id = attrs.get('id') or name
1822 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1836 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1823 filter_enabled = "" if enable_filter else filter_option
1837 filter_enabled = "" if enable_filter else filter_option
1824 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1838 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1825
1839
1826 return literal(select_html+select_script)
1840 return literal(select_html+select_script)
1827
1841
1828
1842
1829 def get_visual_attr(tmpl_context_var, attr_name):
1843 def get_visual_attr(tmpl_context_var, attr_name):
1830 """
1844 """
1831 A safe way to get a variable from visual variable of template context
1845 A safe way to get a variable from visual variable of template context
1832
1846
1833 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1847 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1834 :param attr_name: name of the attribute we fetch from the c.visual
1848 :param attr_name: name of the attribute we fetch from the c.visual
1835 """
1849 """
1836 visual = getattr(tmpl_context_var, 'visual', None)
1850 visual = getattr(tmpl_context_var, 'visual', None)
1837 if not visual:
1851 if not visual:
1838 return
1852 return
1839 else:
1853 else:
1840 return getattr(visual, attr_name, None)
1854 return getattr(visual, attr_name, None)
1841
1855
1842
1856
1843 def get_last_path_part(file_node):
1857 def get_last_path_part(file_node):
1844 if not file_node.path:
1858 if not file_node.path:
1845 return u'/'
1859 return u'/'
1846
1860
1847 path = safe_unicode(file_node.path.split('/')[-1])
1861 path = safe_unicode(file_node.path.split('/')[-1])
1848 return u'../' + path
1862 return u'../' + path
1849
1863
1850
1864
1851 def route_url(*args, **kwargs):
1865 def route_url(*args, **kwargs):
1852 """
1866 """
1853 Wrapper around pyramids `route_url` (fully qualified url) function.
1867 Wrapper around pyramids `route_url` (fully qualified url) function.
1854 """
1868 """
1855 req = get_current_request()
1869 req = get_current_request()
1856 return req.route_url(*args, **kwargs)
1870 return req.route_url(*args, **kwargs)
1857
1871
1858
1872
1859 def route_path(*args, **kwargs):
1873 def route_path(*args, **kwargs):
1860 """
1874 """
1861 Wrapper around pyramids `route_path` function.
1875 Wrapper around pyramids `route_path` function.
1862 """
1876 """
1863 req = get_current_request()
1877 req = get_current_request()
1864 return req.route_path(*args, **kwargs)
1878 return req.route_path(*args, **kwargs)
1865
1879
1866
1880
1867 def route_path_or_none(*args, **kwargs):
1881 def route_path_or_none(*args, **kwargs):
1868 try:
1882 try:
1869 return route_path(*args, **kwargs)
1883 return route_path(*args, **kwargs)
1870 except KeyError:
1884 except KeyError:
1871 return None
1885 return None
1872
1886
1873
1887
1874 def current_route_path(request, **kw):
1888 def current_route_path(request, **kw):
1875 new_args = request.GET.mixed()
1889 new_args = request.GET.mixed()
1876 new_args.update(kw)
1890 new_args.update(kw)
1877 return request.current_route_path(_query=new_args)
1891 return request.current_route_path(_query=new_args)
1878
1892
1879
1893
1880 def curl_api_example(method, args):
1894 def curl_api_example(method, args):
1881 args_json = json.dumps(OrderedDict([
1895 args_json = json.dumps(OrderedDict([
1882 ('id', 1),
1896 ('id', 1),
1883 ('auth_token', 'SECRET'),
1897 ('auth_token', 'SECRET'),
1884 ('method', method),
1898 ('method', method),
1885 ('args', args)
1899 ('args', args)
1886 ]))
1900 ]))
1887
1901
1888 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1902 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1889 api_url=route_url('apiv2'),
1903 api_url=route_url('apiv2'),
1890 args_json=args_json
1904 args_json=args_json
1891 )
1905 )
1892
1906
1893
1907
1894 def api_call_example(method, args):
1908 def api_call_example(method, args):
1895 """
1909 """
1896 Generates an API call example via CURL
1910 Generates an API call example via CURL
1897 """
1911 """
1898 curl_call = curl_api_example(method, args)
1912 curl_call = curl_api_example(method, args)
1899
1913
1900 return literal(
1914 return literal(
1901 curl_call +
1915 curl_call +
1902 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1916 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1903 "and needs to be of `api calls` role."
1917 "and needs to be of `api calls` role."
1904 .format(token_url=route_url('my_account_auth_tokens')))
1918 .format(token_url=route_url('my_account_auth_tokens')))
1905
1919
1906
1920
1907 def notification_description(notification, request):
1921 def notification_description(notification, request):
1908 """
1922 """
1909 Generate notification human readable description based on notification type
1923 Generate notification human readable description based on notification type
1910 """
1924 """
1911 from rhodecode.model.notification import NotificationModel
1925 from rhodecode.model.notification import NotificationModel
1912 return NotificationModel().make_description(
1926 return NotificationModel().make_description(
1913 notification, translate=request.translate)
1927 notification, translate=request.translate)
1914
1928
1915
1929
1916 def go_import_header(request, db_repo=None):
1930 def go_import_header(request, db_repo=None):
1917 """
1931 """
1918 Creates a header for go-import functionality in Go Lang
1932 Creates a header for go-import functionality in Go Lang
1919 """
1933 """
1920
1934
1921 if not db_repo:
1935 if not db_repo:
1922 return
1936 return
1923 if 'go-get' not in request.GET:
1937 if 'go-get' not in request.GET:
1924 return
1938 return
1925
1939
1926 clone_url = db_repo.clone_url()
1940 clone_url = db_repo.clone_url()
1927 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1941 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1928 # we have a repo and go-get flag,
1942 # we have a repo and go-get flag,
1929 return literal('<meta name="go-import" content="{} {} {}">'.format(
1943 return literal('<meta name="go-import" content="{} {} {}">'.format(
1930 prefix, db_repo.repo_type, clone_url))
1944 prefix, db_repo.repo_type, clone_url))
1931
1945
1932
1946
1933 def reviewer_as_json(*args, **kwargs):
1947 def reviewer_as_json(*args, **kwargs):
1934 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1948 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1935 return _reviewer_as_json(*args, **kwargs)
1949 return _reviewer_as_json(*args, **kwargs)
1936
1950
1937
1951
1938 def get_repo_view_type(request):
1952 def get_repo_view_type(request):
1939 route_name = request.matched_route.name
1953 route_name = request.matched_route.name
1940 route_to_view_type = {
1954 route_to_view_type = {
1941 'repo_changelog': 'commits',
1955 'repo_changelog': 'commits',
1942 'repo_commits': 'commits',
1956 'repo_commits': 'commits',
1943 'repo_files': 'files',
1957 'repo_files': 'files',
1944 'repo_summary': 'summary',
1958 'repo_summary': 'summary',
1945 'repo_commit': 'commit'
1959 'repo_commit': 'commit'
1946 }
1960 }
1947
1961
1948 return route_to_view_type.get(route_name)
1962 return route_to_view_type.get(route_name)
1949
1963
1950
1964
1951 def is_active(menu_entry, selected):
1965 def is_active(menu_entry, selected):
1952 """
1966 """
1953 Returns active class for selecting menus in templates
1967 Returns active class for selecting menus in templates
1954 <li class=${h.is_active('settings', current_active)}></li>
1968 <li class=${h.is_active('settings', current_active)}></li>
1955 """
1969 """
1956 if not isinstance(menu_entry, list):
1970 if not isinstance(menu_entry, list):
1957 menu_entry = [menu_entry]
1971 menu_entry = [menu_entry]
1958
1972
1959 if selected in menu_entry:
1973 if selected in menu_entry:
1960 return "active"
1974 return "active"
@@ -1,1201 +1,1201 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%!
3 <%!
4 ## base64 filter e.g ${ example | base64 }
4 ## base64 filter e.g ${ example | base64 }
5 def base64(text):
5 def base64(text):
6 import base64
6 import base64
7 from rhodecode.lib.helpers import safe_str
7 from rhodecode.lib.helpers import safe_str
8 return base64.encodestring(safe_str(text))
8 return base64.encodestring(safe_str(text))
9 %>
9 %>
10
10
11 <%inherit file="root.mako"/>
11 <%inherit file="root.mako"/>
12
12
13 <%include file="/ejs_templates/templates.html"/>
13 <%include file="/ejs_templates/templates.html"/>
14
14
15 <div class="outerwrapper">
15 <div class="outerwrapper">
16 <!-- HEADER -->
16 <!-- HEADER -->
17 <div class="header">
17 <div class="header">
18 <div id="header-inner" class="wrapper">
18 <div id="header-inner" class="wrapper">
19 <div id="logo">
19 <div id="logo">
20 <div class="logo-wrapper">
20 <div class="logo-wrapper">
21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
22 </div>
22 </div>
23 % if c.rhodecode_name:
23 % if c.rhodecode_name:
24 <div class="branding">
24 <div class="branding">
25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
26 </div>
26 </div>
27 % endif
27 % endif
28 </div>
28 </div>
29 <!-- MENU BAR NAV -->
29 <!-- MENU BAR NAV -->
30 ${self.menu_bar_nav()}
30 ${self.menu_bar_nav()}
31 <!-- END MENU BAR NAV -->
31 <!-- END MENU BAR NAV -->
32 </div>
32 </div>
33 </div>
33 </div>
34 ${self.menu_bar_subnav()}
34 ${self.menu_bar_subnav()}
35 <!-- END HEADER -->
35 <!-- END HEADER -->
36
36
37 <!-- CONTENT -->
37 <!-- CONTENT -->
38 <div id="content" class="wrapper">
38 <div id="content" class="wrapper">
39
39
40 <rhodecode-toast id="notifications"></rhodecode-toast>
40 <rhodecode-toast id="notifications"></rhodecode-toast>
41
41
42 <div class="main">
42 <div class="main">
43 ${next.main()}
43 ${next.main()}
44 </div>
44 </div>
45 </div>
45 </div>
46 <!-- END CONTENT -->
46 <!-- END CONTENT -->
47
47
48 </div>
48 </div>
49 <!-- FOOTER -->
49 <!-- FOOTER -->
50 <div id="footer">
50 <div id="footer">
51 <div id="footer-inner" class="title wrapper">
51 <div id="footer-inner" class="title wrapper">
52 <div>
52 <div>
53 <p class="footer-link-right">
53 <p class="footer-link-right">
54 % if c.visual.show_version:
54 % if c.visual.show_version:
55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
56 % endif
56 % endif
57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
58 % if c.visual.rhodecode_support_url:
58 % if c.visual.rhodecode_support_url:
59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
60 % endif
60 % endif
61 </p>
61 </p>
62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
63 <p class="server-instance" style="display:${sid}">
63 <p class="server-instance" style="display:${sid}">
64 ## display hidden instance ID if specially defined
64 ## display hidden instance ID if specially defined
65 % if c.rhodecode_instanceid:
65 % if c.rhodecode_instanceid:
66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
67 % endif
67 % endif
68 </p>
68 </p>
69 </div>
69 </div>
70 </div>
70 </div>
71 </div>
71 </div>
72
72
73 <!-- END FOOTER -->
73 <!-- END FOOTER -->
74
74
75 ### MAKO DEFS ###
75 ### MAKO DEFS ###
76
76
77 <%def name="menu_bar_subnav()">
77 <%def name="menu_bar_subnav()">
78 </%def>
78 </%def>
79
79
80 <%def name="breadcrumbs(class_='breadcrumbs')">
80 <%def name="breadcrumbs(class_='breadcrumbs')">
81 <div class="${class_}">
81 <div class="${class_}">
82 ${self.breadcrumbs_links()}
82 ${self.breadcrumbs_links()}
83 </div>
83 </div>
84 </%def>
84 </%def>
85
85
86 <%def name="admin_menu(active=None)">
86 <%def name="admin_menu(active=None)">
87
87
88 <div id="context-bar">
88 <div id="context-bar">
89 <div class="wrapper">
89 <div class="wrapper">
90 <div class="title">
90 <div class="title">
91 <div class="title-content">
91 <div class="title-content">
92 <div class="title-main">
92 <div class="title-main">
93 % if c.is_super_admin:
93 % if c.is_super_admin:
94 ${_('Super-admin Panel')}
94 ${_('Super-admin Panel')}
95 % else:
95 % else:
96 ${_('Delegated Admin Panel')}
96 ${_('Delegated Admin Panel')}
97 % endif
97 % endif
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101
101
102 <ul id="context-pages" class="navigation horizontal-list">
102 <ul id="context-pages" class="navigation horizontal-list">
103
103
104 ## super-admin case
104 ## super-admin case
105 % if c.is_super_admin:
105 % if c.is_super_admin:
106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
116
116
117 ## delegated admin
117 ## delegated admin
118 % elif c.is_delegated_admin:
118 % elif c.is_delegated_admin:
119 <%
119 <%
120 repositories=c.auth_user.repositories_admin or c.can_create_repo
120 repositories=c.auth_user.repositories_admin or c.can_create_repo
121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
123 %>
123 %>
124
124
125 %if repositories:
125 %if repositories:
126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
127 %endif
127 %endif
128 %if repository_groups:
128 %if repository_groups:
129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
130 %endif
130 %endif
131 %if user_groups:
131 %if user_groups:
132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
133 %endif
133 %endif
134 % endif
134 % endif
135 </ul>
135 </ul>
136
136
137 </div>
137 </div>
138 <div class="clear"></div>
138 <div class="clear"></div>
139 </div>
139 </div>
140 </%def>
140 </%def>
141
141
142 <%def name="dt_info_panel(elements)">
142 <%def name="dt_info_panel(elements)">
143 <dl class="dl-horizontal">
143 <dl class="dl-horizontal">
144 %for dt, dd, title, show_items in elements:
144 %for dt, dd, title, show_items in elements:
145 <dt>${dt}:</dt>
145 <dt>${dt}:</dt>
146 <dd title="${h.tooltip(title)}">
146 <dd title="${h.tooltip(title)}">
147 %if callable(dd):
147 %if callable(dd):
148 ## allow lazy evaluation of elements
148 ## allow lazy evaluation of elements
149 ${dd()}
149 ${dd()}
150 %else:
150 %else:
151 ${dd}
151 ${dd}
152 %endif
152 %endif
153 %if show_items:
153 %if show_items:
154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
155 %endif
155 %endif
156 </dd>
156 </dd>
157
157
158 %if show_items:
158 %if show_items:
159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
160 %for item in show_items:
160 %for item in show_items:
161 <dt></dt>
161 <dt></dt>
162 <dd>${item}</dd>
162 <dd>${item}</dd>
163 %endfor
163 %endfor
164 </div>
164 </div>
165 %endif
165 %endif
166
166
167 %endfor
167 %endfor
168 </dl>
168 </dl>
169 </%def>
169 </%def>
170
170
171 <%def name="tr_info_entry(element)">
171 <%def name="tr_info_entry(element)">
172 <% key, val, title, show_items = element %>
172 <% key, val, title, show_items = element %>
173
173
174 <tr>
174 <tr>
175 <td style="vertical-align: top">${key}</td>
175 <td style="vertical-align: top">${key}</td>
176 <td title="${h.tooltip(title)}">
176 <td title="${h.tooltip(title)}">
177 %if callable(val):
177 %if callable(val):
178 ## allow lazy evaluation of elements
178 ## allow lazy evaluation of elements
179 ${val()}
179 ${val()}
180 %else:
180 %else:
181 ${val}
181 ${val}
182 %endif
182 %endif
183 %if show_items:
183 %if show_items:
184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
185 % for item in show_items:
185 % for item in show_items:
186 <dt></dt>
186 <dt></dt>
187 <dd>${item}</dd>
187 <dd>${item}</dd>
188 % endfor
188 % endfor
189 </div>
189 </div>
190 %endif
190 %endif
191 </td>
191 </td>
192 <td style="vertical-align: top">
192 <td style="vertical-align: top">
193 %if show_items:
193 %if show_items:
194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
195 %endif
195 %endif
196 </td>
196 </td>
197 </tr>
197 </tr>
198
198
199 </%def>
199 </%def>
200
200
201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
202 <%
202 <%
203 if size > 16:
203 if size > 16:
204 gravatar_class = ['gravatar','gravatar-large']
204 gravatar_class = ['gravatar','gravatar-large']
205 else:
205 else:
206 gravatar_class = ['gravatar']
206 gravatar_class = ['gravatar']
207
207
208 data_hovercard_url = ''
208 data_hovercard_url = ''
209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
210
210
211 if tooltip:
211 if tooltip:
212 gravatar_class += ['tooltip-hovercard']
212 gravatar_class += ['tooltip-hovercard']
213 if extra_class:
213 if extra_class:
214 gravatar_class += extra_class
214 gravatar_class += extra_class
215 if tooltip and user:
215 if tooltip and user:
216 if user.username == h.DEFAULT_USER:
216 if user.username == h.DEFAULT_USER:
217 gravatar_class.pop(-1)
217 gravatar_class.pop(-1)
218 else:
218 else:
219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
220 gravatar_class = ' '.join(gravatar_class)
220 gravatar_class = ' '.join(gravatar_class)
221
221
222 %>
222 %>
223 <%doc>
223 <%doc>
224 TODO: johbo: For now we serve double size images to make it smooth
224 TODO: johbo: For now we serve double size images to make it smooth
225 for retina. This is how it worked until now. Should be replaced
225 for retina. This is how it worked until now. Should be replaced
226 with a better solution at some point.
226 with a better solution at some point.
227 </%doc>
227 </%doc>
228
228
229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
230 </%def>
230 </%def>
231
231
232
232
233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
234 <%
234 <%
235 email = h.email_or_none(contact)
235 email = h.email_or_none(contact)
236 rc_user = h.discover_user(contact)
236 rc_user = h.discover_user(contact)
237 %>
237 %>
238
238
239 <div class="${_class}">
239 <div class="${_class}">
240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
242 </div>
242 </div>
243 </%def>
243 </%def>
244
244
245
245
246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
247 <%
247 <%
248 if (size > 16):
248 if (size > 16):
249 gravatar_class = 'icon-user-group-alt'
249 gravatar_class = 'icon-user-group-alt'
250 else:
250 else:
251 gravatar_class = 'icon-user-group-alt'
251 gravatar_class = 'icon-user-group-alt'
252
252
253 if tooltip:
253 if tooltip:
254 gravatar_class += ' tooltip-hovercard'
254 gravatar_class += ' tooltip-hovercard'
255
255
256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
257 %>
257 %>
258 <%doc>
258 <%doc>
259 TODO: johbo: For now we serve double size images to make it smooth
259 TODO: johbo: For now we serve double size images to make it smooth
260 for retina. This is how it worked until now. Should be replaced
260 for retina. This is how it worked until now. Should be replaced
261 with a better solution at some point.
261 with a better solution at some point.
262 </%doc>
262 </%doc>
263
263
264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
265 </%def>
265 </%def>
266
266
267 <%def name="repo_page_title(repo_instance)">
267 <%def name="repo_page_title(repo_instance)">
268 <div class="title-content repo-title">
268 <div class="title-content repo-title">
269
269
270 <div class="title-main">
270 <div class="title-main">
271 ## SVN/HG/GIT icons
271 ## SVN/HG/GIT icons
272 %if h.is_hg(repo_instance):
272 %if h.is_hg(repo_instance):
273 <i class="icon-hg"></i>
273 <i class="icon-hg"></i>
274 %endif
274 %endif
275 %if h.is_git(repo_instance):
275 %if h.is_git(repo_instance):
276 <i class="icon-git"></i>
276 <i class="icon-git"></i>
277 %endif
277 %endif
278 %if h.is_svn(repo_instance):
278 %if h.is_svn(repo_instance):
279 <i class="icon-svn"></i>
279 <i class="icon-svn"></i>
280 %endif
280 %endif
281
281
282 ## public/private
282 ## public/private
283 %if repo_instance.private:
283 %if repo_instance.private:
284 <i class="icon-repo-private"></i>
284 <i class="icon-repo-private"></i>
285 %else:
285 %else:
286 <i class="icon-repo-public"></i>
286 <i class="icon-repo-public"></i>
287 %endif
287 %endif
288
288
289 ## repo name with group name
289 ## repo name with group name
290 ${h.breadcrumb_repo_link(repo_instance)}
290 ${h.breadcrumb_repo_link(repo_instance)}
291
291
292 ## Context Actions
292 ## Context Actions
293 <div class="pull-right">
293 <div class="pull-right">
294 %if c.rhodecode_user.username != h.DEFAULT_USER:
294 %if c.rhodecode_user.username != h.DEFAULT_USER:
295 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
295 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
296
296
297 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
297 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
298 % if c.repository_is_user_following:
298 % if c.repository_is_user_following:
299 <i class="icon-eye-off"></i>${_('Unwatch')}
299 <i class="icon-eye-off"></i>${_('Unwatch')}
300 % else:
300 % else:
301 <i class="icon-eye"></i>${_('Watch')}
301 <i class="icon-eye"></i>${_('Watch')}
302 % endif
302 % endif
303
303
304 </a>
304 </a>
305 %else:
305 %else:
306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 %endif
307 %endif
308 </div>
308 </div>
309
309
310 </div>
310 </div>
311
311
312 ## FORKED
312 ## FORKED
313 %if repo_instance.fork:
313 %if repo_instance.fork:
314 <p class="discreet">
314 <p class="discreet">
315 <i class="icon-code-fork"></i> ${_('Fork of')}
315 <i class="icon-code-fork"></i> ${_('Fork of')}
316 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
316 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
317 </p>
317 </p>
318 %endif
318 %endif
319
319
320 ## IMPORTED FROM REMOTE
320 ## IMPORTED FROM REMOTE
321 %if repo_instance.clone_uri:
321 %if repo_instance.clone_uri:
322 <p class="discreet">
322 <p class="discreet">
323 <i class="icon-code-fork"></i> ${_('Clone from')}
323 <i class="icon-code-fork"></i> ${_('Clone from')}
324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
325 </p>
325 </p>
326 %endif
326 %endif
327
327
328 ## LOCKING STATUS
328 ## LOCKING STATUS
329 %if repo_instance.locked[0]:
329 %if repo_instance.locked[0]:
330 <p class="locking_locked discreet">
330 <p class="locking_locked discreet">
331 <i class="icon-repo-lock"></i>
331 <i class="icon-repo-lock"></i>
332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
333 </p>
333 </p>
334 %elif repo_instance.enable_locking:
334 %elif repo_instance.enable_locking:
335 <p class="locking_unlocked discreet">
335 <p class="locking_unlocked discreet">
336 <i class="icon-repo-unlock"></i>
336 <i class="icon-repo-unlock"></i>
337 ${_('Repository not locked. Pull repository to lock it.')}
337 ${_('Repository not locked. Pull repository to lock it.')}
338 </p>
338 </p>
339 %endif
339 %endif
340
340
341 </div>
341 </div>
342 </%def>
342 </%def>
343
343
344 <%def name="repo_menu(active=None)">
344 <%def name="repo_menu(active=None)">
345 <%
345 <%
346 ## determine if we have "any" option available
346 ## determine if we have "any" option available
347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
348 has_actions = can_lock
348 has_actions = can_lock
349
349
350 %>
350 %>
351 % if c.rhodecode_db_repo.archived:
351 % if c.rhodecode_db_repo.archived:
352 <div class="alert alert-warning text-center">
352 <div class="alert alert-warning text-center">
353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
354 </div>
354 </div>
355 % endif
355 % endif
356
356
357 <!--- REPO CONTEXT BAR -->
357 <!--- REPO CONTEXT BAR -->
358 <div id="context-bar">
358 <div id="context-bar">
359 <div class="wrapper">
359 <div class="wrapper">
360
360
361 <div class="title">
361 <div class="title">
362 ${self.repo_page_title(c.rhodecode_db_repo)}
362 ${self.repo_page_title(c.rhodecode_db_repo)}
363 </div>
363 </div>
364
364
365 <ul id="context-pages" class="navigation horizontal-list">
365 <ul id="context-pages" class="navigation horizontal-list">
366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='', _query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
370
370
371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
373 <li class="${h.is_active('showpullrequest', active)}">
373 <li class="${h.is_active('showpullrequest', active)}">
374 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
374 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
375 <div class="menulabel">
375 <div class="menulabel">
376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
377 </div>
377 </div>
378 </a>
378 </a>
379 </li>
379 </li>
380 %endif
380 %endif
381
381
382 <li class="${h.is_active('artifacts', active)}">
382 <li class="${h.is_active('artifacts', active)}">
383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
384 <div class="menulabel">
384 <div class="menulabel">
385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
386 </div>
386 </div>
387 </a>
387 </a>
388 </li>
388 </li>
389
389
390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
392 %endif
392 %endif
393
393
394 <li class="${h.is_active('options', active)}">
394 <li class="${h.is_active('options', active)}">
395 % if has_actions:
395 % if has_actions:
396 <a class="menulink dropdown">
396 <a class="menulink dropdown">
397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
398 </a>
398 </a>
399 <ul class="submenu">
399 <ul class="submenu">
400 %if can_lock:
400 %if can_lock:
401 %if c.rhodecode_db_repo.locked[0]:
401 %if c.rhodecode_db_repo.locked[0]:
402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
403 %else:
403 %else:
404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
405 %endif
405 %endif
406 %endif
406 %endif
407 </ul>
407 </ul>
408 % endif
408 % endif
409 </li>
409 </li>
410
410
411 </ul>
411 </ul>
412 </div>
412 </div>
413 <div class="clear"></div>
413 <div class="clear"></div>
414 </div>
414 </div>
415
415
416 <!--- REPO END CONTEXT BAR -->
416 <!--- REPO END CONTEXT BAR -->
417
417
418 </%def>
418 </%def>
419
419
420 <%def name="repo_group_page_title(repo_group_instance)">
420 <%def name="repo_group_page_title(repo_group_instance)">
421 <div class="title-content">
421 <div class="title-content">
422 <div class="title-main">
422 <div class="title-main">
423 ## Repository Group icon
423 ## Repository Group icon
424 <i class="icon-repo-group"></i>
424 <i class="icon-repo-group"></i>
425
425
426 ## repo name with group name
426 ## repo name with group name
427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
428 </div>
428 </div>
429
429
430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
431 <div class="repo-group-desc discreet">
431 <div class="repo-group-desc discreet">
432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
433 </div>
433 </div>
434
434
435 </div>
435 </div>
436 </%def>
436 </%def>
437
437
438
438
439 <%def name="repo_group_menu(active=None)">
439 <%def name="repo_group_menu(active=None)">
440 <%
440 <%
441 gr_name = c.repo_group.group_name if c.repo_group else None
441 gr_name = c.repo_group.group_name if c.repo_group else None
442 # create repositories with write permission on group is set to true
442 # create repositories with write permission on group is set to true
443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
444
444
445 %>
445 %>
446
446
447
447
448 <!--- REPO GROUP CONTEXT BAR -->
448 <!--- REPO GROUP CONTEXT BAR -->
449 <div id="context-bar">
449 <div id="context-bar">
450 <div class="wrapper">
450 <div class="wrapper">
451 <div class="title">
451 <div class="title">
452 ${self.repo_group_page_title(c.repo_group)}
452 ${self.repo_group_page_title(c.repo_group)}
453 </div>
453 </div>
454
454
455 <ul id="context-pages" class="navigation horizontal-list">
455 <ul id="context-pages" class="navigation horizontal-list">
456 <li class="${h.is_active('home', active)}">
456 <li class="${h.is_active('home', active)}">
457 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
457 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
458 </li>
458 </li>
459 % if c.is_super_admin or group_admin:
459 % if c.is_super_admin or group_admin:
460 <li class="${h.is_active('settings', active)}">
460 <li class="${h.is_active('settings', active)}">
461 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
461 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
462 </li>
462 </li>
463 % endif
463 % endif
464
464
465 </ul>
465 </ul>
466 </div>
466 </div>
467 <div class="clear"></div>
467 <div class="clear"></div>
468 </div>
468 </div>
469
469
470 <!--- REPO GROUP CONTEXT BAR -->
470 <!--- REPO GROUP CONTEXT BAR -->
471
471
472 </%def>
472 </%def>
473
473
474
474
475 <%def name="usermenu(active=False)">
475 <%def name="usermenu(active=False)">
476 <%
476 <%
477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
478
478
479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
480 # create repositories with write permission on group is set to true
480 # create repositories with write permission on group is set to true
481
481
482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
486
486
487 can_create_repos = c.is_super_admin or c.can_create_repo
487 can_create_repos = c.is_super_admin or c.can_create_repo
488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
489
489
490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
492 %>
492 %>
493
493
494 % if not_anonymous:
494 % if not_anonymous:
495 <%
495 <%
496 default_target_group = dict()
496 default_target_group = dict()
497 if c.rhodecode_user.personal_repo_group:
497 if c.rhodecode_user.personal_repo_group:
498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
499 %>
499 %>
500
500
501 ## create action
501 ## create action
502 <li>
502 <li>
503 <a href="#create-actions" onclick="return false;" class="menulink childs">
503 <a href="#create-actions" onclick="return false;" class="menulink childs">
504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
505 </a>
505 </a>
506
506
507 <div class="action-menu submenu">
507 <div class="action-menu submenu">
508
508
509 <ol>
509 <ol>
510 ## scope of within a repository
510 ## scope of within a repository
511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
512 <li class="submenu-title">${_('This Repository')}</li>
512 <li class="submenu-title">${_('This Repository')}</li>
513 <li>
513 <li>
514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
515 </li>
515 </li>
516 % if can_fork:
516 % if can_fork:
517 <li>
517 <li>
518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
519 </li>
519 </li>
520 % endif
520 % endif
521 % endif
521 % endif
522
522
523 ## scope of within repository groups
523 ## scope of within repository groups
524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
525 <li class="submenu-title">${_('This Repository Group')}</li>
525 <li class="submenu-title">${_('This Repository Group')}</li>
526
526
527 % if can_create_repos_in_group:
527 % if can_create_repos_in_group:
528 <li>
528 <li>
529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
530 </li>
530 </li>
531 % endif
531 % endif
532
532
533 % if can_create_repo_groups_in_group:
533 % if can_create_repo_groups_in_group:
534 <li>
534 <li>
535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
536 </li>
536 </li>
537 % endif
537 % endif
538 % endif
538 % endif
539
539
540 ## personal group
540 ## personal group
541 % if c.rhodecode_user.personal_repo_group:
541 % if c.rhodecode_user.personal_repo_group:
542 <li class="submenu-title">Personal Group</li>
542 <li class="submenu-title">Personal Group</li>
543
543
544 <li>
544 <li>
545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
546 </li>
546 </li>
547
547
548 <li>
548 <li>
549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
550 </li>
550 </li>
551 % endif
551 % endif
552
552
553 ## Global actions
553 ## Global actions
554 <li class="submenu-title">RhodeCode</li>
554 <li class="submenu-title">RhodeCode</li>
555 % if can_create_repos:
555 % if can_create_repos:
556 <li>
556 <li>
557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
558 </li>
558 </li>
559 % endif
559 % endif
560
560
561 % if can_create_repo_groups:
561 % if can_create_repo_groups:
562 <li>
562 <li>
563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
564 </li>
564 </li>
565 % endif
565 % endif
566
566
567 <li>
567 <li>
568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
569 </li>
569 </li>
570
570
571 </ol>
571 </ol>
572
572
573 </div>
573 </div>
574 </li>
574 </li>
575
575
576 ## notifications
576 ## notifications
577 <li>
577 <li>
578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
579 ${c.unread_notifications}
579 ${c.unread_notifications}
580 </a>
580 </a>
581 </li>
581 </li>
582 % endif
582 % endif
583
583
584 ## USER MENU
584 ## USER MENU
585 <li id="quick_login_li" class="${'active' if active else ''}">
585 <li id="quick_login_li" class="${'active' if active else ''}">
586 % if c.rhodecode_user.username == h.DEFAULT_USER:
586 % if c.rhodecode_user.username == h.DEFAULT_USER:
587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
588 ${gravatar(c.rhodecode_user.email, 20)}
588 ${gravatar(c.rhodecode_user.email, 20)}
589 <span class="user">
589 <span class="user">
590 <span>${_('Sign in')}</span>
590 <span>${_('Sign in')}</span>
591 </span>
591 </span>
592 </a>
592 </a>
593 % else:
593 % else:
594 ## logged in user
594 ## logged in user
595 <a id="quick_login_link" class="menulink childs">
595 <a id="quick_login_link" class="menulink childs">
596 ${gravatar(c.rhodecode_user.email, 20)}
596 ${gravatar(c.rhodecode_user.email, 20)}
597 <span class="user">
597 <span class="user">
598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
599 <div class="show_more"></div>
599 <div class="show_more"></div>
600 </span>
600 </span>
601 </a>
601 </a>
602 ## subnav with menu for logged in user
602 ## subnav with menu for logged in user
603 <div class="user-menu submenu">
603 <div class="user-menu submenu">
604 <div id="quick_login">
604 <div id="quick_login">
605 %if c.rhodecode_user.username != h.DEFAULT_USER:
605 %if c.rhodecode_user.username != h.DEFAULT_USER:
606 <div class="">
606 <div class="">
607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
609 <div class="email">${c.rhodecode_user.email}</div>
609 <div class="email">${c.rhodecode_user.email}</div>
610 </div>
610 </div>
611 <div class="">
611 <div class="">
612 <ol class="links">
612 <ol class="links">
613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
614 % if c.rhodecode_user.personal_repo_group:
614 % if c.rhodecode_user.personal_repo_group:
615 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
615 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
616 % endif
616 % endif
617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
618
618
619 % if c.debug_style:
619 % if c.debug_style:
620 <li>
620 <li>
621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
622 <div class="menulabel">${_('[Style]')}</div>
622 <div class="menulabel">${_('[Style]')}</div>
623 </a>
623 </a>
624 </li>
624 </li>
625 % endif
625 % endif
626
626
627 ## bookmark-items
627 ## bookmark-items
628 <li class="bookmark-items">
628 <li class="bookmark-items">
629 ${_('Bookmarks')}
629 ${_('Bookmarks')}
630 <div class="pull-right">
630 <div class="pull-right">
631 <a href="${h.route_path('my_account_bookmarks')}">
631 <a href="${h.route_path('my_account_bookmarks')}">
632
632
633 <i class="icon-cog"></i>
633 <i class="icon-cog"></i>
634 </a>
634 </a>
635 </div>
635 </div>
636 </li>
636 </li>
637 % if not c.bookmark_items:
637 % if not c.bookmark_items:
638 <li>
638 <li>
639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
640 </li>
640 </li>
641 % endif
641 % endif
642 % for item in c.bookmark_items:
642 % for item in c.bookmark_items:
643 <li>
643 <li>
644 % if item.repository:
644 % if item.repository:
645 <div>
645 <div>
646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
647 <code>${item.position}</code>
647 <code>${item.position}</code>
648 % if item.repository.repo_type == 'hg':
648 % if item.repository.repo_type == 'hg':
649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
650 % elif item.repository.repo_type == 'git':
650 % elif item.repository.repo_type == 'git':
651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
652 % elif item.repository.repo_type == 'svn':
652 % elif item.repository.repo_type == 'svn':
653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
654 % endif
654 % endif
655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
656 </a>
656 </a>
657 </div>
657 </div>
658 % elif item.repository_group:
658 % elif item.repository_group:
659 <div>
659 <div>
660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
661 <code>${item.position}</code>
661 <code>${item.position}</code>
662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
664 </a>
664 </a>
665 </div>
665 </div>
666 % else:
666 % else:
667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
668 <code>${item.position}</code>
668 <code>${item.position}</code>
669 ${item.title}
669 ${item.title}
670 </a>
670 </a>
671 % endif
671 % endif
672 </li>
672 </li>
673 % endfor
673 % endfor
674
674
675 <li class="logout">
675 <li class="logout">
676 ${h.secure_form(h.route_path('logout'), request=request)}
676 ${h.secure_form(h.route_path('logout'), request=request)}
677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
678 ${h.end_form()}
678 ${h.end_form()}
679 </li>
679 </li>
680 </ol>
680 </ol>
681 </div>
681 </div>
682 %endif
682 %endif
683 </div>
683 </div>
684 </div>
684 </div>
685
685
686 % endif
686 % endif
687 </li>
687 </li>
688 </%def>
688 </%def>
689
689
690 <%def name="menu_items(active=None)">
690 <%def name="menu_items(active=None)">
691 <%
691 <%
692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
693 notice_display = 'none' if len(notice_messages) == 0 else ''
693 notice_display = 'none' if len(notice_messages) == 0 else ''
694 %>
694 %>
695 <style>
695 <style>
696
696
697 </style>
697 </style>
698
698
699 <ul id="quick" class="main_nav navigation horizontal-list">
699 <ul id="quick" class="main_nav navigation horizontal-list">
700 ## notice box for important system messages
700 ## notice box for important system messages
701 <li style="display: ${notice_display}">
701 <li style="display: ${notice_display}">
702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
703 <div class="menulabel-notice ${notice_level}" >
703 <div class="menulabel-notice ${notice_level}" >
704 ${len(notice_messages)}
704 ${len(notice_messages)}
705 </div>
705 </div>
706 </a>
706 </a>
707 </li>
707 </li>
708 <div class="notice-messages-container" style="display: none">
708 <div class="notice-messages-container" style="display: none">
709 <div class="notice-messages">
709 <div class="notice-messages">
710 <table class="rctable">
710 <table class="rctable">
711 % for notice in notice_messages:
711 % for notice in notice_messages:
712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
713 <td style="vertical-align: text-top; width: 20px">
713 <td style="vertical-align: text-top; width: 20px">
714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
715 </td>
715 </td>
716 <td>
716 <td>
717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
718 ${notice['subject']}
718 ${notice['subject']}
719
719
720 <div id="notice-${notice['msg_id']}" style="display: none">
720 <div id="notice-${notice['msg_id']}" style="display: none">
721 ${h.render(notice['body'], renderer='markdown')}
721 ${h.render(notice['body'], renderer='markdown')}
722 </div>
722 </div>
723 </td>
723 </td>
724 <td style="vertical-align: text-top; width: 35px;">
724 <td style="vertical-align: text-top; width: 35px;">
725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
726 <i class="icon-remove icon-filled-red"></i>
726 <i class="icon-remove icon-filled-red"></i>
727 </a>
727 </a>
728 </td>
728 </td>
729 </tr>
729 </tr>
730
730
731 % endfor
731 % endfor
732 </table>
732 </table>
733 </div>
733 </div>
734 </div>
734 </div>
735 ## Main filter
735 ## Main filter
736 <li>
736 <li>
737 <div class="menulabel main_filter_box">
737 <div class="menulabel main_filter_box">
738 <div class="main_filter_input_box">
738 <div class="main_filter_input_box">
739 <ul class="searchItems">
739 <ul class="searchItems">
740
740
741 <li class="searchTag searchTagIcon">
741 <li class="searchTag searchTagIcon">
742 <i class="icon-search"></i>
742 <i class="icon-search"></i>
743 </li>
743 </li>
744
744
745 % if c.template_context['search_context']['repo_id']:
745 % if c.template_context['search_context']['repo_id']:
746 <li class="searchTag searchTagFilter searchTagHidable" >
746 <li class="searchTag searchTagFilter searchTagHidable" >
747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
748 <span class="tag">
748 <span class="tag">
749 This repo
749 This repo
750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
751 </span>
751 </span>
752 ##</a>
752 ##</a>
753 </li>
753 </li>
754 % elif c.template_context['search_context']['repo_group_id']:
754 % elif c.template_context['search_context']['repo_group_id']:
755 <li class="searchTag searchTagFilter searchTagHidable">
755 <li class="searchTag searchTagFilter searchTagHidable">
756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
757 <span class="tag">
757 <span class="tag">
758 This group
758 This group
759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
760 </span>
760 </span>
761 ##</a>
761 ##</a>
762 </li>
762 </li>
763 % endif
763 % endif
764
764
765 <li class="searchTagInput">
765 <li class="searchTagInput">
766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
767 </li>
767 </li>
768 <li class="searchTag searchTagHelp">
768 <li class="searchTag searchTagHelp">
769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
770 </li>
770 </li>
771 </ul>
771 </ul>
772 </div>
772 </div>
773 </div>
773 </div>
774
774
775 <div id="main_filter_help" style="display: none">
775 <div id="main_filter_help" style="display: none">
776 - Use '/' key to quickly access this field.
776 - Use '/' key to quickly access this field.
777
777
778 - Enter a name of repository, or repository group for quick search.
778 - Enter a name of repository, or repository group for quick search.
779
779
780 - Prefix query to allow special search:
780 - Prefix query to allow special search:
781
781
782 user:admin, to search for usernames, always global
782 user:admin, to search for usernames, always global
783
783
784 user_group:devops, to search for user groups, always global
784 user_group:devops, to search for user groups, always global
785
785
786 pr:303, to search for pull request number, title, or description, always global
786 pr:303, to search for pull request number, title, or description, always global
787
787
788 commit:efced4, to search for commits, scoped to repositories or groups
788 commit:efced4, to search for commits, scoped to repositories or groups
789
789
790 file:models.py, to search for file paths, scoped to repositories or groups
790 file:models.py, to search for file paths, scoped to repositories or groups
791
791
792 % if c.template_context['search_context']['repo_id']:
792 % if c.template_context['search_context']['repo_id']:
793 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
793 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
794 % elif c.template_context['search_context']['repo_group_id']:
794 % elif c.template_context['search_context']['repo_group_id']:
795 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
795 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
796 % else:
796 % else:
797 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
797 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
798 % endif
798 % endif
799 </div>
799 </div>
800 </li>
800 </li>
801
801
802 ## ROOT MENU
802 ## ROOT MENU
803 <li class="${h.is_active('home', active)}">
803 <li class="${h.is_active('home', active)}">
804 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
804 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
805 <div class="menulabel">${_('Home')}</div>
805 <div class="menulabel">${_('Home')}</div>
806 </a>
806 </a>
807 </li>
807 </li>
808
808
809 %if c.rhodecode_user.username != h.DEFAULT_USER:
809 %if c.rhodecode_user.username != h.DEFAULT_USER:
810 <li class="${h.is_active('journal', active)}">
810 <li class="${h.is_active('journal', active)}">
811 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
811 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
812 <div class="menulabel">${_('Journal')}</div>
812 <div class="menulabel">${_('Journal')}</div>
813 </a>
813 </a>
814 </li>
814 </li>
815 %else:
815 %else:
816 <li class="${h.is_active('journal', active)}">
816 <li class="${h.is_active('journal', active)}">
817 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
817 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
818 <div class="menulabel">${_('Public journal')}</div>
818 <div class="menulabel">${_('Public journal')}</div>
819 </a>
819 </a>
820 </li>
820 </li>
821 %endif
821 %endif
822
822
823 <li class="${h.is_active('gists', active)}">
823 <li class="${h.is_active('gists', active)}">
824 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
824 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
825 <div class="menulabel">${_('Gists')}</div>
825 <div class="menulabel">${_('Gists')}</div>
826 </a>
826 </a>
827 </li>
827 </li>
828
828
829 % if c.is_super_admin or c.is_delegated_admin:
829 % if c.is_super_admin or c.is_delegated_admin:
830 <li class="${h.is_active('admin', active)}">
830 <li class="${h.is_active('admin', active)}">
831 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
831 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
832 <div class="menulabel">${_('Admin')} </div>
832 <div class="menulabel">${_('Admin')} </div>
833 </a>
833 </a>
834 </li>
834 </li>
835 % endif
835 % endif
836
836
837 ## render extra user menu
837 ## render extra user menu
838 ${usermenu(active=(active=='my_account'))}
838 ${usermenu(active=(active=='my_account'))}
839
839
840 </ul>
840 </ul>
841
841
842 <script type="text/javascript">
842 <script type="text/javascript">
843 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
843 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
844
844
845 var formatRepoResult = function(result, container, query, escapeMarkup) {
845 var formatRepoResult = function(result, container, query, escapeMarkup) {
846 return function(data, escapeMarkup) {
846 return function(data, escapeMarkup) {
847 if (!data.repo_id){
847 if (!data.repo_id){
848 return data.text; // optgroup text Repositories
848 return data.text; // optgroup text Repositories
849 }
849 }
850
850
851 var tmpl = '';
851 var tmpl = '';
852 var repoType = data['repo_type'];
852 var repoType = data['repo_type'];
853 var repoName = data['text'];
853 var repoName = data['text'];
854
854
855 if(data && data.type == 'repo'){
855 if(data && data.type == 'repo'){
856 if(repoType === 'hg'){
856 if(repoType === 'hg'){
857 tmpl += '<i class="icon-hg"></i> ';
857 tmpl += '<i class="icon-hg"></i> ';
858 }
858 }
859 else if(repoType === 'git'){
859 else if(repoType === 'git'){
860 tmpl += '<i class="icon-git"></i> ';
860 tmpl += '<i class="icon-git"></i> ';
861 }
861 }
862 else if(repoType === 'svn'){
862 else if(repoType === 'svn'){
863 tmpl += '<i class="icon-svn"></i> ';
863 tmpl += '<i class="icon-svn"></i> ';
864 }
864 }
865 if(data['private']){
865 if(data['private']){
866 tmpl += '<i class="icon-lock" ></i> ';
866 tmpl += '<i class="icon-lock" ></i> ';
867 }
867 }
868 else if(visualShowPublicIcon){
868 else if(visualShowPublicIcon){
869 tmpl += '<i class="icon-unlock-alt"></i> ';
869 tmpl += '<i class="icon-unlock-alt"></i> ';
870 }
870 }
871 }
871 }
872 tmpl += escapeMarkup(repoName);
872 tmpl += escapeMarkup(repoName);
873 return tmpl;
873 return tmpl;
874
874
875 }(result, escapeMarkup);
875 }(result, escapeMarkup);
876 };
876 };
877
877
878 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
878 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
879 return function(data, escapeMarkup) {
879 return function(data, escapeMarkup) {
880 if (!data.repo_group_id){
880 if (!data.repo_group_id){
881 return data.text; // optgroup text Repositories
881 return data.text; // optgroup text Repositories
882 }
882 }
883
883
884 var tmpl = '';
884 var tmpl = '';
885 var repoGroupName = data['text'];
885 var repoGroupName = data['text'];
886
886
887 if(data){
887 if(data){
888
888
889 tmpl += '<i class="icon-repo-group"></i> ';
889 tmpl += '<i class="icon-repo-group"></i> ';
890
890
891 }
891 }
892 tmpl += escapeMarkup(repoGroupName);
892 tmpl += escapeMarkup(repoGroupName);
893 return tmpl;
893 return tmpl;
894
894
895 }(result, escapeMarkup);
895 }(result, escapeMarkup);
896 };
896 };
897
897
898 var escapeRegExChars = function (value) {
898 var escapeRegExChars = function (value) {
899 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
899 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
900 };
900 };
901
901
902 var getRepoIcon = function(repo_type) {
902 var getRepoIcon = function(repo_type) {
903 if (repo_type === 'hg') {
903 if (repo_type === 'hg') {
904 return '<i class="icon-hg"></i> ';
904 return '<i class="icon-hg"></i> ';
905 }
905 }
906 else if (repo_type === 'git') {
906 else if (repo_type === 'git') {
907 return '<i class="icon-git"></i> ';
907 return '<i class="icon-git"></i> ';
908 }
908 }
909 else if (repo_type === 'svn') {
909 else if (repo_type === 'svn') {
910 return '<i class="icon-svn"></i> ';
910 return '<i class="icon-svn"></i> ';
911 }
911 }
912 return ''
912 return ''
913 };
913 };
914
914
915 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
915 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
916
916
917 if (value.split(':').length === 2) {
917 if (value.split(':').length === 2) {
918 value = value.split(':')[1]
918 value = value.split(':')[1]
919 }
919 }
920
920
921 var searchType = data['type'];
921 var searchType = data['type'];
922 var searchSubType = data['subtype'];
922 var searchSubType = data['subtype'];
923 var valueDisplay = data['value_display'];
923 var valueDisplay = data['value_display'];
924 var valueIcon = data['value_icon'];
924 var valueIcon = data['value_icon'];
925
925
926 var pattern = '(' + escapeRegExChars(value) + ')';
926 var pattern = '(' + escapeRegExChars(value) + ')';
927
927
928 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
928 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
929
929
930 // highlight match
930 // highlight match
931 if (searchType != 'text') {
931 if (searchType != 'text') {
932 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
932 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
933 }
933 }
934
934
935 var icon = '';
935 var icon = '';
936
936
937 if (searchType === 'hint') {
937 if (searchType === 'hint') {
938 icon += '<i class="icon-repo-group"></i> ';
938 icon += '<i class="icon-repo-group"></i> ';
939 }
939 }
940 // full text search/hints
940 // full text search/hints
941 else if (searchType === 'search') {
941 else if (searchType === 'search') {
942 if (valueIcon === undefined) {
942 if (valueIcon === undefined) {
943 icon += '<i class="icon-more"></i> ';
943 icon += '<i class="icon-more"></i> ';
944 } else {
944 } else {
945 icon += valueIcon + ' ';
945 icon += valueIcon + ' ';
946 }
946 }
947
947
948 if (searchSubType !== undefined && searchSubType == 'repo') {
948 if (searchSubType !== undefined && searchSubType == 'repo') {
949 valueDisplay += '<div class="pull-right tag">repository</div>';
949 valueDisplay += '<div class="pull-right tag">repository</div>';
950 }
950 }
951 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
951 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
952 valueDisplay += '<div class="pull-right tag">repo group</div>';
952 valueDisplay += '<div class="pull-right tag">repo group</div>';
953 }
953 }
954 }
954 }
955 // repository
955 // repository
956 else if (searchType === 'repo') {
956 else if (searchType === 'repo') {
957
957
958 var repoIcon = getRepoIcon(data['repo_type']);
958 var repoIcon = getRepoIcon(data['repo_type']);
959 icon += repoIcon;
959 icon += repoIcon;
960
960
961 if (data['private']) {
961 if (data['private']) {
962 icon += '<i class="icon-lock" ></i> ';
962 icon += '<i class="icon-lock" ></i> ';
963 }
963 }
964 else if (visualShowPublicIcon) {
964 else if (visualShowPublicIcon) {
965 icon += '<i class="icon-unlock-alt"></i> ';
965 icon += '<i class="icon-unlock-alt"></i> ';
966 }
966 }
967 }
967 }
968 // repository groups
968 // repository groups
969 else if (searchType === 'repo_group') {
969 else if (searchType === 'repo_group') {
970 icon += '<i class="icon-repo-group"></i> ';
970 icon += '<i class="icon-repo-group"></i> ';
971 }
971 }
972 // user group
972 // user group
973 else if (searchType === 'user_group') {
973 else if (searchType === 'user_group') {
974 icon += '<i class="icon-group"></i> ';
974 icon += '<i class="icon-group"></i> ';
975 }
975 }
976 // user
976 // user
977 else if (searchType === 'user') {
977 else if (searchType === 'user') {
978 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
978 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
979 }
979 }
980 // pull request
980 // pull request
981 else if (searchType === 'pull_request') {
981 else if (searchType === 'pull_request') {
982 icon += '<i class="icon-merge"></i> ';
982 icon += '<i class="icon-merge"></i> ';
983 }
983 }
984 // commit
984 // commit
985 else if (searchType === 'commit') {
985 else if (searchType === 'commit') {
986 var repo_data = data['repo_data'];
986 var repo_data = data['repo_data'];
987 var repoIcon = getRepoIcon(repo_data['repository_type']);
987 var repoIcon = getRepoIcon(repo_data['repository_type']);
988 if (repoIcon) {
988 if (repoIcon) {
989 icon += repoIcon;
989 icon += repoIcon;
990 } else {
990 } else {
991 icon += '<i class="icon-tag"></i>';
991 icon += '<i class="icon-tag"></i>';
992 }
992 }
993 }
993 }
994 // file
994 // file
995 else if (searchType === 'file') {
995 else if (searchType === 'file') {
996 var repo_data = data['repo_data'];
996 var repo_data = data['repo_data'];
997 var repoIcon = getRepoIcon(repo_data['repository_type']);
997 var repoIcon = getRepoIcon(repo_data['repository_type']);
998 if (repoIcon) {
998 if (repoIcon) {
999 icon += repoIcon;
999 icon += repoIcon;
1000 } else {
1000 } else {
1001 icon += '<i class="icon-tag"></i>';
1001 icon += '<i class="icon-tag"></i>';
1002 }
1002 }
1003 }
1003 }
1004 // generic text
1004 // generic text
1005 else if (searchType === 'text') {
1005 else if (searchType === 'text') {
1006 icon = '';
1006 icon = '';
1007 }
1007 }
1008
1008
1009 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1009 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1010 return tmpl.format(icon, valueDisplay);
1010 return tmpl.format(icon, valueDisplay);
1011 };
1011 };
1012
1012
1013 var handleSelect = function(element, suggestion) {
1013 var handleSelect = function(element, suggestion) {
1014 if (suggestion.type === "hint") {
1014 if (suggestion.type === "hint") {
1015 // we skip action
1015 // we skip action
1016 $('#main_filter').focus();
1016 $('#main_filter').focus();
1017 }
1017 }
1018 else if (suggestion.type === "text") {
1018 else if (suggestion.type === "text") {
1019 // we skip action
1019 // we skip action
1020 $('#main_filter').focus();
1020 $('#main_filter').focus();
1021
1021
1022 } else {
1022 } else {
1023 window.location = suggestion['url'];
1023 window.location = suggestion['url'];
1024 }
1024 }
1025 };
1025 };
1026
1026
1027 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1027 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1028 if (queryLowerCase.split(':').length === 2) {
1028 if (queryLowerCase.split(':').length === 2) {
1029 queryLowerCase = queryLowerCase.split(':')[1]
1029 queryLowerCase = queryLowerCase.split(':')[1]
1030 }
1030 }
1031 if (suggestion.type === "text") {
1031 if (suggestion.type === "text") {
1032 // special case we don't want to "skip" display for
1032 // special case we don't want to "skip" display for
1033 return true
1033 return true
1034 }
1034 }
1035 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1035 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1036 };
1036 };
1037
1037
1038 var cleanContext = {
1038 var cleanContext = {
1039 repo_view_type: null,
1039 repo_view_type: null,
1040
1040
1041 repo_id: null,
1041 repo_id: null,
1042 repo_name: "",
1042 repo_name: "",
1043
1043
1044 repo_group_id: null,
1044 repo_group_id: null,
1045 repo_group_name: null
1045 repo_group_name: null
1046 };
1046 };
1047 var removeGoToFilter = function () {
1047 var removeGoToFilter = function () {
1048 $('.searchTagHidable').hide();
1048 $('.searchTagHidable').hide();
1049 $('#main_filter').autocomplete(
1049 $('#main_filter').autocomplete(
1050 'setOptions', {params:{search_context: cleanContext}});
1050 'setOptions', {params:{search_context: cleanContext}});
1051 };
1051 };
1052
1052
1053 $('#main_filter').autocomplete({
1053 $('#main_filter').autocomplete({
1054 serviceUrl: pyroutes.url('goto_switcher_data'),
1054 serviceUrl: pyroutes.url('goto_switcher_data'),
1055 params: {
1055 params: {
1056 "search_context": templateContext.search_context
1056 "search_context": templateContext.search_context
1057 },
1057 },
1058 minChars:2,
1058 minChars:2,
1059 maxHeight:400,
1059 maxHeight:400,
1060 deferRequestBy: 300, //miliseconds
1060 deferRequestBy: 300, //miliseconds
1061 tabDisabled: true,
1061 tabDisabled: true,
1062 autoSelectFirst: false,
1062 autoSelectFirst: false,
1063 containerClass: 'autocomplete-qfilter-suggestions',
1063 containerClass: 'autocomplete-qfilter-suggestions',
1064 formatResult: autocompleteMainFilterFormatResult,
1064 formatResult: autocompleteMainFilterFormatResult,
1065 lookupFilter: autocompleteMainFilterResult,
1065 lookupFilter: autocompleteMainFilterResult,
1066 onSelect: function (element, suggestion) {
1066 onSelect: function (element, suggestion) {
1067 handleSelect(element, suggestion);
1067 handleSelect(element, suggestion);
1068 return false;
1068 return false;
1069 },
1069 },
1070 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1070 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1071 if (jqXHR !== 'abort') {
1071 if (jqXHR !== 'abort') {
1072 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1072 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1073 SwalNoAnimation.fire({
1073 SwalNoAnimation.fire({
1074 icon: 'error',
1074 icon: 'error',
1075 title: _gettext('Error during search operation'),
1075 title: _gettext('Error during search operation'),
1076 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1076 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1077 }).then(function(result) {
1077 }).then(function(result) {
1078 window.location.reload();
1078 window.location.reload();
1079 })
1079 })
1080 }
1080 }
1081 },
1081 },
1082 onSearchStart: function (params) {
1082 onSearchStart: function (params) {
1083 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1083 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1084 },
1084 },
1085 onSearchComplete: function (query, suggestions) {
1085 onSearchComplete: function (query, suggestions) {
1086 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1086 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1087 },
1087 },
1088 });
1088 });
1089
1089
1090 showMainFilterBox = function () {
1090 showMainFilterBox = function () {
1091 $('#main_filter_help').toggle();
1091 $('#main_filter_help').toggle();
1092 };
1092 };
1093
1093
1094 $('#main_filter').on('keydown.autocomplete', function (e) {
1094 $('#main_filter').on('keydown.autocomplete', function (e) {
1095
1095
1096 var BACKSPACE = 8;
1096 var BACKSPACE = 8;
1097 var el = $(e.currentTarget);
1097 var el = $(e.currentTarget);
1098 if(e.which === BACKSPACE){
1098 if(e.which === BACKSPACE){
1099 var inputVal = el.val();
1099 var inputVal = el.val();
1100 if (inputVal === ""){
1100 if (inputVal === ""){
1101 removeGoToFilter()
1101 removeGoToFilter()
1102 }
1102 }
1103 }
1103 }
1104 });
1104 });
1105
1105
1106 var dismissNotice = function(noticeId) {
1106 var dismissNotice = function(noticeId) {
1107
1107
1108 var url = pyroutes.url('user_notice_dismiss',
1108 var url = pyroutes.url('user_notice_dismiss',
1109 {"user_id": templateContext.rhodecode_user.user_id});
1109 {"user_id": templateContext.rhodecode_user.user_id});
1110
1110
1111 var postData = {
1111 var postData = {
1112 'csrf_token': CSRF_TOKEN,
1112 'csrf_token': CSRF_TOKEN,
1113 'notice_id': noticeId,
1113 'notice_id': noticeId,
1114 };
1114 };
1115
1115
1116 var success = function(response) {
1116 var success = function(response) {
1117 $('#notice-message-' + noticeId).remove();
1117 $('#notice-message-' + noticeId).remove();
1118 return false;
1118 return false;
1119 };
1119 };
1120 var failure = function(data, textStatus, xhr) {
1120 var failure = function(data, textStatus, xhr) {
1121 alert("error processing request: " + textStatus);
1121 alert("error processing request: " + textStatus);
1122 return false;
1122 return false;
1123 };
1123 };
1124 ajaxPOST(url, postData, success, failure);
1124 ajaxPOST(url, postData, success, failure);
1125 }
1125 }
1126 </script>
1126 </script>
1127 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1127 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1128 </%def>
1128 </%def>
1129
1129
1130 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1130 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1131 <div class="modal-dialog">
1131 <div class="modal-dialog">
1132 <div class="modal-content">
1132 <div class="modal-content">
1133 <div class="modal-header">
1133 <div class="modal-header">
1134 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1134 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1135 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1135 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1136 </div>
1136 </div>
1137 <div class="modal-body">
1137 <div class="modal-body">
1138 <div class="block-left">
1138 <div class="block-left">
1139 <table class="keyboard-mappings">
1139 <table class="keyboard-mappings">
1140 <tbody>
1140 <tbody>
1141 <tr>
1141 <tr>
1142 <th></th>
1142 <th></th>
1143 <th>${_('Site-wide shortcuts')}</th>
1143 <th>${_('Site-wide shortcuts')}</th>
1144 </tr>
1144 </tr>
1145 <%
1145 <%
1146 elems = [
1146 elems = [
1147 ('/', 'Use quick search box'),
1147 ('/', 'Use quick search box'),
1148 ('g h', 'Goto home page'),
1148 ('g h', 'Goto home page'),
1149 ('g g', 'Goto my private gists page'),
1149 ('g g', 'Goto my private gists page'),
1150 ('g G', 'Goto my public gists page'),
1150 ('g G', 'Goto my public gists page'),
1151 ('g 0-9', 'Goto bookmarked items from 0-9'),
1151 ('g 0-9', 'Goto bookmarked items from 0-9'),
1152 ('n r', 'New repository page'),
1152 ('n r', 'New repository page'),
1153 ('n g', 'New gist page'),
1153 ('n g', 'New gist page'),
1154 ]
1154 ]
1155 %>
1155 %>
1156 %for key, desc in elems:
1156 %for key, desc in elems:
1157 <tr>
1157 <tr>
1158 <td class="keys">
1158 <td class="keys">
1159 <span class="key tag">${key}</span>
1159 <span class="key tag">${key}</span>
1160 </td>
1160 </td>
1161 <td>${desc}</td>
1161 <td>${desc}</td>
1162 </tr>
1162 </tr>
1163 %endfor
1163 %endfor
1164 </tbody>
1164 </tbody>
1165 </table>
1165 </table>
1166 </div>
1166 </div>
1167 <div class="block-left">
1167 <div class="block-left">
1168 <table class="keyboard-mappings">
1168 <table class="keyboard-mappings">
1169 <tbody>
1169 <tbody>
1170 <tr>
1170 <tr>
1171 <th></th>
1171 <th></th>
1172 <th>${_('Repositories')}</th>
1172 <th>${_('Repositories')}</th>
1173 </tr>
1173 </tr>
1174 <%
1174 <%
1175 elems = [
1175 elems = [
1176 ('g s', 'Goto summary page'),
1176 ('g s', 'Goto summary page'),
1177 ('g c', 'Goto changelog page'),
1177 ('g c', 'Goto changelog page'),
1178 ('g f', 'Goto files page'),
1178 ('g f', 'Goto files page'),
1179 ('g F', 'Goto files page with file search activated'),
1179 ('g F', 'Goto files page with file search activated'),
1180 ('g p', 'Goto pull requests page'),
1180 ('g p', 'Goto pull requests page'),
1181 ('g o', 'Goto repository settings'),
1181 ('g o', 'Goto repository settings'),
1182 ('g O', 'Goto repository access permissions settings'),
1182 ('g O', 'Goto repository access permissions settings'),
1183 ]
1183 ]
1184 %>
1184 %>
1185 %for key, desc in elems:
1185 %for key, desc in elems:
1186 <tr>
1186 <tr>
1187 <td class="keys">
1187 <td class="keys">
1188 <span class="key tag">${key}</span>
1188 <span class="key tag">${key}</span>
1189 </td>
1189 </td>
1190 <td>${desc}</td>
1190 <td>${desc}</td>
1191 </tr>
1191 </tr>
1192 %endfor
1192 %endfor
1193 </tbody>
1193 </tbody>
1194 </table>
1194 </table>
1195 </div>
1195 </div>
1196 </div>
1196 </div>
1197 <div class="modal-footer">
1197 <div class="modal-footer">
1198 </div>
1198 </div>
1199 </div><!-- /.modal-content -->
1199 </div><!-- /.modal-content -->
1200 </div><!-- /.modal-dialog -->
1200 </div><!-- /.modal-dialog -->
1201 </div><!-- /.modal -->
1201 </div><!-- /.modal -->
@@ -1,117 +1,116 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Files Add').format(c.repo_name)}
4 ${_('{} Files Add').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()"></%def>
14 <%def name="breadcrumbs_links()"></%def>
15
15
16 <%def name="menu_bar_subnav()">
16 <%def name="menu_bar_subnav()">
17 ${self.repo_menu(active='files')}
17 ${self.repo_menu(active='files')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21
21
22 <div class="box">
22 <div class="box">
23
23
24 <div class="edit-file-title">
24 <div class="edit-file-title">
25 <span class="title-heading">${_('Add new file')} @ <code>${h.show_id(c.commit)}</code></span>
25 <span class="title-heading">${_('Add new file')} @ <code>${h.show_id(c.commit)}</code></span>
26 % if c.commit.branch:
26 % if c.commit.branch:
27 <span class="tag branchtag">
27 <span class="tag branchtag">
28 <i class="icon-branch"></i> ${c.commit.branch}
28 <i class="icon-branch"></i> ${c.commit.branch}
29 </span>
29 </span>
30 % endif
30 % endif
31 </div>
31 </div>
32
32
33 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
33 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 <div class="edit-file-fieldset">
34 <div class="edit-file-fieldset">
35 <div class="path-items">
35 <div class="path-items">
36 <ul>
36 <ul>
37 <li class="breadcrumb-path">
37 <li class="breadcrumb-path">
38 <div>
38 <div>
39 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
39 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.f_path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=False, linkify_last_item=True, copy_path_icon=False)} /
40 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
41 </div>
40 </div>
42 </li>
41 </li>
43 <li class="location-path">
42 <li class="location-path">
44 <input class="file-name-input input-small" type="text" value="${request.GET.get('filename')}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
43 <input class="file-name-input input-small" type="text" value="${request.GET.get('filename')}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
45 </li>
44 </li>
46 </ul>
45 </ul>
47 </div>
46 </div>
48
47
49 </div>
48 </div>
50
49
51 <div class="table">
50 <div class="table">
52 <div>
51 <div>
53 <div id="codeblock" class="codeblock">
52 <div id="codeblock" class="codeblock">
54 <div class="editor-items">
53 <div class="editor-items">
55 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
54 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
56 ${_('Edit')}
55 ${_('Edit')}
57 </div>
56 </div>
58
57
59 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
58 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
60 ${_('Preview')}
59 ${_('Preview')}
61 </div>
60 </div>
62
61
63 <div class="pull-right">
62 <div class="pull-right">
64 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off'))], extra_classes=['last-item'])}
63 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off'))], extra_classes=['last-item'])}
65 </div>
64 </div>
66 <div class="pull-right">
65 <div class="pull-right">
67 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))], enable_filter=True)}
66 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))], enable_filter=True)}
68 </div>
67 </div>
69 </div>
68 </div>
70
69
71 <div id="editor_container">
70 <div id="editor_container">
72 <pre id="editor_pre"></pre>
71 <pre id="editor_pre"></pre>
73 <textarea id="editor" name="content" ></textarea>
72 <textarea id="editor" name="content" ></textarea>
74 <div id="editor_preview"></div>
73 <div id="editor_preview"></div>
75 </div>
74 </div>
76 </div>
75 </div>
77 </div>
76 </div>
78 </div>
77 </div>
79
78
80 <div class="edit-file-fieldset">
79 <div class="edit-file-fieldset">
81 <div class="fieldset">
80 <div class="fieldset">
82 <div class="message">
81 <div class="message">
83 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
82 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
84 </div>
83 </div>
85 </div>
84 </div>
86 <div class="pull-left">
85 <div class="pull-left">
87 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
86 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
88 </div>
87 </div>
89 </div>
88 </div>
90 ${h.end_form()}
89 ${h.end_form()}
91 </div>
90 </div>
92
91
93 <script type="text/javascript">
92 <script type="text/javascript">
94
93
95 $(document).ready(function () {
94 $(document).ready(function () {
96 var modes_select = $('#set_mode');
95 var modes_select = $('#set_mode');
97 var filename_selector = '#filename';
96 var filename_selector = '#filename';
98 fillCodeMirrorOptions(modes_select);
97 fillCodeMirrorOptions(modes_select);
99
98
100 fileEditor = new FileEditor('#editor');
99 fileEditor = new FileEditor('#editor');
101
100
102 // on change of select field set mode
101 // on change of select field set mode
103 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
102 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
104
103
105 // on entering the new filename set mode, from given extension
104 // on entering the new filename set mode, from given extension
106 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
105 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
107
106
108 $('#filename').focus();
107 $('#filename').focus();
109
108
110 var commit_id = "${c.commit.raw_id}";
109 var commit_id = "${c.commit.raw_id}";
111 var f_path = "${c.f_path}";
110 var f_path = "${c.f_path}";
112
111
113 checkFileHead($('#eform'), commit_id, f_path, 'create')
112 checkFileHead($('#eform'), commit_id, f_path, 'create')
114 });
113 });
115
114
116 </script>
115 </script>
117 </%def>
116 </%def>
@@ -1,95 +1,98 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <%
3 <%
4 if request.GET.get('at'):
4 at_ref = request.GET.get('at')
5 query={'at': request.GET.get('at')}
5 if at_ref:
6 query={'at': at_ref}
7 default_commit_id = at_ref or c.rhodecode_db_repo.landing_ref_name
6 else:
8 else:
7 query=None
9 query=None
10 default_commit_id = c.commit.raw_id
8 %>
11 %>
9 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
12 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
10 <table class="code-browser rctable repo_summary">
13 <table class="code-browser rctable repo_summary">
11 <thead>
14 <thead>
12 <tr>
15 <tr>
13 <th>${_('Name')}</th>
16 <th>${_('Name')}</th>
14 <th>${_('Size')}</th>
17 <th>${_('Size')}</th>
15 <th>${_('Modified')}</th>
18 <th>${_('Modified')}</th>
16 <th>${_('Last Commit')}</th>
19 <th>${_('Last Commit')}</th>
17 <th>${_('Author')}</th>
20 <th>${_('Author')}</th>
18 </tr>
21 </tr>
19 </thead>
22 </thead>
20
23
21 <tbody id="tbody">
24 <tbody id="tbody">
22 <tr>
25 <tr>
23 <td colspan="5">
26 <td colspan="5">
24 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path, request.GET.get('at'), limit_items=True)}
27 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True)}
25 </td>
28 </td>
26 </tr>
29 </tr>
27
30
28 <% has_files = False %>
31 <% has_files = False %>
29 % for cnt,node in enumerate(c.file):
32 % for cnt,node in enumerate(c.file):
30 <% has_files = True %>
33 <% has_files = True %>
31 <tr class="parity${(cnt % 2)}">
34 <tr class="parity${(cnt % 2)}">
32 <td class="td-componentname">
35 <td class="td-componentname">
33 % if node.is_submodule():
36 % if node.is_submodule():
34 <span class="submodule-dir">
37 <span class="submodule-dir">
35 % if node.url.startswith('http://') or node.url.startswith('https://'):
38 % if node.url.startswith('http://') or node.url.startswith('https://'):
36 <a href="${node.url}">
39 <a href="${node.url}">
37 <i class="icon-directory browser-dir"></i>${node.name}
40 <i class="icon-directory browser-dir"></i>${node.name}
38 </a>
41 </a>
39 % else:
42 % else:
40 <i class="icon-directory browser-dir"></i>${node.name}
43 <i class="icon-directory browser-dir"></i>${node.name}
41 % endif
44 % endif
42 </span>
45 </span>
43 % else:
46 % else:
44 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=h.safe_unicode(node.path), _query=query)}">
47 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=default_commit_id,f_path=h.safe_unicode(node.path), _query=query)}">
45 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
48 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
46 </a>
49 </a>
47 % endif
50 % endif
48 </td>
51 </td>
49 %if node.is_file():
52 %if node.is_file():
50 <td class="td-size" data-attr-name="size">
53 <td class="td-size" data-attr-name="size">
51 % if c.full_load:
54 % if c.full_load:
52 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
55 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
53 % else:
56 % else:
54 ${_('Loading ...')}
57 ${_('Loading ...')}
55 % endif
58 % endif
56 </td>
59 </td>
57 <td class="td-time" data-attr-name="modified_at">
60 <td class="td-time" data-attr-name="modified_at">
58 % if c.full_load:
61 % if c.full_load:
59 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
62 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
60 % endif
63 % endif
61 </td>
64 </td>
62 <td class="td-hash" data-attr-name="commit_id">
65 <td class="td-hash" data-attr-name="commit_id">
63 % if c.full_load:
66 % if c.full_load:
64 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
67 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
65 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
68 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
66 </div>
69 </div>
67 % endif
70 % endif
68 </td>
71 </td>
69 <td class="td-user" data-attr-name="author">
72 <td class="td-user" data-attr-name="author">
70 % if c.full_load:
73 % if c.full_load:
71 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
74 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
72 % endif
75 % endif
73 </td>
76 </td>
74 %else:
77 %else:
75 <td></td>
78 <td></td>
76 <td></td>
79 <td></td>
77 <td></td>
80 <td></td>
78 <td></td>
81 <td></td>
79 %endif
82 %endif
80 </tr>
83 </tr>
81 % endfor
84 % endfor
82
85
83 % if not has_files:
86 % if not has_files:
84 <tr>
87 <tr>
85 <td colspan="5">
88 <td colspan="5">
86 ##empty-dir mostly SVN
89 ##empty-dir mostly SVN
87 &nbsp;
90 &nbsp;
88 </td>
91 </td>
89 </tr>
92 </tr>
90 % endif
93 % endif
91
94
92 </tbody>
95 </tbody>
93 <tbody id="tbody_filtered"></tbody>
96 <tbody id="tbody_filtered"></tbody>
94 </table>
97 </table>
95 </div>
98 </div>
@@ -1,93 +1,92 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Files Delete').format(c.repo_name)}
4 ${_('{} Files Delete').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()"></%def>
14 <%def name="breadcrumbs_links()"></%def>
15
15
16 <%def name="menu_bar_subnav()">
16 <%def name="menu_bar_subnav()">
17 ${self.repo_menu(active='files')}
17 ${self.repo_menu(active='files')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21
21
22 <div class="box">
22 <div class="box">
23
23
24 <div class="edit-file-title">
24 <div class="edit-file-title">
25 <span class="title-heading">${_('Delete file')} @ <code>${h.show_id(c.commit)}</code></span>
25 <span class="title-heading">${_('Delete file')} @ <code>${h.show_id(c.commit)}</code></span>
26 % if c.commit.branch:
26 % if c.commit.branch:
27 <span class="tag branchtag">
27 <span class="tag branchtag">
28 <i class="icon-branch"></i> ${c.commit.branch}
28 <i class="icon-branch"></i> ${c.commit.branch}
29 </span>
29 </span>
30 % endif
30 % endif
31 </div>
31 </div>
32
32
33 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
33 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 <div class="edit-file-fieldset">
34 <div class="edit-file-fieldset">
35 <div class="path-items">
35 <div class="path-items">
36 <li class="breadcrumb-path">
36 <li class="breadcrumb-path">
37 <div>
37 <div>
38 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
38 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
39 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.file.dir_path)}">${c.file.dir_path}</a> ${('/' if c.file.dir_path else '')}
40 </div>
39 </div>
41 </li>
40 </li>
42 <li class="location-path">
41 <li class="location-path">
43 <input type="hidden" value="${c.f_path}" name="root_path">
42 <input type="hidden" value="${c.f_path}" name="root_path">
44 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" disabled="disabled">
43 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" disabled="disabled">
45 </li>
44 </li>
46 </div>
45 </div>
47
46
48 </div>
47 </div>
49
48
50 <div id="codeblock" class="codeblock delete-file-preview">
49 <div id="codeblock" class="codeblock delete-file-preview">
51 <div class="code-body">
50 <div class="code-body">
52 %if c.file.is_binary:
51 %if c.file.is_binary:
53 ${_('Binary file (%s)') % c.file.mimetype}
52 ${_('Binary file (%s)') % c.file.mimetype}
54 %else:
53 %else:
55 %if c.file.size < c.visual.cut_off_limit_file:
54 %if c.file.size < c.visual.cut_off_limit_file:
56 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
55 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
57 %else:
56 %else:
58 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
57 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
59 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
58 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
60 %endif
59 %endif
61 %endif
60 %endif
62 </div>
61 </div>
63 </div>
62 </div>
64
63
65 <div class="edit-file-fieldset">
64 <div class="edit-file-fieldset">
66 <div class="fieldset">
65 <div class="fieldset">
67 <div class="message">
66 <div class="message">
68 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
67 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
69 </div>
68 </div>
70 </div>
69 </div>
71 <div class="pull-left">
70 <div class="pull-left">
72 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-danger-action")}
71 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-danger-action")}
73 </div>
72 </div>
74 </div>
73 </div>
75 ${h.end_form()}
74 ${h.end_form()}
76 </div>
75 </div>
77
76
78
77
79 <script type="text/javascript">
78 <script type="text/javascript">
80
79
81 $(document).ready(function () {
80 $(document).ready(function () {
82
81
83 fileEditor = new FileEditor('#editor');
82 fileEditor = new FileEditor('#editor');
84
83
85 var commit_id = "${c.commit.raw_id}";
84 var commit_id = "${c.commit.raw_id}";
86 var f_path = "${c.f_path}";
85 var f_path = "${c.f_path}";
87
86
88 checkFileHead($('#eform'), commit_id, f_path, 'delete');
87 checkFileHead($('#eform'), commit_id, f_path, 'delete');
89 });
88 });
90
89
91 </script>
90 </script>
92
91
93 </%def>
92 </%def>
@@ -1,129 +1,128 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Files Edit').format(c.repo_name)}
4 ${_('{} Files Edit').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()"></%def>
14 <%def name="breadcrumbs_links()"></%def>
15
15
16 <%def name="menu_bar_subnav()">
16 <%def name="menu_bar_subnav()">
17 ${self.repo_menu(active='files')}
17 ${self.repo_menu(active='files')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21
21
22 <div class="box">
22 <div class="box">
23
23
24 <div class="edit-file-title">
24 <div class="edit-file-title">
25 <span class="title-heading">${_('Edit file')} @ <code>${h.show_id(c.commit)}</code></span>
25 <span class="title-heading">${_('Edit file')} @ <code>${h.show_id(c.commit)}</code></span>
26 % if c.commit.branch:
26 % if c.commit.branch:
27 <span class="tag branchtag">
27 <span class="tag branchtag">
28 <i class="icon-branch"></i> ${c.commit.branch}
28 <i class="icon-branch"></i> ${c.commit.branch}
29 </span>
29 </span>
30 % endif
30 % endif
31 </div>
31 </div>
32
32
33 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
33 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
34 <div class="edit-file-fieldset">
34 <div class="edit-file-fieldset">
35 <div class="path-items">
35 <div class="path-items">
36 <ul>
36 <ul>
37 <li class="breadcrumb-path">
37 <li class="breadcrumb-path">
38 <div>
38 <div>
39 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
39 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True, hide_last_item=True, copy_path_icon=False)} /
40 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.file.dir_path)}">${c.file.dir_path}</a> ${('/' if c.file.dir_path else '')}
41 </div>
40 </div>
42 </li>
41 </li>
43 <li class="location-path">
42 <li class="location-path">
44 <input type="hidden" value="${c.f_path}" name="root_path">
43 <input type="hidden" value="${c.f_path}" name="root_path">
45 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
44 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
46 </li>
45 </li>
47 </ul>
46 </ul>
48 </div>
47 </div>
49
48
50 </div>
49 </div>
51
50
52 <div class="table">
51 <div class="table">
53 <div>
52 <div>
54
53
55 <div id="codeblock" class="codeblock">
54 <div id="codeblock" class="codeblock">
56 <div class="editor-items">
55 <div class="editor-items">
57 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
56 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
58 ${_('Edit')}
57 ${_('Edit')}
59 </div>
58 </div>
60
59
61 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
60 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
62 ${_('Preview')}
61 ${_('Preview')}
63 </div>
62 </div>
64
63
65 <div class="pull-right">
64 <div class="pull-right">
66 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off')),])}
65 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off')),])}
67 </div>
66 </div>
68 <div class="pull-right">
67 <div class="pull-right">
69 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))],enable_filter=True)}
68 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))],enable_filter=True)}
70 </div>
69 </div>
71 </div>
70 </div>
72
71
73 <div id="editor_container">
72 <div id="editor_container">
74 <pre id="editor_pre"></pre>
73 <pre id="editor_pre"></pre>
75 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
74 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
76 <div id="editor_preview" ></div>
75 <div id="editor_preview" ></div>
77 </div>
76 </div>
78 </div>
77 </div>
79 </div>
78 </div>
80 </div>
79 </div>
81
80
82 <div class="edit-file-fieldset">
81 <div class="edit-file-fieldset">
83 <div class="fieldset">
82 <div class="fieldset">
84 <div class="message">
83 <div class="message">
85 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
84 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
86 </div>
85 </div>
87 </div>
86 </div>
88 <div class="pull-left">
87 <div class="pull-left">
89 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
88 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
90 </div>
89 </div>
91 </div>
90 </div>
92 ${h.end_form()}
91 ${h.end_form()}
93 </div>
92 </div>
94
93
95 <script type="text/javascript">
94 <script type="text/javascript">
96
95
97 $(document).ready(function() {
96 $(document).ready(function() {
98 var modes_select = $('#set_mode');
97 var modes_select = $('#set_mode');
99 var filename_selector = '#filename';
98 var filename_selector = '#filename';
100 fillCodeMirrorOptions(modes_select);
99 fillCodeMirrorOptions(modes_select);
101
100
102 fileEditor = new FileEditor('#editor');
101 fileEditor = new FileEditor('#editor');
103
102
104 // try to detect the mode based on the file we edit
103 // try to detect the mode based on the file we edit
105 var detected_mode = detectCodeMirrorMode("${c.file.name}", "${c.file.mimetype}");
104 var detected_mode = detectCodeMirrorMode("${c.file.name}", "${c.file.mimetype}");
106
105
107 if (detected_mode) {
106 if (detected_mode) {
108 setCodeMirrorMode(fileEditor.cm, detected_mode);
107 setCodeMirrorMode(fileEditor.cm, detected_mode);
109
108
110 var mimetype = $(modes_select).find("option[mode={0}]".format(detected_mode)).val();
109 var mimetype = $(modes_select).find("option[mode={0}]".format(detected_mode)).val();
111 $(modes_select).select2("val", mimetype).trigger('change');
110 $(modes_select).select2("val", mimetype).trigger('change');
112 }
111 }
113
112
114 // on change of select field set mode
113 // on change of select field set mode
115 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
114 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
116
115
117 // on entering the new filename set mode, from given extension
116 // on entering the new filename set mode, from given extension
118 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
117 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
119
118
120 var commit_id = "${c.commit.raw_id}";
119 var commit_id = "${c.commit.raw_id}";
121 var f_path = "${c.f_path}";
120 var f_path = "${c.f_path}";
122
121
123 checkFileHead($('#eform'), commit_id, f_path, 'edit')
122 checkFileHead($('#eform'), commit_id, f_path, 'edit')
124
123
125 });
124 });
126
125
127
126
128 </script>
127 </script>
129 </%def>
128 </%def>
@@ -1,179 +1,189 b''
1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
2
2
3 <%
4 at_ref = request.GET.get('at')
5 if at_ref:
6 query={'at': at_ref}
7 default_commit_id = at_ref or c.rhodecode_db_repo.landing_ref_name
8 else:
9 query=None
10 default_commit_id = c.commit.raw_id
11 %>
12
3 <div id="codeblock" class="browserblock">
13 <div id="codeblock" class="browserblock">
4 <div class="browser-header">
14 <div class="browser-header">
5 <div class="browser-nav">
15 <div class="browser-nav">
6 <div class="pull-left">
16 <div class="pull-left">
7 ## loads the history for a file
17 ## loads the history for a file
8 ${h.hidden('file_refs_filter')}
18 ${h.hidden('file_refs_filter')}
9 </div>
19 </div>
10
20
11 <div class="pull-right">
21 <div class="pull-right">
12
22
13 ## Download
23 ## Download
14 % if c.lf_node:
24 % if c.lf_node:
15 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
25 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
16 ${_('Download largefile')}
26 ${_('Download largefile')}
17 </a>
27 </a>
18 % else:
28 % else:
19 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
29 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
20 ${_('Download file')}
30 ${_('Download file')}
21 </a>
31 </a>
22 % endif
32 % endif
23
33
24 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
34 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
25 ## on branch head, can edit files
35 ## on branch head, can edit files
26 %if c.on_branch_head and c.branch_or_raw_id:
36 %if c.on_branch_head and c.branch_or_raw_id:
27 ## binary files are delete only
37 ## binary files are delete only
28 % if c.file.is_binary:
38 % if c.file.is_binary:
29 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
39 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
30 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path),class_="btn btn-danger")}
40 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query),class_="btn btn-danger")}
31 % else:
41 % else:
32 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path)}">
42 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
33 ${_('Edit on branch: ')}<code>${c.branch_name}</code>
43 ${_('Edit on branch: ')}<code>${c.branch_name}</code>
34 </a>
44 </a>
35
45
36 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path)}">
46 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
37 ${_('Delete')}
47 ${_('Delete')}
38 </a>
48 </a>
39 % endif
49 % endif
40 ## not on head, forbid all
50 ## not on head, forbid all
41 % else:
51 % else:
42 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
52 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
43 ${h.link_to(_('Delete'), '#Delete', class_="btn btn-default btn-danger disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
53 ${h.link_to(_('Delete'), '#Delete', class_="btn btn-default btn-danger disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
44 % endif
54 % endif
45 %endif
55 %endif
46
56
47 </div>
57 </div>
48 </div>
58 </div>
49 <div id="file_history_container"></div>
59 <div id="file_history_container"></div>
50
60
51 </div>
61 </div>
52 </div>
62 </div>
53
63
54 <div class="codeblock">
64 <div class="codeblock">
55 <div class=" codeblock-header">
65 <div class=" codeblock-header">
56 <div class="file-filename">
66 <div class="file-filename">
57 <i class="icon-file"></i> ${c.file}
67 <i class="icon-file"></i> ${c.file}
58 </div>
68 </div>
59
69
60 <div class="file-stats">
70 <div class="file-stats">
61
71
62 <div class="stats-info">
72 <div class="stats-info">
63 <span class="stats-first-item">
73 <span class="stats-first-item">
64 % if c.file_size_too_big:
74 % if c.file_size_too_big:
65 0 ${(_('lines'))}
75 0 ${(_('lines'))}
66 % else:
76 % else:
67 ${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}
77 ${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}
68 % endif
78 % endif
69 </span>
79 </span>
70
80
71 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
81 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
72 % if c.lf_node:
82 % if c.lf_node:
73 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
83 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
74 % endif
84 % endif
75 <span>
85 <span>
76 | ${c.file.mimetype}
86 | ${c.file.mimetype}
77 </span>
87 </span>
78
88
79 % if not c.file_size_too_big:
89 % if not c.file_size_too_big:
80 <span> |
90 <span> |
81 ${h.get_lexer_for_filenode(c.file).__class__.__name__}
91 ${h.get_lexer_for_filenode(c.file).__class__.__name__}
82 </span>
92 </span>
83 % endif
93 % endif
84
94
85 </div>
95 </div>
86 </div>
96 </div>
87 </div>
97 </div>
88
98
89 <div class="path clear-fix">
99 <div class="path clear-fix">
90 <div class="pull-left">
100 <div class="pull-left">
91 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path, request.GET.get('at'))}
101 ${h.files_breadcrumbs(c.repo_name, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'))}
92 </div>
102 </div>
93
103
94 <div class="pull-right stats">
104 <div class="pull-right stats">
95 <a id="file_history_overview" href="#loadHistory">
105 <a id="file_history_overview" href="#loadHistory">
96 ${_('History')}
106 ${_('History')}
97 </a>
107 </a>
98 |
108 |
99 %if c.annotate:
109 %if c.annotate:
100 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
110 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
101 %else:
111 %else:
102 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
112 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
103 %endif
113 %endif
104 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
114 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
105 % if not c.file.is_binary:
115 % if not c.file.is_binary:
106 |<a href="#copySource" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${c.file.content}">${_('Copy content')}</a>
116 |<a href="#copySource" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${c.file.content}">${_('Copy content')}</a>
107 |<a href="#copyPermaLink" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${h.route_url('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">${_('Copy permalink')}</a>
117 |<a href="#copyPermaLink" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${h.route_url('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">${_('Copy permalink')}</a>
108 % endif
118 % endif
109
119
110 </div>
120 </div>
111 <div class="clear-fix"></div>
121 <div class="clear-fix"></div>
112 </div>
122 </div>
113
123
114 <div class="code-body clear-fix ">
124 <div class="code-body clear-fix ">
115 %if c.file.is_binary:
125 %if c.file.is_binary:
116 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
126 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
117 % if rendered_binary:
127 % if rendered_binary:
118 <div class="text-center">
128 <div class="text-center">
119 ${rendered_binary}
129 ${rendered_binary}
120 </div>
130 </div>
121 % else:
131 % else:
122 <div>
132 <div>
123 ${_('Binary file ({})').format(c.file.mimetype)}
133 ${_('Binary file ({})').format(c.file.mimetype)}
124 </div>
134 </div>
125 % endif
135 % endif
126 %else:
136 %else:
127 % if c.file_size_too_big:
137 % if c.file_size_too_big:
128 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
138 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
129 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
139 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
130 % else:
140 % else:
131 %if c.renderer and not c.annotate:
141 %if c.renderer and not c.annotate:
132 ## pick relative url based on renderer
142 ## pick relative url based on renderer
133 <%
143 <%
134 relative_urls = {
144 relative_urls = {
135 'raw': h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
145 'raw': h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
136 'standard': h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
146 'standard': h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
137 }
147 }
138 %>
148 %>
139 ${h.render(c.file.content, renderer=c.renderer, relative_urls=relative_urls)}
149 ${h.render(c.file.content, renderer=c.renderer, relative_urls=relative_urls)}
140 %else:
150 %else:
141 <table class="cb codehilite">
151 <table class="cb codehilite">
142 %if c.annotate:
152 %if c.annotate:
143 <% color_hasher = h.color_hasher() %>
153 <% color_hasher = h.color_hasher() %>
144 %for annotation, lines in c.annotated_lines:
154 %for annotation, lines in c.annotated_lines:
145 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
155 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
146 %endfor
156 %endfor
147 %else:
157 %else:
148 %for line_num, tokens in enumerate(c.lines, 1):
158 %for line_num, tokens in enumerate(c.lines, 1):
149 ${sourceblock.render_line(line_num, tokens)}
159 ${sourceblock.render_line(line_num, tokens)}
150 %endfor
160 %endfor
151 %endif
161 %endif
152 </table>
162 </table>
153 %endif
163 %endif
154 % endif
164 % endif
155 %endif
165 %endif
156 </div>
166 </div>
157
167
158 </div>
168 </div>
159
169
160 <script type="text/javascript">
170 <script type="text/javascript">
161 % if request.GET.get('mark'):
171 % if request.GET.get('mark'):
162
172
163 $(function(){
173 $(function(){
164 $(".codehilite").mark(
174 $(".codehilite").mark(
165 "${request.GET.get('mark')}",
175 "${request.GET.get('mark')}",
166 {
176 {
167 "className": 'match',
177 "className": 'match',
168 "accuracy": "complementary",
178 "accuracy": "complementary",
169 "ignorePunctuation": ":._(){}[]!'+=".split(""),
179 "ignorePunctuation": ":._(){}[]!'+=".split(""),
170 "each": function(el) {
180 "each": function(el) {
171 // and also highlight lines !
181 // and also highlight lines !
172 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
182 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
173 }
183 }
174 }
184 }
175 );
185 );
176
186
177 });
187 });
178 % endif
188 % endif
179 </script>
189 </script>
@@ -1,165 +1,165 b''
1 <%namespace name="search" file="/search/search.mako"/>
1 <%namespace name="search" file="/search/search.mako"/>
2
2
3 <%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
3 <%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
4 % if has_matched_content:
4 % if has_matched_content:
5 ${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
5 ${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
6 % else:
6 % else:
7 ${_('No content matched')} <br/>
7 ${_('No content matched')} <br/>
8 % endif
8 % endif
9
9
10 %if len(matching_lines) > shown_matching_lines:
10 %if len(matching_lines) > shown_matching_lines:
11 <a href="${url}">
11 <a href="${url}">
12 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
12 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
13 </a>
13 </a>
14 %endif
14 %endif
15 </%def>
15 </%def>
16
16
17 <div class="search-results">
17 <div class="search-results">
18 <% query_mark = c.searcher.query_to_mark(c.cur_query, 'content') %>
18 <% query_mark = c.searcher.query_to_mark(c.cur_query, 'content') %>
19
19
20 %for entry in c.formatted_results:
20 %for entry in c.formatted_results:
21
21
22 <%
22 <%
23 file_content = entry['content_highlight'] or entry['content']
23 file_content = entry['content_highlight'] or entry['content']
24 mimetype = entry.get('mimetype')
24 mimetype = entry.get('mimetype')
25 filepath = entry.get('path')
25 filepath = entry.get('path')
26 max_lines = h.safe_int(request.GET.get('max_lines', '10'))
26 max_lines = h.safe_int(request.GET.get('max_lines', '10'))
27 line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
27 line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
28
28
29 match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
29 match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
30 terms = c.cur_query
30 terms = c.cur_query
31
31
32 if c.searcher.is_es_6:
32 if c.searcher.is_es_6:
33 # use empty terms so we default to markers usage
33 # use empty terms so we default to markers usage
34 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
34 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
35 else:
35 else:
36 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
36 total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
37
37
38 shown_matching_lines = 0
38 shown_matching_lines = 0
39 lines_of_interest = set()
39 lines_of_interest = set()
40 for line_number in matching_lines:
40 for line_number in matching_lines:
41 if len(lines_of_interest) < max_lines:
41 if len(lines_of_interest) < max_lines:
42 lines_of_interest |= set(range(
42 lines_of_interest |= set(range(
43 max(line_number - line_context, 0),
43 max(line_number - line_context, 0),
44 min(line_number + line_context, total_lines + 1)))
44 min(line_number + line_context, total_lines + 1)))
45 shown_matching_lines += 1
45 shown_matching_lines += 1
46 lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
46 lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
47
47
48 html_formatter = h.SearchContentCodeHtmlFormatter(
48 html_formatter = h.SearchContentCodeHtmlFormatter(
49 linenos=True,
49 linenos=True,
50 cssclass="code-highlight",
50 cssclass="code-highlight",
51 url=match_file_url,
51 url=match_file_url,
52 query_terms=terms,
52 query_terms=terms,
53 only_line_numbers=lines_of_interest
53 only_line_numbers=lines_of_interest
54 )
54 )
55
55
56 has_matched_content = len(lines_of_interest) >= 1
56 has_matched_content = len(lines_of_interest) >= 1
57
57
58 %>
58 %>
59 ## search results are additionally filtered, and this check is just a safe gate
59 ## search results are additionally filtered, and this check is just a safe gate
60 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
60 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
61 <div class="codeblock">
61 <div class="codeblock">
62 <h1>
62 <h1>
63 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
63 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
64 ${search.repo_icon(repo_type)}
64 ${search.repo_icon(repo_type)}
65 ${h.link_to(entry['repository'], h.route_path('repo_summary', repo_name=entry['repository']))}
65 ${h.link_to(entry['repository'], h.route_path('repo_summary', repo_name=entry['repository']))}
66 </h1>
66 </h1>
67
67
68 <div class="codeblock-header">
68 <div class="codeblock-header">
69
69
70 <div class="file-filename">
70 <div class="file-filename">
71 <i class="icon-file"></i> ${entry['f_path'].split('/')[-1]}
71 <i class="icon-file"></i> ${entry['f_path'].split('/')[-1]}
72 </div>
72 </div>
73
73
74 <div class="file-stats">
74 <div class="file-stats">
75 <div class="stats-info">
75 <div class="stats-info">
76 <span class="stats-first-item">
76 <span class="stats-first-item">
77 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
77 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
78 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
78 (${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
79 </span>
79 </span>
80 <span>
80 <span>
81 % if entry.get('size'):
81 % if entry.get('size'):
82 | ${h.format_byte_size_binary(entry['size'])}
82 | ${h.format_byte_size_binary(entry['size'])}
83 % endif
83 % endif
84 </span>
84 </span>
85 <span>
85 <span>
86 % if entry.get('mimetype'):
86 % if entry.get('mimetype'):
87 | ${entry.get('mimetype', "unknown mimetype")}
87 | ${entry.get('mimetype', "unknown mimetype")}
88 % endif
88 % endif
89 </span>
89 </span>
90 </div>
90 </div>
91 </div>
91 </div>
92 </div>
92 </div>
93
93
94 <div class="path clear-fix">
94 <div class="path clear-fix">
95 <div class="pull-left">
95 <div class="pull-left">
96 ${h.files_breadcrumbs(entry['repository'],entry.get('commit_id', 'tip'),entry['f_path'], linkify_last_item=True)}
96 ${h.files_breadcrumbs(entry['repository'], entry.get('commit_id', 'tip'), entry['f_path'], linkify_last_item=True)}
97 </div>
97 </div>
98
98
99 <div class="pull-right stats">
99 <div class="pull-right stats">
100 ## <a id="file_history_overview_full" href="${h.route_path('repo_commits_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
100 ## <a id="file_history_overview_full" href="${h.route_path('repo_commits_file',repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
101 ## ${_('Show Full History')}
101 ## ${_('Show Full History')}
102 ## </a>
102 ## </a>
103 ## | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
103 ## | ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
104 ## | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
104 ## | ${h.link_to(_('Raw'), h.route_path('repo_file_raw', repo_name=entry.get('repository',''),commit_id=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
105 <div class="search-tags">
105 <div class="search-tags">
106
106
107 <% repo_group = entry.get('repository_group')%>
107 <% repo_group = entry.get('repository_group')%>
108 ## hiden if in repo group view
108 ## hiden if in repo group view
109 % if repo_group and not c.repo_group_name:
109 % if repo_group and not c.repo_group_name:
110 <span class="tag tag8">
110 <span class="tag tag8">
111 ${search.repo_group_icon()}
111 ${search.repo_group_icon()}
112 <a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
112 <a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
113 </span>
113 </span>
114 % endif
114 % endif
115 ## hiden if in repo view
115 ## hiden if in repo view
116 % if not c.repo_name:
116 % if not c.repo_name:
117 <span class="tag tag8">
117 <span class="tag tag8">
118 ${search.repo_icon(repo_type)}
118 ${search.repo_icon(repo_type)}
119 <a href="${h.route_path('search_repo', repo_name=entry.get('repo_name'), _query={'q': c.cur_query})}">${_('Narrow to this repository')}</a>
119 <a href="${h.route_path('search_repo', repo_name=entry.get('repo_name'), _query={'q': c.cur_query})}">${_('Narrow to this repository')}</a>
120 </span>
120 </span>
121 % endif
121 % endif
122 </div>
122 </div>
123
123
124 </div>
124 </div>
125 <div class="clear-fix"></div>
125 <div class="clear-fix"></div>
126 </div>
126 </div>
127
127
128
128
129 <div class="code-body search-code-body clear-fix">
129 <div class="code-body search-code-body clear-fix">
130 ${highlight_text_file(
130 ${highlight_text_file(
131 has_matched_content=has_matched_content,
131 has_matched_content=has_matched_content,
132 file_content=file_content,
132 file_content=file_content,
133 lexer=lexer,
133 lexer=lexer,
134 html_formatter=html_formatter,
134 html_formatter=html_formatter,
135 matching_lines=matching_lines,
135 matching_lines=matching_lines,
136 shown_matching_lines=shown_matching_lines,
136 shown_matching_lines=shown_matching_lines,
137 url=match_file_url,
137 url=match_file_url,
138 use_hl_filter=c.searcher.is_es_6
138 use_hl_filter=c.searcher.is_es_6
139 )}
139 )}
140 </div>
140 </div>
141
141
142 </div>
142 </div>
143 % endif
143 % endif
144 %endfor
144 %endfor
145 </div>
145 </div>
146 %if c.cur_query and c.formatted_results:
146 %if c.cur_query and c.formatted_results:
147 <div class="pagination-wh pagination-left" >
147 <div class="pagination-wh pagination-left" >
148 ${c.formatted_results.render()}
148 ${c.formatted_results.render()}
149 </div>
149 </div>
150 %endif
150 %endif
151
151
152 %if c.cur_query:
152 %if c.cur_query:
153 <script type="text/javascript">
153 <script type="text/javascript">
154 $(function(){
154 $(function(){
155 $(".search-code-body").mark(
155 $(".search-code-body").mark(
156 "${query_mark}",
156 "${query_mark}",
157 {
157 {
158 "className": 'match',
158 "className": 'match',
159 "accuracy": "complementary",
159 "accuracy": "complementary",
160 "ignorePunctuation": ":._(){}[]!'+=".split("")
160 "ignorePunctuation": ":._(){}[]!'+=".split("")
161 }
161 }
162 );
162 );
163 })
163 })
164 </script>
164 </script>
165 %endif
165 %endif
@@ -1,53 +1,53 b''
1 <%namespace name="search" file="/search/search.mako"/>
1 <%namespace name="search" file="/search/search.mako"/>
2
2
3 % if c.formatted_results:
3 % if c.formatted_results:
4
4
5 <table class="rctable search-results">
5 <table class="rctable search-results">
6 <tr>
6 <tr>
7 <th>${_('Repository')}</th>
7 <th>${_('Repository')}</th>
8 <th>
8 <th>
9 <a href="${search.field_sort('file')}">${_('File')}</a>
9 <a href="${search.field_sort('file')}">${_('File')}</a>
10 </th>
10 </th>
11 <th>
11 <th>
12 <a href="${search.field_sort('size')}">${_('Size')}</a>
12 <a href="${search.field_sort('size')}">${_('Size')}</a>
13 </th>
13 </th>
14 <th>
14 <th>
15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
16 </th>
16 </th>
17 </tr>
17 </tr>
18 %for entry in c.formatted_results:
18 %for entry in c.formatted_results:
19 ## search results are additionally filtered, and this check is just a safe gate
19 ## search results are additionally filtered, and this check is just a safe gate
20 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
20 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
21 <tr class="body">
21 <tr class="body">
22 <td class="td-componentname">
22 <td class="td-componentname">
23 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
23 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
24 ${search.repo_icon(repo_type)}
24 ${search.repo_icon(repo_type)}
25 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
25 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
26 </td>
26 </td>
27 <td class="td-componentname">
27 <td class="td-componentname">
28 <i class="icon-file"></i>
28 <i class="icon-file"></i>
29 ${h.link_to(h.literal(entry['f_path']),
29 ${h.link_to(h.literal(entry['f_path']),
30 h.route_path('repo_files',repo_name=entry['repository'],commit_id='tip',f_path=entry['f_path']))}
30 h.route_path('repo_files',repo_name=entry['repository'],commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
31 </td>
31 </td>
32 <td>
32 <td>
33 %if entry.get('size'):
33 %if entry.get('size'):
34 ${h.format_byte_size_binary(entry['size'])}
34 ${h.format_byte_size_binary(entry['size'])}
35 %endif
35 %endif
36 </td>
36 </td>
37 <td>
37 <td>
38 %if entry.get('lines'):
38 %if entry.get('lines'):
39 ${entry.get('lines', 0.)}
39 ${entry.get('lines', 0.)}
40 %endif
40 %endif
41 </td>
41 </td>
42 </tr>
42 </tr>
43 % endif
43 % endif
44 %endfor
44 %endfor
45 </table>
45 </table>
46
46
47 %if c.cur_query:
47 %if c.cur_query:
48 <div class="pagination-wh pagination-left">
48 <div class="pagination-wh pagination-left">
49 ${c.formatted_results.render()}
49 ${c.formatted_results.render()}
50 </div>
50 </div>
51 %endif
51 %endif
52
52
53 % endif
53 % endif
@@ -1,242 +1,241 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 copy
21 import copy
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.lib import helpers
25 from rhodecode.lib import helpers
26 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.lib.utils2 import AttributeDict
27 from rhodecode.model.settings import IssueTrackerSettingsModel
27 from rhodecode.model.settings import IssueTrackerSettingsModel
28 from rhodecode.tests import no_newline_id_generator
28 from rhodecode.tests import no_newline_id_generator
29
29
30
30
31 @pytest.mark.parametrize('url, expected_url', [
31 @pytest.mark.parametrize('url, expected_url', [
32 ('http://rc.com', '<a href="http://rc.com">http://rc.com</a>'),
32 ('http://rc.com', '<a href="http://rc.com">http://rc.com</a>'),
33 ('http://rc.com/test', '<a href="http://rc.com/test">http://rc.com/test</a>'),
33 ('http://rc.com/test', '<a href="http://rc.com/test">http://rc.com/test</a>'),
34 ('http://rc.com/!foo', '<a href="http://rc.com/!foo">http://rc.com/!foo</a>'),
34 ('http://rc.com/!foo', '<a href="http://rc.com/!foo">http://rc.com/!foo</a>'),
35 ('http://rc.com/&foo', '<a href="http://rc.com/&amp;foo">http://rc.com/&amp;foo</a>'),
35 ('http://rc.com/&foo', '<a href="http://rc.com/&amp;foo">http://rc.com/&amp;foo</a>'),
36 ('http://rc.com/?foo-1&bar=1', '<a href="http://rc.com/?foo-1&amp;bar=1">http://rc.com/?foo-1&amp;bar=1</a>'),
36 ('http://rc.com/?foo-1&bar=1', '<a href="http://rc.com/?foo-1&amp;bar=1">http://rc.com/?foo-1&amp;bar=1</a>'),
37 ('http://rc.com?foo-1&bar=1', '<a href="http://rc.com?foo-1&amp;bar=1">http://rc.com?foo-1&amp;bar=1</a>'),
37 ('http://rc.com?foo-1&bar=1', '<a href="http://rc.com?foo-1&amp;bar=1">http://rc.com?foo-1&amp;bar=1</a>'),
38 ('http://rc.com/#foo', '<a href="http://rc.com/#foo">http://rc.com/#foo</a>'),
38 ('http://rc.com/#foo', '<a href="http://rc.com/#foo">http://rc.com/#foo</a>'),
39 ('http://rc.com/@foo', '<a href="http://rc.com/@foo">http://rc.com/@foo</a>'),
39 ('http://rc.com/@foo', '<a href="http://rc.com/@foo">http://rc.com/@foo</a>'),
40 ])
40 ])
41 def test_urlify_text(url, expected_url):
41 def test_urlify_text(url, expected_url):
42 assert helpers.urlify_text(url) == expected_url
42 assert helpers.urlify_text(url) == expected_url
43
43
44
44
45 @pytest.mark.parametrize('repo_name, commit_id, path, expected_result', [
45 @pytest.mark.parametrize('repo_name, commit_id, path, expected_result', [
46 # Simple case 1
46 # Simple case 1
47 ('repo', 'commit', 'a/b',
47 ('repo', 'commit', 'a/b',
48 '<a href="/repo/files/commit/"><i class="icon-home"></i></a>'
48 '<a href="/repo/files/commit/"><i class="icon-home"></i></a>'
49 ' / '
49 ' / '
50 '<a href="/repo/files/commit/a">a</a>'
50 '<a href="/repo/files/commit/a">a</a>'
51 ' / '
51 ' / '
52 'b'),
52 'b'),
53
53
54 # Simple case
54 # Simple case
55 ('rX<X', 'cX<X', 'pX<X/aX<X/bX<X',
55 ('rX<X', 'cX<X', 'pX<X/aX<X/bX<X',
56 '<a href="/rX%3CX/files/cX%3CX/"><i class="icon-home"></i></a>'
56 '<a href="/rX%3CX/files/cX%3CX/"><i class="icon-home"></i></a>'
57 ' / '
57 ' / '
58 '<a href="/rX%3CX/files/cX%3CX/pX%3CX">pX&lt;X</a>'
58 '<a href="/rX%3CX/files/cX%3CX/pX%3CX">pX&lt;X</a>'
59 ' / '
59 ' / '
60 '<a href="/rX%3CX/files/cX%3CX/pX%3CX/aX%3CX">aX&lt;X</a>'
60 '<a href="/rX%3CX/files/cX%3CX/pX%3CX/aX%3CX">aX&lt;X</a>'
61 ' / '
61 ' / '
62 'bX&lt;X'),
62 'bX&lt;X'),
63
63
64 # Path with only one segment
64 # Path with only one segment
65 ('rX<X', 'cX<X', 'pX<X',
65 ('rX<X', 'cX<X', 'pX<X',
66 '<a href="/rX%3CX/files/cX%3CX/"><i class="icon-home"></i></a>'
66 '<a href="/rX%3CX/files/cX%3CX/"><i class="icon-home"></i></a>'
67 ' / '
67 ' / '
68 'pX&lt;X'),
68 'pX&lt;X'),
69
69
70 # Empty path
70 # Empty path
71 ('rX<X', 'cX<X', '',
71 ('rX<X', 'cX<X', '',
72 '<i class="icon-home"></i>'),
72 '<i class="icon-home"></i>'),
73
73
74 # simple quote
74 # simple quote
75 ('rX"X', 'cX"X', 'pX"X/aX"X/bX"X',
75 ('rX"X', 'cX"X', 'pX"X/aX"X/bX"X',
76 '<a href="/rX%22X/files/cX%22X/"><i class="icon-home"></i></a>'
76 '<a href="/rX%22X/files/cX%22X/"><i class="icon-home"></i></a>'
77 ' / '
77 ' / '
78 '<a href="/rX%22X/files/cX%22X/pX%22X">pX&#34;X</a>'
78 '<a href="/rX%22X/files/cX%22X/pX%22X">pX&#34;X</a>'
79 ' / '
79 ' / '
80 '<a href="/rX%22X/files/cX%22X/pX%22X/aX%22X">aX&#34;X</a>'
80 '<a href="/rX%22X/files/cX%22X/pX%22X/aX%22X">aX&#34;X</a>'
81 ' / '
81 ' / '
82 'bX&#34;X'),
82 'bX&#34;X'),
83
83
84 ], ids=['simple1', 'simple2', 'one_segment', 'empty_path', 'simple_quote'])
84 ], ids=['simple1', 'simple2', 'one_segment', 'empty_path', 'simple_quote'])
85 def test_files_breadcrumbs_xss(
85 def test_files_breadcrumbs_xss(repo_name, commit_id, path, app, expected_result):
86 repo_name, commit_id, path, app, expected_result):
86 result = helpers.files_breadcrumbs(repo_name, 'hg', commit_id, path)
87 result = helpers.files_breadcrumbs(repo_name, commit_id, path)
88 # Expect it to encode all path fragments properly. This is important
87 # Expect it to encode all path fragments properly. This is important
89 # because it returns an instance of `literal`.
88 # because it returns an instance of `literal`.
90 if path != '':
89 if path != '':
91 expected_result = expected_result + helpers.files_icon.format(helpers.escape(path))
90 expected_result = expected_result + helpers.files_icon.format(helpers.escape(path))
92 assert result == expected_result
91 assert result == expected_result
93
92
94
93
95 def test_format_binary():
94 def test_format_binary():
96 assert helpers.format_byte_size_binary(298489462784) == '278.0 GiB'
95 assert helpers.format_byte_size_binary(298489462784) == '278.0 GiB'
97
96
98
97
99 @pytest.mark.parametrize('text_string, pattern, expected', [
98 @pytest.mark.parametrize('text_string, pattern, expected', [
100 ('No issue here', '(?:#)(?P<issue_id>\d+)', []),
99 ('No issue here', '(?:#)(?P<issue_id>\d+)', []),
101 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
100 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
102 [{'url': 'http://r.io/{repo}/i/42', 'id': '42'}]),
101 [{'url': 'http://r.io/{repo}/i/42', 'id': '42'}]),
103 ('Fix #42, #53', '(?:#)(?P<issue_id>\d+)', [
102 ('Fix #42, #53', '(?:#)(?P<issue_id>\d+)', [
104 {'url': 'http://r.io/{repo}/i/42', 'id': '42'},
103 {'url': 'http://r.io/{repo}/i/42', 'id': '42'},
105 {'url': 'http://r.io/{repo}/i/53', 'id': '53'}]),
104 {'url': 'http://r.io/{repo}/i/53', 'id': '53'}]),
106 ('Fix #42', '(?:#)?<issue_id>\d+)', []), # Broken regex
105 ('Fix #42', '(?:#)?<issue_id>\d+)', []), # Broken regex
107 ])
106 ])
108 def test_extract_issues(backend, text_string, pattern, expected):
107 def test_extract_issues(backend, text_string, pattern, expected):
109 repo = backend.create_repo()
108 repo = backend.create_repo()
110 config = {
109 config = {
111 '123': {
110 '123': {
112 'uid': '123',
111 'uid': '123',
113 'pat': pattern,
112 'pat': pattern,
114 'url': 'http://r.io/${repo}/i/${issue_id}',
113 'url': 'http://r.io/${repo}/i/${issue_id}',
115 'pref': '#',
114 'pref': '#',
116 'desc': 'Test Pattern'
115 'desc': 'Test Pattern'
117 }
116 }
118 }
117 }
119
118
120 def get_settings_mock(self, cache=True):
119 def get_settings_mock(self, cache=True):
121 return config
120 return config
122
121
123 with mock.patch.object(IssueTrackerSettingsModel,
122 with mock.patch.object(IssueTrackerSettingsModel,
124 'get_settings', get_settings_mock):
123 'get_settings', get_settings_mock):
125 text, issues = helpers.process_patterns(text_string, repo.repo_name)
124 text, issues = helpers.process_patterns(text_string, repo.repo_name)
126
125
127 expected = copy.deepcopy(expected)
126 expected = copy.deepcopy(expected)
128 for item in expected:
127 for item in expected:
129 item['url'] = item['url'].format(repo=repo.repo_name)
128 item['url'] = item['url'].format(repo=repo.repo_name)
130
129
131 assert issues == expected
130 assert issues == expected
132
131
133
132
134 @pytest.mark.parametrize('text_string, pattern, link_format, expected_text', [
133 @pytest.mark.parametrize('text_string, pattern, link_format, expected_text', [
135 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'html',
134 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'html',
136 'Fix <a class="tooltip issue-tracker-link" href="http://r.io/{repo}/i/42" title="Test Pattern">#42</a>'),
135 'Fix <a class="tooltip issue-tracker-link" href="http://r.io/{repo}/i/42" title="Test Pattern">#42</a>'),
137
136
138 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'markdown',
137 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'markdown',
139 'Fix [#42](http://r.io/{repo}/i/42)'),
138 'Fix [#42](http://r.io/{repo}/i/42)'),
140
139
141 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'rst',
140 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'rst',
142 'Fix `#42 <http://r.io/{repo}/i/42>`_'),
141 'Fix `#42 <http://r.io/{repo}/i/42>`_'),
143
142
144 ('Fix #42', '(?:#)?<issue_id>\d+)', 'html',
143 ('Fix #42', '(?:#)?<issue_id>\d+)', 'html',
145 'Fix #42'), # Broken regex
144 'Fix #42'), # Broken regex
146 ])
145 ])
147 def test_process_patterns_repo(backend, text_string, pattern, expected_text, link_format):
146 def test_process_patterns_repo(backend, text_string, pattern, expected_text, link_format):
148 repo = backend.create_repo()
147 repo = backend.create_repo()
149
148
150 def get_settings_mock(self, cache=True):
149 def get_settings_mock(self, cache=True):
151 return {
150 return {
152 '123': {
151 '123': {
153 'uid': '123',
152 'uid': '123',
154 'pat': pattern,
153 'pat': pattern,
155 'url': 'http://r.io/${repo}/i/${issue_id}',
154 'url': 'http://r.io/${repo}/i/${issue_id}',
156 'pref': '#',
155 'pref': '#',
157 'desc': 'Test Pattern'
156 'desc': 'Test Pattern'
158 }
157 }
159 }
158 }
160
159
161 with mock.patch.object(IssueTrackerSettingsModel,
160 with mock.patch.object(IssueTrackerSettingsModel,
162 'get_settings', get_settings_mock):
161 'get_settings', get_settings_mock):
163 processed_text, issues = helpers.process_patterns(
162 processed_text, issues = helpers.process_patterns(
164 text_string, repo.repo_name, link_format)
163 text_string, repo.repo_name, link_format)
165
164
166 assert processed_text == expected_text.format(repo=repo.repo_name)
165 assert processed_text == expected_text.format(repo=repo.repo_name)
167
166
168
167
169 @pytest.mark.parametrize('text_string, pattern, expected_text', [
168 @pytest.mark.parametrize('text_string, pattern, expected_text', [
170 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
169 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
171 'Fix <a class="tooltip issue-tracker-link" href="http://r.io/i/42" title="Test Pattern">#42</a>'),
170 'Fix <a class="tooltip issue-tracker-link" href="http://r.io/i/42" title="Test Pattern">#42</a>'),
172 ('Fix #42', '(?:#)?<issue_id>\d+)',
171 ('Fix #42', '(?:#)?<issue_id>\d+)',
173 'Fix #42'), # Broken regex
172 'Fix #42'), # Broken regex
174 ])
173 ])
175 def test_process_patterns_no_repo(text_string, pattern, expected_text):
174 def test_process_patterns_no_repo(text_string, pattern, expected_text):
176
175
177 def get_settings_mock(self, cache=True):
176 def get_settings_mock(self, cache=True):
178 return {
177 return {
179 '123': {
178 '123': {
180 'uid': '123',
179 'uid': '123',
181 'pat': pattern,
180 'pat': pattern,
182 'url': 'http://r.io/i/${issue_id}',
181 'url': 'http://r.io/i/${issue_id}',
183 'pref': '#',
182 'pref': '#',
184 'desc': 'Test Pattern'
183 'desc': 'Test Pattern'
185 }
184 }
186 }
185 }
187
186
188 with mock.patch.object(IssueTrackerSettingsModel,
187 with mock.patch.object(IssueTrackerSettingsModel,
189 'get_global_settings', get_settings_mock):
188 'get_global_settings', get_settings_mock):
190 processed_text, issues = helpers.process_patterns(
189 processed_text, issues = helpers.process_patterns(
191 text_string, '')
190 text_string, '')
192
191
193 assert processed_text == expected_text
192 assert processed_text == expected_text
194
193
195
194
196 def test_process_patterns_non_existent_repo_name(backend):
195 def test_process_patterns_non_existent_repo_name(backend):
197 text_string = 'Fix #42'
196 text_string = 'Fix #42'
198 pattern = '(?:#)(?P<issue_id>\d+)'
197 pattern = '(?:#)(?P<issue_id>\d+)'
199 expected_text = ('Fix <a class="tooltip issue-tracker-link" '
198 expected_text = ('Fix <a class="tooltip issue-tracker-link" '
200 'href="http://r.io/do-not-exist/i/42" title="Test Pattern">#42</a>')
199 'href="http://r.io/do-not-exist/i/42" title="Test Pattern">#42</a>')
201
200
202 def get_settings_mock(self, cache=True):
201 def get_settings_mock(self, cache=True):
203 return {
202 return {
204 '123': {
203 '123': {
205 'uid': '123',
204 'uid': '123',
206 'pat': pattern,
205 'pat': pattern,
207 'url': 'http://r.io/${repo}/i/${issue_id}',
206 'url': 'http://r.io/${repo}/i/${issue_id}',
208 'pref': '#',
207 'pref': '#',
209 'desc': 'Test Pattern'
208 'desc': 'Test Pattern'
210 }
209 }
211 }
210 }
212
211
213 with mock.patch.object(IssueTrackerSettingsModel,
212 with mock.patch.object(IssueTrackerSettingsModel,
214 'get_global_settings', get_settings_mock):
213 'get_global_settings', get_settings_mock):
215 processed_text, issues = helpers.process_patterns(
214 processed_text, issues = helpers.process_patterns(
216 text_string, 'do-not-exist')
215 text_string, 'do-not-exist')
217
216
218 assert processed_text == expected_text
217 assert processed_text == expected_text
219
218
220
219
221 def test_get_visual_attr(baseapp):
220 def test_get_visual_attr(baseapp):
222 from rhodecode.apps._base import TemplateArgs
221 from rhodecode.apps._base import TemplateArgs
223 c = TemplateArgs()
222 c = TemplateArgs()
224 assert None is helpers.get_visual_attr(c, 'fakse')
223 assert None is helpers.get_visual_attr(c, 'fakse')
225
224
226 # emulate the c.visual behaviour
225 # emulate the c.visual behaviour
227 c.visual = AttributeDict({})
226 c.visual = AttributeDict({})
228 assert None is helpers.get_visual_attr(c, 'some_var')
227 assert None is helpers.get_visual_attr(c, 'some_var')
229
228
230 c.visual.some_var = 'foobar'
229 c.visual.some_var = 'foobar'
231 assert 'foobar' == helpers.get_visual_attr(c, 'some_var')
230 assert 'foobar' == helpers.get_visual_attr(c, 'some_var')
232
231
233
232
234 @pytest.mark.parametrize('test_text, inclusive, expected_text', [
233 @pytest.mark.parametrize('test_text, inclusive, expected_text', [
235 ('just a string', False, 'just a string'),
234 ('just a string', False, 'just a string'),
236 ('just a string\n', False, 'just a string'),
235 ('just a string\n', False, 'just a string'),
237 ('just a string\n next line', False, 'just a string...'),
236 ('just a string\n next line', False, 'just a string...'),
238 ('just a string\n next line', True, 'just a string\n...'),
237 ('just a string\n next line', True, 'just a string\n...'),
239 ], ids=no_newline_id_generator)
238 ], ids=no_newline_id_generator)
240 def test_chop_at(test_text, inclusive, expected_text):
239 def test_chop_at(test_text, inclusive, expected_text):
241 assert helpers.chop_at_smart(
240 assert helpers.chop_at_smart(
242 test_text, '\n', inclusive, '...') == expected_text
241 test_text, '\n', inclusive, '...') == expected_text
General Comments 0
You need to be logged in to leave comments. Login now