@@ -301,96 +301,104 @@ def make_map(config):
rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
controller='journal', action='toggle_following',
conditions=dict(method=["POST"]))
#SEARCH
rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
rmap.connect('search_repo', '%s/search/{search_repo:.*}' % ADMIN_PREFIX,
controller='search')
#LOGIN/LOGOUT/REGISTER/SIGN IN
rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
action='logout')
rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
action='register')
rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
controller='login', action='password_reset')
rmap.connect('reset_password_confirmation',
'%s/password_reset_confirmation' % ADMIN_PREFIX,
controller='login', action='password_reset_confirmation')
#FEEDS
rmap.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
controller='feed', action='rss',
conditions=dict(function=check_repo))
rmap.connect('atom_feed_home', '/{repo_name:.*}/feed/atom',
controller='feed', action='atom',
#==========================================================================
# REPOSITORY ROUTES
rmap.connect('summary_home', '/{repo_name:.*}',
controller='summary',
rmap.connect('repos_group_home', '/{group_name:.*}',
controller='admin/repos_groups', action="show_by_name",
conditions=dict(function=check_group))
rmap.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
controller='changeset', revision='tip',
rmap.connect('changeset_comment', '/{repo_name:.*}/changeset/{revision}/comment',
controller='changeset', revision='tip', action='comment',
rmap.connect('changeset_comment_delete', '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
controller='changeset', action='delete_comment',
rmap.connect('raw_changeset_home',
'/{repo_name:.*}/raw-changeset/{revision}',
controller='changeset', action='raw_changeset',
revision='tip', conditions=dict(function=check_repo))
rmap.connect('summary_home', '/{repo_name:.*}/summary',
controller='summary', conditions=dict(function=check_repo))
rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
controller='shortlog', conditions=dict(function=check_repo))
rmap.connect('branches_home', '/{repo_name:.*}/branches',
controller='branches', conditions=dict(function=check_repo))
rmap.connect('tags_home', '/{repo_name:.*}/tags',
controller='tags', conditions=dict(function=check_repo))
rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
controller='changelog', conditions=dict(function=check_repo))
rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
controller='changelog', action='changelog_details',
rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
controller='files', revision='tip', f_path='',
rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
controller='files', action='diff', revision='tip', f_path='',
rmap.connect('files_rawfile_home',
'/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
controller='files', action='rawfile', revision='tip',
f_path='', conditions=dict(function=check_repo))
rmap.connect('files_raw_home',
'/{repo_name:.*}/raw/{revision}/{f_path:.*}',
controller='files', action='raw', revision='tip', f_path='',
rmap.connect('files_annotate_home',
'/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
controller='files', action='annotate', revision='tip',
rmap.connect('files_edit_home',
# -*- coding: utf-8 -*-
"""
rhodecode.controllers.changeset
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
changeset controller for pylons showoing changes beetween
revisions
:created_on: Apr 25, 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 pylons import tmpl_context as c, url, request, response
from pylons.i18n.translation import _
from pylons.controllers.util import redirect
from pylons.decorators import jsonify
import rhodecode.lib.helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
NotAnonymous
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.compat import OrderedDict
from rhodecode.model.db import ChangesetComment
from rhodecode.model.comment import ChangesetCommentsModel
from vcs.exceptions import RepositoryError, ChangesetError, \
ChangesetDoesNotExistError
from vcs.nodes import FileNode
from vcs.utils import diffs as differ
log = logging.getLogger(__name__)
class ChangesetController(BaseRepoController):
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def __before__(self):
super(ChangesetController, self).__before__()
c.affected_files_cut_off = 60
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]
try:
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)
else:
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')
except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
log.error(traceback.format_exc())
h.flash(str(e), category='warning')
return redirect(url('home'))
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
c.comments = []
for cs in c.cs_ranges:
c.comments.extend(ChangesetComment.query()\
.filter(ChangesetComment.repo_id == c.rhodecode_db_repo.repo_id)\
.filter(ChangesetComment.commit_id == cs.raw_id)\
.filter(ChangesetComment.line_no == None)\
.filter(ChangesetComment.f_path == None).all())
# 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]
except IndexError:
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:
@@ -207,48 +219,67 @@ class ChangesetController(BaseRepoContro
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')
cs1 = filenode_old.last_changeset.raw_id
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')
def comment(self, repo_name, revision):
ccmodel = ChangesetCommentsModel()
ccmodel.create(text=request.POST.get('text'),
repo_id=c.rhodecode_db_repo.repo_id,
user_id=c.rhodecode_user.user_id,
commit_id=revision, f_path=request.POST.get('f_path'),
line_no = request.POST.get('line'))
return redirect(h.url('changeset_home', repo_name=repo_name,
revision=revision))
@jsonify
@HasRepoPermissionAnyDecorator('hg.admin', 'repository.admin')
def delete_comment(self, comment_id):
ccmodel.delete(comment_id=comment_id)
return True
"""Helper functions
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to both as 'h'.
import random
import hashlib
import StringIO
import urllib
import math
from datetime import datetime
from pygments.formatters import HtmlFormatter
from pygments import highlight as code_highlight
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.markup_renderer import MarkupRenderer
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()
session[token_key] = token
if hasattr(session, 'save'):
session.save()
return session[token_key]
class _GetError(object):
"""Get error from form_errors, and represent it as span wrapped error
message
:param field_name: field to fetch errors for
:param form_errors: form errors dict
def __call__(self, field_name, form_errors):
tmpl = """<span class="error_msg">%s</span>"""
if form_errors and form_errors.has_key(field_name):
return literal(tmpl % form_errors.get(field_name))
get_error = _GetError()
class _ToolTip(object):
def __call__(self, tooltip_title, trim_at=50):
"""Special function just to wrap our text into nice formatted
autowrapped text
@@ -486,97 +488,97 @@ class RepoPage(Page):
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))
self.items = list(self.collection[self.first_item:self.last_item+1])
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, 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):
@@ -623,48 +625,51 @@ def fancy_file_stats(stats):
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))
def rst(source):
return literal('<div class="rst-block">%s</div>' % MarkupRenderer.rst(source))
new file 100644
rhodecode.model.comment
~~~~~~~~~~~~~~~~~~~~~~~
comments model for RhodeCode
:created_on: Nov 11, 2011
from rhodecode.model import BaseModel
class ChangesetCommentsModel(BaseModel):
def create(self, text, repo_id, user_id, commit_id, f_path=None,
line_no=None):
Creates new comment for changeset
:param text:
:param repo_id:
:param user_id:
:param commit_id:
:param f_path:
:param line_no:
comment = ChangesetComment()
comment.repo_id = repo_id
comment.user_id = user_id
comment.commit_id = commit_id
comment.text = text
comment.f_path = f_path
comment.line_no = line_no
self.sa.add(comment)
self.sa.commit()
return comment
def delete(self, comment_id):
Deletes given comment
:param comment_id:
comment = ChangesetComment.get(comment_id)
self.sa.delete(comment)
@@ -1050,55 +1050,71 @@ class CacheInvalidation(Base, BaseModel)
None otherwise. `cache_active = False` means that this cache
state is not valid and needs to be invalidated
:param key:
return cls.query()\
.filter(CacheInvalidation.cache_key == key)\
.filter(CacheInvalidation.cache_active == False)\
.scalar()
@classmethod
def set_invalidate(cls, key):
Mark this Cache key for invalidation
log.debug('marking %s for invalidation' % key)
inv_obj = Session().query(cls)\
.filter(cls.cache_key == key).scalar()
if inv_obj:
inv_obj.cache_active = False
log.debug('cache key not found in invalidation db -> creating one')
inv_obj = CacheInvalidation(key)
Session.add(inv_obj)
Session.commit()
except Exception:
Session.rollback()
def set_valid(cls, key):
Mark this cache key as active and currently cached
inv_obj = Session().query(CacheInvalidation)\
.filter(CacheInvalidation.cache_key == key).scalar()
inv_obj.cache_active = True
class ChangesetComment(Base, BaseModel):
__tablename__ = 'changeset_comments'
__table_args__ = ({'extend_existing':True},)
comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
commit_id = Column('commit_id', String(100), nullable=False)
line_no = Column('line_no', Integer(), nullable=True)
f_path = Column('f_path', String(1000), nullable=True)
user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
text = Column('text', String(25000), nullable=False)
modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
author = relationship('User')
repo = relationship('Repository')
class DbMigrateVersion(Base, BaseModel):
__tablename__ = 'db_migrate_version'
__table_args__ = {'extend_existing':True}
repository_id = Column('repository_id', String(250), primary_key=True)
repository_path = Column('repository_path', Text)
version = Column('version', Integer)
div.diffblock {
overflow: auto;
padding: 0px;
border: 1px solid #ccc;
background: #f8f8f8;
font-size: 100%;
line-height: 100%;
/* new */
line-height: 125%;
}
div.diffblock.margined{
margin: 0px 20px 0px 20px;
div.diffblock .code-header{
border-bottom: 1px solid #CCCCCC;
background: #EEEEEE;
padding:10px 0 10px 0;
div.diffblock .code-header div{
margin-left:25px;
font-weight: bold;
div.diffblock .code-body{
background: #FFFFFF;
div.diffblock pre.raw{
color:#000000;
table.code-difftable{
border-collapse: collapse;
width: 99%;
table.code-difftable td:target *{
background: repeat scroll 0 0 #FFFFBE !important;
text-decoration: underline;
table.code-difftable td {
padding: 0 !important;
background: none !important;
border:0 !important;
.code-difftable .context{
background:none repeat scroll 0 0 #DDE7EF;
.code-difftable .add{
background:none repeat scroll 0 0 #DDFFDD;
.code-difftable .add ins{
background:none repeat scroll 0 0 #AAFFAA;
text-decoration:none;
.code-difftable .del{
background:none repeat scroll 0 0 #FFDDDD;
.code-difftable .del del{
@@ -3090,48 +3090,246 @@ padding-top: .5em !important;
div.readme .readme_box p {
color: black !important;
margin: 1em 0 !important;
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 {
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;
-webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
/** RST STYLE **/
div.rst-block {
padding:0px;
div.rst-block h2 {
font-weight: normal;
background-color: #fafafa;
clear:both;
overflow:hidden;
margin:0;
padding:0 20px 10px;
div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
border-bottom: 0 !important;
margin: 0 !important;
div.rst-block h1:first-child {
padding-top: .25em !important;
div.rst-block h2, div.rst-block h3 {
margin-top: 1.5em !important;
border-top: 4px solid #e0e0e0 !important;
padding-top: .5em !important;
div.rst-block p {
div.rst-block ul {
div.rst-block ol {
div.rst-block pre, code {
div.rst-block code {
div.rst-block pre code {
div.rst-block pre {
.comments {
padding:10px 20px;
.comments .comment {
margin-top: 10px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
.comments .comment .meta {
padding: 6px;
border-bottom: 1px solid #ddd;
.comments .comment .meta img {
vertical-align: middle;
.comments .comment .meta .user {
.comments .comment .meta .date {
float: right;
.comments .comment .text {
margin-top: 7px;
padding-bottom: 13px;
.comments .comments-number{
padding:0px 0px 10px 0px;
.comment-form .clearfix{
background: #EEE;
padding: 10px;
div.comment-form {
margin-top: 20px;
.comment-form strong {
display: block;
margin-bottom: 15px;
.comment-form textarea {
width: 100%;
height: 100px;
font-family: 'Monaco', 'Courier', 'Courier New', monospace;
form.comment-form {
margin-left: 10px;
.comment-form-submit {
margin-top: 5px;
margin-left: 525px;
.file-comments {
display: none;
.comment-form .comment {
.comment-form .comment-help{
padding: 0px 0px 5px 0px;
color: #666;
.comment-form .comment-button{
padding-top:5px;
.add-another-button {
margin-bottom: 10px;
.comment .buttons {
@@ -63,77 +63,101 @@
</div>
%if len(c.changeset.parents)>1:
<div class="merge">
${_('merge')}<img alt="merge" src="${h.url("/images/icons/arrow_join.png")}"/>
%endif
%if c.changeset.parents:
%for p_cs in reversed(c.changeset.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)}
%endfor
%else:
<div class="parent">${_('No parents')}</div>
<span class="logtags">
<span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
%for tag in c.changeset.tags:
<span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
</span>
<span style="font-size:1.1em;font-weight: bold">
${_('%s files affected with %s additions and %s deletions.') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
<div class="cs_files">
%for change,filenode,diff,cs1,cs2,stat in c.changes:
<div class="cs_${change}">
<div class="node">${h.link_to(h.safe_unicode(filenode.path),
h.url.current(anchor=h.repo_name_slug('C%s' % h.safe_unicode(filenode.path))))}</div>
<div class="changes">${h.fancy_file_stats(stat)}</div>
% if c.cut_off:
${_('Changeset was too big and was cut off...')}
% endif
%if change !='removed':
<div style="clear:both;height:10px"></div>
<div class="diffblock">
<div class="diffblock margined">
<div id="${h.repo_name_slug('C%s' % h.safe_unicode(filenode.path))}" class="code-header">
<div class="changeset_header">
<span class="changeset_file">
${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
%if 1:
» <span>${h.link_to(_('diff'),
h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff'))}</span>
» <span>${h.link_to(_('raw diff'),
h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw'))}</span>
» <span>${h.link_to(_('download diff'),
h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download'))}</span>
<div class="code-body">
%if diff:
${diff|n}
${_('No changes in this file')}
<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
<div class="comments">
<div class="comments-number">${len(c.comments)} comment(s)</div>
%for co in c.comments:
${comment.comment_block(co)}
%if c.rhodecode_user.username != 'default':
<div class="comment-form">
${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=c.changeset.raw_id))}
<strong>Leave a comment</strong>
<div class="clearfix">
<div class="comment-help">${_('Comments parsed using RST syntax')}</div>
${h.textarea('text')}
<div class="comment-button">
${h.submit('save', _('Comment'), class_='ui-button')}
${h.end_form()}
</%def>
##usage:
## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
## ${comment.comment_block(co)}
##
<%def name="comment_block(co)">
<div class="comment" id="comment-${co.comment_id}">
<div class="meta">
<span class="user">
<img src="${h.gravatar_url(co.author.email, 20)}" />
${co.author.username}
<a href="${h.url.current(anchor='comment-%s' % co.comment_id)}"> ${_('commented on')} </a>
${h.short_id(co.commit_id)}
%if co.f_path:
${_(' in file ')}
${co.f_path}:L${co.line_no}
<span class="date">
${h.age(co.modified_at)}
<div class="text">
%if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
<div class="buttons">
<a href="javascript:void(0);" onClick="deleteComment(${co.comment_id})" class="">${_('Delete')}</a>
${h.rst(co.text)|n}
\ No newline at end of file
Status change: