# -*- coding: utf-8 -*-
"""
rhodecode.controllers.changelog
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
changelog controller for rhodecode
:created_on: Apr 21, 2010
:author: marcink
:copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
:license: GPLv3, see COPYING for more details.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import traceback
from mercurial import graphmod
from pylons import request, session, tmpl_context as c
from pylons import request, url, session, tmpl_context as c
from pylons.controllers.util import redirect
from pylons.i18n.translation import _
import rhodecode.lib.helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.helpers import RepoPage
from rhodecode.lib.compat import json
from vcs.exceptions import RepositoryError, ChangesetError, \
ChangesetDoesNotExistError,BranchDoesNotExistError
log = logging.getLogger(__name__)
class ChangelogController(BaseRepoController):
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def __before__(self):
super(ChangelogController, self).__before__()
c.affected_files_cut_off = 60
def index(self):
limit = 100
default = 20
if request.params.get('size'):
try:
int_size = int(request.params.get('size'))
except ValueError:
int_size = default
int_size = int_size if int_size <= limit else limit
c.size = int_size
session['changelog_size'] = c.size
session.save()
else:
c.size = int(session.get('changelog_size', default))
p = int(request.params.get('page', 1))
branch_name = request.params.get('branch', None)
if branch_name:
collection = [z for z in
c.rhodecode_repo.get_changesets(start=0,
branch_name=branch_name)]
c.total_cs = len(collection)
collection = list(c.rhodecode_repo)
c.total_cs = len(c.rhodecode_repo)
c.pagination = RepoPage(c.rhodecode_repo, page=p,
item_count=c.total_cs, items_per_page=c.size,
branch_name=branch_name)
self._graph(c.rhodecode_repo, c.total_cs, c.size, p)
c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
items_per_page=c.size, branch=branch_name)
except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
log.error(traceback.format_exc())
h.flash(str(e), category='warning')
return redirect(url('home'))
self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
c.branch_name = branch_name
c.branch_filters = [('',_('All Branches'))] + \
[(k,k) for k in c.rhodecode_repo.branches.keys()]
return render('changelog/changelog.html')
def changelog_details(self, cs):
if request.environ.get('HTTP_X_PARTIAL_XHR'):
c.cs = c.rhodecode_repo.get_changeset(cs)
return render('changelog/changelog_details.html')
def _graph(self, repo, repo_size, size, p):
def _graph(self, repo, collection, repo_size, size, p):
Generates a DAG graph for mercurial
:param repo: repo instance
:param size: number of commits to show
:param p: page number
if not repo.revisions:
if not collection:
c.jsdata = json.dumps([])
return
revcount = min(repo_size, size)
offset = 1 if p == 1 else ((p - 1) * revcount + 1)
rev_end = repo.revisions.index(repo.revisions[(-1 * offset)])
rev_end = collection.index(collection[(-1 * offset)])
except IndexError:
rev_end = repo.revisions.index(repo.revisions[-1])
rev_end = collection.index(collection[-1])
rev_start = max(0, rev_end - revcount)
data = []
rev_end += 1
if repo.alias == 'git':
for _ in xrange(rev_start, rev_end):
vtx = [0, 1]
edges = [[0, 0, 1]]
data.append(['', vtx, edges])
elif repo.alias == 'hg':
revs = list(reversed(xrange(rev_start, rev_end)))
c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs))
for (id, type, ctx, vtx, edges) in c.dag:
if type != graphmod.CHANGESET:
continue
c.jsdata = json.dumps(data)
rhodecode.controllers.changeset
changeset controller for pylons showoing changes beetween
revisions
:created_on: Apr 25, 2010
from pylons import tmpl_context as c, url, request, response
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.compat import OrderedDict
ChangesetDoesNotExistError
from vcs.nodes import FileNode
from vcs.utils import diffs as differ
class ChangesetController(BaseRepoController):
super(ChangesetController, self).__before__()
def index(self, revision):
def wrap_to_table(str):
return '''<table class="code-difftable">
<tr class="line">
<td class="lineno new"></td>
<td class="code"><pre>%s</pre></td>
</tr>
</table>''' % str
#get ranges of revisions if preset
rev_range = revision.split('...')[:2]
if len(rev_range) == 2:
rev_start = rev_range[0]
rev_end = rev_range[1]
rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
end=rev_end)
rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
c.cs_ranges = list(rev_ranges)
if not c.cs_ranges:
raise RepositoryError('Changeset range returned empty result')
c.changes = OrderedDict()
c.sum_added = 0
c.sum_removed = 0
c.lines_added = 0
c.lines_deleted = 0
c.cut_off = False # defines if cut off limit is reached
# Iterate over ranges (default changeset view is always one changeset)
for changeset in c.cs_ranges:
c.changes[changeset.raw_id] = []
changeset_parent = changeset.parents[0]
changeset_parent = None
#==================================================================
# ADDED FILES
for node in changeset.added:
filenode_old = FileNode(node.path, '', EmptyChangeset())
if filenode_old.is_binary or node.is_binary:
diff = wrap_to_table(_('binary file'))
st = (0, 0)
# in this case node.size is good parameter since those are
# added nodes and their size defines how many changes were
# made
c.sum_added += node.size
if c.sum_added < self.cut_off_limit:
f_gitdiff = differ.get_gitdiff(filenode_old, node)
d = differ.DiffProcessor(f_gitdiff, format='gitdiff')
st = d.stat()
diff = d.as_html()
diff = wrap_to_table(_('Changeset is to big and '
'was cut off, see raw '
'changeset instead'))
c.cut_off = True
break
cs1 = None
cs2 = node.last_changeset.raw_id
c.lines_added += st[0]
c.lines_deleted += st[1]
c.changes[changeset.raw_id].append(('added', node, diff,
cs1, cs2, st))
# CHANGED FILES
if not c.cut_off:
for node in changeset.changed:
filenode_old = changeset_parent.get_node(node.path)
except ChangesetError:
log.warning('Unable to fetch parent node for diff')
filenode_old = FileNode(node.path, '',
EmptyChangeset())
if c.sum_removed < self.cut_off_limit:
d = differ.DiffProcessor(f_gitdiff,
format='gitdiff')
if (st[0] + st[1]) * 256 > self.cut_off_limit:
diff = wrap_to_table(_('Diff is to big '
'and was cut off, see '
'raw diff instead'))
if diff:
c.sum_removed += len(diff)
cs1 = filenode_old.last_changeset.raw_id
c.changes[changeset.raw_id].append(('changed', node, diff,
# REMOVED FILES
for node in changeset.removed:
c.changes[changeset.raw_id].append(('removed', node, None,
None, None, (0, 0)))
if len(c.cs_ranges) == 1:
c.changeset = c.cs_ranges[0]
c.changes = c.changes[c.changeset.raw_id]
return render('changeset/changeset.html')
return render('changeset/changeset_range.html')
def raw_changeset(self, revision):
method = request.GET.get('diff', 'show')
c.scm_type = c.rhodecode_repo.alias
c.changeset = c.rhodecode_repo.get_changeset(revision)
except RepositoryError:
c.changeset_parent = c.changeset.parents[0]
c.changeset_parent = None
c.changes = []
for node in c.changeset.added:
filenode_old = FileNode(node.path, '')
diff = _('binary file') + '\n'
diff = differ.DiffProcessor(f_gitdiff,
format='gitdiff').raw_diff()
c.changes.append(('added', node, diff, cs1, cs2))
for node in c.changeset.changed:
filenode_old = c.changeset_parent.get_node(node.path)
diff = _('binary file')
c.changes.append(('changed', node, diff, cs1, cs2))
response.content_type = 'text/plain'
if method == 'download':
response.content_disposition = 'attachment; filename=%s.patch' \
% revision
c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
c.changeset.parents])
c.diffs = ''
for x in c.changes:
c.diffs += x[2]
return render('changeset/raw_changeset.html')
rhodecode.controllers.shortlog
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shortlog controller for rhodecode
:created_on: Apr 18, 2010
from pylons import tmpl_context as c, request, url
class ShortlogController(BaseRepoController):
super(ShortlogController, self).__before__()
def index(self, repo_name):
size = int(request.params.get('size', 20))
def url_generator(**kw):
return url('shortlog_home', repo_name=repo_name, size=size, **kw)
c.repo_changesets = RepoPage(c.rhodecode_repo, page=p,
items_per_page=size,
url=url_generator)
items_per_page=size, url=url_generator)
c.shortlog_data = render('shortlog/shortlog_data.html')
return c.shortlog_data
r = render('shortlog/shortlog.html')
return r
@@ -99,574 +99,572 @@ class _FilesBreadCrumbs(object):
if isinstance(paths, str):
paths = safe_unicode(paths)
url_l = [link_to(repo_name, url('files_home',
repo_name=repo_name,
revision=rev, f_path=''))]
paths_l = paths.split('/')
for cnt, p in enumerate(paths_l):
if p != '':
url_l.append(link_to(p, url('files_home',
revision=rev,
f_path='/'.join(paths_l[:cnt + 1]))))
return literal('/'.join(url_l))
files_breadcrumbs = _FilesBreadCrumbs()
class CodeHtmlFormatter(HtmlFormatter):
"""My code Html Formatter for source codes
def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
def _wrap_code(self, source):
for cnt, it in enumerate(source):
i, t = it
t = '<div id="L%s">%s</div>' % (cnt + 1, t)
yield i, t
def _wrap_tablelinenos(self, inner):
dummyoutfile = StringIO.StringIO()
lncount = 0
for t, line in inner:
if t:
lncount += 1
dummyoutfile.write(line)
fl = self.linenostart
mw = len(str(lncount + fl - 1))
sp = self.linenospecial
st = self.linenostep
la = self.lineanchors
aln = self.anchorlinenos
nocls = self.noclasses
if sp:
lines = []
for i in range(fl, fl + lncount):
if i % st == 0:
if i % sp == 0:
if aln:
lines.append('<a href="#%s%d" class="special">%*d</a>' %
(la, i, mw, i))
lines.append('<span class="special">%*d</span>' % (mw, i))
lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
lines.append('%*d' % (mw, i))
lines.append('')
ls = '\n'.join(lines)
# in case you wonder about the seemingly redundant <div> here: since the
# content in the other cell also is wrapped in a div, some browsers in
# some configurations seem to mess up the formatting...
if nocls:
yield 0, ('<table class="%stable">' % self.cssclass +
'<tr><td><div class="linenodiv" '
'style="background-color: #f0f0f0; padding-right: 10px">'
'<pre style="line-height: 125%">' +
ls + '</pre></div></td><td id="hlcode" class="code">')
'<tr><td class="linenos"><div class="linenodiv"><pre>' +
yield 0, dummyoutfile.getvalue()
yield 0, '</td></tr></table>'
def pygmentize(filenode, **kwargs):
"""pygmentize function using pygments
:param filenode:
return literal(code_highlight(filenode.content,
filenode.lexer, CodeHtmlFormatter(**kwargs)))
def pygmentize_annotation(repo_name, filenode, **kwargs):
"""pygmentize function for annotation
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
:returns: RGB tuple
def hsv_to_rgb(h, s, v):
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
golden_ratio = 0.618033988749895
h = 0.22717784590367374
for _ in xrange(n):
h += golden_ratio
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)
cgenerator = gen_color()
def get_color_string(cs):
if color_dict.has_key(cs):
col = color_dict[cs]
col = color_dict[cs] = cgenerator.next()
return "color: rgb(%s)! important;" % (', '.join(col))
def url_func(repo_name):
def _url_func(changeset):
author = changeset.author
date = changeset.date
message = tooltip(changeset.message)
tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
" %s<br/><b>Date:</b> %s</b><br/><b>Message:"
"</b> %s<br/></div>")
tooltip_html = tooltip_html % (author, date, message)
lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
short_id(changeset.raw_id))
uri = link_to(
lnk_format,
url('changeset_home', repo_name=repo_name,
revision=changeset.raw_id),
style=get_color_string(changeset.raw_id),
class_='tooltip',
title=tooltip_html
)
uri += '\n'
return uri
return _url_func
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)
flash = _Flash()
#==============================================================================
# SCM FILTERS available via h.
from vcs.utils import author_name, author_email
from rhodecode.lib import credentials_filter, age as _age
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 bool2icon(value):
"""Returns True/False values represented as small html image of true/false
icons
:param value: bool value
if value is True:
return HTML.tag('img', src=url("/images/icons/accept.png"),
alt=_('True'))
if value is False:
return HTML.tag('img', src=url("/images/icons/cancel.png"),
alt=_('False'))
return value
def action_parser(user_log, feed=False):
"""This helper will action_map the specified string action into translated
fancy names with icons and links
:param user_log: user log instance
:param feed: use output for feeds (no html and fancy icons)
action = user_log.action
action_params = ' '
x = action.split(':')
if len(x) > 1:
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(',')
repo_name = user_log.repository.repo_name
repo = user_log.repository.scm_instance
message = lambda rev: get_changeset_safe(repo, rev).message
cs_links = []
cs_links.append(" " + ', '.join ([link_to(rev,
url('changeset_home',
revision=rev), title=tooltip(message(rev)),
class_='tooltip') for rev in revs[:revs_limit] ]))
compare_view = (' <div class="compare_view tooltip" title="%s">'
'<a href="%s">%s</a> '
'</div>' % (_('Show all combined changesets %s->%s' \
% (revs[0], revs[-1])),
revision='%s...%s' % (revs[0], revs[-1])
),
_('compare view'))
if len(revs) > revs_limit:
uniq_id = revs[0]
html_tmpl = ('<span> %s '
'<a class="show_more" id="_%s" href="#more">%s</a> '
'%s</span>')
if not feed:
cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
% (len(revs) - revs_limit),
_('revisions')))
html_tmpl = '<span id="%s" style="display:none"> %s </span>'
html_tmpl = '<span id="%s"> %s </span>'
cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
repo_name=repo_name, revision=rev),
title=message(rev), class_='tooltip')
for rev in revs[revs_limit:revs_top_limit]])))
if len(revs) > 1:
cs_links.append(compare_view)
return ''.join(cs_links)
def get_fork_name():
repo_name = action_params
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_str = action_map.get(action, action)
if feed:
action = action_str[0].replace('[', '').replace(']', '')
action = action_str[0].replace('[', '<span class="journal_highlight">')\
.replace(']', '</span>')
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_params = None
tmpl = """<img src="%s%s" alt="%s"/>"""
map = {'user_deleted_repo':'database_delete.png',
'user_created_repo':'database_add.png',
'user_forked_repo':'arrow_divide.png',
'user_updated_repo':'database_edit.png',
'admin_deleted_repo':'database_delete.png',
'admin_created_repo':'database_add.png',
'admin_forked_repo':'arrow_divide.png',
'admin_updated_repo':'database_edit.png',
'push':'script_add.png',
'push_local':'script_edit.png',
'push_remote':'connect.png',
'pull':'down_16.png',
'started_following_repo':'heart_add.png',
'stopped_following_repo':'heart_delete.png',
return literal(tmpl % ((url('/images/icons/')),
map.get(action, action), action))
# PERMS
from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
HasRepoPermissionAny, HasRepoPermissionAll
# GRAVATAR URL
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)
ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
default = 'identicon'
baseurl_nossl = "http://www.gravatar.com/avatar/"
baseurl_ssl = "https://secure.gravatar.com/avatar/"
baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
if isinstance(email_address, unicode):
#hashlib crashes on unicode items
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)})
return gravatar_url
# REPO PAGER, PAGER FOR REPOSITORY
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
self._url_generator = url
# Safe the kwargs class-wide so they can be used in the pager() method
self.kwargs = kwargs
# Save a reference to the collection
self.original_collection = collection
self.collection = collection
# The self.page is the number of the current page.
# The first page has the number 1!
self.page = int(page) # make it int() if we get it as a string
except (ValueError, TypeError):
self.page = 1
self.items_per_page = items_per_page
# Unless the user tells us how many items the collections has
# we calculate that ourselves.
if item_count is not None:
self.item_count = item_count
self.item_count = len(self.collection)
# Compute the number of the first and last available page
if self.item_count > 0:
self.first_page = 1
self.page_count = int(math.ceil(float(self.item_count) /
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
if self.page > self.last_page:
self.page = self.last_page
elif self.page < self.first_page:
self.page = self.first_page
# Note: the number of items on this page can be less than
# items_per_page if the last page is not full
self.first_item = max(0, (self.item_count) - (self.page *
items_per_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,
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:
self.previous_page = self.page - 1
self.previous_page = None
if self.page < self.last_page:
self.next_page = self.page + 1
self.next_page = None
# No items available
self.first_page = None
self.page_count = 0
self.last_page = None
self.first_item = None
self.last_item = None
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:
pref = ': <br/> '
suf = ''
if len(nodes) > 30:
suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
return literal(pref + '<br/> '.join([safe_unicode(x.path)
for x in nodes[:30]]) + suf)
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
if not groups:
return repo_name
def make_link(group):
return link_to(group.name, url('repos_group_home',
group_name=group.group_name))
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
a, d, t = stats[0], stats[1], stats[0] + stats[1]
width = 100
unit = float(width) / (t or 1)
# needs > 9% of width to be visible or 0 to be hidden
a_p = max(9, unit * a) if a > 0 else 0
d_p = max(9, unit * d) if d > 0 else 0
p_sum = a_p + d_p
if p_sum > width:
#adjust the percentage to be == 100% since we adjusted to 9
if a_p > d_p:
a_p = a_p - (p_sum - width)
d_p = d_p - (p_sum - width)
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]
if l_type == 'a' and d_v:
#case when added and deleted are present
return ' '.join(map(map_getter, ['tl', 'bl']))
if l_type == 'a' and not d_v:
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
if l_type == 'd' and a_v:
return ' '.join(map(map_getter, ['tr', 'br']))
if l_type == 'd' and not a_v:
d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
a_p, a_v)
d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
d_p, d_v)
return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
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]))+)')
def url_func(match_obj):
url_full = match_obj.groups()[0]
return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
return literal(url_pat.sub(url_func, text))
@@ -1533,893 +1533,914 @@ div.form div.fields div.field div.button
border-left: 1px solid #b3b3b3;
border-right: 1px solid #eaeaea;
border-bottom: 1px solid #eaeaea;
color: #000;
font-size: 11px;
margin: 0;
padding: 7px 7px 6px;
#login div.form div.fields div.buttons {
clear: both;
overflow: hidden;
border-top: 1px solid #DDD;
text-align: right;
padding: 10px 0 0;
#login div.form div.links {
margin: 10px 0 0;
padding: 0 0 2px;
#quick_login {
top: 31px;
background-color: rgb(0, 51, 103);
z-index: 999;
height: 150px;
position: absolute;
margin-left: -16px;
width: 281px;
-webkit-border-radius: 0px 0px 4px 4px;
-khtml-border-radius: 0px 0px 4px 4px;
-moz-border-radius: 0px 0px 4px 4px;
border-radius: 0px 0px 4px 4px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
#quick_login .password_forgoten {
padding-right: 10px;
padding-top: 0px;
float: left;
#quick_login .password_forgoten a {
font-size: 10px
#quick_login .register {
padding-top: 5px;
#quick_login .register a {
#quick_login div.form div.fields {
padding-top: 2px;
padding-left: 10px;
#quick_login div.form div.fields div.field {
padding: 5px;
#quick_login div.form div.fields div.field div.label label {
color: #fff;
padding-bottom: 3px;
#quick_login div.form div.fields div.field div.input input {
width: 236px;
background: #FFF;
border-top: 1px solid #b3b3b3;
padding: 5px 7px 4px;
#quick_login div.form div.fields div.buttons {
padding: 10px 14px 0px 5px;
#quick_login div.form div.links {
#register div.title {
position: relative;
background-color: #eedc94;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1),
to(#eedc94) );
background-image: -moz-linear-gradient(top, #003b76, #00376e);
background-image: -ms-linear-gradient(top, #003b76, #00376e);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),
color-stop(100%, #00376e) );
background-image: -webkit-linear-gradient(top, #003b76, #00376e) );
background-image: -o-linear-gradient(top, #003b76, #00376e) );
background-image: linear-gradient(top, #003b76, #00376e);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
endColorstr='#00376e', GradientType=0 );
margin: 0 auto;
padding: 0;
#register div.inner {
border-top: none;
border-bottom: none;
padding: 20px;
#register div.form div.fields div.field div.label {
width: 135px;
margin: 2px 10px 0 0;
padding: 5px 0 0 5px;
#register div.form div.fields div.field div.input input {
width: 300px;
#register div.form div.fields div.buttons {
text-align: left;
padding: 10px 0 0 150px;
#register div.form div.activation_msg {
padding-top: 4px;
padding-bottom: 4px;
#journal .journal_day {
font-size: 20px;
padding: 10px 0px;
border-bottom: 2px solid #DDD;
margin-left: 10px;
margin-right: 10px;
#journal .journal_container {
margin: 0px 5px 0px 10px;
#journal .journal_action_container {
padding-left: 38px;
#journal .journal_user {
color: #747474;
font-size: 14px;
font-weight: bold;
height: 30px;
#journal .journal_icon {
padding-right: 4px;
padding-top: 3px;
#journal .journal_action {
min-height: 2px;
float: left
#journal .journal_action_params {
clear: left;
padding-left: 22px;
#journal .journal_repo {
margin-left: 6px;
#journal .date {
color: #777777;
#journal .journal_repo .journal_repo_name {
font-size: 1.1em;
#journal .compare_view {
padding: 5px 0px 5px 0px;
width: 95px;
.journal_highlight {
padding: 0 2px;
vertical-align: bottom;
.trending_language_tbl,.trending_language_tbl td {
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
.trending_language {
background-color: #003367;
color: #FFF;
display: block;
min-width: 20px;
text-decoration: none;
height: 12px;
margin-bottom: 4px;
margin-left: 5px;
white-space: pre;
padding: 3px;
h3.files_location {
font-size: 1.8em;
font-weight: 700;
border-bottom: none !important;
margin: 10px 0 !important;
#files_data dl dt {
width: 115px;
#files_data dl dd {
padding: 5px !important;
#changeset_content {
border: 1px solid #CCC;
#changeset_compare_view_content {
#changeset_content .container {
min-height: 120px;
font-size: 1.2em;
#changeset_compare_view_content .compare_view_commits {
width: auto !important;
#changeset_compare_view_content .compare_view_commits td {
padding: 0px 0px 0px 12px !important;
#changeset_content .container .right {
float: right;
width: 25%;
#changeset_content .container .left .message {
font-style: italic;
color: #556CB5;
white-space: pre-wrap;
.cs_files .cur_cs {
margin: 10px 2px;
.cs_files .node {
.cs_files .changes {
.cs_files .changes .added {
background-color: #BBFFBB;
text-align: center;
font-size: 90%;
.cs_files .changes .deleted {
background-color: #FF8888;
.cs_files .cs_added {
background: url("../images/icons/page_white_add.png") no-repeat scroll
3px;
height: 16px;
padding-left: 20px;
margin-top: 7px;
.cs_files .cs_changed {
background: url("../images/icons/page_white_edit.png") no-repeat scroll
.cs_files .cs_removed {
background: url("../images/icons/page_white_delete.png") no-repeat
scroll 3px;
#graph {
#graph_nodes {
margin-right: -6px;
margin-top: 0px;
#graph_content {
width: 800px;
#graph_content .container_header {
padding: 10px;
height: 45px;
#graph_content #rev_range_container {
#graph_content .container {
border-bottom: 1px solid #CCC;
border-left: 1px solid #CCC;
border-right: 1px solid #CCC;
min-height: 70px;
#graph_content .container .right {
width: 28%;
padding-bottom: 5px;
#graph_content .container .left .date {
#graph_content .container .left .date span {
vertical-align: text-top;
#graph_content .container .left .author {
height: 22px;
#graph_content .container .left .author .user {
color: #444444;
font-size: 12px;
margin-left: -4px;
margin-top: 4px;
#graph_content .container .left .message {
font-size: 100%;
#graph_content .container .left .message a:hover{
.right div {
.right .changes .changed_total {
border: 1px solid #DDD;
min-width: 45px;
cursor: pointer;
background: #FD8;
.right .changes .added,.changed,.removed {
min-width: 15px;
cursor: help;
.right .changes .large {
background: #54A9F7;
.right .changes .added {
background: #BFB;
.right .changes .changed {
.right .changes .removed {
background: #F88;
.right .merge {
vertical-align: top;
font-size: 0.75em;
.right .parent {
font-family: monospace;
.right .logtags .branchtag {
background: #FFF url("../images/icons/arrow_branch.png") no-repeat right
6px;
font-size: 0.8em;
padding: 11px 16px 0 0;
.right .logtags .tagtag {
background: #FFF url("../images/icons/tag_blue.png") no-repeat right 6px;
padding: 2px 2px 2px 2px;
.right .logtags{
.right .logtags .branchtag,.logtags .branchtag {
padding: 1px 3px 2px;
background-color: #bfbfbf;
font-size: 9.75px;
color: #ffffff;
text-transform: uppercase;
white-space: nowrap;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
padding-left:4px;
.right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
.right .logtags .tagtag,.logtags .tagtag {
background-color: #62cffc;
.right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
div.browserblock {
border: 1px solid #ccc;
background: #f8f8f8;
line-height: 125%;
div.browserblock .browser-header {
padding: 10px 0px 15px 0px;
width: 100%;
div.browserblock .browser-nav {
div.browserblock .browser-branch {
div.browserblock .browser-branch label {
color: #4A4A4A;
div.browserblock .browser-header span {
div.browserblock .browser-search {
padding: 8px 8px 0px 5px;
height: 20px;
div.browserblock #node_filter_box {
div.browserblock .search_activate {
div.browserblock .add_node {
padding-left: 5px;
div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
{
text-decoration: none !important;
div.browserblock .browser-body {
background: #EEE;
border-top: 1px solid #CCC;
table.code-browser {
border-collapse: collapse;
table.code-browser tr {
margin: 3px;
table.code-browser thead th {
background-color: #EEE;
table.code-browser tbody td {
table.code-browser .browser-file {
background: url("../images/icons/document_16.png") no-repeat scroll 3px;
.diffblock .changeset_file {
background: url("../images/icons/file.png") no-repeat scroll 3px;
.diffblock .changeset_header {
margin-left: 6px !important;
table.code-browser .browser-dir {
background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
.box .search {
padding: 0 20px 10px;
.box .search div.search_path {
background: none repeat scroll 0 0 #EEE;
color: blue;
margin-bottom: 10px;
padding: 10px 0;
.box .search div.search_path div.link {
margin-left: 25px;
.box .search div.search_path div.link a {
color: #003367;
#path_unlock {
color: red;
padding-left: 4px;
.info_box span {
margin-left: 3px;
margin-right: 3px;
.info_box .rev {
font-size: 1.6em;
vertical-align: sub;
.info_box input#at_rev,.info_box input#size {
padding: 1px 5px 1px;
.info_box input#view {
padding: 4px 3px 2px 2px;
.yui-overlay,.yui-panel-container {
visibility: hidden;
z-index: 2;
.yui-tt {
color: #666;
background-color: #FFF;
border: 2px solid #003367;
font: 100% sans-serif;
width: auto;
opacity: 1px;
padding: 8px;
-webkit-border-radius: 8px 8px 8px 8px;
-khtml-border-radius: 8px 8px 8px 8px;
-moz-border-radius: 8px 8px 8px 8px;
border-radius: 8px 8px 8px 8px;
.ac {
.ac .yui-ac {
.ac .perm_ac {
width: 15em;
.ac .yui-ac-input {
.ac .yui-ac-container {
top: 1.6em;
.ac .yui-ac-content {
border: 1px solid gray;
background: #fff;
z-index: 9050;
.ac .yui-ac-shadow {
background: #000;
-moz-opacity: 0.1px;
opacity: .10;
filter: alpha(opacity = 10);
z-index: 9049;
margin: .3em;
.ac .yui-ac-content ul {
.ac .yui-ac-content li {
cursor: default;
padding: 2px 5px;
.ac .yui-ac-content li.yui-ac-prehighlight {
background: #B3D4FF;
.ac .yui-ac-content li.yui-ac-highlight {
background: #556CB5;
.follow {
background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
width: 20px;
margin-top: 2px;
.following {
background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
.currently_following {
.add_icon {
background: url("../images/icons/add.png") no-repeat scroll 3px;
.edit_icon {
background: url("../images/icons/folder_edit.png") no-repeat scroll 3px;
.delete_icon {
background: url("../images/icons/delete.png") no-repeat scroll 3px;
.refresh_icon {
background: url("../images/icons/arrow_refresh.png") no-repeat scroll
.pull_icon {
background: url("../images/icons/connect.png") no-repeat scroll 3px;
.rss_icon {
background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
.atom_icon {
background: url("../images/icons/atom.png") no-repeat scroll 3px;
.archive_icon {
background: url("../images/icons/compress.png") no-repeat scroll 3px;
padding-top: 1px;
.start_following_icon {
.stop_following_icon {
.action_button {
border: 0;
display: inline;
.action_button:hover {
text-decoration: underline;
#switch_repos {
height: 25px;
z-index: 1;
#switch_repos select {
min-width: 150px;
max-height: 250px;
.breadcrumbs {
border: medium none;
% if c.repo_branches:
<table class="table_disp">
<tr>
<th class="left">${_('date')}</th>
<th class="left">${_('name')}</th>
<th class="left">${_('author')}</th>
<th class="left">${_('revision')}</th>
<th class="left">${_('links')}</th>
%for cnt,branch in enumerate(c.repo_branches.items()):
<tr class="parity${cnt%2}">
<td><span class="tooltip" title="${h.age(branch[1].date)}">${branch[1].date}</span>
</td>
<td>
<span class="logtags">
<span class="branchtag">${h.link_to(branch[0],
h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))}</span>
</span>
<td title="${branch[1].author}">${h.person(branch[1].author)}</td>
<td>r${branch[1].revision}:${h.short_id(branch[1].raw_id)}</td>
<td class="nowrap">
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))}
|
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))}
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id),class_="ui-button-small")}
<span style="color:#515151">|</span>
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id),class_="ui-button-small")}
%endfor
% if hasattr(c,'repo_closed_branches') and c.repo_closed_branches:
%for cnt,branch in enumerate(c.repo_closed_branches.items()):
<span class="branchtag">${h.link_to(branch[0]+' [closed]',
%endif
</table>
%else:
${_('There are no branches yet')}
\ No newline at end of file
## -*- coding: utf-8 -*-
<%inherit file="/base/base.html"/>
<%def name="title()">
${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
</%def>
<%def name="breadcrumbs_links()">
${h.link_to(u'Home',h.url('/'))}
»
${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
<%def name="page_nav()">
${self.menu('changelog')}
<%def name="main()">
<div class="box">
<!-- box / title -->
<div class="title">
${self.breadcrumbs()}
</div>
<div class="table">
% if c.pagination:
<div id="graph">
<div id="graph_nodes">
<canvas id="graph_canvas"></canvas>
<div id="graph_content">
<div class="container_header">
${h.form(h.url.current(),method='get')}
<div class="info_box">
<div class="info_box" style="float:left">
${h.submit('set',_('Show'),class_="ui-button-small")}
${h.text('size',size=1,value=c.size)}
<span class="rev">${_('revisions')}</span>
${h.end_form()}
<div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
<div id="rev_range_container" style="display:none"></div>
%for cnt,cs in enumerate(c.pagination):
<div id="chg_${cnt+1}" class="container">
<div class="left">
<div class="date">
${h.checkbox(cs.short_id,class_="changeset_range")}
<span>${_('commit')} ${cs.revision}: ${h.short_id(cs.raw_id)}@${cs.date}</span>
<div class="author">
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
<div title="${h.email_or_none(cs.author)}" class="user">${h.person(cs.author)}</div>
##<span><a href="mailto:${h.email_or_none(cs.author)}">${h.email_or_none(cs.author)}</a></span><br/>
<div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
<div class="right">
<div id="${cs.raw_id}_changes_info" class="changes">
<span id="${cs.raw_id}" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</span>
%if len(cs.parents)>1:
<div class="merge">
${_('merge')}<img alt="merge" src="${h.url('/images/icons/arrow_join.png')}"/>
<div class="merge">${_('merge')}</div>
%if cs.parents:
%for p_cs in reversed(cs.parents):
<div class="parent">${_('Parent')} ${p_cs.revision}: ${h.link_to(h.short_id(p_cs.raw_id),
h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}
<div class="parent">${_('No parents')}</div>
%if cs.branch:
<span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
%for tag in cs.tags:
<span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
<div class="pagination-wh pagination-left">
${c.pagination.pager('$link_previous ~2~ $link_next')}
<script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
<script type="text/javascript">
YAHOO.util.Event.onDOMReady(function(){
//Monitor range checkboxes and build a link to changesets
//ranges
var checkboxes = YUD.getElementsByClassName('changeset_range');
var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
YUE.on(checkboxes,'click',function(e){
var checked_checkboxes = [];
for (pos in checkboxes){
if(checkboxes[pos].checked){
checked_checkboxes.push(checkboxes[pos]);
if(checked_checkboxes.length>1){
var rev_end = checked_checkboxes[0].name;
var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
var url = url_tmpl.replace('__REVRANGE__',
rev_start+'...'+rev_end);
var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
link = link.replace('__S',rev_start);
link = link.replace('__E',rev_end);
YUD.get('rev_range_container').innerHTML = link;
YUD.setStyle('rev_range_container','display','');
else{
YUD.setStyle('rev_range_container','display','none');
});
//Fetch changeset details
YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
var id = e.currentTarget.id
var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
var url = url.replace('__CS__',id);
ypjax(url,id+'_changes_info',function(){tooltip_activate()});
// change branch filter
YUE.on(YUD.get('branch_filter'),'change',function(e){
var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
console.log(selected_branch);
var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
var url = url.replace('__BRANCH__',selected_branch);
if(selected_branch != ''){
window.location = url;
}else{
window.location = url_main;
function set_canvas(heads) {
var c = document.getElementById('graph_nodes');
var t = document.getElementById('graph_content');
canvas = document.getElementById('graph_canvas');
var div_h = t.clientHeight;
c.style.height=div_h+'px';
canvas.setAttribute('height',div_h);
c.style.height=max_w+'px';
canvas.setAttribute('width',max_w);
};
var heads = 1;
var max_heads = 0;
var jsdata = ${c.jsdata|n};
for( var i=0;i<jsdata.length;i++){
var m = Math.max.apply(Math, jsdata[i][1]);
if (m>max_heads){
max_heads = m;
var max_w = Math.max(100,max_heads*25);
set_canvas(max_w);
var r = new BranchRenderer();
r.render(jsdata,max_w);
</script>
${_('There are no changes yet')}
% if c.repo_changesets:
<table>
<th class="left">${_('commit message')}</th>
<th class="left">${_('age')}</th>
<th class="left">${_('branch')}</th>
<th class="left">${_('tags')}</th>
%for cnt,cs in enumerate(c.repo_changesets):
${h.link_to(h.truncate(cs.message,50),
h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
title=cs.message)}
<td><span class="tooltip" title="${cs.date}">
${h.age(cs.date)}</span>
<td title="${cs.author}">${h.person(cs.author)}</td>
<td>r${cs.revision}:${h.short_id(cs.raw_id)}</td>
<span class="branchtag">${cs.branch}</span>
<span class="tagtag">${tag}</span>
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),class_="ui-button-small")}
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id),class_="ui-button-small")}
YUE.onDOMReady(function(){
YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
YUE.preventDefault(e);
},'.pager_link');
${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
%if c.repo_tags:
%for cnt,tag in enumerate(c.repo_tags.items()):
<td><span class="tooltip" title="${h.age(tag[1].date)}">
${tag[1].date}</span>
<span class="tagtag">${h.link_to(tag[0],
h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id))}</span>
<td title="${tag[1].author}">${h.person(tag[1].author)}</td>
<td>r${tag[1].revision}:${h.short_id(tag[1].raw_id)}</td>
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id))}
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id))}
${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id),class_="ui-button-small")}
${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id),class_="ui-button-small")}
${_('There are no tags yet')}
Status change: