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