@@ -4,92 +4,185 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 collections import defaultdict
from webob.exc import HTTPForbidden
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
from vcs.exceptions import RepositoryError, ChangesetError, \
ChangesetDoesNotExistError
from vcs.nodes import FileNode
import rhodecode.lib.helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib import diffs
from rhodecode.model.db import ChangesetComment
from rhodecode.model.comment import ChangesetCommentsModel
from rhodecode.model.meta import Session
log = logging.getLogger(__name__)
def anchor_url(revision,path):
fid = h.FID(revision, path)
return h.url.current(anchor=fid,**request.GET)
def get_ignore_ws(fid, GET):
ig_ws_global = request.GET.get('ignorews')
ig_ws = filter(lambda k:k.startswith('WS'),GET.getall(fid))
if ig_ws:
try:
return int(ig_ws[0].split(':')[-1])
except:
pass
return ig_ws_global
def _ignorews_url(fileid=None):
params = defaultdict(list)
lbl = _('show white space')
ig_ws = get_ignore_ws(fileid, request.GET)
ln_ctx = get_line_ctx(fileid, request.GET)
# global option
if fileid is None:
if ig_ws is None:
params['ignorews'] += [1]
lbl = _('ignore white space')
ctx_key = 'context'
ctx_val = ln_ctx
# per file options
else:
params[fileid] += ['WS:1']
ctx_key = fileid
ctx_val = 'C:%s' % ln_ctx
# if we have passed in ln_ctx pass it along to our params
if ln_ctx:
params[ctx_key] += [ctx_val]
params['anchor'] = fileid
return h.link_to(lbl, h.url.current(**params))
def get_line_ctx(fid, GET):
ln_ctx_global = request.GET.get('context')
ln_ctx = filter(lambda k:k.startswith('C'),GET.getall(fid))
retval = ln_ctx[0].split(':')[-1]
retval = ln_ctx_global
return int(retval)
return
def _context_url(fileid=None):
Generates url for context lines
:param fileid:
ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
if ln_ctx > 0:
params['context'] += [ln_ctx]
ig_ws_key = 'ignorews'
ig_ws_val = 1
# per file option
params[fileid] += ['C:%s' % ln_ctx]
ig_ws_key = fileid
ig_ws_val = 'WS:%s' % 1
params[ig_ws_key] += [ig_ws_val]
lbl = _('%s line context') % ln_ctx
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_
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):
ignore_whitespace = request.GET.get('ignorews') == '1'
line_context = request.GET.get('context', 3)
def wrap_to_table(str):
</table>''' % str
c.anchor_url = anchor_url
c.ignorews_url = _ignorews_url
c.context_url = _context_url
#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')
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
@@ -109,92 +202,98 @@ class ChangesetController(BaseRepoContro
inlines = ChangesetCommentsModel()\
.get_inline_comments(c.rhodecode_db_repo.repo_id,
changeset.raw_id)
c.inline_comments.extend(inlines)
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
fid = h.FID(revision, node.path)
line_context_lcl = get_line_ctx(fid, request.GET)
ignore_whitespace_lcl = get_ignore_ws(fid, request.GET)
if c.sum_added < self.cut_off_limit:
f_gitdiff = diffs.get_gitdiff(filenode_old, node,
ignore_whitespace=ignore_whitespace,
context=line_context)
ignore_whitespace=ignore_whitespace_lcl,
context=line_context_lcl)
d = diffs.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:
ignore_whitespace_lcl = get_ignore_ws(fid, request.GET,)
d = diffs.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,
@@ -31,48 +31,59 @@ from webhelpers.text import chop_at, col
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, _make_safe_id_component
from rhodecode.lib.annotate import annotate_highlight
from rhodecode.lib.utils import repo_name_slug
from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
from rhodecode.lib.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
safeid = _make_safe_id_component
def FID(raw_id,path):
Creates a uniqe ID for filenode based on it's path and revision
:param raw_id:
:param path:
return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
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
@@ -1869,49 +1869,51 @@ h3.files_location {
min-height: 120px;
font-size: 1.2em;
overflow: hidden;
}
#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%;
text-align: right;
#changeset_content .container .left .message {
font-style: italic;
color: #556CB5;
white-space: pre-wrap;
#changeset_content .container .left .message a:hover {
text-decoration: none;
.cs_files .cur_cs {
margin: 10px 2px;
font-weight: bold;
.cs_files .node {
float: left;
.cs_files .changes {
color:#003367;
.cs_files .changes .added {
background-color: #BBFFBB;
text-align: center;
font-size: 9px;
padding: 2px 0px 2px 0px;
.cs_files .changes .deleted {
@@ -2233,69 +2235,69 @@ table.code-browser thead th {
table.code-browser tbody td {
padding-left: 10px;
height: 20px;
table.code-browser .browser-file {
background: url("../images/icons/document_16.png") no-repeat scroll 3px;
height: 16px;
padding-left: 20px;
text-align: left;
.diffblock .changeset_header {
.diffblock .changeset_file {
background: url("../images/icons/file.png") no-repeat scroll 3px;
padding: 2px 0px 2px 22px;
.diffblock .diff-menu-wrapper{
.diffblock .changeset_header .diff-menu{
.diffblock .diff-menu{
position: absolute;
background: none repeat scroll 0 0 #FFFFFF;
border-color: #003367 #666666 #666666;
border-right: 1px solid #666666;
border-style: solid solid solid;
border-width: 1px;
box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
margin-top:5px;
margin-left:1px;
.diffblock .changeset_header .diff-menu ul li {
.diffblock .diff-menu ul li {
padding: 0px 0px 0px 0px !important;
.diffblock .changeset_header .diff-menu ul li a{
.diffblock .diff-menu ul li a{
display: block;
padding: 3px 8px 3px 8px !important;
.diffblock .changeset_header .diff-menu ul li a:hover{
.diffblock .diff-menu ul li a:hover{
background-color: #EEEEEE;
table.code-browser .browser-dir {
background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
.box .search {
clear: both;
margin: 0;
padding: 0 20px 10px;
.box .search div.search_path {
background: none repeat scroll 0 0 #EEE;
border: 1px solid #CCC;
color: blue;
margin-bottom: 10px;
padding: 10px 0;
@@ -3668,48 +3670,53 @@ form.comment-inline-form {
/*****************************************************************************
DIFFS CSS
******************************************************************************/
div.diffblock {
overflow: auto;
padding: 0px;
border: 1px solid #ccc;
background: #f8f8f8;
font-size: 100%;
line-height: 100%;
/* new */
line-height: 125%;
-webkit-border-radius: 6px 6px 0px 0px;
-moz-border-radius: 6px 6px 0px 0px;
border-radius: 6px 6px 0px 0px;
div.diffblock.margined{
margin: 0px 20px 0px 20px;
div.diffblock .code-header{
border-bottom: 1px solid #CCCCCC;
background: #EEEEEE;
padding:10px 0 10px 0;
height: 14px;
div.diffblock .code-header .date{
float:left;
text-transform: uppercase;
div.diffblock .code-header div{
margin-left:4px;
font-size: 14px;
div.diffblock .code-body{
background: #FFFFFF;
div.diffblock pre.raw{
color:#000000;
table.code-difftable{
border-collapse: collapse;
width: 99%;
table.code-difftable td {
padding: 0 !important;
background: none !important;
border:0 !important;
vertical-align: none !important;
table.code-difftable .context{
## -*- coding: utf-8 -*-
<%inherit file="/base/base.html"/>
<%def name="title()">
${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${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))}
${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
<%def name="page_nav()">
${self.menu('changelog')}
<%def name="fid(raw_id,path)" filter="strip">
<%
return 'C-%s-%s' % (h.short_id(raw_id),h.safeid(h.safe_unicode(path)))
%>
<%def name="main()">
<div class="box">
<!-- box / title -->
<div class="title">
${self.breadcrumbs()}
</div>
<div class="table">
<div class="diffblock">
<div class="code-header">
<div>
<span>${h.link_to(_('raw diff'),
h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show'))}</span>
» <span>${h.link_to(_('download diff'),
h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download'))}</span>
<div class="comments-number" style="float:right;padding-right:5px">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
<div class="date">${_('commit')} ${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} ${c.changeset.date}</div>
<div class="diff-menu-wrapper">
<img class="diff-menu-activate" style="cursor: pointer" alt="diff-menu" src="${h.url('/images/icons/script_gear.png')}" />
<div class="diff-menu" style="display:none">
<ul>
<li>${h.link_to(_('raw diff'),h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show'))}</li>
<li>${h.link_to(_('download diff'),h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download'))}</li>
<li>${c.ignorews_url()}</li>
<li>${c.context_url()}</li>
</ul>
<div id="changeset_content">
<div class="container">
<div class="left">
<div class="date">${_('commit')} ${c.changeset.revision}: ${h.short_id(c.changeset.raw_id)}@${c.changeset.date}</div>
<div class="author">
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
<span>${h.person(c.changeset.author)}</span><br/>
<span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
<div class="message">${h.link_to(h.wrap_paragraphs(c.changeset.message),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</div>
<div class="right">
<div class="changes">
% if len(c.changeset.affected_files) <= c.affected_files_cut_off:
<span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
<span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
<span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
% else:
<span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
<span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
<span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
% endif
%if len(c.changeset.parents)>1:
<div class="merge">
${_('merge')}<img alt="merge" src="${h.url('/images/icons/arrow_join.png')}"/>
@@ -79,113 +78,115 @@
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>
%endif
<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">
%if change != 'removed':
${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=self.fid(filenode.changeset.raw_id,filenode.path)))}
${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path))}
${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=self.fid('',filenode.path)))}
${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
<div class="changes">${h.fancy_file_stats(stat)}</div>
% if c.cut_off:
${_('Changeset was too big and was cut off...')}
%if change !='removed':
<div style="clear:both;height:10px"></div>
<div class="diffblock margined comm" id="${self.fid(filenode.changeset.raw_id,filenode.path)}">
<div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
<div class="diffblock margined comm">
<div class="changeset_header">
<div 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)))}
<img class="diff-menu-activate" style="margin-bottom:-6px;cursor: pointer" alt="diff-menu" src="${h.url('/images/icons/script_gear.png')}" />
<li>${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'))}</li>
<li>${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'))}</li>
<li>${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'))}</li>
<li>${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
<li>${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}</li>
<span style="float:right;margin-top:-3px">
<label>
${_('show inline comments')}
${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=self.fid(filenode.changeset.raw_id,filenode.path))}
${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
</label>
<div class="code-body">
<div class="full_f_path" path="${filenode.path}"></div>
%if diff:
${diff|n}
${_('No changes in this file')}
<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
## template for inline comment form
${comment.comment_inline_form()}
<div class="comments">
<div class="comments-number">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
%for path, lines in c.inline_comments:
<div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${self.fid(c.changeset.raw_id,path)}">
<div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.FID(c.changeset.raw_id,path)}">
% for line,comments in lines.iteritems():
<div class="inline-comment-placeholder-line" line="${line}" target_id="${h.safeid(h.safe_unicode(path))}">
%for co in comments:
${comment.comment_block(co)}
%for co in c.comments:
%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')} <a href="${h.url('rst_help')}">RST</a> ${_('syntax')}
${_('with')} <span style="color:#003367" class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span> ${_('support')}
${h.textarea('text')}
Status change: