##// END OF EJS Templates
ui: new file tree switcher...
marcink -
r3655:6431a1ec new-ui
parent child Browse files
Show More
@@ -1,1395 +1,1393 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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
29 29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 30 from pyramid.view import view_config
31 31 from pyramid.renderers import render
32 32 from pyramid.response import Response
33 33
34 34 import rhodecode
35 35 from rhodecode.apps._base import RepoAppView
36 36
37 37
38 38 from rhodecode.lib import diffs, helpers as h, rc_cache
39 39 from rhodecode.lib import audit_logger
40 40 from rhodecode.lib.view_utils import parse_path_ref
41 41 from rhodecode.lib.exceptions import NonRelativePathError
42 42 from rhodecode.lib.codeblocks import (
43 43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 44 from rhodecode.lib.utils2 import (
45 45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
46 46 from rhodecode.lib.auth import (
47 47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 48 from rhodecode.lib.vcs import path as vcspath
49 49 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 50 from rhodecode.lib.vcs.conf import settings
51 51 from rhodecode.lib.vcs.nodes import FileNode
52 52 from rhodecode.lib.vcs.exceptions import (
53 53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 55 NodeDoesNotExistError, CommitError, NodeError)
56 56
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.db import Repository
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class RepoFilesView(RepoAppView):
64 64
65 65 @staticmethod
66 66 def adjust_file_path_for_svn(f_path, repo):
67 67 """
68 68 Computes the relative path of `f_path`.
69 69
70 70 This is mainly based on prefix matching of the recognized tags and
71 71 branches in the underlying repository.
72 72 """
73 73 tags_and_branches = itertools.chain(
74 74 repo.branches.iterkeys(),
75 75 repo.tags.iterkeys())
76 76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77 77
78 78 for name in tags_and_branches:
79 79 if f_path.startswith('{}/'.format(name)):
80 80 f_path = vcspath.relpath(f_path, name)
81 81 break
82 82 return f_path
83 83
84 84 def load_default_context(self):
85 85 c = self._get_local_tmpl_context(include_app_defaults=True)
86 86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 87 c.enable_downloads = self.db_repo.enable_downloads
88 88 return c
89 89
90 90 def _ensure_not_locked(self):
91 91 _ = self.request.translate
92 92
93 93 repo = self.db_repo
94 94 if repo.enable_locking and repo.locked[0]:
95 95 h.flash(_('This repository has been locked by %s on %s')
96 96 % (h.person_by_id(repo.locked[0]),
97 97 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 98 'warning')
99 99 files_url = h.route_path(
100 100 'repo_files:default_path',
101 101 repo_name=self.db_repo_name, commit_id='tip')
102 102 raise HTTPFound(files_url)
103 103
104 104 def check_branch_permission(self, branch_name):
105 105 _ = self.request.translate
106 106
107 107 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
108 108 self.db_repo_name, branch_name)
109 109 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
110 110 h.flash(
111 111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
112 112 'warning')
113 113 files_url = h.route_path(
114 114 'repo_files:default_path',
115 115 repo_name=self.db_repo_name, commit_id='tip')
116 116 raise HTTPFound(files_url)
117 117
118 118 def _get_commit_and_path(self):
119 119 default_commit_id = self.db_repo.landing_rev[1]
120 120 default_f_path = '/'
121 121
122 122 commit_id = self.request.matchdict.get(
123 123 'commit_id', default_commit_id)
124 124 f_path = self._get_f_path(self.request.matchdict, default_f_path)
125 125 return commit_id, f_path
126 126
127 127 def _get_default_encoding(self, c):
128 128 enc_list = getattr(c, 'default_encodings', [])
129 129 return enc_list[0] if enc_list else 'UTF-8'
130 130
131 131 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
132 132 """
133 133 This is a safe way to get commit. If an error occurs it redirects to
134 134 tip with proper message
135 135
136 136 :param commit_id: id of commit to fetch
137 137 :param redirect_after: toggle redirection
138 138 """
139 139 _ = self.request.translate
140 140
141 141 try:
142 142 return self.rhodecode_vcs_repo.get_commit(commit_id)
143 143 except EmptyRepositoryError:
144 144 if not redirect_after:
145 145 return None
146 146
147 147 _url = h.route_path(
148 148 'repo_files_add_file',
149 149 repo_name=self.db_repo_name, commit_id=0, f_path='',
150 150 _anchor='edit')
151 151
152 152 if h.HasRepoPermissionAny(
153 153 'repository.write', 'repository.admin')(self.db_repo_name):
154 154 add_new = h.link_to(
155 155 _('Click here to add a new file.'), _url, class_="alert-link")
156 156 else:
157 157 add_new = ""
158 158
159 159 h.flash(h.literal(
160 160 _('There are no files yet. %s') % add_new), category='warning')
161 161 raise HTTPFound(
162 162 h.route_path('repo_summary', repo_name=self.db_repo_name))
163 163
164 164 except (CommitDoesNotExistError, LookupError):
165 165 msg = _('No such commit exists for this repository')
166 166 h.flash(msg, category='error')
167 167 raise HTTPNotFound()
168 168 except RepositoryError as e:
169 169 h.flash(safe_str(h.escape(e)), category='error')
170 170 raise HTTPNotFound()
171 171
172 172 def _get_filenode_or_redirect(self, commit_obj, path):
173 173 """
174 174 Returns file_node, if error occurs or given path is directory,
175 175 it'll redirect to top level path
176 176 """
177 177 _ = self.request.translate
178 178
179 179 try:
180 180 file_node = commit_obj.get_node(path)
181 181 if file_node.is_dir():
182 182 raise RepositoryError('The given path is a directory')
183 183 except CommitDoesNotExistError:
184 184 log.exception('No such commit exists for this repository')
185 185 h.flash(_('No such commit exists for this repository'), category='error')
186 186 raise HTTPNotFound()
187 187 except RepositoryError as e:
188 188 log.warning('Repository error while fetching '
189 189 'filenode `%s`. Err:%s', path, e)
190 190 h.flash(safe_str(h.escape(e)), category='error')
191 191 raise HTTPNotFound()
192 192
193 193 return file_node
194 194
195 195 def _is_valid_head(self, commit_id, repo):
196 196 branch_name = sha_commit_id = ''
197 197 is_head = False
198 198
199 199 if h.is_svn(repo) and not repo.is_empty():
200 200 # Note: Subversion only has one head.
201 201 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
202 202 is_head = True
203 203 return branch_name, sha_commit_id, is_head
204 204
205 205 for _branch_name, branch_commit_id in repo.branches.items():
206 206 # simple case we pass in branch name, it's a HEAD
207 207 if commit_id == _branch_name:
208 208 is_head = True
209 209 branch_name = _branch_name
210 210 sha_commit_id = branch_commit_id
211 211 break
212 212 # case when we pass in full sha commit_id, which is a head
213 213 elif commit_id == branch_commit_id:
214 214 is_head = True
215 215 branch_name = _branch_name
216 216 sha_commit_id = branch_commit_id
217 217 break
218 218
219 219 # checked branches, means we only need to try to get the branch/commit_sha
220 220 if not repo.is_empty:
221 221 commit = repo.get_commit(commit_id=commit_id)
222 222 if commit:
223 223 branch_name = commit.branch
224 224 sha_commit_id = commit.raw_id
225 225
226 226 return branch_name, sha_commit_id, is_head
227 227
228 def _get_tree_at_commit(
229 self, c, commit_id, f_path, full_load=False):
228 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
230 229
231 230 repo_id = self.db_repo.repo_id
232 231 force_recache = self.get_recache_flag()
233 232
234 233 cache_seconds = safe_int(
235 234 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
236 235 cache_on = not force_recache and cache_seconds > 0
237 236 log.debug(
238 237 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
239 238 'with caching: %s[TTL: %ss]' % (
240 239 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
241 240
242 241 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
243 242 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
244 243
245 244 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
246 245 condition=cache_on)
247 246 def compute_file_tree(repo_id, commit_id, f_path, full_load):
248 247 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
249 248 repo_id, commit_id, f_path)
250 249
251 250 c.full_load = full_load
252 251 return render(
253 252 'rhodecode:templates/files/files_browser_tree.mako',
254 253 self._get_template_context(c), self.request)
255 254
256 255 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
257 256
258 257 def _get_archive_spec(self, fname):
259 258 log.debug('Detecting archive spec for: `%s`', fname)
260 259
261 260 fileformat = None
262 261 ext = None
263 262 content_type = None
264 263 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
265 264 content_type, extension = ext_data
266 265
267 266 if fname.endswith(extension):
268 267 fileformat = a_type
269 268 log.debug('archive is of type: %s', fileformat)
270 269 ext = extension
271 270 break
272 271
273 272 if not fileformat:
274 273 raise ValueError()
275 274
276 275 # left over part of whole fname is the commit
277 276 commit_id = fname[:-len(ext)]
278 277
279 278 return commit_id, ext, fileformat, content_type
280 279
281 280 @LoginRequired()
282 281 @HasRepoPermissionAnyDecorator(
283 282 'repository.read', 'repository.write', 'repository.admin')
284 283 @view_config(
285 284 route_name='repo_archivefile', request_method='GET',
286 285 renderer=None)
287 286 def repo_archivefile(self):
288 287 # archive cache config
289 288 from rhodecode import CONFIG
290 289 _ = self.request.translate
291 290 self.load_default_context()
292 291
293 292 fname = self.request.matchdict['fname']
294 293 subrepos = self.request.GET.get('subrepos') == 'true'
295 294
296 295 if not self.db_repo.enable_downloads:
297 296 return Response(_('Downloads disabled'))
298 297
299 298 try:
300 299 commit_id, ext, fileformat, content_type = \
301 300 self._get_archive_spec(fname)
302 301 except ValueError:
303 302 return Response(_('Unknown archive type for: `{}`').format(
304 303 h.escape(fname)))
305 304
306 305 try:
307 306 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
308 307 except CommitDoesNotExistError:
309 308 return Response(_('Unknown commit_id {}').format(
310 309 h.escape(commit_id)))
311 310 except EmptyRepositoryError:
312 311 return Response(_('Empty repository'))
313 312
314 313 archive_name = '%s-%s%s%s' % (
315 314 safe_str(self.db_repo_name.replace('/', '_')),
316 315 '-sub' if subrepos else '',
317 316 safe_str(commit.short_id), ext)
318 317
319 318 use_cached_archive = False
320 319 archive_cache_enabled = CONFIG.get(
321 320 'archive_cache_dir') and not self.request.GET.get('no_cache')
322 321 cached_archive_path = None
323 322
324 323 if archive_cache_enabled:
325 324 # check if we it's ok to write
326 325 if not os.path.isdir(CONFIG['archive_cache_dir']):
327 326 os.makedirs(CONFIG['archive_cache_dir'])
328 327 cached_archive_path = os.path.join(
329 328 CONFIG['archive_cache_dir'], archive_name)
330 329 if os.path.isfile(cached_archive_path):
331 330 log.debug('Found cached archive in %s', cached_archive_path)
332 331 fd, archive = None, cached_archive_path
333 332 use_cached_archive = True
334 333 else:
335 334 log.debug('Archive %s is not yet cached', archive_name)
336 335
337 336 if not use_cached_archive:
338 337 # generate new archive
339 338 fd, archive = tempfile.mkstemp()
340 339 log.debug('Creating new temp archive in %s', archive)
341 340 try:
342 341 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
343 342 except ImproperArchiveTypeError:
344 343 return _('Unknown archive type')
345 344 if archive_cache_enabled:
346 345 # if we generated the archive and we have cache enabled
347 346 # let's use this for future
348 347 log.debug('Storing new archive in %s', cached_archive_path)
349 348 shutil.move(archive, cached_archive_path)
350 349 archive = cached_archive_path
351 350
352 351 # store download action
353 352 audit_logger.store_web(
354 353 'repo.archive.download', action_data={
355 354 'user_agent': self.request.user_agent,
356 355 'archive_name': archive_name,
357 356 'archive_spec': fname,
358 357 'archive_cached': use_cached_archive},
359 358 user=self._rhodecode_user,
360 359 repo=self.db_repo,
361 360 commit=True
362 361 )
363 362
364 363 def get_chunked_archive(archive_path):
365 364 with open(archive_path, 'rb') as stream:
366 365 while True:
367 366 data = stream.read(16 * 1024)
368 367 if not data:
369 368 if fd: # fd means we used temporary file
370 369 os.close(fd)
371 370 if not archive_cache_enabled:
372 371 log.debug('Destroying temp archive %s', archive_path)
373 372 os.remove(archive_path)
374 373 break
375 374 yield data
376 375
377 376 response = Response(app_iter=get_chunked_archive(archive))
378 377 response.content_disposition = str(
379 378 'attachment; filename=%s' % archive_name)
380 379 response.content_type = str(content_type)
381 380
382 381 return response
383 382
384 383 def _get_file_node(self, commit_id, f_path):
385 384 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
386 385 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
387 386 try:
388 387 node = commit.get_node(f_path)
389 388 if node.is_dir():
390 389 raise NodeError('%s path is a %s not a file'
391 390 % (node, type(node)))
392 391 except NodeDoesNotExistError:
393 392 commit = EmptyCommit(
394 393 commit_id=commit_id,
395 394 idx=commit.idx,
396 395 repo=commit.repository,
397 396 alias=commit.repository.alias,
398 397 message=commit.message,
399 398 author=commit.author,
400 399 date=commit.date)
401 400 node = FileNode(f_path, '', commit=commit)
402 401 else:
403 402 commit = EmptyCommit(
404 403 repo=self.rhodecode_vcs_repo,
405 404 alias=self.rhodecode_vcs_repo.alias)
406 405 node = FileNode(f_path, '', commit=commit)
407 406 return node
408 407
409 408 @LoginRequired()
410 409 @HasRepoPermissionAnyDecorator(
411 410 'repository.read', 'repository.write', 'repository.admin')
412 411 @view_config(
413 412 route_name='repo_files_diff', request_method='GET',
414 413 renderer=None)
415 414 def repo_files_diff(self):
416 415 c = self.load_default_context()
417 416 f_path = self._get_f_path(self.request.matchdict)
418 417 diff1 = self.request.GET.get('diff1', '')
419 418 diff2 = self.request.GET.get('diff2', '')
420 419
421 420 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
422 421
423 422 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
424 423 line_context = self.request.GET.get('context', 3)
425 424
426 425 if not any((diff1, diff2)):
427 426 h.flash(
428 427 'Need query parameter "diff1" or "diff2" to generate a diff.',
429 428 category='error')
430 429 raise HTTPBadRequest()
431 430
432 431 c.action = self.request.GET.get('diff')
433 432 if c.action not in ['download', 'raw']:
434 433 compare_url = h.route_path(
435 434 'repo_compare',
436 435 repo_name=self.db_repo_name,
437 436 source_ref_type='rev',
438 437 source_ref=diff1,
439 438 target_repo=self.db_repo_name,
440 439 target_ref_type='rev',
441 440 target_ref=diff2,
442 441 _query=dict(f_path=f_path))
443 442 # redirect to new view if we render diff
444 443 raise HTTPFound(compare_url)
445 444
446 445 try:
447 446 node1 = self._get_file_node(diff1, path1)
448 447 node2 = self._get_file_node(diff2, f_path)
449 448 except (RepositoryError, NodeError):
450 449 log.exception("Exception while trying to get node from repository")
451 450 raise HTTPFound(
452 451 h.route_path('repo_files', repo_name=self.db_repo_name,
453 452 commit_id='tip', f_path=f_path))
454 453
455 454 if all(isinstance(node.commit, EmptyCommit)
456 455 for node in (node1, node2)):
457 456 raise HTTPNotFound()
458 457
459 458 c.commit_1 = node1.commit
460 459 c.commit_2 = node2.commit
461 460
462 461 if c.action == 'download':
463 462 _diff = diffs.get_gitdiff(node1, node2,
464 463 ignore_whitespace=ignore_whitespace,
465 464 context=line_context)
466 465 diff = diffs.DiffProcessor(_diff, format='gitdiff')
467 466
468 467 response = Response(self.path_filter.get_raw_patch(diff))
469 468 response.content_type = 'text/plain'
470 469 response.content_disposition = (
471 470 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
472 471 )
473 472 charset = self._get_default_encoding(c)
474 473 if charset:
475 474 response.charset = charset
476 475 return response
477 476
478 477 elif c.action == 'raw':
479 478 _diff = diffs.get_gitdiff(node1, node2,
480 479 ignore_whitespace=ignore_whitespace,
481 480 context=line_context)
482 481 diff = diffs.DiffProcessor(_diff, format='gitdiff')
483 482
484 483 response = Response(self.path_filter.get_raw_patch(diff))
485 484 response.content_type = 'text/plain'
486 485 charset = self._get_default_encoding(c)
487 486 if charset:
488 487 response.charset = charset
489 488 return response
490 489
491 490 # in case we ever end up here
492 491 raise HTTPNotFound()
493 492
494 493 @LoginRequired()
495 494 @HasRepoPermissionAnyDecorator(
496 495 'repository.read', 'repository.write', 'repository.admin')
497 496 @view_config(
498 497 route_name='repo_files_diff_2way_redirect', request_method='GET',
499 498 renderer=None)
500 499 def repo_files_diff_2way_redirect(self):
501 500 """
502 501 Kept only to make OLD links work
503 502 """
504 503 f_path = self._get_f_path_unchecked(self.request.matchdict)
505 504 diff1 = self.request.GET.get('diff1', '')
506 505 diff2 = self.request.GET.get('diff2', '')
507 506
508 507 if not any((diff1, diff2)):
509 508 h.flash(
510 509 'Need query parameter "diff1" or "diff2" to generate a diff.',
511 510 category='error')
512 511 raise HTTPBadRequest()
513 512
514 513 compare_url = h.route_path(
515 514 'repo_compare',
516 515 repo_name=self.db_repo_name,
517 516 source_ref_type='rev',
518 517 source_ref=diff1,
519 518 target_ref_type='rev',
520 519 target_ref=diff2,
521 520 _query=dict(f_path=f_path, diffmode='sideside',
522 521 target_repo=self.db_repo_name,))
523 522 raise HTTPFound(compare_url)
524 523
525 524 @LoginRequired()
526 525 @HasRepoPermissionAnyDecorator(
527 526 'repository.read', 'repository.write', 'repository.admin')
528 527 @view_config(
529 528 route_name='repo_files', request_method='GET',
530 529 renderer=None)
531 530 @view_config(
532 531 route_name='repo_files:default_path', request_method='GET',
533 532 renderer=None)
534 533 @view_config(
535 534 route_name='repo_files:default_commit', request_method='GET',
536 535 renderer=None)
537 536 @view_config(
538 537 route_name='repo_files:rendered', request_method='GET',
539 538 renderer=None)
540 539 @view_config(
541 540 route_name='repo_files:annotated', request_method='GET',
542 541 renderer=None)
543 542 def repo_files(self):
544 543 c = self.load_default_context()
545 544
546 545 view_name = getattr(self.request.matched_route, 'name', None)
547 546
548 547 c.annotate = view_name == 'repo_files:annotated'
549 548 # default is false, but .rst/.md files later are auto rendered, we can
550 549 # overwrite auto rendering by setting this GET flag
551 550 c.renderer = view_name == 'repo_files:rendered' or \
552 551 not self.request.GET.get('no-render', False)
553 552
554 553 # redirect to given commit_id from form if given
555 554 get_commit_id = self.request.GET.get('at_rev', None)
556 555 if get_commit_id:
557 556 self._get_commit_or_redirect(get_commit_id)
558 557
559 558 commit_id, f_path = self._get_commit_and_path()
560 559 c.commit = self._get_commit_or_redirect(commit_id)
561 560 c.branch = self.request.GET.get('branch', None)
562 561 c.f_path = f_path
563 562
564 563 # prev link
565 564 try:
566 565 prev_commit = c.commit.prev(c.branch)
567 566 c.prev_commit = prev_commit
568 567 c.url_prev = h.route_path(
569 568 'repo_files', repo_name=self.db_repo_name,
570 569 commit_id=prev_commit.raw_id, f_path=f_path)
571 570 if c.branch:
572 571 c.url_prev += '?branch=%s' % c.branch
573 572 except (CommitDoesNotExistError, VCSError):
574 573 c.url_prev = '#'
575 574 c.prev_commit = EmptyCommit()
576 575
577 576 # next link
578 577 try:
579 578 next_commit = c.commit.next(c.branch)
580 579 c.next_commit = next_commit
581 580 c.url_next = h.route_path(
582 581 'repo_files', repo_name=self.db_repo_name,
583 582 commit_id=next_commit.raw_id, f_path=f_path)
584 583 if c.branch:
585 584 c.url_next += '?branch=%s' % c.branch
586 585 except (CommitDoesNotExistError, VCSError):
587 586 c.url_next = '#'
588 587 c.next_commit = EmptyCommit()
589 588
590 589 # files or dirs
591 590 try:
592 591 c.file = c.commit.get_node(f_path)
593 592 c.file_author = True
594 593 c.file_tree = ''
595 594
596 595 # load file content
597 596 if c.file.is_file():
598 597 c.lf_node = c.file.get_largefile_node()
599 598
600 599 c.file_source_page = 'true'
601 600 c.file_last_commit = c.file.last_commit
602 601 if c.file.size < c.visual.cut_off_limit_diff:
603 602 if c.annotate: # annotation has precedence over renderer
604 603 c.annotated_lines = filenode_as_annotated_lines_tokens(
605 604 c.file
606 605 )
607 606 else:
608 607 c.renderer = (
609 608 c.renderer and h.renderer_from_filename(c.file.path)
610 609 )
611 610 if not c.renderer:
612 611 c.lines = filenode_as_lines_tokens(c.file)
613 612
614 613 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
615 614 commit_id, self.rhodecode_vcs_repo)
616 615 c.on_branch_head = is_head
617 616
618 617 branch = c.commit.branch if (
619 618 c.commit.branch and '/' not in c.commit.branch) else None
620 619 c.branch_or_raw_id = branch or c.commit.raw_id
621 620 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
622 621
623 622 author = c.file_last_commit.author
624 623 c.authors = [[
625 624 h.email(author),
626 625 h.person(author, 'username_or_name_or_email'),
627 626 1
628 627 ]]
629 628
630 629 else: # load tree content at path
631 630 c.file_source_page = 'false'
632 631 c.authors = []
633 632 # this loads a simple tree without metadata to speed things up
634 633 # later via ajax we call repo_nodetree_full and fetch whole
635 c.file_tree = self._get_tree_at_commit(
636 c, c.commit.raw_id, f_path)
634 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
637 635
638 636 except RepositoryError as e:
639 637 h.flash(safe_str(h.escape(e)), category='error')
640 638 raise HTTPNotFound()
641 639
642 640 if self.request.environ.get('HTTP_X_PJAX'):
643 641 html = render('rhodecode:templates/files/files_pjax.mako',
644 642 self._get_template_context(c), self.request)
645 643 else:
646 644 html = render('rhodecode:templates/files/files.mako',
647 645 self._get_template_context(c), self.request)
648 646 return Response(html)
649 647
650 648 @HasRepoPermissionAnyDecorator(
651 649 'repository.read', 'repository.write', 'repository.admin')
652 650 @view_config(
653 651 route_name='repo_files:annotated_previous', request_method='GET',
654 652 renderer=None)
655 653 def repo_files_annotated_previous(self):
656 654 self.load_default_context()
657 655
658 656 commit_id, f_path = self._get_commit_and_path()
659 657 commit = self._get_commit_or_redirect(commit_id)
660 658 prev_commit_id = commit.raw_id
661 659 line_anchor = self.request.GET.get('line_anchor')
662 660 is_file = False
663 661 try:
664 662 _file = commit.get_node(f_path)
665 663 is_file = _file.is_file()
666 664 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
667 665 pass
668 666
669 667 if is_file:
670 668 history = commit.get_path_history(f_path)
671 669 prev_commit_id = history[1].raw_id \
672 670 if len(history) > 1 else prev_commit_id
673 671 prev_url = h.route_path(
674 672 'repo_files:annotated', repo_name=self.db_repo_name,
675 673 commit_id=prev_commit_id, f_path=f_path,
676 674 _anchor='L{}'.format(line_anchor))
677 675
678 676 raise HTTPFound(prev_url)
679 677
680 678 @LoginRequired()
681 679 @HasRepoPermissionAnyDecorator(
682 680 'repository.read', 'repository.write', 'repository.admin')
683 681 @view_config(
684 682 route_name='repo_nodetree_full', request_method='GET',
685 683 renderer=None, xhr=True)
686 684 @view_config(
687 685 route_name='repo_nodetree_full:default_path', request_method='GET',
688 686 renderer=None, xhr=True)
689 687 def repo_nodetree_full(self):
690 688 """
691 689 Returns rendered html of file tree that contains commit date,
692 690 author, commit_id for the specified combination of
693 691 repo, commit_id and file path
694 692 """
695 693 c = self.load_default_context()
696 694
697 695 commit_id, f_path = self._get_commit_and_path()
698 696 commit = self._get_commit_or_redirect(commit_id)
699 697 try:
700 698 dir_node = commit.get_node(f_path)
701 699 except RepositoryError as e:
702 700 return Response('error: {}'.format(h.escape(safe_str(e))))
703 701
704 702 if dir_node.is_file():
705 703 return Response('')
706 704
707 705 c.file = dir_node
708 706 c.commit = commit
709 707
710 708 html = self._get_tree_at_commit(
711 709 c, commit.raw_id, dir_node.path, full_load=True)
712 710
713 711 return Response(html)
714 712
715 713 def _get_attachement_headers(self, f_path):
716 714 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
717 715 safe_path = f_name.replace('"', '\\"')
718 716 encoded_path = urllib.quote(f_name)
719 717
720 718 return "attachment; " \
721 719 "filename=\"{}\"; " \
722 720 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
723 721
724 722 @LoginRequired()
725 723 @HasRepoPermissionAnyDecorator(
726 724 'repository.read', 'repository.write', 'repository.admin')
727 725 @view_config(
728 726 route_name='repo_file_raw', request_method='GET',
729 727 renderer=None)
730 728 def repo_file_raw(self):
731 729 """
732 730 Action for show as raw, some mimetypes are "rendered",
733 731 those include images, icons.
734 732 """
735 733 c = self.load_default_context()
736 734
737 735 commit_id, f_path = self._get_commit_and_path()
738 736 commit = self._get_commit_or_redirect(commit_id)
739 737 file_node = self._get_filenode_or_redirect(commit, f_path)
740 738
741 739 raw_mimetype_mapping = {
742 740 # map original mimetype to a mimetype used for "show as raw"
743 741 # you can also provide a content-disposition to override the
744 742 # default "attachment" disposition.
745 743 # orig_type: (new_type, new_dispo)
746 744
747 745 # show images inline:
748 746 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
749 747 # for example render an SVG with javascript inside or even render
750 748 # HTML.
751 749 'image/x-icon': ('image/x-icon', 'inline'),
752 750 'image/png': ('image/png', 'inline'),
753 751 'image/gif': ('image/gif', 'inline'),
754 752 'image/jpeg': ('image/jpeg', 'inline'),
755 753 'application/pdf': ('application/pdf', 'inline'),
756 754 }
757 755
758 756 mimetype = file_node.mimetype
759 757 try:
760 758 mimetype, disposition = raw_mimetype_mapping[mimetype]
761 759 except KeyError:
762 760 # we don't know anything special about this, handle it safely
763 761 if file_node.is_binary:
764 762 # do same as download raw for binary files
765 763 mimetype, disposition = 'application/octet-stream', 'attachment'
766 764 else:
767 765 # do not just use the original mimetype, but force text/plain,
768 766 # otherwise it would serve text/html and that might be unsafe.
769 767 # Note: underlying vcs library fakes text/plain mimetype if the
770 768 # mimetype can not be determined and it thinks it is not
771 769 # binary.This might lead to erroneous text display in some
772 770 # cases, but helps in other cases, like with text files
773 771 # without extension.
774 772 mimetype, disposition = 'text/plain', 'inline'
775 773
776 774 if disposition == 'attachment':
777 775 disposition = self._get_attachement_headers(f_path)
778 776
779 777 def stream_node():
780 778 yield file_node.raw_bytes
781 779
782 780 response = Response(app_iter=stream_node())
783 781 response.content_disposition = disposition
784 782 response.content_type = mimetype
785 783
786 784 charset = self._get_default_encoding(c)
787 785 if charset:
788 786 response.charset = charset
789 787
790 788 return response
791 789
792 790 @LoginRequired()
793 791 @HasRepoPermissionAnyDecorator(
794 792 'repository.read', 'repository.write', 'repository.admin')
795 793 @view_config(
796 794 route_name='repo_file_download', request_method='GET',
797 795 renderer=None)
798 796 @view_config(
799 797 route_name='repo_file_download:legacy', request_method='GET',
800 798 renderer=None)
801 799 def repo_file_download(self):
802 800 c = self.load_default_context()
803 801
804 802 commit_id, f_path = self._get_commit_and_path()
805 803 commit = self._get_commit_or_redirect(commit_id)
806 804 file_node = self._get_filenode_or_redirect(commit, f_path)
807 805
808 806 if self.request.GET.get('lf'):
809 807 # only if lf get flag is passed, we download this file
810 808 # as LFS/Largefile
811 809 lf_node = file_node.get_largefile_node()
812 810 if lf_node:
813 811 # overwrite our pointer with the REAL large-file
814 812 file_node = lf_node
815 813
816 814 disposition = self._get_attachement_headers(f_path)
817 815
818 816 def stream_node():
819 817 yield file_node.raw_bytes
820 818
821 819 response = Response(app_iter=stream_node())
822 820 response.content_disposition = disposition
823 821 response.content_type = file_node.mimetype
824 822
825 823 charset = self._get_default_encoding(c)
826 824 if charset:
827 825 response.charset = charset
828 826
829 827 return response
830 828
831 829 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
832 830
833 831 cache_seconds = safe_int(
834 832 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
835 833 cache_on = cache_seconds > 0
836 834 log.debug(
837 835 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
838 836 'with caching: %s[TTL: %ss]' % (
839 837 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
840 838
841 839 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
842 840 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
843 841
844 842 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
845 843 condition=cache_on)
846 844 def compute_file_search(repo_id, commit_id, f_path):
847 845 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
848 846 repo_id, commit_id, f_path)
849 847 try:
850 848 _d, _f = ScmModel().get_nodes(
851 849 repo_name, commit_id, f_path, flat=False)
852 850 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
853 851 log.exception(safe_str(e))
854 852 h.flash(safe_str(h.escape(e)), category='error')
855 853 raise HTTPFound(h.route_path(
856 854 'repo_files', repo_name=self.db_repo_name,
857 855 commit_id='tip', f_path='/'))
858 856 return _d + _f
859 857
860 858 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
861 859
862 860 @LoginRequired()
863 861 @HasRepoPermissionAnyDecorator(
864 862 'repository.read', 'repository.write', 'repository.admin')
865 863 @view_config(
866 864 route_name='repo_files_nodelist', request_method='GET',
867 865 renderer='json_ext', xhr=True)
868 866 def repo_nodelist(self):
869 867 self.load_default_context()
870 868
871 869 commit_id, f_path = self._get_commit_and_path()
872 870 commit = self._get_commit_or_redirect(commit_id)
873 871
874 872 metadata = self._get_nodelist_at_commit(
875 873 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
876 874 return {'nodes': metadata}
877 875
878 876 def _create_references(
879 877 self, branches_or_tags, symbolic_reference, f_path):
880 878 items = []
881 879 for name, commit_id in branches_or_tags.items():
882 880 sym_ref = symbolic_reference(commit_id, name, f_path)
883 881 items.append((sym_ref, name))
884 882 return items
885 883
886 884 def _symbolic_reference(self, commit_id, name, f_path):
887 885 return commit_id
888 886
889 887 def _symbolic_reference_svn(self, commit_id, name, f_path):
890 888 new_f_path = vcspath.join(name, f_path)
891 889 return u'%s@%s' % (new_f_path, commit_id)
892 890
893 891 def _get_node_history(self, commit_obj, f_path, commits=None):
894 892 """
895 893 get commit history for given node
896 894
897 895 :param commit_obj: commit to calculate history
898 896 :param f_path: path for node to calculate history for
899 897 :param commits: if passed don't calculate history and take
900 898 commits defined in this list
901 899 """
902 900 _ = self.request.translate
903 901
904 902 # calculate history based on tip
905 903 tip = self.rhodecode_vcs_repo.get_commit()
906 904 if commits is None:
907 905 pre_load = ["author", "branch"]
908 906 try:
909 907 commits = tip.get_path_history(f_path, pre_load=pre_load)
910 908 except (NodeDoesNotExistError, CommitError):
911 909 # this node is not present at tip!
912 910 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
913 911
914 912 history = []
915 913 commits_group = ([], _("Changesets"))
916 914 for commit in commits:
917 915 branch = ' (%s)' % commit.branch if commit.branch else ''
918 916 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
919 917 commits_group[0].append((commit.raw_id, n_desc,))
920 918 history.append(commits_group)
921 919
922 920 symbolic_reference = self._symbolic_reference
923 921
924 922 if self.rhodecode_vcs_repo.alias == 'svn':
925 923 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
926 924 f_path, self.rhodecode_vcs_repo)
927 925 if adjusted_f_path != f_path:
928 926 log.debug(
929 927 'Recognized svn tag or branch in file "%s", using svn '
930 928 'specific symbolic references', f_path)
931 929 f_path = adjusted_f_path
932 930 symbolic_reference = self._symbolic_reference_svn
933 931
934 932 branches = self._create_references(
935 933 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
936 934 branches_group = (branches, _("Branches"))
937 935
938 936 tags = self._create_references(
939 937 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
940 938 tags_group = (tags, _("Tags"))
941 939
942 940 history.append(branches_group)
943 941 history.append(tags_group)
944 942
945 943 return history, commits
946 944
947 945 @LoginRequired()
948 946 @HasRepoPermissionAnyDecorator(
949 947 'repository.read', 'repository.write', 'repository.admin')
950 948 @view_config(
951 949 route_name='repo_file_history', request_method='GET',
952 950 renderer='json_ext')
953 951 def repo_file_history(self):
954 952 self.load_default_context()
955 953
956 954 commit_id, f_path = self._get_commit_and_path()
957 955 commit = self._get_commit_or_redirect(commit_id)
958 956 file_node = self._get_filenode_or_redirect(commit, f_path)
959 957
960 958 if file_node.is_file():
961 959 file_history, _hist = self._get_node_history(commit, f_path)
962 960
963 961 res = []
964 962 for obj in file_history:
965 963 res.append({
966 964 'text': obj[1],
967 965 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
968 966 })
969 967
970 968 data = {
971 969 'more': False,
972 970 'results': res
973 971 }
974 972 return data
975 973
976 974 log.warning('Cannot fetch history for directory')
977 975 raise HTTPBadRequest()
978 976
979 977 @LoginRequired()
980 978 @HasRepoPermissionAnyDecorator(
981 979 'repository.read', 'repository.write', 'repository.admin')
982 980 @view_config(
983 981 route_name='repo_file_authors', request_method='GET',
984 982 renderer='rhodecode:templates/files/file_authors_box.mako')
985 983 def repo_file_authors(self):
986 984 c = self.load_default_context()
987 985
988 986 commit_id, f_path = self._get_commit_and_path()
989 987 commit = self._get_commit_or_redirect(commit_id)
990 988 file_node = self._get_filenode_or_redirect(commit, f_path)
991 989
992 990 if not file_node.is_file():
993 991 raise HTTPBadRequest()
994 992
995 993 c.file_last_commit = file_node.last_commit
996 994 if self.request.GET.get('annotate') == '1':
997 995 # use _hist from annotation if annotation mode is on
998 996 commit_ids = set(x[1] for x in file_node.annotate)
999 997 _hist = (
1000 998 self.rhodecode_vcs_repo.get_commit(commit_id)
1001 999 for commit_id in commit_ids)
1002 1000 else:
1003 1001 _f_history, _hist = self._get_node_history(commit, f_path)
1004 1002 c.file_author = False
1005 1003
1006 1004 unique = collections.OrderedDict()
1007 1005 for commit in _hist:
1008 1006 author = commit.author
1009 1007 if author not in unique:
1010 1008 unique[commit.author] = [
1011 1009 h.email(author),
1012 1010 h.person(author, 'username_or_name_or_email'),
1013 1011 1 # counter
1014 1012 ]
1015 1013
1016 1014 else:
1017 1015 # increase counter
1018 1016 unique[commit.author][2] += 1
1019 1017
1020 1018 c.authors = [val for val in unique.values()]
1021 1019
1022 1020 return self._get_template_context(c)
1023 1021
1024 1022 @LoginRequired()
1025 1023 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1026 1024 @view_config(
1027 1025 route_name='repo_files_remove_file', request_method='GET',
1028 1026 renderer='rhodecode:templates/files/files_delete.mako')
1029 1027 def repo_files_remove_file(self):
1030 1028 _ = self.request.translate
1031 1029 c = self.load_default_context()
1032 1030 commit_id, f_path = self._get_commit_and_path()
1033 1031
1034 1032 self._ensure_not_locked()
1035 1033 _branch_name, _sha_commit_id, is_head = \
1036 1034 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1037 1035
1038 1036 if not is_head:
1039 1037 h.flash(_('You can only delete files with commit '
1040 1038 'being a valid branch head.'), category='warning')
1041 1039 raise HTTPFound(
1042 1040 h.route_path('repo_files',
1043 1041 repo_name=self.db_repo_name, commit_id='tip',
1044 1042 f_path=f_path))
1045 1043
1046 1044 self.check_branch_permission(_branch_name)
1047 1045 c.commit = self._get_commit_or_redirect(commit_id)
1048 1046 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1049 1047
1050 1048 c.default_message = _(
1051 1049 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1052 1050 c.f_path = f_path
1053 1051
1054 1052 return self._get_template_context(c)
1055 1053
1056 1054 @LoginRequired()
1057 1055 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1058 1056 @CSRFRequired()
1059 1057 @view_config(
1060 1058 route_name='repo_files_delete_file', request_method='POST',
1061 1059 renderer=None)
1062 1060 def repo_files_delete_file(self):
1063 1061 _ = self.request.translate
1064 1062
1065 1063 c = self.load_default_context()
1066 1064 commit_id, f_path = self._get_commit_and_path()
1067 1065
1068 1066 self._ensure_not_locked()
1069 1067 _branch_name, _sha_commit_id, is_head = \
1070 1068 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1071 1069
1072 1070 if not is_head:
1073 1071 h.flash(_('You can only delete files with commit '
1074 1072 'being a valid branch head.'), category='warning')
1075 1073 raise HTTPFound(
1076 1074 h.route_path('repo_files',
1077 1075 repo_name=self.db_repo_name, commit_id='tip',
1078 1076 f_path=f_path))
1079 1077 self.check_branch_permission(_branch_name)
1080 1078
1081 1079 c.commit = self._get_commit_or_redirect(commit_id)
1082 1080 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1083 1081
1084 1082 c.default_message = _(
1085 1083 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1086 1084 c.f_path = f_path
1087 1085 node_path = f_path
1088 1086 author = self._rhodecode_db_user.full_contact
1089 1087 message = self.request.POST.get('message') or c.default_message
1090 1088 try:
1091 1089 nodes = {
1092 1090 node_path: {
1093 1091 'content': ''
1094 1092 }
1095 1093 }
1096 1094 ScmModel().delete_nodes(
1097 1095 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1098 1096 message=message,
1099 1097 nodes=nodes,
1100 1098 parent_commit=c.commit,
1101 1099 author=author,
1102 1100 )
1103 1101
1104 1102 h.flash(
1105 1103 _('Successfully deleted file `{}`').format(
1106 1104 h.escape(f_path)), category='success')
1107 1105 except Exception:
1108 1106 log.exception('Error during commit operation')
1109 1107 h.flash(_('Error occurred during commit'), category='error')
1110 1108 raise HTTPFound(
1111 1109 h.route_path('repo_commit', repo_name=self.db_repo_name,
1112 1110 commit_id='tip'))
1113 1111
1114 1112 @LoginRequired()
1115 1113 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1116 1114 @view_config(
1117 1115 route_name='repo_files_edit_file', request_method='GET',
1118 1116 renderer='rhodecode:templates/files/files_edit.mako')
1119 1117 def repo_files_edit_file(self):
1120 1118 _ = self.request.translate
1121 1119 c = self.load_default_context()
1122 1120 commit_id, f_path = self._get_commit_and_path()
1123 1121
1124 1122 self._ensure_not_locked()
1125 1123 _branch_name, _sha_commit_id, is_head = \
1126 1124 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1127 1125
1128 1126 if not is_head:
1129 1127 h.flash(_('You can only edit files with commit '
1130 1128 'being a valid branch head.'), category='warning')
1131 1129 raise HTTPFound(
1132 1130 h.route_path('repo_files',
1133 1131 repo_name=self.db_repo_name, commit_id='tip',
1134 1132 f_path=f_path))
1135 1133 self.check_branch_permission(_branch_name)
1136 1134
1137 1135 c.commit = self._get_commit_or_redirect(commit_id)
1138 1136 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1139 1137
1140 1138 if c.file.is_binary:
1141 1139 files_url = h.route_path(
1142 1140 'repo_files',
1143 1141 repo_name=self.db_repo_name,
1144 1142 commit_id=c.commit.raw_id, f_path=f_path)
1145 1143 raise HTTPFound(files_url)
1146 1144
1147 1145 c.default_message = _(
1148 1146 'Edited file {} via RhodeCode Enterprise').format(f_path)
1149 1147 c.f_path = f_path
1150 1148
1151 1149 return self._get_template_context(c)
1152 1150
1153 1151 @LoginRequired()
1154 1152 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1155 1153 @CSRFRequired()
1156 1154 @view_config(
1157 1155 route_name='repo_files_update_file', request_method='POST',
1158 1156 renderer=None)
1159 1157 def repo_files_update_file(self):
1160 1158 _ = self.request.translate
1161 1159 c = self.load_default_context()
1162 1160 commit_id, f_path = self._get_commit_and_path()
1163 1161
1164 1162 self._ensure_not_locked()
1165 1163 _branch_name, _sha_commit_id, is_head = \
1166 1164 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1167 1165
1168 1166 if not is_head:
1169 1167 h.flash(_('You can only edit files with commit '
1170 1168 'being a valid branch head.'), category='warning')
1171 1169 raise HTTPFound(
1172 1170 h.route_path('repo_files',
1173 1171 repo_name=self.db_repo_name, commit_id='tip',
1174 1172 f_path=f_path))
1175 1173
1176 1174 self.check_branch_permission(_branch_name)
1177 1175
1178 1176 c.commit = self._get_commit_or_redirect(commit_id)
1179 1177 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1180 1178
1181 1179 if c.file.is_binary:
1182 1180 raise HTTPFound(
1183 1181 h.route_path('repo_files',
1184 1182 repo_name=self.db_repo_name,
1185 1183 commit_id=c.commit.raw_id,
1186 1184 f_path=f_path))
1187 1185
1188 1186 c.default_message = _(
1189 1187 'Edited file {} via RhodeCode Enterprise').format(f_path)
1190 1188 c.f_path = f_path
1191 1189 old_content = c.file.content
1192 1190 sl = old_content.splitlines(1)
1193 1191 first_line = sl[0] if sl else ''
1194 1192
1195 1193 r_post = self.request.POST
1196 1194 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1197 1195 line_ending_mode = detect_mode(first_line, 0)
1198 1196 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1199 1197
1200 1198 message = r_post.get('message') or c.default_message
1201 1199 org_f_path = c.file.unicode_path
1202 1200 filename = r_post['filename']
1203 1201 org_filename = c.file.name
1204 1202
1205 1203 if content == old_content and filename == org_filename:
1206 1204 h.flash(_('No changes'), category='warning')
1207 1205 raise HTTPFound(
1208 1206 h.route_path('repo_commit', repo_name=self.db_repo_name,
1209 1207 commit_id='tip'))
1210 1208 try:
1211 1209 mapping = {
1212 1210 org_f_path: {
1213 1211 'org_filename': org_f_path,
1214 1212 'filename': os.path.join(c.file.dir_path, filename),
1215 1213 'content': content,
1216 1214 'lexer': '',
1217 1215 'op': 'mod',
1218 1216 'mode': c.file.mode
1219 1217 }
1220 1218 }
1221 1219
1222 1220 ScmModel().update_nodes(
1223 1221 user=self._rhodecode_db_user.user_id,
1224 1222 repo=self.db_repo,
1225 1223 message=message,
1226 1224 nodes=mapping,
1227 1225 parent_commit=c.commit,
1228 1226 )
1229 1227
1230 1228 h.flash(
1231 1229 _('Successfully committed changes to file `{}`').format(
1232 1230 h.escape(f_path)), category='success')
1233 1231 except Exception:
1234 1232 log.exception('Error occurred during commit')
1235 1233 h.flash(_('Error occurred during commit'), category='error')
1236 1234 raise HTTPFound(
1237 1235 h.route_path('repo_commit', repo_name=self.db_repo_name,
1238 1236 commit_id='tip'))
1239 1237
1240 1238 @LoginRequired()
1241 1239 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1242 1240 @view_config(
1243 1241 route_name='repo_files_add_file', request_method='GET',
1244 1242 renderer='rhodecode:templates/files/files_add.mako')
1245 1243 def repo_files_add_file(self):
1246 1244 _ = self.request.translate
1247 1245 c = self.load_default_context()
1248 1246 commit_id, f_path = self._get_commit_and_path()
1249 1247
1250 1248 self._ensure_not_locked()
1251 1249
1252 1250 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1253 1251 if c.commit is None:
1254 1252 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1255 1253 c.default_message = (_('Added file via RhodeCode Enterprise'))
1256 1254 c.f_path = f_path.lstrip('/') # ensure not relative path
1257 1255
1258 1256 if self.rhodecode_vcs_repo.is_empty:
1259 1257 # for empty repository we cannot check for current branch, we rely on
1260 1258 # c.commit.branch instead
1261 1259 _branch_name = c.commit.branch
1262 1260 is_head = True
1263 1261 else:
1264 1262 _branch_name, _sha_commit_id, is_head = \
1265 1263 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1266 1264
1267 1265 if not is_head:
1268 1266 h.flash(_('You can only add files with commit '
1269 1267 'being a valid branch head.'), category='warning')
1270 1268 raise HTTPFound(
1271 1269 h.route_path('repo_files',
1272 1270 repo_name=self.db_repo_name, commit_id='tip',
1273 1271 f_path=f_path))
1274 1272
1275 1273 self.check_branch_permission(_branch_name)
1276 1274
1277 1275 return self._get_template_context(c)
1278 1276
1279 1277 @LoginRequired()
1280 1278 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1281 1279 @CSRFRequired()
1282 1280 @view_config(
1283 1281 route_name='repo_files_create_file', request_method='POST',
1284 1282 renderer=None)
1285 1283 def repo_files_create_file(self):
1286 1284 _ = self.request.translate
1287 1285 c = self.load_default_context()
1288 1286 commit_id, f_path = self._get_commit_and_path()
1289 1287
1290 1288 self._ensure_not_locked()
1291 1289
1292 1290 r_post = self.request.POST
1293 1291
1294 1292 c.commit = self._get_commit_or_redirect(
1295 1293 commit_id, redirect_after=False)
1296 1294 if c.commit is None:
1297 1295 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1298 1296
1299 1297 if self.rhodecode_vcs_repo.is_empty:
1300 1298 # for empty repository we cannot check for current branch, we rely on
1301 1299 # c.commit.branch instead
1302 1300 _branch_name = c.commit.branch
1303 1301 is_head = True
1304 1302 else:
1305 1303 _branch_name, _sha_commit_id, is_head = \
1306 1304 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1307 1305
1308 1306 if not is_head:
1309 1307 h.flash(_('You can only add files with commit '
1310 1308 'being a valid branch head.'), category='warning')
1311 1309 raise HTTPFound(
1312 1310 h.route_path('repo_files',
1313 1311 repo_name=self.db_repo_name, commit_id='tip',
1314 1312 f_path=f_path))
1315 1313
1316 1314 self.check_branch_permission(_branch_name)
1317 1315
1318 1316 c.default_message = (_('Added file via RhodeCode Enterprise'))
1319 1317 c.f_path = f_path
1320 1318 unix_mode = 0
1321 1319 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1322 1320
1323 1321 message = r_post.get('message') or c.default_message
1324 1322 filename = r_post.get('filename')
1325 1323 location = r_post.get('location', '') # dir location
1326 1324 file_obj = r_post.get('upload_file', None)
1327 1325
1328 1326 if file_obj is not None and hasattr(file_obj, 'filename'):
1329 1327 filename = r_post.get('filename_upload')
1330 1328 content = file_obj.file
1331 1329
1332 1330 if hasattr(content, 'file'):
1333 1331 # non posix systems store real file under file attr
1334 1332 content = content.file
1335 1333
1336 1334 if self.rhodecode_vcs_repo.is_empty:
1337 1335 default_redirect_url = h.route_path(
1338 1336 'repo_summary', repo_name=self.db_repo_name)
1339 1337 else:
1340 1338 default_redirect_url = h.route_path(
1341 1339 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1342 1340
1343 1341 # If there's no commit, redirect to repo summary
1344 1342 if type(c.commit) is EmptyCommit:
1345 1343 redirect_url = h.route_path(
1346 1344 'repo_summary', repo_name=self.db_repo_name)
1347 1345 else:
1348 1346 redirect_url = default_redirect_url
1349 1347
1350 1348 if not filename:
1351 1349 h.flash(_('No filename'), category='warning')
1352 1350 raise HTTPFound(redirect_url)
1353 1351
1354 1352 # extract the location from filename,
1355 1353 # allows using foo/bar.txt syntax to create subdirectories
1356 1354 subdir_loc = filename.rsplit('/', 1)
1357 1355 if len(subdir_loc) == 2:
1358 1356 location = os.path.join(location, subdir_loc[0])
1359 1357
1360 1358 # strip all crap out of file, just leave the basename
1361 1359 filename = os.path.basename(filename)
1362 1360 node_path = os.path.join(location, filename)
1363 1361 author = self._rhodecode_db_user.full_contact
1364 1362
1365 1363 try:
1366 1364 nodes = {
1367 1365 node_path: {
1368 1366 'content': content
1369 1367 }
1370 1368 }
1371 1369 ScmModel().create_nodes(
1372 1370 user=self._rhodecode_db_user.user_id,
1373 1371 repo=self.db_repo,
1374 1372 message=message,
1375 1373 nodes=nodes,
1376 1374 parent_commit=c.commit,
1377 1375 author=author,
1378 1376 )
1379 1377
1380 1378 h.flash(
1381 1379 _('Successfully committed new file `{}`').format(
1382 1380 h.escape(node_path)), category='success')
1383 1381 except NonRelativePathError:
1384 1382 log.exception('Non Relative path found')
1385 1383 h.flash(_(
1386 1384 'The location specified must be a relative path and must not '
1387 1385 'contain .. in the path'), category='warning')
1388 1386 raise HTTPFound(default_redirect_url)
1389 1387 except (NodeError, NodeAlreadyExistsError) as e:
1390 1388 h.flash(_(h.escape(e)), category='error')
1391 1389 except Exception:
1392 1390 log.exception('Error occurred during commit')
1393 1391 h.flash(_('Error occurred during commit'), category='error')
1394 1392
1395 1393 raise HTTPFound(default_redirect_url)
@@ -1,393 +1,393 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import string
23 23 import rhodecode
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.lib.view_utils import get_format_ref_id
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 30 from rhodecode.lib import helpers as h, rc_cache
31 31 from rhodecode.lib.utils2 import safe_str, safe_int
32 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 33 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 36 from rhodecode.lib.vcs.exceptions import (
37 37 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 38 from rhodecode.model.db import Statistics, CacheKey, User
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.repo import ReadmeFinder
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class RepoSummaryView(RepoAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50 c.rhodecode_repo = None
51 51 if not c.repository_requirements_missing:
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53 return c
54 54
55 55 def _get_readme_data(self, db_repo, renderer_type):
56 56
57 57 log.debug('Looking for README file')
58 58
59 59 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
60 60 db_repo.repo_id, CacheKey.CACHE_TYPE_README)
61 61 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
62 62 repo_id=self.db_repo.repo_id)
63 63 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
64 64
65 65 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
66 66 def generate_repo_readme(repo_id, _repo_name, _renderer_type):
67 67 readme_data = None
68 68 readme_node = None
69 69 readme_filename = None
70 70 commit = self._get_landing_commit_or_none(db_repo)
71 71 if commit:
72 72 log.debug("Searching for a README file.")
73 73 readme_node = ReadmeFinder(_renderer_type).search(commit)
74 74 if readme_node:
75 75 relative_urls = {
76 76 'raw': h.route_path(
77 77 'repo_file_raw', repo_name=_repo_name,
78 78 commit_id=commit.raw_id, f_path=readme_node.path),
79 79 'standard': h.route_path(
80 80 'repo_files', repo_name=_repo_name,
81 81 commit_id=commit.raw_id, f_path=readme_node.path),
82 82 }
83 83 readme_data = self._render_readme_or_none(
84 84 commit, readme_node, relative_urls)
85 85 readme_filename = readme_node.path
86 86 return readme_data, readme_filename
87 87
88 88 inv_context_manager = rc_cache.InvalidationContext(
89 89 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
90 90 with inv_context_manager as invalidation_context:
91 91 args = (db_repo.repo_id, db_repo.repo_name, renderer_type,)
92 92 # re-compute and store cache if we get invalidate signal
93 93 if invalidation_context.should_invalidate():
94 94 instance = generate_repo_readme.refresh(*args)
95 95 else:
96 96 instance = generate_repo_readme(*args)
97 97
98 98 log.debug(
99 99 'Repo readme generated and computed in %.3fs',
100 100 inv_context_manager.compute_time)
101 101 return instance
102 102
103 103 def _get_landing_commit_or_none(self, db_repo):
104 104 log.debug("Getting the landing commit.")
105 105 try:
106 106 commit = db_repo.get_landing_commit()
107 107 if not isinstance(commit, EmptyCommit):
108 108 return commit
109 109 else:
110 110 log.debug("Repository is empty, no README to render.")
111 111 except CommitError:
112 112 log.exception(
113 113 "Problem getting commit when trying to render the README.")
114 114
115 115 def _render_readme_or_none(self, commit, readme_node, relative_urls):
116 116 log.debug(
117 117 'Found README file `%s` rendering...', readme_node.path)
118 118 renderer = MarkupRenderer()
119 119 try:
120 120 html_source = renderer.render(
121 121 readme_node.content, filename=readme_node.path)
122 122 if relative_urls:
123 123 return relative_links(html_source, relative_urls)
124 124 return html_source
125 125 except Exception:
126 126 log.exception(
127 127 "Exception while trying to render the README")
128 128
129 129 def _load_commits_context(self, c):
130 130 p = safe_int(self.request.GET.get('page'), 1)
131 131 size = safe_int(self.request.GET.get('size'), 10)
132 132
133 133 def url_generator(**kw):
134 134 query_params = {
135 135 'size': size
136 136 }
137 137 query_params.update(kw)
138 138 return h.route_path(
139 139 'repo_summary_commits',
140 140 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
141 141
142 142 pre_load = ['author', 'branch', 'date', 'message']
143 143 try:
144 144 collection = self.rhodecode_vcs_repo.get_commits(
145 145 pre_load=pre_load, translate_tags=False)
146 146 except EmptyRepositoryError:
147 147 collection = self.rhodecode_vcs_repo
148 148
149 149 c.repo_commits = h.RepoPage(
150 150 collection, page=p, items_per_page=size, url=url_generator)
151 151 page_ids = [x.raw_id for x in c.repo_commits]
152 152 c.comments = self.db_repo.get_comments(page_ids)
153 153 c.statuses = self.db_repo.statuses(page_ids)
154 154
155 155 def _prepare_and_set_clone_url(self, c):
156 156 username = ''
157 157 if self._rhodecode_user.username != User.DEFAULT_USER:
158 158 username = safe_str(self._rhodecode_user.username)
159 159
160 160 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
161 161 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
162 162
163 163 if '{repo}' in _def_clone_uri:
164 164 _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}')
165 165 elif '{repoid}' in _def_clone_uri:
166 166 _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}')
167 167
168 168 c.clone_repo_url = self.db_repo.clone_url(
169 169 user=username, uri_tmpl=_def_clone_uri)
170 170 c.clone_repo_url_id = self.db_repo.clone_url(
171 171 user=username, uri_tmpl=_def_clone_uri_id)
172 172 c.clone_repo_url_ssh = self.db_repo.clone_url(
173 173 uri_tmpl=_def_clone_uri_ssh, ssh=True)
174 174
175 175 @LoginRequired()
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 @view_config(
179 179 route_name='repo_summary_commits', request_method='GET',
180 180 renderer='rhodecode:templates/summary/summary_commits.mako')
181 181 def summary_commits(self):
182 182 c = self.load_default_context()
183 183 self._prepare_and_set_clone_url(c)
184 184 self._load_commits_context(c)
185 185 return self._get_template_context(c)
186 186
187 187 @LoginRequired()
188 188 @HasRepoPermissionAnyDecorator(
189 189 'repository.read', 'repository.write', 'repository.admin')
190 190 @view_config(
191 191 route_name='repo_summary', request_method='GET',
192 192 renderer='rhodecode:templates/summary/summary.mako')
193 193 @view_config(
194 194 route_name='repo_summary_slash', request_method='GET',
195 195 renderer='rhodecode:templates/summary/summary.mako')
196 196 @view_config(
197 197 route_name='repo_summary_explicit', request_method='GET',
198 198 renderer='rhodecode:templates/summary/summary.mako')
199 199 def summary(self):
200 200 c = self.load_default_context()
201 201
202 202 # Prepare the clone URL
203 203 self._prepare_and_set_clone_url(c)
204 204
205 205 # If enabled, get statistics data
206 206
207 207 c.show_stats = bool(self.db_repo.enable_statistics)
208 208
209 209 stats = Session().query(Statistics) \
210 210 .filter(Statistics.repository == self.db_repo) \
211 211 .scalar()
212 212
213 213 c.stats_percentage = 0
214 214
215 215 if stats and stats.languages:
216 216 c.no_data = False is self.db_repo.enable_statistics
217 217 lang_stats_d = json.loads(stats.languages)
218 218
219 219 # Sort first by decreasing count and second by the file extension,
220 220 # so we have a consistent output.
221 221 lang_stats_items = sorted(lang_stats_d.iteritems(),
222 222 key=lambda k: (-k[1], k[0]))[:10]
223 223 lang_stats = [(x, {"count": y,
224 224 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
225 225 for x, y in lang_stats_items]
226 226
227 227 c.trending_languages = json.dumps(lang_stats)
228 228 else:
229 229 c.no_data = True
230 230 c.trending_languages = json.dumps({})
231 231
232 232 scm_model = ScmModel()
233 233 c.enable_downloads = self.db_repo.enable_downloads
234 234 c.repository_followers = scm_model.get_followers(self.db_repo)
235 235 c.repository_forks = scm_model.get_forks(self.db_repo)
236 236 c.repository_is_user_following = scm_model.is_following_repo(
237 237 self.db_repo_name, self._rhodecode_user.user_id)
238 238
239 239 # first interaction with the VCS instance after here...
240 240 if c.repository_requirements_missing:
241 241 self.request.override_renderer = \
242 242 'rhodecode:templates/summary/missing_requirements.mako'
243 243 return self._get_template_context(c)
244 244
245 245 c.readme_data, c.readme_file = \
246 246 self._get_readme_data(self.db_repo, c.visual.default_renderer)
247 247
248 248 # loads the summary commits template context
249 249 self._load_commits_context(c)
250 250
251 251 return self._get_template_context(c)
252 252
253 253 def get_request_commit_id(self):
254 254 return self.request.matchdict['commit_id']
255 255
256 256 @LoginRequired()
257 257 @HasRepoPermissionAnyDecorator(
258 258 'repository.read', 'repository.write', 'repository.admin')
259 259 @view_config(
260 260 route_name='repo_stats', request_method='GET',
261 261 renderer='json_ext')
262 262 def repo_stats(self):
263 263 commit_id = self.get_request_commit_id()
264 264 show_stats = bool(self.db_repo.enable_statistics)
265 265 repo_id = self.db_repo.repo_id
266 266
267 267 cache_seconds = safe_int(
268 268 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
269 269 cache_on = cache_seconds > 0
270 270 log.debug(
271 271 'Computing REPO TREE for repo_id %s commit_id `%s` '
272 272 'with caching: %s[TTL: %ss]' % (
273 273 repo_id, commit_id, cache_on, cache_seconds or 0))
274 274
275 275 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
276 276 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
277 277
278 278 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
279 279 condition=cache_on)
280 280 def compute_stats(repo_id, commit_id, show_stats):
281 281 code_stats = {}
282 282 size = 0
283 283 try:
284 284 scm_instance = self.db_repo.scm_instance()
285 285 commit = scm_instance.get_commit(commit_id)
286 286
287 287 for node in commit.get_filenodes_generator():
288 288 size += node.size
289 289 if not show_stats:
290 290 continue
291 291 ext = string.lower(node.extension)
292 292 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
293 293 if ext_info:
294 294 if ext in code_stats:
295 295 code_stats[ext]['count'] += 1
296 296 else:
297 297 code_stats[ext] = {"count": 1, "desc": ext_info}
298 298 except (EmptyRepositoryError, CommitDoesNotExistError):
299 299 pass
300 300 return {'size': h.format_byte_size_binary(size),
301 301 'code_stats': code_stats}
302 302
303 303 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
304 304 return stats
305 305
306 306 @LoginRequired()
307 307 @HasRepoPermissionAnyDecorator(
308 308 'repository.read', 'repository.write', 'repository.admin')
309 309 @view_config(
310 310 route_name='repo_refs_data', request_method='GET',
311 311 renderer='json_ext')
312 312 def repo_refs_data(self):
313 313 _ = self.request.translate
314 314 self.load_default_context()
315 315
316 316 repo = self.rhodecode_vcs_repo
317 317 refs_to_create = [
318 318 (_("Branch"), repo.branches, 'branch'),
319 319 (_("Tag"), repo.tags, 'tag'),
320 320 (_("Bookmark"), repo.bookmarks, 'book'),
321 321 ]
322 322 res = self._create_reference_data(
323 323 repo, self.db_repo_name, refs_to_create)
324 324 data = {
325 325 'more': False,
326 326 'results': res
327 327 }
328 328 return data
329 329
330 330 @LoginRequired()
331 331 @HasRepoPermissionAnyDecorator(
332 332 'repository.read', 'repository.write', 'repository.admin')
333 333 @view_config(
334 334 route_name='repo_refs_changelog_data', request_method='GET',
335 335 renderer='json_ext')
336 336 def repo_refs_changelog_data(self):
337 337 _ = self.request.translate
338 338 self.load_default_context()
339 339
340 340 repo = self.rhodecode_vcs_repo
341 341
342 342 refs_to_create = [
343 343 (_("Branches"), repo.branches, 'branch'),
344 344 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
345 345 # TODO: enable when vcs can handle bookmarks filters
346 346 # (_("Bookmarks"), repo.bookmarks, "book"),
347 347 ]
348 348 res = self._create_reference_data(
349 349 repo, self.db_repo_name, refs_to_create)
350 350 data = {
351 351 'more': False,
352 352 'results': res
353 353 }
354 354 return data
355 355
356 356 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
357 357 format_ref_id = get_format_ref_id(repo)
358 358
359 359 result = []
360 360 for title, refs, ref_type in refs_to_create:
361 361 if refs:
362 362 result.append({
363 363 'text': title,
364 364 'children': self._create_reference_items(
365 365 repo, full_repo_name, refs, ref_type,
366 366 format_ref_id),
367 367 })
368 368 return result
369 369
370 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
371 format_ref_id):
370 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
372 371 result = []
373 372 is_svn = h.is_svn(repo)
374 373 for ref_name, raw_id in refs.iteritems():
375 374 files_url = self._create_files_url(
376 375 repo, full_repo_name, ref_name, raw_id, is_svn)
377 376 result.append({
378 377 'text': ref_name,
379 378 'id': format_ref_id(ref_name, raw_id),
380 379 'raw_id': raw_id,
381 380 'type': ref_type,
382 381 'files_url': files_url,
382 'idx': 0,
383 383 })
384 384 return result
385 385
386 386 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
387 387 use_commit_id = '/' in ref_name or is_svn
388 388 return h.route_path(
389 389 'repo_files',
390 390 repo_name=full_repo_name,
391 391 f_path=ref_name if is_svn else '',
392 392 commit_id=raw_id if use_commit_id else ref_name,
393 393 _query=dict(at=ref_name))
@@ -1,2042 +1,2045 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202 get_error = _GetError()
203 203
204 204
205 205 class _ToolTip(object):
206 206
207 207 def __call__(self, tooltip_title, trim_at=50):
208 208 """
209 209 Special function just to wrap our text into nice formatted
210 210 autowrapped text
211 211
212 212 :param tooltip_title:
213 213 """
214 214 tooltip_title = escape(tooltip_title)
215 215 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
216 216 return tooltip_title
217 217 tooltip = _ToolTip()
218 218
219 219
220 def files_breadcrumbs(repo_name, commit_id, file_path):
220 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None):
221 221 if isinstance(file_path, str):
222 222 file_path = safe_unicode(file_path)
223 route_qry = {'at': at_ref} if at_ref else None
223 224
224 225 # TODO: johbo: Is this always a url like path, or is this operating
225 226 # system dependent?
226 227 path_segments = file_path.split('/')
227 228
228 229 repo_name_html = escape(repo_name)
229 230 if len(path_segments) == 1 and path_segments[0] == '':
230 231 url_segments = [repo_name_html]
231 232 else:
232 233 url_segments = [
233 234 link_to(
234 235 repo_name_html,
235 236 route_path(
236 237 'repo_files',
237 238 repo_name=repo_name,
238 239 commit_id=commit_id,
239 f_path=''),
240 class_='pjax-link')]
240 f_path='',
241 _query=route_qry),
242 )]
241 243
242 244 last_cnt = len(path_segments) - 1
243 245 for cnt, segment in enumerate(path_segments):
244 246 if not segment:
245 247 continue
246 248 segment_html = escape(segment)
247 249
248 250 if cnt != last_cnt:
249 251 url_segments.append(
250 252 link_to(
251 253 segment_html,
252 254 route_path(
253 255 'repo_files',
254 256 repo_name=repo_name,
255 257 commit_id=commit_id,
256 f_path='/'.join(path_segments[:cnt + 1])),
257 class_='pjax-link'))
258 f_path='/'.join(path_segments[:cnt + 1]),
259 _query=route_qry),
260 ))
258 261 else:
259 262 url_segments.append(segment_html)
260 263
261 264 return literal('/'.join(url_segments))
262 265
263 266
264 267 def code_highlight(code, lexer, formatter, use_hl_filter=False):
265 268 """
266 269 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
267 270
268 271 If ``outfile`` is given and a valid file object (an object
269 272 with a ``write`` method), the result will be written to it, otherwise
270 273 it is returned as a string.
271 274 """
272 275 if use_hl_filter:
273 276 # add HL filter
274 277 from rhodecode.lib.index import search_utils
275 278 lexer.add_filter(search_utils.ElasticSearchHLFilter())
276 279 return pygments.format(pygments.lex(code, lexer), formatter)
277 280
278 281
279 282 class CodeHtmlFormatter(HtmlFormatter):
280 283 """
281 284 My code Html Formatter for source codes
282 285 """
283 286
284 287 def wrap(self, source, outfile):
285 288 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
286 289
287 290 def _wrap_code(self, source):
288 291 for cnt, it in enumerate(source):
289 292 i, t = it
290 293 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
291 294 yield i, t
292 295
293 296 def _wrap_tablelinenos(self, inner):
294 297 dummyoutfile = StringIO.StringIO()
295 298 lncount = 0
296 299 for t, line in inner:
297 300 if t:
298 301 lncount += 1
299 302 dummyoutfile.write(line)
300 303
301 304 fl = self.linenostart
302 305 mw = len(str(lncount + fl - 1))
303 306 sp = self.linenospecial
304 307 st = self.linenostep
305 308 la = self.lineanchors
306 309 aln = self.anchorlinenos
307 310 nocls = self.noclasses
308 311 if sp:
309 312 lines = []
310 313
311 314 for i in range(fl, fl + lncount):
312 315 if i % st == 0:
313 316 if i % sp == 0:
314 317 if aln:
315 318 lines.append('<a href="#%s%d" class="special">%*d</a>' %
316 319 (la, i, mw, i))
317 320 else:
318 321 lines.append('<span class="special">%*d</span>' % (mw, i))
319 322 else:
320 323 if aln:
321 324 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
322 325 else:
323 326 lines.append('%*d' % (mw, i))
324 327 else:
325 328 lines.append('')
326 329 ls = '\n'.join(lines)
327 330 else:
328 331 lines = []
329 332 for i in range(fl, fl + lncount):
330 333 if i % st == 0:
331 334 if aln:
332 335 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 336 else:
334 337 lines.append('%*d' % (mw, i))
335 338 else:
336 339 lines.append('')
337 340 ls = '\n'.join(lines)
338 341
339 342 # in case you wonder about the seemingly redundant <div> here: since the
340 343 # content in the other cell also is wrapped in a div, some browsers in
341 344 # some configurations seem to mess up the formatting...
342 345 if nocls:
343 346 yield 0, ('<table class="%stable">' % self.cssclass +
344 347 '<tr><td><div class="linenodiv" '
345 348 'style="background-color: #f0f0f0; padding-right: 10px">'
346 349 '<pre style="line-height: 125%">' +
347 350 ls + '</pre></div></td><td id="hlcode" class="code">')
348 351 else:
349 352 yield 0, ('<table class="%stable">' % self.cssclass +
350 353 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
351 354 ls + '</pre></div></td><td id="hlcode" class="code">')
352 355 yield 0, dummyoutfile.getvalue()
353 356 yield 0, '</td></tr></table>'
354 357
355 358
356 359 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
357 360 def __init__(self, **kw):
358 361 # only show these line numbers if set
359 362 self.only_lines = kw.pop('only_line_numbers', [])
360 363 self.query_terms = kw.pop('query_terms', [])
361 364 self.max_lines = kw.pop('max_lines', 5)
362 365 self.line_context = kw.pop('line_context', 3)
363 366 self.url = kw.pop('url', None)
364 367
365 368 super(CodeHtmlFormatter, self).__init__(**kw)
366 369
367 370 def _wrap_code(self, source):
368 371 for cnt, it in enumerate(source):
369 372 i, t = it
370 373 t = '<pre>%s</pre>' % t
371 374 yield i, t
372 375
373 376 def _wrap_tablelinenos(self, inner):
374 377 yield 0, '<table class="code-highlight %stable">' % self.cssclass
375 378
376 379 last_shown_line_number = 0
377 380 current_line_number = 1
378 381
379 382 for t, line in inner:
380 383 if not t:
381 384 yield t, line
382 385 continue
383 386
384 387 if current_line_number in self.only_lines:
385 388 if last_shown_line_number + 1 != current_line_number:
386 389 yield 0, '<tr>'
387 390 yield 0, '<td class="line">...</td>'
388 391 yield 0, '<td id="hlcode" class="code"></td>'
389 392 yield 0, '</tr>'
390 393
391 394 yield 0, '<tr>'
392 395 if self.url:
393 396 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
394 397 self.url, current_line_number, current_line_number)
395 398 else:
396 399 yield 0, '<td class="line"><a href="">%i</a></td>' % (
397 400 current_line_number)
398 401 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
399 402 yield 0, '</tr>'
400 403
401 404 last_shown_line_number = current_line_number
402 405
403 406 current_line_number += 1
404 407
405 408 yield 0, '</table>'
406 409
407 410
408 411 def hsv_to_rgb(h, s, v):
409 412 """ Convert hsv color values to rgb """
410 413
411 414 if s == 0.0:
412 415 return v, v, v
413 416 i = int(h * 6.0) # XXX assume int() truncates!
414 417 f = (h * 6.0) - i
415 418 p = v * (1.0 - s)
416 419 q = v * (1.0 - s * f)
417 420 t = v * (1.0 - s * (1.0 - f))
418 421 i = i % 6
419 422 if i == 0:
420 423 return v, t, p
421 424 if i == 1:
422 425 return q, v, p
423 426 if i == 2:
424 427 return p, v, t
425 428 if i == 3:
426 429 return p, q, v
427 430 if i == 4:
428 431 return t, p, v
429 432 if i == 5:
430 433 return v, p, q
431 434
432 435
433 436 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
434 437 """
435 438 Generator for getting n of evenly distributed colors using
436 439 hsv color and golden ratio. It always return same order of colors
437 440
438 441 :param n: number of colors to generate
439 442 :param saturation: saturation of returned colors
440 443 :param lightness: lightness of returned colors
441 444 :returns: RGB tuple
442 445 """
443 446
444 447 golden_ratio = 0.618033988749895
445 448 h = 0.22717784590367374
446 449
447 450 for _ in xrange(n):
448 451 h += golden_ratio
449 452 h %= 1
450 453 HSV_tuple = [h, saturation, lightness]
451 454 RGB_tuple = hsv_to_rgb(*HSV_tuple)
452 455 yield map(lambda x: str(int(x * 256)), RGB_tuple)
453 456
454 457
455 458 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
456 459 """
457 460 Returns a function which when called with an argument returns a unique
458 461 color for that argument, eg.
459 462
460 463 :param n: number of colors to generate
461 464 :param saturation: saturation of returned colors
462 465 :param lightness: lightness of returned colors
463 466 :returns: css RGB string
464 467
465 468 >>> color_hash = color_hasher()
466 469 >>> color_hash('hello')
467 470 'rgb(34, 12, 59)'
468 471 >>> color_hash('hello')
469 472 'rgb(34, 12, 59)'
470 473 >>> color_hash('other')
471 474 'rgb(90, 224, 159)'
472 475 """
473 476
474 477 color_dict = {}
475 478 cgenerator = unique_color_generator(
476 479 saturation=saturation, lightness=lightness)
477 480
478 481 def get_color_string(thing):
479 482 if thing in color_dict:
480 483 col = color_dict[thing]
481 484 else:
482 485 col = color_dict[thing] = cgenerator.next()
483 486 return "rgb(%s)" % (', '.join(col))
484 487
485 488 return get_color_string
486 489
487 490
488 491 def get_lexer_safe(mimetype=None, filepath=None):
489 492 """
490 493 Tries to return a relevant pygments lexer using mimetype/filepath name,
491 494 defaulting to plain text if none could be found
492 495 """
493 496 lexer = None
494 497 try:
495 498 if mimetype:
496 499 lexer = get_lexer_for_mimetype(mimetype)
497 500 if not lexer:
498 501 lexer = get_lexer_for_filename(filepath)
499 502 except pygments.util.ClassNotFound:
500 503 pass
501 504
502 505 if not lexer:
503 506 lexer = get_lexer_by_name('text')
504 507
505 508 return lexer
506 509
507 510
508 511 def get_lexer_for_filenode(filenode):
509 512 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
510 513 return lexer
511 514
512 515
513 516 def pygmentize(filenode, **kwargs):
514 517 """
515 518 pygmentize function using pygments
516 519
517 520 :param filenode:
518 521 """
519 522 lexer = get_lexer_for_filenode(filenode)
520 523 return literal(code_highlight(filenode.content, lexer,
521 524 CodeHtmlFormatter(**kwargs)))
522 525
523 526
524 527 def is_following_repo(repo_name, user_id):
525 528 from rhodecode.model.scm import ScmModel
526 529 return ScmModel().is_following_repo(repo_name, user_id)
527 530
528 531
529 532 class _Message(object):
530 533 """A message returned by ``Flash.pop_messages()``.
531 534
532 535 Converting the message to a string returns the message text. Instances
533 536 also have the following attributes:
534 537
535 538 * ``message``: the message text.
536 539 * ``category``: the category specified when the message was created.
537 540 """
538 541
539 542 def __init__(self, category, message):
540 543 self.category = category
541 544 self.message = message
542 545
543 546 def __str__(self):
544 547 return self.message
545 548
546 549 __unicode__ = __str__
547 550
548 551 def __html__(self):
549 552 return escape(safe_unicode(self.message))
550 553
551 554
552 555 class Flash(object):
553 556 # List of allowed categories. If None, allow any category.
554 557 categories = ["warning", "notice", "error", "success"]
555 558
556 559 # Default category if none is specified.
557 560 default_category = "notice"
558 561
559 562 def __init__(self, session_key="flash", categories=None,
560 563 default_category=None):
561 564 """
562 565 Instantiate a ``Flash`` object.
563 566
564 567 ``session_key`` is the key to save the messages under in the user's
565 568 session.
566 569
567 570 ``categories`` is an optional list which overrides the default list
568 571 of categories.
569 572
570 573 ``default_category`` overrides the default category used for messages
571 574 when none is specified.
572 575 """
573 576 self.session_key = session_key
574 577 if categories is not None:
575 578 self.categories = categories
576 579 if default_category is not None:
577 580 self.default_category = default_category
578 581 if self.categories and self.default_category not in self.categories:
579 582 raise ValueError(
580 583 "unrecognized default category %r" % (self.default_category,))
581 584
582 585 def pop_messages(self, session=None, request=None):
583 586 """
584 587 Return all accumulated messages and delete them from the session.
585 588
586 589 The return value is a list of ``Message`` objects.
587 590 """
588 591 messages = []
589 592
590 593 if not session:
591 594 if not request:
592 595 request = get_current_request()
593 596 session = request.session
594 597
595 598 # Pop the 'old' pylons flash messages. They are tuples of the form
596 599 # (category, message)
597 600 for cat, msg in session.pop(self.session_key, []):
598 601 messages.append(_Message(cat, msg))
599 602
600 603 # Pop the 'new' pyramid flash messages for each category as list
601 604 # of strings.
602 605 for cat in self.categories:
603 606 for msg in session.pop_flash(queue=cat):
604 607 messages.append(_Message(cat, msg))
605 608 # Map messages from the default queue to the 'notice' category.
606 609 for msg in session.pop_flash():
607 610 messages.append(_Message('notice', msg))
608 611
609 612 session.save()
610 613 return messages
611 614
612 615 def json_alerts(self, session=None, request=None):
613 616 payloads = []
614 617 messages = flash.pop_messages(session=session, request=request)
615 618 if messages:
616 619 for message in messages:
617 620 subdata = {}
618 621 if hasattr(message.message, 'rsplit'):
619 622 flash_data = message.message.rsplit('|DELIM|', 1)
620 623 org_message = flash_data[0]
621 624 if len(flash_data) > 1:
622 625 subdata = json.loads(flash_data[1])
623 626 else:
624 627 org_message = message.message
625 628 payloads.append({
626 629 'message': {
627 630 'message': u'{}'.format(org_message),
628 631 'level': message.category,
629 632 'force': True,
630 633 'subdata': subdata
631 634 }
632 635 })
633 636 return json.dumps(payloads)
634 637
635 638 def __call__(self, message, category=None, ignore_duplicate=False,
636 639 session=None, request=None):
637 640
638 641 if not session:
639 642 if not request:
640 643 request = get_current_request()
641 644 session = request.session
642 645
643 646 session.flash(
644 647 message, queue=category, allow_duplicate=not ignore_duplicate)
645 648
646 649
647 650 flash = Flash()
648 651
649 652 #==============================================================================
650 653 # SCM FILTERS available via h.
651 654 #==============================================================================
652 655 from rhodecode.lib.vcs.utils import author_name, author_email
653 656 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
654 657 from rhodecode.model.db import User, ChangesetStatus
655 658
656 659 capitalize = lambda x: x.capitalize()
657 660 email = author_email
658 661 short_id = lambda x: x[:12]
659 662 hide_credentials = lambda x: ''.join(credentials_filter(x))
660 663
661 664
662 665 import pytz
663 666 import tzlocal
664 667 local_timezone = tzlocal.get_localzone()
665 668
666 669
667 670 def age_component(datetime_iso, value=None, time_is_local=False):
668 671 title = value or format_date(datetime_iso)
669 672 tzinfo = '+00:00'
670 673
671 674 # detect if we have a timezone info, otherwise, add it
672 675 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
673 676 force_timezone = os.environ.get('RC_TIMEZONE', '')
674 677 if force_timezone:
675 678 force_timezone = pytz.timezone(force_timezone)
676 679 timezone = force_timezone or local_timezone
677 680 offset = timezone.localize(datetime_iso).strftime('%z')
678 681 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
679 682
680 683 return literal(
681 684 '<time class="timeago tooltip" '
682 685 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
683 686 datetime_iso, title, tzinfo))
684 687
685 688
686 689 def _shorten_commit_id(commit_id, commit_len=None):
687 690 if commit_len is None:
688 691 request = get_current_request()
689 692 commit_len = request.call_context.visual.show_sha_length
690 693 return commit_id[:commit_len]
691 694
692 695
693 696 def show_id(commit, show_idx=None, commit_len=None):
694 697 """
695 698 Configurable function that shows ID
696 699 by default it's r123:fffeeefffeee
697 700
698 701 :param commit: commit instance
699 702 """
700 703 if show_idx is None:
701 704 request = get_current_request()
702 705 show_idx = request.call_context.visual.show_revision_number
703 706
704 707 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
705 708 if show_idx:
706 709 return 'r%s:%s' % (commit.idx, raw_id)
707 710 else:
708 711 return '%s' % (raw_id, )
709 712
710 713
711 714 def format_date(date):
712 715 """
713 716 use a standardized formatting for dates used in RhodeCode
714 717
715 718 :param date: date/datetime object
716 719 :return: formatted date
717 720 """
718 721
719 722 if date:
720 723 _fmt = "%a, %d %b %Y %H:%M:%S"
721 724 return safe_unicode(date.strftime(_fmt))
722 725
723 726 return u""
724 727
725 728
726 729 class _RepoChecker(object):
727 730
728 731 def __init__(self, backend_alias):
729 732 self._backend_alias = backend_alias
730 733
731 734 def __call__(self, repository):
732 735 if hasattr(repository, 'alias'):
733 736 _type = repository.alias
734 737 elif hasattr(repository, 'repo_type'):
735 738 _type = repository.repo_type
736 739 else:
737 740 _type = repository
738 741 return _type == self._backend_alias
739 742
740 743
741 744 is_git = _RepoChecker('git')
742 745 is_hg = _RepoChecker('hg')
743 746 is_svn = _RepoChecker('svn')
744 747
745 748
746 749 def get_repo_type_by_name(repo_name):
747 750 repo = Repository.get_by_repo_name(repo_name)
748 751 if repo:
749 752 return repo.repo_type
750 753
751 754
752 755 def is_svn_without_proxy(repository):
753 756 if is_svn(repository):
754 757 from rhodecode.model.settings import VcsSettingsModel
755 758 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
756 759 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
757 760 return False
758 761
759 762
760 763 def discover_user(author):
761 764 """
762 765 Tries to discover RhodeCode User based on the autho string. Author string
763 766 is typically `FirstName LastName <email@address.com>`
764 767 """
765 768
766 769 # if author is already an instance use it for extraction
767 770 if isinstance(author, User):
768 771 return author
769 772
770 773 # Valid email in the attribute passed, see if they're in the system
771 774 _email = author_email(author)
772 775 if _email != '':
773 776 user = User.get_by_email(_email, case_insensitive=True, cache=True)
774 777 if user is not None:
775 778 return user
776 779
777 780 # Maybe it's a username, we try to extract it and fetch by username ?
778 781 _author = author_name(author)
779 782 user = User.get_by_username(_author, case_insensitive=True, cache=True)
780 783 if user is not None:
781 784 return user
782 785
783 786 return None
784 787
785 788
786 789 def email_or_none(author):
787 790 # extract email from the commit string
788 791 _email = author_email(author)
789 792
790 793 # If we have an email, use it, otherwise
791 794 # see if it contains a username we can get an email from
792 795 if _email != '':
793 796 return _email
794 797 else:
795 798 user = User.get_by_username(
796 799 author_name(author), case_insensitive=True, cache=True)
797 800
798 801 if user is not None:
799 802 return user.email
800 803
801 804 # No valid email, not a valid user in the system, none!
802 805 return None
803 806
804 807
805 808 def link_to_user(author, length=0, **kwargs):
806 809 user = discover_user(author)
807 810 # user can be None, but if we have it already it means we can re-use it
808 811 # in the person() function, so we save 1 intensive-query
809 812 if user:
810 813 author = user
811 814
812 815 display_person = person(author, 'username_or_name_or_email')
813 816 if length:
814 817 display_person = shorter(display_person, length)
815 818
816 819 if user:
817 820 return link_to(
818 821 escape(display_person),
819 822 route_path('user_profile', username=user.username),
820 823 **kwargs)
821 824 else:
822 825 return escape(display_person)
823 826
824 827
825 828 def link_to_group(users_group_name, **kwargs):
826 829 return link_to(
827 830 escape(users_group_name),
828 831 route_path('user_group_profile', user_group_name=users_group_name),
829 832 **kwargs)
830 833
831 834
832 835 def person(author, show_attr="username_and_name"):
833 836 user = discover_user(author)
834 837 if user:
835 838 return getattr(user, show_attr)
836 839 else:
837 840 _author = author_name(author)
838 841 _email = email(author)
839 842 return _author or _email
840 843
841 844
842 845 def author_string(email):
843 846 if email:
844 847 user = User.get_by_email(email, case_insensitive=True, cache=True)
845 848 if user:
846 849 if user.first_name or user.last_name:
847 850 return '%s %s &lt;%s&gt;' % (
848 851 user.first_name, user.last_name, email)
849 852 else:
850 853 return email
851 854 else:
852 855 return email
853 856 else:
854 857 return None
855 858
856 859
857 860 def person_by_id(id_, show_attr="username_and_name"):
858 861 # attr to return from fetched user
859 862 person_getter = lambda usr: getattr(usr, show_attr)
860 863
861 864 #maybe it's an ID ?
862 865 if str(id_).isdigit() or isinstance(id_, int):
863 866 id_ = int(id_)
864 867 user = User.get(id_)
865 868 if user is not None:
866 869 return person_getter(user)
867 870 return id_
868 871
869 872
870 873 def gravatar_with_user(request, author, show_disabled=False):
871 874 _render = request.get_partial_renderer(
872 875 'rhodecode:templates/base/base.mako')
873 876 return _render('gravatar_with_user', author, show_disabled=show_disabled)
874 877
875 878
876 879 tags_paterns = OrderedDict((
877 880 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
878 881 '<div class="metatag" tag="lang">\\2</div>')),
879 882
880 883 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
881 884 '<div class="metatag" tag="see">see: \\1 </div>')),
882 885
883 886 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
884 887 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
885 888
886 889 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
887 890 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
888 891
889 892 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
890 893 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
891 894
892 895 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
893 896 '<div class="metatag" tag="state \\1">\\1</div>')),
894 897
895 898 # label in grey
896 899 ('label', (re.compile(r'\[([a-z]+)\]'),
897 900 '<div class="metatag" tag="label">\\1</div>')),
898 901
899 902 # generic catch all in grey
900 903 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
901 904 '<div class="metatag" tag="generic">\\1</div>')),
902 905 ))
903 906
904 907
905 908 def extract_metatags(value):
906 909 """
907 910 Extract supported meta-tags from given text value
908 911 """
909 912 tags = []
910 913 if not value:
911 914 return tags, ''
912 915
913 916 for key, val in tags_paterns.items():
914 917 pat, replace_html = val
915 918 tags.extend([(key, x.group()) for x in pat.finditer(value)])
916 919 value = pat.sub('', value)
917 920
918 921 return tags, value
919 922
920 923
921 924 def style_metatag(tag_type, value):
922 925 """
923 926 converts tags from value into html equivalent
924 927 """
925 928 if not value:
926 929 return ''
927 930
928 931 html_value = value
929 932 tag_data = tags_paterns.get(tag_type)
930 933 if tag_data:
931 934 pat, replace_html = tag_data
932 935 # convert to plain `unicode` instead of a markup tag to be used in
933 936 # regex expressions. safe_unicode doesn't work here
934 937 html_value = pat.sub(replace_html, unicode(value))
935 938
936 939 return html_value
937 940
938 941
939 942 def bool2icon(value, show_at_false=True):
940 943 """
941 944 Returns boolean value of a given value, represented as html element with
942 945 classes that will represent icons
943 946
944 947 :param value: given value to convert to html node
945 948 """
946 949
947 950 if value: # does bool conversion
948 951 return HTML.tag('i', class_="icon-true")
949 952 else: # not true as bool
950 953 if show_at_false:
951 954 return HTML.tag('i', class_="icon-false")
952 955 return HTML.tag('i')
953 956
954 957 #==============================================================================
955 958 # PERMS
956 959 #==============================================================================
957 960 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
958 961 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
959 962 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
960 963 csrf_token_key
961 964
962 965
963 966 #==============================================================================
964 967 # GRAVATAR URL
965 968 #==============================================================================
966 969 class InitialsGravatar(object):
967 970 def __init__(self, email_address, first_name, last_name, size=30,
968 971 background=None, text_color='#fff'):
969 972 self.size = size
970 973 self.first_name = first_name
971 974 self.last_name = last_name
972 975 self.email_address = email_address
973 976 self.background = background or self.str2color(email_address)
974 977 self.text_color = text_color
975 978
976 979 def get_color_bank(self):
977 980 """
978 981 returns a predefined list of colors that gravatars can use.
979 982 Those are randomized distinct colors that guarantee readability and
980 983 uniqueness.
981 984
982 985 generated with: http://phrogz.net/css/distinct-colors.html
983 986 """
984 987 return [
985 988 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
986 989 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
987 990 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
988 991 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
989 992 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
990 993 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
991 994 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
992 995 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
993 996 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
994 997 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
995 998 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
996 999 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
997 1000 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
998 1001 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
999 1002 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1000 1003 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1001 1004 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1002 1005 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1003 1006 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1004 1007 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1005 1008 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1006 1009 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1007 1010 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1008 1011 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1009 1012 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1010 1013 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1011 1014 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1012 1015 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1013 1016 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1014 1017 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1015 1018 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1016 1019 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1017 1020 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1018 1021 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1019 1022 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1020 1023 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1021 1024 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1022 1025 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1023 1026 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1024 1027 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1025 1028 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1026 1029 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1027 1030 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1028 1031 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1029 1032 '#4f8c46', '#368dd9', '#5c0073'
1030 1033 ]
1031 1034
1032 1035 def rgb_to_hex_color(self, rgb_tuple):
1033 1036 """
1034 1037 Converts an rgb_tuple passed to an hex color.
1035 1038
1036 1039 :param rgb_tuple: tuple with 3 ints represents rgb color space
1037 1040 """
1038 1041 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1039 1042
1040 1043 def email_to_int_list(self, email_str):
1041 1044 """
1042 1045 Get every byte of the hex digest value of email and turn it to integer.
1043 1046 It's going to be always between 0-255
1044 1047 """
1045 1048 digest = md5_safe(email_str.lower())
1046 1049 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1047 1050
1048 1051 def pick_color_bank_index(self, email_str, color_bank):
1049 1052 return self.email_to_int_list(email_str)[0] % len(color_bank)
1050 1053
1051 1054 def str2color(self, email_str):
1052 1055 """
1053 1056 Tries to map in a stable algorithm an email to color
1054 1057
1055 1058 :param email_str:
1056 1059 """
1057 1060 color_bank = self.get_color_bank()
1058 1061 # pick position (module it's length so we always find it in the
1059 1062 # bank even if it's smaller than 256 values
1060 1063 pos = self.pick_color_bank_index(email_str, color_bank)
1061 1064 return color_bank[pos]
1062 1065
1063 1066 def normalize_email(self, email_address):
1064 1067 import unicodedata
1065 1068 # default host used to fill in the fake/missing email
1066 1069 default_host = u'localhost'
1067 1070
1068 1071 if not email_address:
1069 1072 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1070 1073
1071 1074 email_address = safe_unicode(email_address)
1072 1075
1073 1076 if u'@' not in email_address:
1074 1077 email_address = u'%s@%s' % (email_address, default_host)
1075 1078
1076 1079 if email_address.endswith(u'@'):
1077 1080 email_address = u'%s%s' % (email_address, default_host)
1078 1081
1079 1082 email_address = unicodedata.normalize('NFKD', email_address)\
1080 1083 .encode('ascii', 'ignore')
1081 1084 return email_address
1082 1085
1083 1086 def get_initials(self):
1084 1087 """
1085 1088 Returns 2 letter initials calculated based on the input.
1086 1089 The algorithm picks first given email address, and takes first letter
1087 1090 of part before @, and then the first letter of server name. In case
1088 1091 the part before @ is in a format of `somestring.somestring2` it replaces
1089 1092 the server letter with first letter of somestring2
1090 1093
1091 1094 In case function was initialized with both first and lastname, this
1092 1095 overrides the extraction from email by first letter of the first and
1093 1096 last name. We add special logic to that functionality, In case Full name
1094 1097 is compound, like Guido Von Rossum, we use last part of the last name
1095 1098 (Von Rossum) picking `R`.
1096 1099
1097 1100 Function also normalizes the non-ascii characters to they ascii
1098 1101 representation, eg Δ„ => A
1099 1102 """
1100 1103 import unicodedata
1101 1104 # replace non-ascii to ascii
1102 1105 first_name = unicodedata.normalize(
1103 1106 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1104 1107 last_name = unicodedata.normalize(
1105 1108 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1106 1109
1107 1110 # do NFKD encoding, and also make sure email has proper format
1108 1111 email_address = self.normalize_email(self.email_address)
1109 1112
1110 1113 # first push the email initials
1111 1114 prefix, server = email_address.split('@', 1)
1112 1115
1113 1116 # check if prefix is maybe a 'first_name.last_name' syntax
1114 1117 _dot_split = prefix.rsplit('.', 1)
1115 1118 if len(_dot_split) == 2 and _dot_split[1]:
1116 1119 initials = [_dot_split[0][0], _dot_split[1][0]]
1117 1120 else:
1118 1121 initials = [prefix[0], server[0]]
1119 1122
1120 1123 # then try to replace either first_name or last_name
1121 1124 fn_letter = (first_name or " ")[0].strip()
1122 1125 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1123 1126
1124 1127 if fn_letter:
1125 1128 initials[0] = fn_letter
1126 1129
1127 1130 if ln_letter:
1128 1131 initials[1] = ln_letter
1129 1132
1130 1133 return ''.join(initials).upper()
1131 1134
1132 1135 def get_img_data_by_type(self, font_family, img_type):
1133 1136 default_user = """
1134 1137 <svg xmlns="http://www.w3.org/2000/svg"
1135 1138 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1136 1139 viewBox="-15 -10 439.165 429.164"
1137 1140
1138 1141 xml:space="preserve"
1139 1142 style="background:{background};" >
1140 1143
1141 1144 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1142 1145 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1143 1146 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1144 1147 168.596,153.916,216.671,
1145 1148 204.583,216.671z" fill="{text_color}"/>
1146 1149 <path d="M407.164,374.717L360.88,
1147 1150 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1148 1151 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1149 1152 15.366-44.203,23.488-69.076,23.488c-24.877,
1150 1153 0-48.762-8.122-69.078-23.488
1151 1154 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1152 1155 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1153 1156 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1154 1157 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1155 1158 19.402-10.527 C409.699,390.129,
1156 1159 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1157 1160 </svg>""".format(
1158 1161 size=self.size,
1159 1162 background='#979797', # @grey4
1160 1163 text_color=self.text_color,
1161 1164 font_family=font_family)
1162 1165
1163 1166 return {
1164 1167 "default_user": default_user
1165 1168 }[img_type]
1166 1169
1167 1170 def get_img_data(self, svg_type=None):
1168 1171 """
1169 1172 generates the svg metadata for image
1170 1173 """
1171 1174 fonts = [
1172 1175 '-apple-system',
1173 1176 'BlinkMacSystemFont',
1174 1177 'Segoe UI',
1175 1178 'Roboto',
1176 1179 'Oxygen-Sans',
1177 1180 'Ubuntu',
1178 1181 'Cantarell',
1179 1182 'Helvetica Neue',
1180 1183 'sans-serif'
1181 1184 ]
1182 1185 font_family = ','.join(fonts)
1183 1186 if svg_type:
1184 1187 return self.get_img_data_by_type(font_family, svg_type)
1185 1188
1186 1189 initials = self.get_initials()
1187 1190 img_data = """
1188 1191 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1189 1192 width="{size}" height="{size}"
1190 1193 style="width: 100%; height: 100%; background-color: {background}"
1191 1194 viewBox="0 0 {size} {size}">
1192 1195 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1193 1196 pointer-events="auto" fill="{text_color}"
1194 1197 font-family="{font_family}"
1195 1198 style="font-weight: 400; font-size: {f_size}px;">{text}
1196 1199 </text>
1197 1200 </svg>""".format(
1198 1201 size=self.size,
1199 1202 f_size=self.size/2.05, # scale the text inside the box nicely
1200 1203 background=self.background,
1201 1204 text_color=self.text_color,
1202 1205 text=initials.upper(),
1203 1206 font_family=font_family)
1204 1207
1205 1208 return img_data
1206 1209
1207 1210 def generate_svg(self, svg_type=None):
1208 1211 img_data = self.get_img_data(svg_type)
1209 1212 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1210 1213
1211 1214
1212 1215 def initials_gravatar(email_address, first_name, last_name, size=30):
1213 1216 svg_type = None
1214 1217 if email_address == User.DEFAULT_USER_EMAIL:
1215 1218 svg_type = 'default_user'
1216 1219 klass = InitialsGravatar(email_address, first_name, last_name, size)
1217 1220 return klass.generate_svg(svg_type=svg_type)
1218 1221
1219 1222
1220 1223 def gravatar_url(email_address, size=30, request=None):
1221 1224 request = get_current_request()
1222 1225 _use_gravatar = request.call_context.visual.use_gravatar
1223 1226 _gravatar_url = request.call_context.visual.gravatar_url
1224 1227
1225 1228 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1226 1229
1227 1230 email_address = email_address or User.DEFAULT_USER_EMAIL
1228 1231 if isinstance(email_address, unicode):
1229 1232 # hashlib crashes on unicode items
1230 1233 email_address = safe_str(email_address)
1231 1234
1232 1235 # empty email or default user
1233 1236 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1234 1237 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1235 1238
1236 1239 if _use_gravatar:
1237 1240 # TODO: Disuse pyramid thread locals. Think about another solution to
1238 1241 # get the host and schema here.
1239 1242 request = get_current_request()
1240 1243 tmpl = safe_str(_gravatar_url)
1241 1244 tmpl = tmpl.replace('{email}', email_address)\
1242 1245 .replace('{md5email}', md5_safe(email_address.lower())) \
1243 1246 .replace('{netloc}', request.host)\
1244 1247 .replace('{scheme}', request.scheme)\
1245 1248 .replace('{size}', safe_str(size))
1246 1249 return tmpl
1247 1250 else:
1248 1251 return initials_gravatar(email_address, '', '', size=size)
1249 1252
1250 1253
1251 1254 class Page(_Page):
1252 1255 """
1253 1256 Custom pager to match rendering style with paginator
1254 1257 """
1255 1258
1256 1259 def _get_pos(self, cur_page, max_page, items):
1257 1260 edge = (items / 2) + 1
1258 1261 if (cur_page <= edge):
1259 1262 radius = max(items / 2, items - cur_page)
1260 1263 elif (max_page - cur_page) < edge:
1261 1264 radius = (items - 1) - (max_page - cur_page)
1262 1265 else:
1263 1266 radius = items / 2
1264 1267
1265 1268 left = max(1, (cur_page - (radius)))
1266 1269 right = min(max_page, cur_page + (radius))
1267 1270 return left, cur_page, right
1268 1271
1269 1272 def _range(self, regexp_match):
1270 1273 """
1271 1274 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1272 1275
1273 1276 Arguments:
1274 1277
1275 1278 regexp_match
1276 1279 A "re" (regular expressions) match object containing the
1277 1280 radius of linked pages around the current page in
1278 1281 regexp_match.group(1) as a string
1279 1282
1280 1283 This function is supposed to be called as a callable in
1281 1284 re.sub.
1282 1285
1283 1286 """
1284 1287 radius = int(regexp_match.group(1))
1285 1288
1286 1289 # Compute the first and last page number within the radius
1287 1290 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1288 1291 # -> leftmost_page = 5
1289 1292 # -> rightmost_page = 9
1290 1293 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1291 1294 self.last_page,
1292 1295 (radius * 2) + 1)
1293 1296 nav_items = []
1294 1297
1295 1298 # Create a link to the first page (unless we are on the first page
1296 1299 # or there would be no need to insert '..' spacers)
1297 1300 if self.page != self.first_page and self.first_page < leftmost_page:
1298 1301 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1299 1302
1300 1303 # Insert dots if there are pages between the first page
1301 1304 # and the currently displayed page range
1302 1305 if leftmost_page - self.first_page > 1:
1303 1306 # Wrap in a SPAN tag if nolink_attr is set
1304 1307 text = '..'
1305 1308 if self.dotdot_attr:
1306 1309 text = HTML.span(c=text, **self.dotdot_attr)
1307 1310 nav_items.append(text)
1308 1311
1309 1312 for thispage in xrange(leftmost_page, rightmost_page + 1):
1310 1313 # Hilight the current page number and do not use a link
1311 1314 if thispage == self.page:
1312 1315 text = '%s' % (thispage,)
1313 1316 # Wrap in a SPAN tag if nolink_attr is set
1314 1317 if self.curpage_attr:
1315 1318 text = HTML.span(c=text, **self.curpage_attr)
1316 1319 nav_items.append(text)
1317 1320 # Otherwise create just a link to that page
1318 1321 else:
1319 1322 text = '%s' % (thispage,)
1320 1323 nav_items.append(self._pagerlink(thispage, text))
1321 1324
1322 1325 # Insert dots if there are pages between the displayed
1323 1326 # page numbers and the end of the page range
1324 1327 if self.last_page - rightmost_page > 1:
1325 1328 text = '..'
1326 1329 # Wrap in a SPAN tag if nolink_attr is set
1327 1330 if self.dotdot_attr:
1328 1331 text = HTML.span(c=text, **self.dotdot_attr)
1329 1332 nav_items.append(text)
1330 1333
1331 1334 # Create a link to the very last page (unless we are on the last
1332 1335 # page or there would be no need to insert '..' spacers)
1333 1336 if self.page != self.last_page and rightmost_page < self.last_page:
1334 1337 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1335 1338
1336 1339 ## prerender links
1337 1340 #_page_link = url.current()
1338 1341 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1339 1342 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1340 1343 return self.separator.join(nav_items)
1341 1344
1342 1345 def pager(self, format='~2~', page_param='page', partial_param='partial',
1343 1346 show_if_single_page=False, separator=' ', onclick=None,
1344 1347 symbol_first='<<', symbol_last='>>',
1345 1348 symbol_previous='<', symbol_next='>',
1346 1349 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1347 1350 curpage_attr={'class': 'pager_curpage'},
1348 1351 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1349 1352
1350 1353 self.curpage_attr = curpage_attr
1351 1354 self.separator = separator
1352 1355 self.pager_kwargs = kwargs
1353 1356 self.page_param = page_param
1354 1357 self.partial_param = partial_param
1355 1358 self.onclick = onclick
1356 1359 self.link_attr = link_attr
1357 1360 self.dotdot_attr = dotdot_attr
1358 1361
1359 1362 # Don't show navigator if there is no more than one page
1360 1363 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1361 1364 return ''
1362 1365
1363 1366 from string import Template
1364 1367 # Replace ~...~ in token format by range of pages
1365 1368 result = re.sub(r'~(\d+)~', self._range, format)
1366 1369
1367 1370 # Interpolate '%' variables
1368 1371 result = Template(result).safe_substitute({
1369 1372 'first_page': self.first_page,
1370 1373 'last_page': self.last_page,
1371 1374 'page': self.page,
1372 1375 'page_count': self.page_count,
1373 1376 'items_per_page': self.items_per_page,
1374 1377 'first_item': self.first_item,
1375 1378 'last_item': self.last_item,
1376 1379 'item_count': self.item_count,
1377 1380 'link_first': self.page > self.first_page and \
1378 1381 self._pagerlink(self.first_page, symbol_first) or '',
1379 1382 'link_last': self.page < self.last_page and \
1380 1383 self._pagerlink(self.last_page, symbol_last) or '',
1381 1384 'link_previous': self.previous_page and \
1382 1385 self._pagerlink(self.previous_page, symbol_previous) \
1383 1386 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1384 1387 'link_next': self.next_page and \
1385 1388 self._pagerlink(self.next_page, symbol_next) \
1386 1389 or HTML.span(symbol_next, class_="pg-next disabled")
1387 1390 })
1388 1391
1389 1392 return literal(result)
1390 1393
1391 1394
1392 1395 #==============================================================================
1393 1396 # REPO PAGER, PAGER FOR REPOSITORY
1394 1397 #==============================================================================
1395 1398 class RepoPage(Page):
1396 1399
1397 1400 def __init__(self, collection, page=1, items_per_page=20,
1398 1401 item_count=None, url=None, **kwargs):
1399 1402
1400 1403 """Create a "RepoPage" instance. special pager for paging
1401 1404 repository
1402 1405 """
1403 1406 self._url_generator = url
1404 1407
1405 1408 # Safe the kwargs class-wide so they can be used in the pager() method
1406 1409 self.kwargs = kwargs
1407 1410
1408 1411 # Save a reference to the collection
1409 1412 self.original_collection = collection
1410 1413
1411 1414 self.collection = collection
1412 1415
1413 1416 # The self.page is the number of the current page.
1414 1417 # The first page has the number 1!
1415 1418 try:
1416 1419 self.page = int(page) # make it int() if we get it as a string
1417 1420 except (ValueError, TypeError):
1418 1421 self.page = 1
1419 1422
1420 1423 self.items_per_page = items_per_page
1421 1424
1422 1425 # Unless the user tells us how many items the collections has
1423 1426 # we calculate that ourselves.
1424 1427 if item_count is not None:
1425 1428 self.item_count = item_count
1426 1429 else:
1427 1430 self.item_count = len(self.collection)
1428 1431
1429 1432 # Compute the number of the first and last available page
1430 1433 if self.item_count > 0:
1431 1434 self.first_page = 1
1432 1435 self.page_count = int(math.ceil(float(self.item_count) /
1433 1436 self.items_per_page))
1434 1437 self.last_page = self.first_page + self.page_count - 1
1435 1438
1436 1439 # Make sure that the requested page number is the range of
1437 1440 # valid pages
1438 1441 if self.page > self.last_page:
1439 1442 self.page = self.last_page
1440 1443 elif self.page < self.first_page:
1441 1444 self.page = self.first_page
1442 1445
1443 1446 # Note: the number of items on this page can be less than
1444 1447 # items_per_page if the last page is not full
1445 1448 self.first_item = max(0, (self.item_count) - (self.page *
1446 1449 items_per_page))
1447 1450 self.last_item = ((self.item_count - 1) - items_per_page *
1448 1451 (self.page - 1))
1449 1452
1450 1453 self.items = list(self.collection[self.first_item:self.last_item + 1])
1451 1454
1452 1455 # Links to previous and next page
1453 1456 if self.page > self.first_page:
1454 1457 self.previous_page = self.page - 1
1455 1458 else:
1456 1459 self.previous_page = None
1457 1460
1458 1461 if self.page < self.last_page:
1459 1462 self.next_page = self.page + 1
1460 1463 else:
1461 1464 self.next_page = None
1462 1465
1463 1466 # No items available
1464 1467 else:
1465 1468 self.first_page = None
1466 1469 self.page_count = 0
1467 1470 self.last_page = None
1468 1471 self.first_item = None
1469 1472 self.last_item = None
1470 1473 self.previous_page = None
1471 1474 self.next_page = None
1472 1475 self.items = []
1473 1476
1474 1477 # This is a subclass of the 'list' type. Initialise the list now.
1475 1478 list.__init__(self, reversed(self.items))
1476 1479
1477 1480
1478 1481 def breadcrumb_repo_link(repo):
1479 1482 """
1480 1483 Makes a breadcrumbs path link to repo
1481 1484
1482 1485 ex::
1483 1486 group >> subgroup >> repo
1484 1487
1485 1488 :param repo: a Repository instance
1486 1489 """
1487 1490
1488 1491 path = [
1489 1492 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1490 1493 for group in repo.groups_with_parents
1491 1494 ] + [
1492 1495 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1493 1496 ]
1494 1497
1495 1498 return literal(' &raquo; '.join(path))
1496 1499
1497 1500
1498 1501 def breadcrumb_repo_group_link(repo_group):
1499 1502 """
1500 1503 Makes a breadcrumbs path link to repo
1501 1504
1502 1505 ex::
1503 1506 group >> subgroup
1504 1507
1505 1508 :param repo_group: a Repository Group instance
1506 1509 """
1507 1510
1508 1511 path = [
1509 1512 link_to(group.name,
1510 1513 route_path('repo_group_home', repo_group_name=group.group_name))
1511 1514 for group in repo_group.parents
1512 1515 ] + [
1513 1516 link_to(repo_group.name,
1514 1517 route_path('repo_group_home', repo_group_name=repo_group.group_name))
1515 1518 ]
1516 1519
1517 1520 return literal(' &raquo; '.join(path))
1518 1521
1519 1522
1520 1523 def format_byte_size_binary(file_size):
1521 1524 """
1522 1525 Formats file/folder sizes to standard.
1523 1526 """
1524 1527 if file_size is None:
1525 1528 file_size = 0
1526 1529
1527 1530 formatted_size = format_byte_size(file_size, binary=True)
1528 1531 return formatted_size
1529 1532
1530 1533
1531 1534 def urlify_text(text_, safe=True):
1532 1535 """
1533 1536 Extrac urls from text and make html links out of them
1534 1537
1535 1538 :param text_:
1536 1539 """
1537 1540
1538 1541 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1539 1542 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1540 1543
1541 1544 def url_func(match_obj):
1542 1545 url_full = match_obj.groups()[0]
1543 1546 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1544 1547 _newtext = url_pat.sub(url_func, text_)
1545 1548 if safe:
1546 1549 return literal(_newtext)
1547 1550 return _newtext
1548 1551
1549 1552
1550 1553 def urlify_commits(text_, repository):
1551 1554 """
1552 1555 Extract commit ids from text and make link from them
1553 1556
1554 1557 :param text_:
1555 1558 :param repository: repo name to build the URL with
1556 1559 """
1557 1560
1558 1561 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1559 1562
1560 1563 def url_func(match_obj):
1561 1564 commit_id = match_obj.groups()[1]
1562 1565 pref = match_obj.groups()[0]
1563 1566 suf = match_obj.groups()[2]
1564 1567
1565 1568 tmpl = (
1566 1569 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1567 1570 '%(commit_id)s</a>%(suf)s'
1568 1571 )
1569 1572 return tmpl % {
1570 1573 'pref': pref,
1571 1574 'cls': 'revision-link',
1572 1575 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1573 1576 'commit_id': commit_id,
1574 1577 'suf': suf
1575 1578 }
1576 1579
1577 1580 newtext = URL_PAT.sub(url_func, text_)
1578 1581
1579 1582 return newtext
1580 1583
1581 1584
1582 1585 def _process_url_func(match_obj, repo_name, uid, entry,
1583 1586 return_raw_data=False, link_format='html'):
1584 1587 pref = ''
1585 1588 if match_obj.group().startswith(' '):
1586 1589 pref = ' '
1587 1590
1588 1591 issue_id = ''.join(match_obj.groups())
1589 1592
1590 1593 if link_format == 'html':
1591 1594 tmpl = (
1592 1595 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1593 1596 '%(issue-prefix)s%(id-repr)s'
1594 1597 '</a>')
1595 1598 elif link_format == 'rst':
1596 1599 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1597 1600 elif link_format == 'markdown':
1598 1601 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1599 1602 else:
1600 1603 raise ValueError('Bad link_format:{}'.format(link_format))
1601 1604
1602 1605 (repo_name_cleaned,
1603 1606 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1604 1607
1605 1608 # variables replacement
1606 1609 named_vars = {
1607 1610 'id': issue_id,
1608 1611 'repo': repo_name,
1609 1612 'repo_name': repo_name_cleaned,
1610 1613 'group_name': parent_group_name
1611 1614 }
1612 1615 # named regex variables
1613 1616 named_vars.update(match_obj.groupdict())
1614 1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1615 1618
1616 1619 def quote_cleaner(input_str):
1617 1620 """Remove quotes as it's HTML"""
1618 1621 return input_str.replace('"', '')
1619 1622
1620 1623 data = {
1621 1624 'pref': pref,
1622 1625 'cls': quote_cleaner('issue-tracker-link'),
1623 1626 'url': quote_cleaner(_url),
1624 1627 'id-repr': issue_id,
1625 1628 'issue-prefix': entry['pref'],
1626 1629 'serv': entry['url'],
1627 1630 }
1628 1631 if return_raw_data:
1629 1632 return {
1630 1633 'id': issue_id,
1631 1634 'url': _url
1632 1635 }
1633 1636 return tmpl % data
1634 1637
1635 1638
1636 1639 def get_active_pattern_entries(repo_name):
1637 1640 repo = None
1638 1641 if repo_name:
1639 1642 # Retrieving repo_name to avoid invalid repo_name to explode on
1640 1643 # IssueTrackerSettingsModel but still passing invalid name further down
1641 1644 repo = Repository.get_by_repo_name(repo_name, cache=True)
1642 1645
1643 1646 settings_model = IssueTrackerSettingsModel(repo=repo)
1644 1647 active_entries = settings_model.get_settings(cache=True)
1645 1648 return active_entries
1646 1649
1647 1650
1648 1651 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1649 1652
1650 1653 allowed_formats = ['html', 'rst', 'markdown']
1651 1654 if link_format not in allowed_formats:
1652 1655 raise ValueError('Link format can be only one of:{} got {}'.format(
1653 1656 allowed_formats, link_format))
1654 1657
1655 1658 active_entries = active_entries or get_active_pattern_entries(repo_name)
1656 1659 issues_data = []
1657 1660 newtext = text_string
1658 1661
1659 1662 for uid, entry in active_entries.items():
1660 1663 log.debug('found issue tracker entry with uid %s', uid)
1661 1664
1662 1665 if not (entry['pat'] and entry['url']):
1663 1666 log.debug('skipping due to missing data')
1664 1667 continue
1665 1668
1666 1669 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1667 1670 uid, entry['pat'], entry['url'], entry['pref'])
1668 1671
1669 1672 try:
1670 1673 pattern = re.compile(r'%s' % entry['pat'])
1671 1674 except re.error:
1672 1675 log.exception(
1673 1676 'issue tracker pattern: `%s` failed to compile',
1674 1677 entry['pat'])
1675 1678 continue
1676 1679
1677 1680 data_func = partial(
1678 1681 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1679 1682 return_raw_data=True)
1680 1683
1681 1684 for match_obj in pattern.finditer(text_string):
1682 1685 issues_data.append(data_func(match_obj))
1683 1686
1684 1687 url_func = partial(
1685 1688 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1686 1689 link_format=link_format)
1687 1690
1688 1691 newtext = pattern.sub(url_func, newtext)
1689 1692 log.debug('processed prefix:uid `%s`', uid)
1690 1693
1691 1694 return newtext, issues_data
1692 1695
1693 1696
1694 1697 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1695 1698 """
1696 1699 Parses given text message and makes proper links.
1697 1700 issues are linked to given issue-server, and rest is a commit link
1698 1701
1699 1702 :param commit_text:
1700 1703 :param repository:
1701 1704 """
1702 1705 def escaper(string):
1703 1706 return string.replace('<', '&lt;').replace('>', '&gt;')
1704 1707
1705 1708 newtext = escaper(commit_text)
1706 1709
1707 1710 # extract http/https links and make them real urls
1708 1711 newtext = urlify_text(newtext, safe=False)
1709 1712
1710 1713 # urlify commits - extract commit ids and make link out of them, if we have
1711 1714 # the scope of repository present.
1712 1715 if repository:
1713 1716 newtext = urlify_commits(newtext, repository)
1714 1717
1715 1718 # process issue tracker patterns
1716 1719 newtext, issues = process_patterns(newtext, repository or '',
1717 1720 active_entries=active_pattern_entries)
1718 1721
1719 1722 return literal(newtext)
1720 1723
1721 1724
1722 1725 def render_binary(repo_name, file_obj):
1723 1726 """
1724 1727 Choose how to render a binary file
1725 1728 """
1726 1729
1727 1730 filename = file_obj.name
1728 1731
1729 1732 # images
1730 1733 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1731 1734 if fnmatch.fnmatch(filename, pat=ext):
1732 1735 alt = escape(filename)
1733 1736 src = route_path(
1734 1737 'repo_file_raw', repo_name=repo_name,
1735 1738 commit_id=file_obj.commit.raw_id,
1736 1739 f_path=file_obj.path)
1737 1740 return literal(
1738 1741 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1739 1742
1740 1743
1741 1744 def renderer_from_filename(filename, exclude=None):
1742 1745 """
1743 1746 choose a renderer based on filename, this works only for text based files
1744 1747 """
1745 1748
1746 1749 # ipython
1747 1750 for ext in ['*.ipynb']:
1748 1751 if fnmatch.fnmatch(filename, pat=ext):
1749 1752 return 'jupyter'
1750 1753
1751 1754 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1752 1755 if is_markup:
1753 1756 return is_markup
1754 1757 return None
1755 1758
1756 1759
1757 1760 def render(source, renderer='rst', mentions=False, relative_urls=None,
1758 1761 repo_name=None):
1759 1762
1760 1763 def maybe_convert_relative_links(html_source):
1761 1764 if relative_urls:
1762 1765 return relative_links(html_source, relative_urls)
1763 1766 return html_source
1764 1767
1765 1768 if renderer == 'plain':
1766 1769 return literal(
1767 1770 MarkupRenderer.plain(source, leading_newline=False))
1768 1771
1769 1772 elif renderer == 'rst':
1770 1773 if repo_name:
1771 1774 # process patterns on comments if we pass in repo name
1772 1775 source, issues = process_patterns(
1773 1776 source, repo_name, link_format='rst')
1774 1777
1775 1778 return literal(
1776 1779 '<div class="rst-block">%s</div>' %
1777 1780 maybe_convert_relative_links(
1778 1781 MarkupRenderer.rst(source, mentions=mentions)))
1779 1782
1780 1783 elif renderer == 'markdown':
1781 1784 if repo_name:
1782 1785 # process patterns on comments if we pass in repo name
1783 1786 source, issues = process_patterns(
1784 1787 source, repo_name, link_format='markdown')
1785 1788
1786 1789 return literal(
1787 1790 '<div class="markdown-block">%s</div>' %
1788 1791 maybe_convert_relative_links(
1789 1792 MarkupRenderer.markdown(source, flavored=True,
1790 1793 mentions=mentions)))
1791 1794
1792 1795 elif renderer == 'jupyter':
1793 1796 return literal(
1794 1797 '<div class="ipynb">%s</div>' %
1795 1798 maybe_convert_relative_links(
1796 1799 MarkupRenderer.jupyter(source)))
1797 1800
1798 1801 # None means just show the file-source
1799 1802 return None
1800 1803
1801 1804
1802 1805 def commit_status(repo, commit_id):
1803 1806 return ChangesetStatusModel().get_status(repo, commit_id)
1804 1807
1805 1808
1806 1809 def commit_status_lbl(commit_status):
1807 1810 return dict(ChangesetStatus.STATUSES).get(commit_status)
1808 1811
1809 1812
1810 1813 def commit_time(repo_name, commit_id):
1811 1814 repo = Repository.get_by_repo_name(repo_name)
1812 1815 commit = repo.get_commit(commit_id=commit_id)
1813 1816 return commit.date
1814 1817
1815 1818
1816 1819 def get_permission_name(key):
1817 1820 return dict(Permission.PERMS).get(key)
1818 1821
1819 1822
1820 1823 def journal_filter_help(request):
1821 1824 _ = request.translate
1822 1825 from rhodecode.lib.audit_logger import ACTIONS
1823 1826 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1824 1827
1825 1828 return _(
1826 1829 'Example filter terms:\n' +
1827 1830 ' repository:vcs\n' +
1828 1831 ' username:marcin\n' +
1829 1832 ' username:(NOT marcin)\n' +
1830 1833 ' action:*push*\n' +
1831 1834 ' ip:127.0.0.1\n' +
1832 1835 ' date:20120101\n' +
1833 1836 ' date:[20120101100000 TO 20120102]\n' +
1834 1837 '\n' +
1835 1838 'Actions: {actions}\n' +
1836 1839 '\n' +
1837 1840 'Generate wildcards using \'*\' character:\n' +
1838 1841 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1839 1842 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1840 1843 '\n' +
1841 1844 'Optional AND / OR operators in queries\n' +
1842 1845 ' "repository:vcs OR repository:test"\n' +
1843 1846 ' "username:test AND repository:test*"\n'
1844 1847 ).format(actions=actions)
1845 1848
1846 1849
1847 1850 def not_mapped_error(repo_name):
1848 1851 from rhodecode.translation import _
1849 1852 flash(_('%s repository is not mapped to db perhaps'
1850 1853 ' it was created or renamed from the filesystem'
1851 1854 ' please run the application again'
1852 1855 ' in order to rescan repositories') % repo_name, category='error')
1853 1856
1854 1857
1855 1858 def ip_range(ip_addr):
1856 1859 from rhodecode.model.db import UserIpMap
1857 1860 s, e = UserIpMap._get_ip_range(ip_addr)
1858 1861 return '%s - %s' % (s, e)
1859 1862
1860 1863
1861 1864 def form(url, method='post', needs_csrf_token=True, **attrs):
1862 1865 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1863 1866 if method.lower() != 'get' and needs_csrf_token:
1864 1867 raise Exception(
1865 1868 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1866 1869 'CSRF token. If the endpoint does not require such token you can ' +
1867 1870 'explicitly set the parameter needs_csrf_token to false.')
1868 1871
1869 1872 return wh_form(url, method=method, **attrs)
1870 1873
1871 1874
1872 1875 def secure_form(form_url, method="POST", multipart=False, **attrs):
1873 1876 """Start a form tag that points the action to an url. This
1874 1877 form tag will also include the hidden field containing
1875 1878 the auth token.
1876 1879
1877 1880 The url options should be given either as a string, or as a
1878 1881 ``url()`` function. The method for the form defaults to POST.
1879 1882
1880 1883 Options:
1881 1884
1882 1885 ``multipart``
1883 1886 If set to True, the enctype is set to "multipart/form-data".
1884 1887 ``method``
1885 1888 The method to use when submitting the form, usually either
1886 1889 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1887 1890 hidden input with name _method is added to simulate the verb
1888 1891 over POST.
1889 1892
1890 1893 """
1891 1894 from webhelpers.pylonslib.secure_form import insecure_form
1892 1895
1893 1896 if 'request' in attrs:
1894 1897 session = attrs['request'].session
1895 1898 del attrs['request']
1896 1899 else:
1897 1900 raise ValueError(
1898 1901 'Calling this form requires request= to be passed as argument')
1899 1902
1900 1903 form = insecure_form(form_url, method, multipart, **attrs)
1901 1904 token = literal(
1902 1905 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1903 1906 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1904 1907
1905 1908 return literal("%s\n%s" % (form, token))
1906 1909
1907 1910
1908 1911 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1909 1912 select_html = select(name, selected, options, **attrs)
1910 1913 select2 = """
1911 1914 <script>
1912 1915 $(document).ready(function() {
1913 1916 $('#%s').select2({
1914 1917 containerCssClass: 'drop-menu',
1915 1918 dropdownCssClass: 'drop-menu-dropdown',
1916 1919 dropdownAutoWidth: true%s
1917 1920 });
1918 1921 });
1919 1922 </script>
1920 1923 """
1921 1924 filter_option = """,
1922 1925 minimumResultsForSearch: -1
1923 1926 """
1924 1927 input_id = attrs.get('id') or name
1925 1928 filter_enabled = "" if enable_filter else filter_option
1926 1929 select_script = literal(select2 % (input_id, filter_enabled))
1927 1930
1928 1931 return literal(select_html+select_script)
1929 1932
1930 1933
1931 1934 def get_visual_attr(tmpl_context_var, attr_name):
1932 1935 """
1933 1936 A safe way to get a variable from visual variable of template context
1934 1937
1935 1938 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1936 1939 :param attr_name: name of the attribute we fetch from the c.visual
1937 1940 """
1938 1941 visual = getattr(tmpl_context_var, 'visual', None)
1939 1942 if not visual:
1940 1943 return
1941 1944 else:
1942 1945 return getattr(visual, attr_name, None)
1943 1946
1944 1947
1945 1948 def get_last_path_part(file_node):
1946 1949 if not file_node.path:
1947 1950 return u''
1948 1951
1949 1952 path = safe_unicode(file_node.path.split('/')[-1])
1950 1953 return u'../' + path
1951 1954
1952 1955
1953 1956 def route_url(*args, **kwargs):
1954 1957 """
1955 1958 Wrapper around pyramids `route_url` (fully qualified url) function.
1956 1959 """
1957 1960 req = get_current_request()
1958 1961 return req.route_url(*args, **kwargs)
1959 1962
1960 1963
1961 1964 def route_path(*args, **kwargs):
1962 1965 """
1963 1966 Wrapper around pyramids `route_path` function.
1964 1967 """
1965 1968 req = get_current_request()
1966 1969 return req.route_path(*args, **kwargs)
1967 1970
1968 1971
1969 1972 def route_path_or_none(*args, **kwargs):
1970 1973 try:
1971 1974 return route_path(*args, **kwargs)
1972 1975 except KeyError:
1973 1976 return None
1974 1977
1975 1978
1976 1979 def current_route_path(request, **kw):
1977 1980 new_args = request.GET.mixed()
1978 1981 new_args.update(kw)
1979 1982 return request.current_route_path(_query=new_args)
1980 1983
1981 1984
1982 1985 def api_call_example(method, args):
1983 1986 """
1984 1987 Generates an API call example via CURL
1985 1988 """
1986 1989 args_json = json.dumps(OrderedDict([
1987 1990 ('id', 1),
1988 1991 ('auth_token', 'SECRET'),
1989 1992 ('method', method),
1990 1993 ('args', args)
1991 1994 ]))
1992 1995 return literal(
1993 1996 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
1994 1997 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1995 1998 "and needs to be of `api calls` role."
1996 1999 .format(
1997 2000 api_url=route_url('apiv2'),
1998 2001 token_url=route_url('my_account_auth_tokens'),
1999 2002 data=args_json))
2000 2003
2001 2004
2002 2005 def notification_description(notification, request):
2003 2006 """
2004 2007 Generate notification human readable description based on notification type
2005 2008 """
2006 2009 from rhodecode.model.notification import NotificationModel
2007 2010 return NotificationModel().make_description(
2008 2011 notification, translate=request.translate)
2009 2012
2010 2013
2011 2014 def go_import_header(request, db_repo=None):
2012 2015 """
2013 2016 Creates a header for go-import functionality in Go Lang
2014 2017 """
2015 2018
2016 2019 if not db_repo:
2017 2020 return
2018 2021 if 'go-get' not in request.GET:
2019 2022 return
2020 2023
2021 2024 clone_url = db_repo.clone_url()
2022 2025 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2023 2026 # we have a repo and go-get flag,
2024 2027 return literal('<meta name="go-import" content="{} {} {}">'.format(
2025 2028 prefix, db_repo.repo_type, clone_url))
2026 2029
2027 2030
2028 2031 def reviewer_as_json(*args, **kwargs):
2029 2032 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2030 2033 return _reviewer_as_json(*args, **kwargs)
2031 2034
2032 2035
2033 2036 def get_repo_view_type(request):
2034 2037 route_name = request.matched_route.name
2035 2038 route_to_view_type = {
2036 2039 'repo_changelog': 'changelog',
2037 2040 'repo_files': 'files',
2038 2041 'repo_summary': 'summary',
2039 2042 'repo_commit': 'commit'
2040 2043 }
2041 2044
2042 2045 return route_to_view_type.get(route_name)
@@ -1,310 +1,310 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Search file list
21 21 */
22 22 // global reference to file-node filter
23 23 var _NODEFILTER = {};
24 24
25 25 var fileBrowserListeners = function(node_list_url, url_base){
26 26 var n_filter = $('#node_filter').get(0);
27 27
28 28 _NODEFILTER.filterTimeout = null;
29 29 var nodes = null;
30 30
31 31 _NODEFILTER.fetchNodes = function(callback) {
32 32 $.ajax({url: node_list_url, headers: {'X-PARTIAL-XHR': true}})
33 33 .done(function(data){
34 34 nodes = data.nodes;
35 35 if (callback) {
36 36 callback();
37 37 }
38 38 })
39 39 .fail(function(data){
40 40 console.log('failed to load');
41 41 });
42 42 };
43 43
44 44 _NODEFILTER.fetchNodesCallback = function() {
45 45 $('#node_filter_box_loading').hide();
46 46 $('#node_filter_box').removeClass('hidden').show();
47 47 n_filter.focus();
48 48 if ($('#node_filter').hasClass('init')){
49 49 n_filter.value = '';
50 50 $('#node_filter').removeClass('init');
51 51 }
52 52 };
53 53
54 54 _NODEFILTER.initFilter = function(){
55 55 $('#node_filter_box_loading').removeClass('hidden').show();
56 56 $('#search_activate_id').hide();
57 57 $('#search_deactivate_id').removeClass('hidden').show();
58 58 $('#add_node_id').hide();
59 59 _NODEFILTER.fetchNodes(_NODEFILTER.fetchNodesCallback);
60 60 };
61 61
62 62 _NODEFILTER.resetFilter = function(){
63 63 $('#node_filter_box_loading').hide();
64 64 $('#node_filter_box').hide();
65 65 $('#search_activate_id').show();
66 66 $('#search_deactivate_id').hide();
67 67 $('#add_node_id').show();
68 68 $('#tbody').show();
69 69 $('#tbody_filtered').hide();
70 70 $('#node_filter').val('');
71 71 };
72 72
73 73 _NODEFILTER.fuzzy_match = function(filepath, query) {
74 74 var highlight = [];
75 75 var order = 0;
76 76 for (var i = 0; i < query.length; i++) {
77 77 var match_position = filepath.indexOf(query[i]);
78 78 if (match_position !== -1) {
79 79 var prev_match_position = highlight[highlight.length-1];
80 80 if (prev_match_position === undefined) {
81 81 highlight.push(match_position);
82 82 } else {
83 83 var current_match_position = prev_match_position + match_position + 1;
84 84 highlight.push(current_match_position);
85 85 order = order + current_match_position - prev_match_position;
86 86 }
87 87 filepath = filepath.substring(match_position+1);
88 88 } else {
89 89 return false;
90 90 }
91 91 }
92 92 return {'order': order,
93 93 'highlight': highlight};
94 94 };
95 95
96 96 _NODEFILTER.sortPredicate = function(a, b) {
97 97 if (a.order < b.order) return -1;
98 98 if (a.order > b.order) return 1;
99 99 if (a.filepath < b.filepath) return -1;
100 100 if (a.filepath > b.filepath) return 1;
101 101 return 0;
102 102 };
103 103
104 104 _NODEFILTER.updateFilter = function(elem, e) {
105 105 return function(){
106 106 // Reset timeout
107 107 _NODEFILTER.filterTimeout = null;
108 108 var query = elem.value.toLowerCase();
109 109 var match = [];
110 110 var matches_max = 20;
111 111 if (query !== ""){
112 112 var results = [];
113 113 for(var k=0;k<nodes.length;k++){
114 114 var result = _NODEFILTER.fuzzy_match(
115 115 nodes[k].name.toLowerCase(), query);
116 116 if (result) {
117 117 result.type = nodes[k].type;
118 118 result.filepath = nodes[k].name;
119 119 results.push(result);
120 120 }
121 121 }
122 122 results = results.sort(_NODEFILTER.sortPredicate);
123 123 var limit = matches_max;
124 124 if (results.length < matches_max) {
125 125 limit = results.length;
126 126 }
127 127 for (var i=0; i<limit; i++){
128 128 if(query && results.length > 0){
129 129 var n = results[i].filepath;
130 130 var t = results[i].type;
131 131 var n_hl = n.split("");
132 132 var pos = results[i].highlight;
133 133 for (var j = 0; j < pos.length; j++) {
134 134 n_hl[pos[j]] = "<em>" + n_hl[pos[j]] + "</em>";
135 135 }
136 136 n_hl = n_hl.join("");
137 137 var new_url = url_base.replace('__FPATH__',n);
138 138
139 139 var typeObj = {
140 140 dir: 'icon-directory browser-dir',
141 141 file: 'icon-file-text browser-file'
142 142 };
143 143
144 144 var typeIcon = '<i class="{0}"></i>'.format(typeObj[t]);
145 match.push('<tr class="browser-result"><td><a class="pjax-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url,typeIcon, n_hl));
145 match.push('<tr class="browser-result"><td><a class="match-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url,typeIcon, n_hl));
146 146 }
147 147 }
148 148 if(results.length > limit){
149 149 var truncated_count = results.length - matches_max;
150 150 if (truncated_count === 1) {
151 151 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated result')));
152 152 } else {
153 153 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated results')));
154 154 }
155 155 }
156 156 }
157 157 if (query !== ""){
158 158 $('#tbody').hide();
159 159 $('#tbody_filtered').show();
160 160
161 161 if (match.length === 0){
162 162 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_gettext('No matching files')));
163 163 }
164 164 $('#tbody_filtered').html(match.join(""));
165 165 }
166 166 else{
167 167 $('#tbody').show();
168 168 $('#tbody_filtered').hide();
169 169 }
170 170
171 171 };
172 172 };
173 173
174 174 var scrollDown = function(element){
175 175 var elementBottom = element.offset().top + $(element).outerHeight();
176 176 var windowBottom = window.innerHeight + $(window).scrollTop();
177 177 if (elementBottom > windowBottom) {
178 178 var offset = elementBottom - window.innerHeight;
179 179 $('html,body').scrollTop(offset);
180 180 return false;
181 181 }
182 182 return true;
183 183 };
184 184
185 185 var scrollUp = function(element){
186 186 if (element.offset().top < $(window).scrollTop()) {
187 187 $('html,body').scrollTop(element.offset().top);
188 188 return false;
189 189 }
190 190 return true;
191 191 };
192 192
193 193 $('#filter_activate').click(function() {
194 194 _NODEFILTER.initFilter();
195 195 });
196 196
197 197 $('#filter_deactivate').click(function() {
198 198 _NODEFILTER.resetFilter();
199 199 });
200 200
201 201 $(n_filter).click(function() {
202 202 if ($('#node_filter').hasClass('init')){
203 203 n_filter.value = '';
204 204 $('#node_filter').removeClass('init');
205 205 }
206 206 });
207 207
208 208 $(n_filter).keydown(function(e) {
209 209 if (e.keyCode === 40){ // Down
210 210 if ($('.browser-highlight').length === 0){
211 211 $('.browser-result').first().addClass('browser-highlight');
212 212 } else {
213 213 var next = $('.browser-highlight').next();
214 214 if (next.length !== 0) {
215 215 $('.browser-highlight').removeClass('browser-highlight');
216 216 next.addClass('browser-highlight');
217 217 }
218 218 }
219 219 scrollDown($('.browser-highlight'));
220 220 }
221 221 if (e.keyCode === 38){ // Up
222 222 e.preventDefault();
223 223 if ($('.browser-highlight').length !== 0){
224 224 var next = $('.browser-highlight').prev();
225 225 if (next.length !== 0) {
226 226 $('.browser-highlight').removeClass('browser-highlight');
227 227 next.addClass('browser-highlight');
228 228 }
229 229 }
230 230 scrollUp($('.browser-highlight'));
231 231 }
232 232 if (e.keyCode === 13){ // Enter
233 233 if ($('.browser-highlight').length !== 0){
234 var url = $('.browser-highlight').find('.pjax-link').attr('href');
235 $.pjax({url: url, container: '#pjax-container', timeout: pjaxTimeout});
234 var url = $('.browser-highlight').find('.match-link').attr('href');
235 window.location = url;
236 236 }
237 237 }
238 238 if (e.keyCode === 27){ // Esc
239 239 _NODEFILTER.resetFilter();
240 240 $('html,body').scrollTop(0);
241 241 }
242 242 });
243 243 var capture_keys = [40, 38, 39, 37, 13, 27];
244 244 $(n_filter).keyup(function(e) {
245 245 if ($.inArray(e.keyCode, capture_keys) === -1){
246 246 clearTimeout(_NODEFILTER.filterTimeout);
247 247 _NODEFILTER.filterTimeout = setTimeout(_NODEFILTER.updateFilter(n_filter, e),200);
248 248 }
249 249 });
250 250 };
251 251
252 252 var getIdentNode = function(n){
253 253 // iterate through nodes until matched interesting node
254 254 if (typeof n === 'undefined'){
255 255 return -1;
256 256 }
257 257 if(typeof n.id !== "undefined" && n.id.match('L[0-9]+')){
258 258 return n;
259 259 }
260 260 else{
261 261 return getIdentNode(n.parentNode);
262 262 }
263 263 };
264 264
265 265 var getSelectionLink = function(e) {
266 266 // get selection from start/to nodes
267 267 if (typeof window.getSelection !== "undefined") {
268 268 s = window.getSelection();
269 269
270 270 from = getIdentNode(s.anchorNode);
271 271 till = getIdentNode(s.focusNode);
272 272
273 273 f_int = parseInt(from.id.replace('L',''));
274 274 t_int = parseInt(till.id.replace('L',''));
275 275
276 276 if (f_int > t_int){
277 277 // highlight from bottom
278 278 offset = -35;
279 279 ranges = [t_int,f_int];
280 280 }
281 281 else{
282 282 // highligth from top
283 283 offset = 35;
284 284 ranges = [f_int,t_int];
285 285 }
286 286 // if we select more than 2 lines
287 287 if (ranges[0] !== ranges[1]){
288 288 if($('#linktt').length === 0){
289 289 hl_div = document.createElement('div');
290 290 hl_div.id = 'linktt';
291 291 }
292 292 hl_div.innerHTML = '';
293 293
294 294 anchor = '#L'+ranges[0]+'-'+ranges[1];
295 295 var link = document.createElement('a');
296 296 link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
297 297 link.innerHTML = _gettext('Selection link');
298 298 hl_div.appendChild(link);
299 299 $('#codeblock').append(hl_div);
300 300
301 301 var xy = $(till).offset();
302 302 $('#linktt').addClass('hl-tip-box tip-box');
303 303 $('#linktt').offset({top: xy.top + offset, left: xy.left});
304 304 $('#linktt').css('visibility','visible');
305 305 }
306 306 else{
307 307 $('#linktt').css('visibility','hidden');
308 308 }
309 309 }
310 310 };
@@ -1,44 +1,44 b''
1 1 <%def name="refs(commit)">
2 2 ## Build a cache of refs for selector
3 3 <script>
4 4 fileTreeRefs = {
5 5
6 6 }
7 7 </script>
8 8
9 9 %if commit.merge:
10 10 <span class="mergetag tag">
11 11 <i class="icon-merge">${_('merge')}</i>
12 12 </span>
13 13 %endif
14 14
15 15 %if h.is_hg(c.rhodecode_repo):
16 16 %for book in commit.bookmarks:
17 17 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
18 18 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
19 19 </span>
20 20 <script>
21 fileTreeRefs["${book}"] = {raw_id: "${commit.raw_id}", type:"book"};
21 fileTreeRefs["${book}"] = {raw_id: "${commit.raw_id}", type:"book", text: "${book}"};
22 22 </script>
23 23 %endfor
24 24 %endif
25 25
26 26 %for tag in commit.tags:
27 27 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
28 28 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
29 29 </span>
30 30 <script>
31 fileTreeRefs["${tag}"] = {raw_id: "${commit.raw_id}", type:"tag"};
31 fileTreeRefs["${tag}"] = {raw_id: "${commit.raw_id}", type:"tag", text: "${tag}"};
32 32 </script>
33 33 %endfor
34 34
35 35 %if commit.branch:
36 36 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
37 37 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id,_query=dict(at=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
38 38 </span>
39 39 <script>
40 fileTreeRefs["${commit.branch}"] = {raw_id: "${commit.raw_id}", type:"branch"};
40 fileTreeRefs["${commit.branch}"] = {raw_id: "${commit.raw_id}", type:"branch", text: "${commit.branch}"};
41 41 </script>
42 42 %endif
43 43
44 44 </%def>
@@ -1,386 +1,350 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title(*args)">
4 ${_('%s Files') % c.repo_name}
4 ${_('{} Files').format(c.repo_name)}
5 5 %if hasattr(c,'file'):
6 &middot; ${h.safe_unicode(c.file.path) or '\\'}
6 &middot; ${(h.safe_unicode(c.file.path) or '\\')}
7 7 %endif
8 8
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Files')}
16 16 %if c.file:
17 17 @ ${h.show_id(c.commit)}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='files')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 <div id="pjax-container">
30 <div>
31 31 <div id="files_data">
32 32 <%include file='files_pjax.mako'/>
33 33 </div>
34 34 </div>
35 35 <script>
36 var pjaxTimeout = 5000;
37 36
38 var curState = {
39 commit_id: "${c.commit.raw_id}"
40 };
37 var metadataRequest = null;
38 var fileSourcePage = ${c.file_source_page};
39 var atRef = '${request.GET.get('at', '')}';
41 40
42 41 var getState = function(context) {
43 42 var url = $(location).attr('href');
44 43 var _base_url = '${h.route_path("repo_files",repo_name=c.repo_name,commit_id='',f_path='')}';
45 44 var _annotate_url = '${h.route_path("repo_files:annotated",repo_name=c.repo_name,commit_id='',f_path='')}';
46 45 _base_url = _base_url.replace('//', '/');
47 46 _annotate_url = _annotate_url.replace('//', '/');
48 47
49 48 //extract f_path from url.
50 49 var parts = url.split(_base_url);
51 50 if (parts.length != 2) {
52 51 parts = url.split(_annotate_url);
53 52 if (parts.length != 2) {
54 53 var rev = "tip";
55 54 var f_path = "";
56 55 } else {
57 56 var parts2 = parts[1].split('/');
58 57 var rev = parts2.shift(); // pop the first element which is the revision
59 58 var f_path = parts2.join('/');
60 59 }
61 60
62 61 } else {
63 62 var parts2 = parts[1].split('/');
64 63 var rev = parts2.shift(); // pop the first element which is the revision
65 64 var f_path = parts2.join('/');
66 65 }
67 66
67 var url_params = {
68 repo_name: templateContext.repo_name,
69 commit_id: rev,
70 f_path:'__FPATH__'
71 };
72 if (atRef !== '') {
73 url_params['at'] = atRef
74 }
75
76 var _url_base = pyroutes.url('repo_files', url_params);
68 77 var _node_list_url = pyroutes.url('repo_files_nodelist',
69 78 {repo_name: templateContext.repo_name,
70 79 commit_id: rev, f_path: f_path});
71 var _url_base = pyroutes.url('repo_files',
72 {repo_name: templateContext.repo_name,
73 commit_id: rev, f_path:'__FPATH__'});
80
74 81 return {
75 82 url: url,
76 83 f_path: f_path,
77 84 rev: rev,
78 commit_id: curState.commit_id,
85 commit_id: "${c.commit.raw_id}",
79 86 node_list_url: _node_list_url,
80 87 url_base: _url_base
81 88 };
82 89 };
83 90
84 var metadataRequest = null;
85 91 var getFilesMetadata = function() {
86 92 if (metadataRequest && metadataRequest.readyState != 4) {
87 93 metadataRequest.abort();
88 94 }
89 95 if (fileSourcePage) {
90 96 return false;
91 97 }
92 98
93 99 if ($('#file-tree-wrapper').hasClass('full-load')) {
94 100 // in case our HTML wrapper has full-load class we don't
95 101 // trigger the async load of metadata
96 102 return false;
97 103 }
98 104
99 105 var state = getState('metadata');
100 106 var url_data = {
101 107 'repo_name': templateContext.repo_name,
102 108 'commit_id': state.commit_id,
103 109 'f_path': state.f_path
104 110 };
105 111
106 112 var url = pyroutes.url('repo_nodetree_full', url_data);
107 113
108 114 metadataRequest = $.ajax({url: url});
109 115
110 116 metadataRequest.done(function(data) {
111 117 $('#file-tree').html(data);
112 118 timeagoActivate();
113 119 });
114 120 metadataRequest.fail(function (data, textStatus, errorThrown) {
115 121 console.log(data);
116 122 if (data.status != 0) {
117 123 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
118 124 }
119 125 });
120 126 };
121 127
122 128 var callbacks = function() {
123 var state = getState('callbacks');
124 129 timeagoActivate();
125 130
126 131 if ($('#trimmed_message_box').height() < 50) {
127 132 $('#message_expand').hide();
128 133 }
129 134
130 135 $('#message_expand').on('click', function(e) {
131 136 $('#trimmed_message_box').css('max-height', 'none');
132 137 $(this).hide();
133 138 });
134 139
140 var state = getState('callbacks');
141
135 142 // VIEW FOR FILE SOURCE
136 143 if (fileSourcePage) {
137 144 // variants for with source code, not tree view
138 145
139 146 // select code link event
140 147 $("#hlcode").mouseup(getSelectionLink);
141 148
142 149 // file history select2 used for history, and switch to
143 150 var initialCommitData = {
144 151 id: null,
145 152 text: '${_("Pick Commit")}',
146 153 type: 'sha',
147 154 raw_id: null,
148 155 files_url: null
149 156 };
157
150 158 select2FileHistorySwitcher('#diff1', initialCommitData, state);
151 159
152 160 // show at, diff to actions handlers
153 161 $('#diff1').on('change', function(e) {
154 162 $('#diff_to_commit').removeClass('disabled').removeAttr("disabled");
155 163 $('#diff_to_commit').val(_gettext('Diff to Commit ') + e.val.truncateAfter(8, '...'));
156 164
157 165 $('#show_at_commit').removeClass('disabled').removeAttr("disabled");
158 166 $('#show_at_commit').val(_gettext('Show at Commit ') + e.val.truncateAfter(8, '...'));
159 167 });
160 168
161 169 $('#diff_to_commit').on('click', function(e) {
162 170 var diff1 = $('#diff1').val();
163 171 var diff2 = $('#diff2').val();
164 172
165 173 var url_data = {
166 174 repo_name: templateContext.repo_name,
167 175 source_ref: diff1,
168 176 source_ref_type: 'rev',
169 177 target_ref: diff2,
170 178 target_ref_type: 'rev',
171 179 merge: 1,
172 180 f_path: state.f_path
173 181 };
174 182 window.location = pyroutes.url('repo_compare', url_data);
175 183 });
176 184
177 185 $('#show_at_commit').on('click', function(e) {
178 186 var diff1 = $('#diff1').val();
179 187
180 188 var annotate = $('#annotate').val();
181 189 if (annotate === "True") {
182 190 var url = pyroutes.url('repo_files:annotated',
183 191 {'repo_name': templateContext.repo_name,
184 192 'commit_id': diff1, 'f_path': state.f_path});
185 193 } else {
186 194 var url = pyroutes.url('repo_files',
187 195 {'repo_name': templateContext.repo_name,
188 196 'commit_id': diff1, 'f_path': state.f_path});
189 197 }
190 198 window.location = url;
191 199
192 200 });
193 201
194 202 // show more authors
195 203 $('#show_authors').on('click', function(e) {
196 204 e.preventDefault();
197 205 var url = pyroutes.url('repo_file_authors',
198 206 {'repo_name': templateContext.repo_name,
199 207 'commit_id': state.rev, 'f_path': state.f_path});
200 208
201 209 $.pjax({
202 210 url: url,
203 data: 'annotate=${"1" if c.annotate else "0"}',
211 data: 'annotate=${("1" if c.annotate else "0")}',
204 212 container: '#file_authors',
205 213 push: false,
206 214 timeout: pjaxTimeout
207 215 }).complete(function(){
208 216 $('#show_authors').hide();
209 217 $('#file_authors_title').html(_gettext('All Authors'))
210 218 })
211 219 });
212 220
213 221 // load file short history
214 222 $('#file_history_overview').on('click', function(e) {
215 223 e.preventDefault();
216 224 path = state.f_path;
217 225 if (path.indexOf("#") >= 0) {
218 226 path = path.slice(0, path.indexOf("#"));
219 227 }
220 228 var url = pyroutes.url('repo_changelog_file',
221 229 {'repo_name': templateContext.repo_name,
222 230 'commit_id': state.rev, 'f_path': path, 'limit': 6});
223 231 $('#file_history_container').show();
224 232 $('#file_history_container').html('<div class="file-history-inner">{0}</div>'.format(_gettext('Loading ...')));
225 233
226 234 $.pjax({
227 235 url: url,
228 236 container: '#file_history_container',
229 237 push: false,
230 238 timeout: pjaxTimeout
231 239 })
232 240 });
233 241
234 242 }
235 243 // VIEW FOR FILE TREE BROWSER
236 244 else {
237 245 getFilesMetadata();
238 246
239 247 // fuzzy file filter
240 248 fileBrowserListeners(state.node_list_url, state.url_base);
241 249
242 250 // switch to widget
243 console.log(state)
244 251 var initialCommitData = {
245 at_ref: '${request.GET.get('at')}',
252 at_ref: atRef,
246 253 id: null,
247 254 text: '${c.commit.raw_id}',
248 255 type: 'sha',
249 256 raw_id: '${c.commit.raw_id}',
250 257 idx: ${c.commit.idx},
251 258 files_url: null,
252 259 };
253 260
261 // check if we have ref info.
262 var selectedRef = fileTreeRefs[atRef];
263 if (selectedRef !== undefined) {
264 $.extend(initialCommitData, selectedRef)
265 }
266
254 267 var loadUrl = pyroutes.url('repo_refs_data', {'repo_name': templateContext.repo_name});
255 268
256 269 var select2RefFileSwitcher = function (targetElement, loadUrl, initialData) {
257 270 var formatResult = function (result, container, query) {
258 271 return formatSelect2SelectionRefs(result);
259 272 };
260 273
261 274 var formatSelection = function (data, container) {
262 275 var commit_ref = data;
263 console.log(data)
264 276
265 277 var tmpl = '';
266 278 if (commit_ref.type === 'sha') {
267 279 tmpl = commit_ref.raw_id.substr(0,8);
268 280 } else if (commit_ref.type === 'branch') {
269 281 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
270 282 tmpl = tmpl.concat(escapeHtml(commit_ref.text));
271 283 } else if (commit_ref.type === 'tag') {
272 284 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
273 285 tmpl = tmpl.concat(escapeHtml(commit_ref.text));
274 286 } else if (commit_ref.type === 'book') {
275 287 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
276 288 tmpl = tmpl.concat(escapeHtml(commit_ref.text));
277 289 }
278 290
279 291 tmpl = tmpl.concat('<span class="select-index-number">{0}</span>'.format(commit_ref.idx));
280 292 return tmpl
281 293 };
282 294
283 295 $(targetElement).select2({
284 296 cachedDataSource: {},
285 297 dropdownAutoWidth: true,
286 298 width: "resolve",
287 299 containerCssClass: "drop-menu",
288 300 dropdownCssClass: "drop-menu-dropdown",
289 301 query: function(query) {
290 302 var self = this;
291 303 var cacheKey = '__ALL_FILE_REFS__';
292 304 var cachedData = self.cachedDataSource[cacheKey];
293 305 if (cachedData) {
294 306 var data = select2RefFilterResults(query.term, cachedData);
295 307 query.callback({results: data.results});
296 308 } else {
297 309 $.ajax({
298 310 url: loadUrl,
299 311 data: {},
300 312 dataType: 'json',
301 313 type: 'GET',
302 314 success: function(data) {
303 315 self.cachedDataSource[cacheKey] = data;
304 316 query.callback({results: data.results});
305 317 }
306 318 });
307 319 }
308 320 },
309 321 initSelection: function(element, callback) {
310 322 callback(initialData);
311 323 },
312 324 formatResult: formatResult,
313 325 formatSelection: formatSelection
314 326 });
315 327
316 328 };
317 329
318 330 select2RefFileSwitcher('#refs_filter', loadUrl, initialCommitData);
319 331
320 332 $('#refs_filter').on('change', function(e) {
321 333 var data = $('#refs_filter').select2('data');
322 curState.commit_id = data.raw_id;
323 $.pjax({url: data.files_url, container: '#pjax-container', timeout: pjaxTimeout});
324 });
325
326 $("#prev_commit_link").on('click', function(e) {
327 var data = $(this).data();
328 curState.commit_id = data.commitId;
329 });
330
331 $("#next_commit_link").on('click', function(e) {
332 var data = $(this).data();
333 curState.commit_id = data.commitId;
334 window.location = data.files_url
334 335 });
335 336
336 337 }
337 338 };
338 339
339 $(document).pjax(".pjax-link", "#pjax-container", {
340 "fragment": "#pjax-content",
341 "maxCacheLength": 1000,
342 "timeout": pjaxTimeout
343 });
344
345 // define global back/forward states
346 var isPjaxPopState = false;
347 $(document).on('pjax:popstate', function() {
348 isPjaxPopState = true;
349 });
350
351 $(document).on('pjax:end', function(xhr, options) {
352 if (isPjaxPopState) {
353 isPjaxPopState = false;
354 callbacks();
355 _NODEFILTER.resetFilter();
356 }
357
358 // run callback for tracking if defined for google analytics etc.
359 // this is used to trigger tracking on pjax
360 if (typeof window.rhodecode_statechange_callback !== 'undefined') {
361 var state = getState('statechange');
362 rhodecode_statechange_callback(state.url, null)
363 }
364 });
365
366 $(document).on('pjax:success', function(event, xhr, options) {
367 if (event.target.id == "file_history_container") {
368 $('#file_history_overview').hide();
369 $('#file_history_overview_full').show();
370 timeagoActivate();
371 } else {
372 callbacks();
373 }
374 });
375
376 340 $(document).ready(function() {
377 341 callbacks();
378 342 var search_GET = "${request.GET.get('search','')}";
379 343 if (search_GET === "1") {
380 344 _NODEFILTER.initFilter();
381 345 }
382 346 });
383 347
384 348 </script>
385 349
386 </%def>
350 </%def> No newline at end of file
@@ -1,236 +1,236 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Files Add') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24
25 25 <div class="edit-file-title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 ${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', enctype="multipart/form-data", class_="form-horizontal", request=request)}
29 29 <div class="edit-file-fieldset">
30 30 <div class="fieldset">
31 31 <div id="destination-label" class="left-label">
32 32 ${_('Path')}:
33 33 </div>
34 34 <div class="right-content">
35 35 <div id="specify-custom-path-container">
36 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
36 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))}</span>
37 37 <a class="custom-path-link" id="specify-custom-path" href="#">${_('Specify Custom Path')}</a>
38 38 </div>
39 39 <div id="remove-custom-path-container" style="display: none;">
40 40 ${c.repo_name}/
41 41 <input type="input-small" value="${c.f_path}" size="46" name="location" id="location">
42 42 <a class="custom-path-link" id="remove-custom-path" href="#">${_('Remove Custom Path')}</a>
43 43 </div>
44 44 </div>
45 45 </div>
46 46 <div id="filename_container" class="fieldset">
47 47 <div class="filename-label left-label">
48 48 ${_('Filename')}:
49 49 </div>
50 50 <div class="right-content">
51 51 <input class="input-small" type="text" value="" size="46" name="filename" id="filename">
52 52 <p>${_('or')} <a id="upload_file_enable" href="#">${_('Upload File')}</a></p>
53 53 </div>
54 54 </div>
55 55 <div id="upload_file_container" class="fieldset" style="display: none;">
56 56 <div class="filename-label left-label">
57 57 ${_('Filename')}:
58 58 </div>
59 59 <div class="right-content">
60 60 <input class="input-small" type="text" value="" size="46" name="filename_upload" id="filename_upload" placeholder="${_('No file selected')}">
61 61 </div>
62 62 <div class="filename-label left-label file-upload-label">
63 63 ${_('Upload file')}:
64 64 </div>
65 65 <div class="right-content file-upload-input">
66 66 <label for="upload_file" class="btn btn-default">Browse</label>
67 67
68 68 <input type="file" name="upload_file" id="upload_file">
69 69 <p>${_('or')} <a id="file_enable" href="#">${_('Create New File')}</a></p>
70 70 </div>
71 71 </div>
72 72 </div>
73 73 <div class="table">
74 74 <div id="files_data">
75 75 <div id="codeblock" class="codeblock">
76 76 <div class="code-header form" id="set_mode_header">
77 77 <div class="fields">
78 78 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
79 79 <label for="line_wrap">${_('line wraps')}</label>
80 80 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
81 81
82 82 <div id="render_preview" class="btn btn-small preview hidden" >${_('Preview')}</div>
83 83 </div>
84 84 </div>
85 85 <div id="editor_container">
86 86 <pre id="editor_pre"></pre>
87 87 <textarea id="editor" name="content" ></textarea>
88 88 <div id="editor_preview"></div>
89 89 </div>
90 90 </div>
91 91 </div>
92 92 </div>
93 93
94 94 <div class="edit-file-fieldset">
95 95 <div class="fieldset">
96 96 <div id="commit-message-label" class="commit-message-label left-label">
97 97 ${_('Commit Message')}:
98 98 </div>
99 99 <div class="right-content">
100 100 <div class="message">
101 101 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
102 102 </div>
103 103 </div>
104 104 </div>
105 105 <div class="pull-right">
106 106 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
107 107 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")}
108 108 </div>
109 109 </div>
110 110 ${h.end_form()}
111 111 </div>
112 112 <script type="text/javascript">
113 113
114 114 $('#commit_btn').on('click', function() {
115 115 var button = $(this);
116 116 if (button.hasClass('clicked')) {
117 117 button.attr('disabled', true);
118 118 } else {
119 119 button.addClass('clicked');
120 120 }
121 121 });
122 122
123 123 $('#specify-custom-path').on('click', function(e){
124 124 e.preventDefault();
125 125 $('#specify-custom-path-container').hide();
126 126 $('#remove-custom-path-container').show();
127 127 $('#destination-label').css('margin-top', '13px');
128 128 });
129 129
130 130 $('#remove-custom-path').on('click', function(e){
131 131 e.preventDefault();
132 132 $('#specify-custom-path-container').show();
133 133 $('#remove-custom-path-container').hide();
134 134 $('#location').val('${c.f_path}');
135 135 $('#destination-label').css('margin-top', '0');
136 136 });
137 137
138 138 var hide_upload = function(){
139 139 $('#files_data').show();
140 140 $('#upload_file_container').hide();
141 141 $('#filename_container').show();
142 142 };
143 143
144 144 $('#file_enable').on('click', function(e){
145 145 e.preventDefault();
146 146 hide_upload();
147 147 });
148 148
149 149 $('#upload_file_enable').on('click', function(e){
150 150 e.preventDefault();
151 151 $('#files_data').hide();
152 152 $('#upload_file_container').show();
153 153 $('#filename_container').hide();
154 154 if (detectIE() && detectIE() <= 9) {
155 155 $('#upload_file_container .file-upload-input label').hide();
156 156 $('#upload_file_container .file-upload-input span').hide();
157 157 $('#upload_file_container .file-upload-input input').show();
158 158 }
159 159 });
160 160
161 161 $('#upload_file').on('change', function() {
162 162 if (this.files && this.files[0]) {
163 163 $('#filename_upload').val(this.files[0].name);
164 164 }
165 165 });
166 166
167 167 hide_upload();
168 168
169 169 var renderer = "";
170 170 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}";
171 171 var myCodeMirror = initCodeMirror('editor', reset_url, false);
172 172
173 173 var modes_select = $('#set_mode');
174 174 fillCodeMirrorOptions(modes_select);
175 175
176 176 var filename_selector = '#filename';
177 177 var callback = function(filename, mimetype, mode){
178 178 CodeMirrorPreviewEnable(mode);
179 179 };
180 180 // on change of select field set mode
181 181 setCodeMirrorModeFromSelect(
182 182 modes_select, filename_selector, myCodeMirror, callback);
183 183
184 184 // on entering the new filename set mode, from given extension
185 185 setCodeMirrorModeFromInput(
186 186 modes_select, filename_selector, myCodeMirror, callback);
187 187
188 188 // if the file is renderable set line wraps automatically
189 189 if (renderer !== ""){
190 190 var line_wrap = 'on';
191 191 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
192 192 setCodeMirrorLineWrap(myCodeMirror, true);
193 193 }
194 194
195 195 // on select line wraps change the editor
196 196 $('#line_wrap').on('change', function(e){
197 197 var selected = e.currentTarget;
198 198 var line_wraps = {'on': true, 'off': false}[selected.value];
199 199 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
200 200 });
201 201
202 202 // render preview/edit button
203 203 $('#render_preview').on('click', function(e){
204 204 if($(this).hasClass('preview')){
205 205 $(this).removeClass('preview');
206 206 $(this).html("${_('Edit')}");
207 207 $('#editor_preview').show();
208 208 $(myCodeMirror.getWrapperElement()).hide();
209 209
210 210 var possible_renderer = {
211 211 'rst':'rst',
212 212 'markdown':'markdown',
213 213 'gfm': 'markdown'}[myCodeMirror.getMode().name];
214 214 var _text = myCodeMirror.getValue();
215 215 var _renderer = possible_renderer || DEFAULT_RENDERER;
216 216 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
217 217 $('#editor_preview').html(_gettext('Loading ...'));
218 218 var url = pyroutes.url('repo_commit_comment_preview',
219 219 {'repo_name': '${c.repo_name}',
220 220 'commit_id': '${c.commit.raw_id}'});
221 221
222 222 ajaxPOST(url, post_data, function(o){
223 223 $('#editor_preview').html(o);
224 224 })
225 225 }
226 226 else{
227 227 $(this).addClass('preview');
228 228 $(this).html("${_('Preview')}");
229 229 $('#editor_preview').hide();
230 230 $(myCodeMirror.getWrapperElement()).show();
231 231 }
232 232 });
233 233 $('#filename').focus();
234 234
235 235 </script>
236 236 </%def>
@@ -1,58 +1,58 b''
1 1
2 2 <div id="codeblock" class="browserblock">
3 3 <div class="browser-header">
4 4 <div class="browser-nav">
5 5
6 6 <div class="info_box">
7 7
8 8 <div class="info_box_elem previous">
9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class="pjax-link ${'disabled' if c.url_prev == '#' else ''}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-left"></i></a>
9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class=" ${'disabled' if c.url_prev == '#' else ''}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-left"></i></a>
10 10 </div>
11 11
12 12 ${h.hidden('refs_filter')}
13 13
14 14 <div class="info_box_elem next">
15 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class="pjax-link ${'disabled' if c.url_next == '#' else ''}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>
15 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${'disabled' if c.url_next == '#' else ''}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>
16 16 </div>
17 17 </div>
18 18
19 19 <div id="search_activate_id" class="search_activate">
20 20 <a class="btn btn-default" id="filter_activate" href="javascript:void(0)">${_('Search File List')}</a>
21 21 </div>
22 22 <div id="search_deactivate_id" class="search_activate hidden">
23 23 <a class="btn btn-default" id="filter_deactivate" href="javascript:void(0)">${_('Close File List')}</a>
24 24 </div>
25 25 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
26 26 <div title="${_('Add New File')}" class="btn btn-primary new-file">
27 27 <a href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _anchor='edit')}">
28 28 ${_('Add File')}</a>
29 29 </div>
30 30 % endif
31 31 % if c.enable_downloads:
32 32 <% at_path = '{}.zip'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
33 33 <div title="${_('Download tree at {}').format(at_path)}" class="btn btn-default new-file">
34 34 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
35 35 ${_('Download tree at {}').format(at_path)}
36 36 </a>
37 37 </div>
38 38 % endif
39 39 </div>
40 40
41 41 <div class="browser-search">
42 42 <div class="node-filter">
43 43 <div class="node_filter_box hidden" id="node_filter_box_loading" >${_('Loading file list...')}</div>
44 44 <div class="node_filter_box hidden" id="node_filter_box" >
45 45 <div class="node-filter-path">${h.get_last_path_part(c.file)}/</div>
46 46 <div class="node-filter-input">
47 47 <input class="init" type="text" name="filter" size="25" id="node_filter" autocomplete="off">
48 48 </div>
49 49 </div>
50 50 </div>
51 51 </div>
52 52 </div>
53 53 ## file tree is computed from caches, and filled in
54 54 <div id="file-tree">
55 55 ${c.file_tree |n}
56 56 </div>
57 57
58 58 </div>
@@ -1,82 +1,89 b''
1 <div id="file-tree-wrapper" class="browser-body ${'full-load' if c.full_load else ''}">
1 <%
2 if request.GET.get('at'):
3 query={'at': request.GET.get('at')}
4 else:
5 query=None
6 %>
7 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
2 8 <table class="code-browser rctable repo_summary">
3 9 <thead>
4 10 <tr>
5 11 <th>${_('Name')}</th>
6 12 <th>${_('Size')}</th>
7 13 <th>${_('Modified')}</th>
8 14 <th>${_('Last Commit')}</th>
9 15 <th>${_('Author')}</th>
10 16 </tr>
11 17 </thead>
12 18
13 19 <tbody id="tbody">
14 20 %if c.file.parent:
15 21 <tr class="parity0">
16 22 <td class="td-componentname">
17 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.parent.path)}" class="pjax-link">
23 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.parent.path, _query=query)}">
18 24 <i class="icon-directory"></i>..
19 25 </a>
20 26 </td>
21 27 <td></td>
22 28 <td></td>
23 29 <td></td>
24 30 <td></td>
25 31 </tr>
26 32 %endif
27 33 %for cnt,node in enumerate(c.file):
28 34 <tr class="parity${cnt%2}">
29 35 <td class="td-componentname">
30 36 % if node.is_submodule():
31 37 <span class="submodule-dir">
32 38 % if node.url.startswith('http://') or node.url.startswith('https://'):
33 39 <a href="${node.url}">
34 40 <i class="icon-directory browser-dir"></i>${node.name}
35 41 </a>
36 42 % else:
37 43 <i class="icon-directory browser-dir"></i>${node.name}
38 44 % endif
39 45 </span>
40 46 % else:
41 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=h.safe_unicode(node.path))}" class="pjax-link">
42 <i class="${'icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir'}"></i>${node.name}
47
48 <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)}">
49 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
43 50 </a>
44 51 % endif
45 52 </td>
46 53 %if node.is_file():
47 54 <td class="td-size" data-attr-name="size">
48 55 % if c.full_load:
49 56 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
50 57 % else:
51 58 ${_('Loading ...')}
52 59 % endif
53 60 </td>
54 61 <td class="td-time" data-attr-name="modified_at">
55 62 % if c.full_load:
56 63 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
57 64 % endif
58 65 </td>
59 66 <td class="td-hash" data-attr-name="commit_id">
60 67 % if c.full_load:
61 68 <div class="tooltip" title="${h.tooltip(node.last_commit.message)}">
62 69 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
63 70 </div>
64 71 % endif
65 72 </td>
66 73 <td class="td-user" data-attr-name="author">
67 74 % if c.full_load:
68 75 <span data-author="${node.last_commit.author}" title="${h.tooltip(node.last_commit.author)}">${h.gravatar_with_user(request, node.last_commit.author)|n}</span>
69 76 % endif
70 77 </td>
71 78 %else:
72 79 <td></td>
73 80 <td></td>
74 81 <td></td>
75 82 <td></td>
76 83 %endif
77 84 </tr>
78 85 %endfor
79 86 </tbody>
80 87 <tbody id="tbody_filtered"></tbody>
81 88 </table>
82 89 </div>
@@ -1,72 +1,72 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Files Delete') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Delete file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <div class="edit-file-title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 ${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', class_="form-horizontal", request=request)}
28 28 <div class="edit-file-fieldset">
29 29 <div class="fieldset">
30 30 <div id="destination-label" class="left-label">
31 31 ${_('Path')}:
32 32 </div>
33 33 <div class="right-content">
34 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
34 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))}</span>
35 35 </div>
36 36 </div>
37 37 </div>
38 38
39 39 <div id="codeblock" class="codeblock delete-file-preview">
40 40 <div class="code-body">
41 41 %if c.file.is_binary:
42 42 ${_('Binary file (%s)') % c.file.mimetype}
43 43 %else:
44 44 %if c.file.size < c.visual.cut_off_limit_file:
45 45 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
46 46 %else:
47 47 ${_('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'),
48 48 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
49 49 %endif
50 50 %endif
51 51 </div>
52 52 </div>
53 53
54 54 <div class="edit-file-fieldset">
55 55 <div class="fieldset">
56 56 <div id="commit-message-label" class="commit-message-label left-label">
57 57 ${_('Commit Message')}:
58 58 </div>
59 59 <div class="right-content">
60 60 <div class="message">
61 61 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
62 62 </div>
63 63 </div>
64 64 </div>
65 65 <div class="pull-right">
66 66 ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")}
67 67 ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")}
68 68 </div>
69 69 </div>
70 70 ${h.end_form()}
71 71 </div>
72 72 </%def>
@@ -1,194 +1,194 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s File Edit') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Edit file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <% renderer = h.renderer_from_filename(c.f_path)%>
24 24 <div class="box">
25 25 <div class="edit-file-title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 <div class="edit-file-fieldset">
29 29 <div class="fieldset">
30 30 <div id="destination-label" class="left-label">
31 31 ${_('Path')}:
32 32 </div>
33 33 <div class="right-content">
34 34 <div id="specify-custom-path-container">
35 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
35 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))}</span>
36 36 </div>
37 37 </div>
38 38 </div>
39 39 </div>
40 40
41 41 <div class="table">
42 42 ${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)}
43 43 <div id="codeblock" class="codeblock" >
44 44 <div class="code-header">
45 45 <div class="stats">
46 46 <i class="icon-file"></i>
47 47 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))}</span>
48 48 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
49 49 <span class="item last">${c.file.mimetype}</span>
50 50 <div class="buttons">
51 51 <a class="btn btn-mini" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
52 52 <i class="icon-time"></i> ${_('history')}
53 53 </a>
54 54
55 55 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
56 56 % if not c.file.is_binary:
57 57 %if True:
58 58 ${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),class_="btn btn-mini")}
59 59 %else:
60 60 ${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),class_="btn btn-mini")}
61 61 %endif
62 62
63 63 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
64 64 ${_('raw')}
65 65 </a>
66 66 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
67 67 <i class="icon-archive"></i> ${_('download')}
68 68 </a>
69 69 % endif
70 70 % endif
71 71 </div>
72 72 </div>
73 73 <div class="form">
74 74 <label for="set_mode">${_('Editing file')}:</label>
75 75 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
76 76 <input id="filename" type="text" name="filename" value="${c.file.name}">
77 77
78 78 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
79 79 <label for="line_wrap">${_('line wraps')}</label>
80 80 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
81 81
82 82 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
83 83 </div>
84 84 </div>
85 85 <div id="editor_container">
86 86 <pre id="editor_pre"></pre>
87 87 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
88 88 <div id="editor_preview" ></div>
89 89 </div>
90 90 </div>
91 91 </div>
92 92
93 93 <div class="edit-file-fieldset">
94 94 <div class="fieldset">
95 95 <div id="commit-message-label" class="commit-message-label left-label">
96 96 ${_('Commit Message')}:
97 97 </div>
98 98 <div class="right-content">
99 99 <div class="message">
100 100 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
101 101 </div>
102 102 </div>
103 103 </div>
104 104 <div class="pull-right">
105 105 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
106 106 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
107 107 </div>
108 108 </div>
109 109 ${h.end_form()}
110 110 </div>
111 111
112 112 <script type="text/javascript">
113 113 $(document).ready(function(){
114 114 var renderer = "${renderer}";
115 115 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
116 116 var myCodeMirror = initCodeMirror('editor', reset_url);
117 117
118 118 var modes_select = $('#set_mode');
119 119 fillCodeMirrorOptions(modes_select);
120 120
121 121 // try to detect the mode based on the file we edit
122 122 var mimetype = "${c.file.mimetype}";
123 123 var detected_mode = detectCodeMirrorMode(
124 124 "${c.file.name}", mimetype);
125 125
126 126 if(detected_mode){
127 127 setCodeMirrorMode(myCodeMirror, detected_mode);
128 128 $(modes_select).select2("val", mimetype);
129 129 $(modes_select).change();
130 130 setCodeMirrorMode(myCodeMirror, detected_mode);
131 131 }
132 132
133 133 var filename_selector = '#filename';
134 134 var callback = function(filename, mimetype, mode){
135 135 CodeMirrorPreviewEnable(mode);
136 136 };
137 137 // on change of select field set mode
138 138 setCodeMirrorModeFromSelect(
139 139 modes_select, filename_selector, myCodeMirror, callback);
140 140
141 141 // on entering the new filename set mode, from given extension
142 142 setCodeMirrorModeFromInput(
143 143 modes_select, filename_selector, myCodeMirror, callback);
144 144
145 145 // if the file is renderable set line wraps automatically
146 146 if (renderer !== ""){
147 147 var line_wrap = 'on';
148 148 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
149 149 setCodeMirrorLineWrap(myCodeMirror, true);
150 150 }
151 151 // on select line wraps change the editor
152 152 $('#line_wrap').on('change', function(e){
153 153 var selected = e.currentTarget;
154 154 var line_wraps = {'on': true, 'off': false}[selected.value];
155 155 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
156 156 });
157 157
158 158 // render preview/edit button
159 159 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
160 160 $('#render_preview').removeClass('hidden');
161 161 }
162 162 $('#render_preview').on('click', function(e){
163 163 if($(this).hasClass('preview')){
164 164 $(this).removeClass('preview');
165 165 $(this).html("${_('Edit')}");
166 166 $('#editor_preview').show();
167 167 $(myCodeMirror.getWrapperElement()).hide();
168 168
169 169 var possible_renderer = {
170 170 'rst':'rst',
171 171 'markdown':'markdown',
172 172 'gfm': 'markdown'}[myCodeMirror.getMode().name];
173 173 var _text = myCodeMirror.getValue();
174 174 var _renderer = possible_renderer || DEFAULT_RENDERER;
175 175 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
176 176 $('#editor_preview').html(_gettext('Loading ...'));
177 177 var url = pyroutes.url('repo_commit_comment_preview',
178 178 {'repo_name': '${c.repo_name}',
179 179 'commit_id': '${c.commit.raw_id}'});
180 180 ajaxPOST(url, post_data, function(o){
181 181 $('#editor_preview').html(o);
182 182 })
183 183 }
184 184 else{
185 185 $(this).addClass('preview');
186 186 $(this).html("${_('Preview')}");
187 187 $('#editor_preview').hide();
188 188 $(myCodeMirror.getWrapperElement()).show();
189 189 }
190 190 });
191 191
192 192 })
193 193 </script>
194 194 </%def>
@@ -1,46 +1,42 b''
1 1 <%def name="title(*args)">
2 2 ${_('{} Files').format(c.repo_name)}
3 3 %if hasattr(c,'file'):
4 4 &middot; ${(h.safe_unicode(c.file.path) or '\\')}
5 5 %endif
6 6
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 <div id="pjax-content" data-title="${self.title()}">
13 <script>
14 // set the pageSource variable
15 var fileSourcePage = ${c.file_source_page};
16 </script>
12 <div>
17 13
18 14 <div class="summary-detail">
19 15 <div class="summary-detail-header">
20 16 <div class="breadcrumbs files_location">
21 17 <h4>
22 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path)}
18 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path, request.GET.get('at'))}
23 19 %if c.annotate:
24 20 - ${_('annotation')}
25 21 %endif
26 22 </h4>
27 23 </div>
28 24 </div><!--end summary-detail-header-->
29 25
30 26 % if c.file.is_submodule():
31 27 <span class="submodule-dir">Submodule ${h.escape(c.file.name)}</span>
32 28 % elif c.file.is_dir():
33 29 <%include file='files_tree_header.mako'/>
34 30 % else:
35 31 <%include file='files_source_header.mako'/>
36 32 % endif
37 33
38 34 </div> <!--end summary-detail-->
39 35
40 36 % if c.file.is_dir():
41 37 <%include file='files_browser.mako'/>
42 38 % else:
43 39 <%include file='files_source.mako'/>
44 40 % endif
45 41
46 42 </div>
@@ -1,210 +1,210 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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.rc/test', '<a href="http://rc.rc/test">http://rc.rc/test</a>'),
33 33 ('http://rc.rc/@foo', '<a href="http://rc.rc/@foo">http://rc.rc/@foo</a>'),
34 34 ('http://rc.rc/!foo', '<a href="http://rc.rc/!foo">http://rc.rc/!foo</a>'),
35 35 ('http://rc.rc/&foo', '<a href="http://rc.rc/&foo">http://rc.rc/&foo</a>'),
36 36 ('http://rc.rc/#foo', '<a href="http://rc.rc/#foo">http://rc.rc/#foo</a>'),
37 37 ])
38 38 def test_urlify_text(url, expected_url):
39 39 assert helpers.urlify_text(url) == expected_url
40 40
41 41
42 42 @pytest.mark.parametrize('repo_name, commit_id, path, expected_result', [
43 43 ('rX<X', 'cX<X', 'pX<X/aX<X/bX<X',
44 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/'
45 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/pX%3CX">pX&lt;X</a>/'
46 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/pX%3CX/aX%3CX">aX&lt;X'
44 '<a href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/'
45 '<a href="/rX%3CX/files/cX%3CX/pX%3CX">pX&lt;X</a>/'
46 '<a href="/rX%3CX/files/cX%3CX/pX%3CX/aX%3CX">aX&lt;X'
47 47 '</a>/bX&lt;X'),
48 48 # Path with only one segment
49 49 ('rX<X', 'cX<X', 'pX<X',
50 '<a class="pjax-link" href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/pX&lt;X'),
50 '<a href="/rX%3CX/files/cX%3CX/">rX&lt;X</a>/pX&lt;X'),
51 51 # Empty path
52 52 ('rX<X', 'cX<X', '', 'rX&lt;X'),
53 53 ('rX"X', 'cX"X', 'pX"X/aX"X/bX"X',
54 '<a class="pjax-link" href="/rX%22X/files/cX%22X/">rX&#34;X</a>/'
55 '<a class="pjax-link" href="/rX%22X/files/cX%22X/pX%22X">pX&#34;X</a>/'
56 '<a class="pjax-link" href="/rX%22X/files/cX%22X/pX%22X/aX%22X">aX&#34;X'
54 '<a href="/rX%22X/files/cX%22X/">rX&#34;X</a>/'
55 '<a href="/rX%22X/files/cX%22X/pX%22X">pX&#34;X</a>/'
56 '<a href="/rX%22X/files/cX%22X/pX%22X/aX%22X">aX&#34;X'
57 57 '</a>/bX&#34;X'),
58 58 ], ids=['simple', 'one_segment', 'empty_path', 'simple_quote'])
59 59 def test_files_breadcrumbs_xss(
60 60 repo_name, commit_id, path, app, expected_result):
61 61 result = helpers.files_breadcrumbs(repo_name, commit_id, path)
62 62 # Expect it to encode all path fragments properly. This is important
63 63 # because it returns an instance of `literal`.
64 64 assert result == expected_result
65 65
66 66
67 67 def test_format_binary():
68 68 assert helpers.format_byte_size_binary(298489462784) == '278.0 GiB'
69 69
70 70
71 71 @pytest.mark.parametrize('text_string, pattern, expected', [
72 72 ('No issue here', '(?:#)(?P<issue_id>\d+)', []),
73 73 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
74 74 [{'url': 'http://r.io/{repo}/i/42', 'id': '42'}]),
75 75 ('Fix #42, #53', '(?:#)(?P<issue_id>\d+)', [
76 76 {'url': 'http://r.io/{repo}/i/42', 'id': '42'},
77 77 {'url': 'http://r.io/{repo}/i/53', 'id': '53'}]),
78 78 ('Fix #42', '(?:#)?<issue_id>\d+)', []), # Broken regex
79 79 ])
80 80 def test_extract_issues(backend, text_string, pattern, expected):
81 81 repo = backend.create_repo()
82 82 config = {
83 83 '123': {
84 84 'uid': '123',
85 85 'pat': pattern,
86 86 'url': 'http://r.io/${repo}/i/${issue_id}',
87 87 'pref': '#',
88 88 }
89 89 }
90 90
91 91 def get_settings_mock(self, cache=True):
92 92 return config
93 93
94 94 with mock.patch.object(IssueTrackerSettingsModel,
95 95 'get_settings', get_settings_mock):
96 96 text, issues = helpers.process_patterns(text_string, repo.repo_name)
97 97
98 98 expected = copy.deepcopy(expected)
99 99 for item in expected:
100 100 item['url'] = item['url'].format(repo=repo.repo_name)
101 101
102 102 assert issues == expected
103 103
104 104
105 105 @pytest.mark.parametrize('text_string, pattern, link_format, expected_text', [
106 106 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'html',
107 107 'Fix <a class="issue-tracker-link" href="http://r.io/{repo}/i/42">#42</a>'),
108 108
109 109 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'markdown',
110 110 'Fix [#42](http://r.io/{repo}/i/42)'),
111 111
112 112 ('Fix #42', '(?:#)(?P<issue_id>\d+)', 'rst',
113 113 'Fix `#42 <http://r.io/{repo}/i/42>`_'),
114 114
115 115 ('Fix #42', '(?:#)?<issue_id>\d+)', 'html',
116 116 'Fix #42'), # Broken regex
117 117 ])
118 118 def test_process_patterns_repo(backend, text_string, pattern, expected_text, link_format):
119 119 repo = backend.create_repo()
120 120
121 121 def get_settings_mock(self, cache=True):
122 122 return {
123 123 '123': {
124 124 'uid': '123',
125 125 'pat': pattern,
126 126 'url': 'http://r.io/${repo}/i/${issue_id}',
127 127 'pref': '#',
128 128 }
129 129 }
130 130
131 131 with mock.patch.object(IssueTrackerSettingsModel,
132 132 'get_settings', get_settings_mock):
133 133 processed_text, issues = helpers.process_patterns(
134 134 text_string, repo.repo_name, link_format)
135 135
136 136 assert processed_text == expected_text.format(repo=repo.repo_name)
137 137
138 138
139 139 @pytest.mark.parametrize('text_string, pattern, expected_text', [
140 140 ('Fix #42', '(?:#)(?P<issue_id>\d+)',
141 141 'Fix <a class="issue-tracker-link" href="http://r.io/i/42">#42</a>'),
142 142 ('Fix #42', '(?:#)?<issue_id>\d+)',
143 143 'Fix #42'), # Broken regex
144 144 ])
145 145 def test_process_patterns_no_repo(text_string, pattern, expected_text):
146 146
147 147 def get_settings_mock(self, cache=True):
148 148 return {
149 149 '123': {
150 150 'uid': '123',
151 151 'pat': pattern,
152 152 'url': 'http://r.io/i/${issue_id}',
153 153 'pref': '#',
154 154 }
155 155 }
156 156
157 157 with mock.patch.object(IssueTrackerSettingsModel,
158 158 'get_global_settings', get_settings_mock):
159 159 processed_text, issues = helpers.process_patterns(
160 160 text_string, '')
161 161
162 162 assert processed_text == expected_text
163 163
164 164
165 165 def test_process_patterns_non_existent_repo_name(backend):
166 166 text_string = 'Fix #42'
167 167 pattern = '(?:#)(?P<issue_id>\d+)'
168 168 expected_text = ('Fix <a class="issue-tracker-link" '
169 169 'href="http://r.io/do-not-exist/i/42">#42</a>')
170 170
171 171 def get_settings_mock(self, cache=True):
172 172 return {
173 173 '123': {
174 174 'uid': '123',
175 175 'pat': pattern,
176 176 'url': 'http://r.io/${repo}/i/${issue_id}',
177 177 'pref': '#',
178 178 }
179 179 }
180 180
181 181 with mock.patch.object(IssueTrackerSettingsModel,
182 182 'get_global_settings', get_settings_mock):
183 183 processed_text, issues = helpers.process_patterns(
184 184 text_string, 'do-not-exist')
185 185
186 186 assert processed_text == expected_text
187 187
188 188
189 189 def test_get_visual_attr(baseapp):
190 190 from rhodecode.apps._base import TemplateArgs
191 191 c = TemplateArgs()
192 192 assert None is helpers.get_visual_attr(c, 'fakse')
193 193
194 194 # emulate the c.visual behaviour
195 195 c.visual = AttributeDict({})
196 196 assert None is helpers.get_visual_attr(c, 'some_var')
197 197
198 198 c.visual.some_var = 'foobar'
199 199 assert 'foobar' == helpers.get_visual_attr(c, 'some_var')
200 200
201 201
202 202 @pytest.mark.parametrize('test_text, inclusive, expected_text', [
203 203 ('just a string', False, 'just a string'),
204 204 ('just a string\n', False, 'just a string'),
205 205 ('just a string\n next line', False, 'just a string...'),
206 206 ('just a string\n next line', True, 'just a string\n...'),
207 207 ], ids=no_newline_id_generator)
208 208 def test_chop_at(test_text, inclusive, expected_text):
209 209 assert helpers.chop_at_smart(
210 210 test_text, '\n', inclusive, '...') == expected_text
General Comments 0
You need to be logged in to leave comments. Login now