@@ -3,114 +3,140 @@
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)
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)
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)
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)
@@ -45,59 +45,61 @@ log = logging.getLogger(__name__)
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
@@ -29,31 +29,31 @@ from pylons import tmpl_context as c, re
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
@@ -16,49 +16,49 @@ from pylons import url, request, config
from pylons.i18n.translation import _, ungettext
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
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
from webhelpers.text import chop_at, collapse, convert_accented_entities, \
convert_misc_entities, lchop, plural, rchop, remove_formatting, \
replace_whitespace, urlify, truncate, wrap_paragraphs
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
from vcs.utils.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
def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
Reset button
_set_input_attrs(attrs, type, name, value)
_set_id_attr(attrs, id, name)
convert_boolean_attrs(attrs, ["disabled"])
return HTML.input(**attrs)
reset = _reset
def get_token():
"""Return the current authentication token, creating one if one doesn't
already exist.
token_key = "_authentication_token"
from pylons import session
if not token_key in session:
token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
except AttributeError: # Python < 2.4
token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
@@ -459,49 +459,49 @@ def gravatar_url(email_address, size=30)
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
@@ -510,78 +510,75 @@ class RepoPage(Page):
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
@@ -649,24 +646,25 @@ def fancy_file_stats(stats):
return ' '.join(map(map_getter, ['tr', 'br']))
if l_type == 'd' and not a_v:
return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
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))
@@ -1893,52 +1893,54 @@ h3.files_location {
height: 16px;
padding-left: 20px;
margin-top: 7px;
text-align: left;
}
#graph {
overflow: hidden;
#graph_nodes {
float: left;
margin-right: -6px;
margin-top: 0px;
#graph_content {
width: 800px;
#graph_content .container_header {
border: 1px solid #CCC;
padding: 10px;
height: 45px;
#graph_content #rev_range_container {
padding: 10px 0px;
clear: both;
#graph_content .container {
border-bottom: 1px solid #CCC;
border-left: 1px solid #CCC;
border-right: 1px solid #CCC;
min-height: 70px;
font-size: 1.2em;
#graph_content .container .right {
float: right;
width: 28%;
text-align: right;
padding-bottom: 5px;
#graph_content .container .left .date {
font-weight: 700;
#graph_content .container .left .date span {
@@ -2001,65 +2003,84 @@ h3.files_location {
background: #54A9F7;
.right .changes .added {
background: #BFB;
.right .changes .changed {
background: #FD8;
.right .changes .removed {
background: #F88;
.right .merge {
vertical-align: top;
font-size: 0.75em;
.right .parent {
font-size: 90%;
font-family: monospace;
.right .logtags .branchtag {
background: #FFF url("../images/icons/arrow_branch.png") no-repeat right
6px;
display: block;
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;
font-weight: bold;
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{
text-decoration: none;
.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;
font-size: 100%;
line-height: 125%;
padding: 0;
div.browserblock .browser-header {
background: #FFF;
padding: 10px 0px 15px 0px;
width: 100%;
div.browserblock .browser-nav {
float: left
div.browserblock .browser-branch {
div.browserblock .browser-branch label {
@@ -3074,43 +3095,43 @@ line-height: 1.5em !important;
div.readme .readme_box ul {
list-style: disc !important;
margin: 1em 0 1em 2em !important;
div.readme .readme_box ol {
list-style: decimal;
div.readme .readme_box pre, code {
font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
div.readme .readme_box code {
font-size: 12px !important;
background-color: ghostWhite !important;
color: #444 !important;
padding: 0 .2em !important;
border: 1px solid #dedede !important;
div.readme .readme_box pre code {
padding: 0 !important;
background-color: #eee !important;
border: none !important;
div.readme .readme_box pre {
margin: 1em 0;
font-size: 12px;
background-color: #eee;
border: 1px solid #ddd;
padding: 5px;
color: #444;
overflow: auto;
-webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
% 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
@@ -12,81 +12,80 @@ ${c.repo_name} ${_('Changelog')} - ${c.r
${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>
<%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>
@@ -110,67 +109,81 @@ ${c.repo_name} ${_('Changelog')} - ${c.r
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
// 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')}
@@ -15,48 +15,48 @@
${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")}
<script type="text/javascript">
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');
<div class="pagination-wh pagination-left">
${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
%if c.repo_tags:
<table>
%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: