diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py
--- a/rhodecode/lib/helpers.py
+++ b/rhodecode/lib/helpers.py
@@ -8,22 +8,24 @@ import hashlib
import StringIO
import urllib
import math
+import logging
from datetime import datetime
-from pygments.formatters import HtmlFormatter
+from pygments.formatters.html import HtmlFormatter
from pygments import highlight as code_highlight
from pylons import url, request, config
from pylons.i18n.translation import _, ungettext
+from hashlib import md5
from webhelpers.html import literal, HTML, escape
from webhelpers.html.tools import *
from webhelpers.html.builder import make_tag
from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
- end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
- link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
- password, textarea, title, ul, xml_declaration, radio
-from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
- mail_to, strip_links, strip_tags, tag_re
+ end_form, file, form, hidden, image, javascript_link, link_to, \
+ link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
+ submit, text, password, textarea, title, ul, xml_declaration, radio
+from webhelpers.html.tools import auto_link, button_to, highlight, \
+ js_obfuscate, mail_to, strip_links, strip_tags, tag_re
from webhelpers.number import format_byte_size, format_bit_size
from webhelpers.pylonslib import Flash as _Flash
from webhelpers.pylonslib.secure_form import secure_form
@@ -33,11 +35,15 @@ from webhelpers.text import chop_at, col
from webhelpers.date import time_ago_in_words
from webhelpers.paginate import Page
from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
- convert_boolean_attrs, NotGiven
+ convert_boolean_attrs, NotGiven, _make_safe_id_component
-from vcs.utils.annotate import annotate_highlight
+from rhodecode.lib.annotate import annotate_highlight
from rhodecode.lib.utils import repo_name_slug
-from rhodecode.lib import str2bool, safe_unicode, safe_str,get_changeset_safe
+from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
+from rhodecode.lib.markup_renderer import MarkupRenderer
+
+log = logging.getLogger(__name__)
+
def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
"""
@@ -49,6 +55,19 @@ def _reset(name, value=None, id=NotGiven
return HTML.input(**attrs)
reset = _reset
+safeid = _make_safe_id_component
+
+
+def FID(raw_id, path):
+ """
+ Creates a uniqe ID for filenode based on it's hash of path and revision
+ it's safe to use in urls
+
+ :param raw_id:
+ :param path:
+ """
+
+ return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
def get_token():
@@ -104,10 +123,14 @@ class _FilesBreadCrumbs(object):
paths_l = paths.split('/')
for cnt, p in enumerate(paths_l):
if p != '':
- url_l.append(link_to(p, url('files_home',
- repo_name=repo_name,
- revision=rev,
- f_path='/'.join(paths_l[:cnt + 1]))))
+ url_l.append(link_to(p,
+ url('files_home',
+ repo_name=repo_name,
+ revision=rev,
+ f_path='/'.join(paths_l[:cnt + 1])
+ )
+ )
+ )
return literal('/'.join(url_l))
@@ -198,13 +221,16 @@ def pygmentize(filenode, **kwargs):
return literal(code_highlight(filenode.content,
filenode.lexer, CodeHtmlFormatter(**kwargs)))
+
def pygmentize_annotation(repo_name, filenode, **kwargs):
- """pygmentize function for annotation
+ """
+ pygmentize function for annotation
:param filenode:
"""
color_dict = {}
+
def gen_color(n=10000):
"""generator for getting n of evenly distributed colors using
hsv color and golden ratio. It always return same order of colors
@@ -213,19 +239,26 @@ def pygmentize_annotation(repo_name, fil
"""
def hsv_to_rgb(h, s, v):
- if s == 0.0: return v, v, v
- i = int(h * 6.0) # XXX assume int() truncates!
+ if s == 0.0:
+ return v, v, v
+ i = int(h * 6.0) # XXX assume int() truncates!
f = (h * 6.0) - i
p = v * (1.0 - s)
q = v * (1.0 - s * f)
t = v * (1.0 - s * (1.0 - f))
i = i % 6
- if i == 0: return v, t, p
- if i == 1: return q, v, p
- if i == 2: return p, v, t
- if i == 3: return p, q, v
- if i == 4: return t, p, v
- if i == 5: return v, p, q
+ if i == 0:
+ return v, t, p
+ if i == 1:
+ return q, v, p
+ if i == 2:
+ return p, v, t
+ if i == 3:
+ return p, q, v
+ if i == 4:
+ return t, p, v
+ if i == 5:
+ return v, p, q
golden_ratio = 0.618033988749895
h = 0.22717784590367374
@@ -235,12 +268,12 @@ def pygmentize_annotation(repo_name, fil
h %= 1
HSV_tuple = [h, 0.95, 0.95]
RGB_tuple = hsv_to_rgb(*HSV_tuple)
- yield map(lambda x:str(int(x * 256)), RGB_tuple)
+ yield map(lambda x: str(int(x * 256)), RGB_tuple)
cgenerator = gen_color()
def get_color_string(cs):
- if color_dict.has_key(cs):
+ if cs in color_dict:
col = color_dict[cs]
else:
col = color_dict[cs] = cgenerator.next()
@@ -275,6 +308,7 @@ def pygmentize_annotation(repo_name, fil
return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
+
def is_following_repo(repo_name, user_id):
from rhodecode.model.scm import ScmModel
return ScmModel().is_following_repo(repo_name, user_id)
@@ -284,17 +318,75 @@ flash = _Flash()
#==============================================================================
# SCM FILTERS available via h.
#==============================================================================
-from vcs.utils import author_name, author_email
+from rhodecode.lib.vcs.utils import author_name, author_email
from rhodecode.lib import credentials_filter, age as _age
+from rhodecode.model.db import User
-age = lambda x:_age(x)
+age = lambda x: _age(x)
capitalize = lambda x: x.capitalize()
email = author_email
-email_or_none = lambda x: email(x) if email(x) != x else None
-person = lambda x: author_name(x)
short_id = lambda x: x[:12]
hide_credentials = lambda x: ''.join(credentials_filter(x))
+
+def is_git(repository):
+ if hasattr(repository, 'alias'):
+ _type = repository.alias
+ elif hasattr(repository, 'repo_type'):
+ _type = repository.repo_type
+ else:
+ _type = repository
+ return _type == 'git'
+
+
+def is_hg(repository):
+ if hasattr(repository, 'alias'):
+ _type = repository.alias
+ elif hasattr(repository, 'repo_type'):
+ _type = repository.repo_type
+ else:
+ _type = repository
+ return _type == 'hg'
+
+
+def email_or_none(author):
+ _email = email(author)
+ if _email != '':
+ return _email
+
+ # See if it contains a username we can get an email from
+ user = User.get_by_username(author_name(author), case_insensitive=True,
+ cache=True)
+ if user is not None:
+ return user.email
+
+ # No valid email, not a valid user in the system, none!
+ return None
+
+
+def person(author):
+ # attr to return from fetched user
+ person_getter = lambda usr: usr.username
+
+ # Valid email in the attribute passed, see if they're in the system
+ _email = email(author)
+ if _email != '':
+ user = User.get_by_email(_email, case_insensitive=True, cache=True)
+ if user is not None:
+ return person_getter(user)
+ return _email
+
+ # Maybe it's a username?
+ _author = author_name(author)
+ user = User.get_by_username(_author, case_insensitive=True,
+ cache=True)
+ if user is not None:
+ return person_getter(user)
+
+ # Still nothing? Just pass back the author name then
+ return _author
+
+
def bool2icon(value):
"""Returns True/False values represented as small html image of true/false
icons
@@ -314,7 +406,8 @@ def bool2icon(value):
def action_parser(user_log, feed=False):
- """This helper will action_map the specified string action into translated
+ """
+ This helper will action_map the specified string action into translated
fancy names with icons and links
:param user_log: user log instance
@@ -330,52 +423,84 @@ def action_parser(user_log, feed=False):
action, action_params = x
def get_cs_links():
- revs_limit = 3 #display this amount always
- revs_top_limit = 50 #show upto this amount of changesets hidden
- revs = action_params.split(',')
+ revs_limit = 3 # display this amount always
+ revs_top_limit = 50 # show upto this amount of changesets hidden
+ revs_ids = action_params.split(',')
+ deleted = user_log.repository is None
+ if deleted:
+ return ','.join(revs_ids)
+
repo_name = user_log.repository.repo_name
- from rhodecode.model.scm import ScmModel
repo = user_log.repository.scm_instance
- message = lambda rev: get_changeset_safe(repo, rev).message
+ message = lambda rev: rev.message
+ lnk = lambda rev, repo_name: (
+ link_to('r%s:%s' % (rev.revision, rev.short_id),
+ url('changeset_home', repo_name=repo_name,
+ revision=rev.raw_id),
+ title=tooltip(message(rev)), class_='tooltip')
+ )
+ # get only max revs_top_limit of changeset for performance/ui reasons
+ revs = [
+ x for x in repo.get_changesets(revs_ids[0],
+ revs_ids[:revs_top_limit][-1])
+ ]
+
cs_links = []
- cs_links.append(" " + ', '.join ([link_to(rev,
- url('changeset_home',
- repo_name=repo_name,
- revision=rev), title=tooltip(message(rev)),
- class_='tooltip') for rev in revs[:revs_limit] ]))
+ cs_links.append(" " + ', '.join(
+ [lnk(rev, repo_name) for rev in revs[:revs_limit]]
+ )
+ )
- compare_view = ('
' % (_('Show all combined changesets %s->%s' \
- % (revs[0], revs[-1])),
- url('changeset_home', repo_name=repo_name,
- revision='%s...%s' % (revs[0], revs[-1])
- ),
- _('compare view'))
- )
+ compare_view = (
+ ' ' % (
+ _('Show all combined changesets %s->%s') % (
+ revs_ids[0], revs_ids[-1]
+ ),
+ url('changeset_home', repo_name=repo_name,
+ revision='%s...%s' % (revs_ids[0], revs_ids[-1])
+ ),
+ _('compare view')
+ )
+ )
- if len(revs) > revs_limit:
- uniq_id = revs[0]
- html_tmpl = (' %s '
- '%s '
- '%s')
+ # if we have exactly one more than normally displayed
+ # just display it, takes less space than displaying
+ # "and 1 more revisions"
+ if len(revs_ids) == revs_limit + 1:
+ rev = revs[revs_limit]
+ cs_links.append(", " + lnk(rev, repo_name))
+
+ # hidden-by-default ones
+ if len(revs_ids) > revs_limit + 1:
+ uniq_id = revs_ids[0]
+ html_tmpl = (
+ ' %s %s %s'
+ )
if not feed:
- cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
- % (len(revs) - revs_limit),
- _('revisions')))
+ cs_links.append(html_tmpl % (
+ _('and'),
+ uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
+ _('revisions')
+ )
+ )
if not feed:
- html_tmpl = ' %s '
+ html_tmpl = ', %s '
else:
html_tmpl = ' %s '
- cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
- url('changeset_home',
- repo_name=repo_name, revision=rev),
- title=message(rev), class_='tooltip')
- for rev in revs[revs_limit:revs_top_limit]])))
+ morelinks = ', '.join(
+ [lnk(rev, repo_name) for rev in revs[revs_limit:]]
+ )
+
+ if len(revs_ids) > revs_top_limit:
+ morelinks += ', ...'
+
+ cs_links.append(html_tmpl % (uniq_id, morelinks))
if len(revs) > 1:
cs_links.append(compare_view)
return ''.join(cs_links)
@@ -385,36 +510,39 @@ def action_parser(user_log, feed=False):
return _('fork name ') + str(link_to(action_params, url('summary_home',
repo_name=repo_name,)))
- action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
- 'user_created_repo':(_('[created] repository'), None),
- 'user_forked_repo':(_('[forked] repository'), get_fork_name),
- 'user_updated_repo':(_('[updated] repository'), None),
- 'admin_deleted_repo':(_('[delete] repository'), None),
- 'admin_created_repo':(_('[created] repository'), None),
- 'admin_forked_repo':(_('[forked] repository'), None),
- 'admin_updated_repo':(_('[updated] repository'), None),
- 'push':(_('[pushed] into'), get_cs_links),
- 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
- 'push_remote':(_('[pulled from remote] into'), get_cs_links),
- 'pull':(_('[pulled] from'), None),
- 'started_following_repo':(_('[started following] repository'), None),
- 'stopped_following_repo':(_('[stopped following] repository'), None),
+ action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
+ 'user_created_repo': (_('[created] repository'), None),
+ 'user_created_fork': (_('[created] repository as fork'), None),
+ 'user_forked_repo': (_('[forked] repository'), get_fork_name),
+ 'user_updated_repo': (_('[updated] repository'), None),
+ 'admin_deleted_repo': (_('[delete] repository'), None),
+ 'admin_created_repo': (_('[created] repository'), None),
+ 'admin_forked_repo': (_('[forked] repository'), None),
+ 'admin_updated_repo': (_('[updated] repository'), None),
+ 'push': (_('[pushed] into'), get_cs_links),
+ 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
+ 'push_remote': (_('[pulled from remote] into'), get_cs_links),
+ 'pull': (_('[pulled] from'), None),
+ 'started_following_repo': (_('[started following] repository'), None),
+ 'stopped_following_repo': (_('[stopped following] repository'), None),
}
action_str = action_map.get(action, action)
if feed:
action = action_str[0].replace('[', '').replace(']', '')
else:
- action = action_str[0].replace('[', '')\
- .replace(']', '')
+ action = action_str[0]\
+ .replace('[', '')\
+ .replace(']', '')
- action_params_func = lambda :""
+ action_params_func = lambda: ""
if callable(action_str[1]):
action_params_func = action_str[1]
return [literal(action), action_params_func]
+
def action_parser_icon(user_log):
action = user_log.action
action_params = None
@@ -426,6 +554,7 @@ def action_parser_icon(user_log):
tmpl = """
"""
map = {'user_deleted_repo':'database_delete.png',
'user_created_repo':'database_add.png',
+ 'user_created_fork':'arrow_divide.png',
'user_forked_repo':'arrow_divide.png',
'user_updated_repo':'database_edit.png',
'admin_deleted_repo':'database_delete.png',
@@ -449,6 +578,7 @@ def action_parser_icon(user_log):
from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
HasRepoPermissionAny, HasRepoPermissionAll
+
#==============================================================================
# GRAVATAR URL
#==============================================================================
@@ -456,7 +586,8 @@ HasRepoPermissionAny, HasRepoPermissionA
def gravatar_url(email_address, size=30):
if (not str2bool(config['app_conf'].get('use_gravatar')) or
not email_address or email_address == 'anonymous@rhodecode.org'):
- return url("/images/user%s.png" % size)
+ f = lambda a, l: min(l, key=lambda x: abs(x - a))
+ return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
default = 'identicon'
@@ -469,7 +600,7 @@ def gravatar_url(email_address, size=30)
email_address = safe_str(email_address)
# construct the url
gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
- gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
+ gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
return gravatar_url
@@ -480,7 +611,7 @@ def gravatar_url(email_address, size=30)
class RepoPage(Page):
def __init__(self, collection, page=1, items_per_page=20,
- item_count=None, url=None, branch_name=None, **kwargs):
+ item_count=None, url=None, **kwargs):
"""Create a "RepoPage" instance. special pager for paging
repository
@@ -498,7 +629,7 @@ class RepoPage(Page):
# The self.page is the number of the current page.
# The first page has the number 1!
try:
- self.page = int(page) # make it int() if we get it as a string
+ self.page = int(page) # make it int() if we get it as a string
except (ValueError, TypeError):
self.page = 1
@@ -518,7 +649,8 @@ class RepoPage(Page):
self.items_per_page))
self.last_page = self.first_page + self.page_count - 1
- # Make sure that the requested page number is the range of valid pages
+ # Make sure that the requested page number is the range of
+ # valid pages
if self.page > self.last_page:
self.page = self.last_page
elif self.page < self.first_page:
@@ -531,11 +663,7 @@ class RepoPage(Page):
self.last_item = ((self.item_count - 1) - items_per_page *
(self.page - 1))
- iterator = self.collection.get_changesets(start=self.first_item,
- end=self.last_item,
- reverse=True,
- branch_name=branch_name)
- self.items = list(iterator)
+ self.items = list(self.collection[self.first_item:self.last_item + 1])
# Links to previous and next page
if self.page > self.first_page:
@@ -560,14 +688,14 @@ class RepoPage(Page):
self.items = []
# This is a subclass of the 'list' type. Initialise the list now.
- list.__init__(self, self.items)
+ list.__init__(self, reversed(self.items))
def changed_tooltip(nodes):
"""
Generates a html string for changed nodes in changeset page.
It limits the output to 30 entries
-
+
:param nodes: LazyNodesGenerator
"""
if nodes:
@@ -581,15 +709,14 @@ def changed_tooltip(nodes):
return ': ' + _('No Files')
-
def repo_link(groups_and_repos):
"""
Makes a breadcrumbs link to repo within a group
joins » on each group to create a fancy link
-
+
ex::
group >> subgroup >> repo
-
+
:param groups_and_repos:
"""
groups, repo_name = groups_and_repos
@@ -603,11 +730,12 @@ def repo_link(groups_and_repos):
return literal(' » '.join(map(make_link, groups)) + \
" » " + repo_name)
+
def fancy_file_stats(stats):
"""
Displays a fancy two colored bar for number of added/deleted
lines of code on file
-
+
:param stats: two element list of added/deleted lines of code
"""
@@ -630,13 +758,12 @@ def fancy_file_stats(stats):
a_v = a if a > 0 else ''
d_v = d if d > 0 else ''
-
def cgen(l_type):
- mapping = {'tr':'top-right-rounded-corner',
- 'tl':'top-left-rounded-corner',
- 'br':'bottom-right-rounded-corner',
- 'bl':'bottom-left-rounded-corner'}
- map_getter = lambda x:mapping[x]
+ mapping = {'tr': 'top-right-rounded-corner',
+ 'tl': 'top-left-rounded-corner',
+ 'br': 'bottom-right-rounded-corner',
+ 'bl': 'bottom-left-rounded-corner'}
+ map_getter = lambda x: mapping[x]
if l_type == 'a' and d_v:
#case when added and deleted are present
@@ -651,22 +778,137 @@ def fancy_file_stats(stats):
if l_type == 'd' and not a_v:
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
-
-
- d_a = '%s
' % (cgen('a'),
- a_p, a_v)
- d_d = '%s
' % (cgen('d'),
- d_p, d_v)
+ d_a = '%s
' % (
+ cgen('a'), a_p, a_v
+ )
+ d_d = '%s
' % (
+ cgen('d'), d_p, d_v
+ )
return literal('%s%s
' % (width, d_a, d_d))
-def urlify_text(text):
+def urlify_text(text_):
import re
- url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
+ url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
+ '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
def url_func(match_obj):
url_full = match_obj.groups()[0]
- return '%(url)s' % ({'url':url_full})
+ return '%(url)s' % ({'url': url_full})
+
+ return literal(url_pat.sub(url_func, text_))
+
+
+def urlify_changesets(text_, repository):
+ import re
+ URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
+
+ def url_func(match_obj):
+ rev = match_obj.groups()[0]
+ pref = ''
+ if match_obj.group().startswith(' '):
+ pref = ' '
+ tmpl = (
+ '%(pref)s'
+ '%(rev)s'
+ ''
+ )
+ return tmpl % {
+ 'pref': pref,
+ 'cls': 'revision-link',
+ 'url': url('changeset_home', repo_name=repository, revision=rev),
+ 'rev': rev,
+ }
+
+ newtext = URL_PAT.sub(url_func, text_)
+
+ return newtext
+
+
+def urlify_commit(text_, repository=None, link_=None):
+ """
+ Parses given text message and makes proper links.
+ issues are linked to given issue-server, and rest is a changeset link
+ if link_ is given, in other case it's a plain text
+
+ :param text_:
+ :param repository:
+ :param link_: changeset link
+ """
+ import re
+ import traceback
+
+ # urlify changesets
+ text_ = urlify_changesets(text_, repository)
+
+ def linkify_others(t, l):
+ urls = re.compile(r'(\)',)
+ links = []
+ for e in urls.split(t):
+ if not urls.match(e):
+ links.append('%s' % (l, e))
+ else:
+ links.append(e)
- return literal(url_pat.sub(url_func, text))
+ return ''.join(links)
+ try:
+ conf = config['app_conf']
+
+ URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
+
+ if URL_PAT:
+ ISSUE_SERVER_LNK = conf.get('issue_server_link')
+ ISSUE_PREFIX = conf.get('issue_prefix')
+
+ def url_func(match_obj):
+ pref = ''
+ if match_obj.group().startswith(' '):
+ pref = ' '
+
+ issue_id = ''.join(match_obj.groups())
+ tmpl = (
+ '%(pref)s'
+ '%(issue-prefix)s%(id-repr)s'
+ ''
+ )
+ url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
+ if repository:
+ url = url.replace('{repo}', repository)
+
+ return tmpl % {
+ 'pref': pref,
+ 'cls': 'issue-tracker-link',
+ 'url': url,
+ 'id-repr': issue_id,
+ 'issue-prefix': ISSUE_PREFIX,
+ 'serv': ISSUE_SERVER_LNK,
+ }
+
+ newtext = URL_PAT.sub(url_func, text_)
+
+ if link_:
+ # wrap not links into final link => link_
+ newtext = linkify_others(newtext, link_)
+
+ return literal(newtext)
+ except:
+ log.error(traceback.format_exc())
+ pass
+
+ return text_
+
+
+def rst(source):
+ return literal('%s
' %
+ MarkupRenderer.rst(source))
+
+
+def rst_w_mentions(source):
+ """
+ Wrapped rst renderer with @mention highlighting
+
+ :param source:
+ """
+ return literal('%s
' %
+ MarkupRenderer.rst_with_mentions(source))