@@ -259,193 +259,193 @@ def make_map(config):
"/settings/{setting_id}.{format}/edit",
action="edit", conditions=dict(method=["GET"]))
m.connect("admin_setting", "/settings/{setting_id}",
action="show", conditions=dict(method=["GET"]))
m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
m.connect("admin_settings_my_account", "/my_account",
action="my_account", conditions=dict(method=["GET"]))
m.connect("admin_settings_my_account_update", "/my_account_update",
action="my_account_update", conditions=dict(method=["PUT"]))
m.connect("admin_settings_create_repository", "/create_repository",
action="create_repository", conditions=dict(method=["GET"]))
#ADMIN MAIN PAGES
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/admin') as m:
m.connect('admin_home', '', action='index')
m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
action='add_repo')
#==========================================================================
# API V1
controller='api/api') as m:
m.connect('api', '/api')
#USER JOURNAL
rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal')
rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
controller='journal', action="public_journal")
rmap.connect('public_journal_rss', '%s/public_journal_rss' % ADMIN_PREFIX,
controller='journal', action="public_journal_rss")
rmap.connect('public_journal_atom',
'%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
action="public_journal_atom")
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',
conditions = dict(function=check_repo, method=["DELETE"]))
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',
'/{repo_name:.*}/edit/{revision}/{f_path:.*}',
controller='files', action='edit', revision='tip',
rmap.connect('files_add_home',
'/{repo_name:.*}/add/{revision}/{f_path:.*}',
controller='files', action='add', revision='tip',
rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
controller='files', action='archivefile',
rmap.connect('files_nodelist_home',
'/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
controller='files', action='nodelist',
rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
controller='settings', action="delete",
conditions=dict(method=["DELETE"], function=check_repo))
rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
controller='settings', action="update",
conditions=dict(method=["PUT"], function=check_repo))
rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
controller='settings', action='index',
rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
controller='settings', action='fork_create',
conditions=dict(function=check_repo, method=["POST"]))
rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
controller='settings', action='fork',
rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
controller='followers', action='followers',
rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
controller='forks', action='forks',
return rmap
# -*- 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, \
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
from webob.exc import HTTPForbidden
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
@@ -185,101 +186,106 @@ class ChangesetController(BaseRepoContro
cs1 = filenode_old.last_changeset.raw_id
cs2 = node.last_changeset.raw_id
c.lines_added += st[0]
c.lines_deleted += st[1]
c.changes[changeset.raw_id].append(('changed', node, diff,
cs1, cs2, st))
# REMOVED FILES
if not c.cut_off:
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()
cs1 = None
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')
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
co = ChangesetComment.get(comment_id)
if (h.HasPermissionAny('hg.admin', 'repository.admin')() or
co.author.user_id == c.rhodecode_user.user_id):
raise HTTPForbidden()
@@ -3170,166 +3170,165 @@ line-height: 1.5em !important;
div.rst-block h1:first-child {
padding-top: .25em !important;
}
div.rst-block h2, div.rst-block h3 {
margin: 1em 0 !important;
div.rst-block h2 {
margin-top: 1.5em !important;
border-top: 4px solid #e0e0e0 !important;
padding-top: .5em !important;
div.rst-block p {
color: black !important;
line-height: 1.5em !important;
div.rst-block ul {
list-style: disc !important;
margin: 1em 0 1em 2em !important;
div.rst-block ol {
list-style: decimal;
div.rst-block pre, code {
font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
div.rst-block code {
font-size: 12px !important;
background-color: ghostWhite !important;
color: #444 !important;
padding: 0 .2em !important;
border: 1px solid #dedede !important;
div.rst-block pre code {
padding: 0 !important;
background-color: #eee !important;
border: none !important;
div.rst-block 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;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
.comments {
padding:10px 20px;
.comments .comment {
margin-top: 10px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
.comments .comment .meta {
background: #f8f8f8;
padding: 6px;
border-bottom: 1px solid #ddd;
.comments .comment .meta img {
vertical-align: middle;
.comments .comment .meta .user {
font-weight: bold;
.comments .comment .meta .date {
float: right;
.comments .comment .text {
margin-top: 7px;
padding-bottom: 13px;
padding: 8px 6px 6px 14px;
background-color: #FAFAFA;
.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 {
position: absolute;
right:40px;
@@ -193,98 +193,108 @@ var tooltip_activate = function(){
// autogenerate one for tooltip
if (!tts[i].id){
tts[i].id='tt'+((i*100)+tts.length);
ids.push(tts[i].id);
return ids
};
var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
context: [[toolTipsId()],"tl","bl",null,[0,5]],
monitorresize:false,
xyoffset :[0,0],
autodismissdelay:300000,
hidedelay:5,
showdelay:20,
});
/**
* show more
*/
var show_more_event = function(){
YUE.on(YUD.getElementsByClassName('show_more'),'click',function(e){
var el = e.target;
YUD.setStyle(YUD.get(el.id.substring(1)),'display','');
YUD.setStyle(el.parentNode,'display','none');
* Quick filter widget
*
* @param target: filter input target
* @param nodes: list of nodes in html we want to filter.
* @param display_element function that takes current node from nodes and
* does hide or show based on the node
var q_filter = function(target,nodes,display_element){
var nodes = nodes;
var q_filter_field = YUD.get(target);
var F = YAHOO.namespace(target);
YUE.on(q_filter_field,'click',function(){
q_filter_field.value = '';
YUE.on(q_filter_field,'keyup',function(e){
clearTimeout(F.filterTimeout);
F.filterTimeout = setTimeout(F.updateFilter,600);
F.filterTimeout = null;
var show_node = function(node){
YUD.setStyle(node,'display','')
var hide_node = function(node){
YUD.setStyle(node,'display','none');
F.updateFilter = function() {
// Reset timeout
var obsolete = [];
var req = q_filter_field.value.toLowerCase();
var l = nodes.length;
var i;
var showing = 0;
for (i=0;i<l;i++ ){
var n = nodes[i];
var target_element = display_element(n)
if(req && n.innerHTML.toLowerCase().indexOf(req) == -1){
hide_node(target_element);
else{
show_node(target_element);
showing+=1;
// if repo_count is set update the number
var cnt = YUD.get('repo_count');
if(cnt){
YUD.get('repo_count').innerHTML = showing;
var ajaxPOST = function(url,postData,success) {
var sUrl = url;
var callback = {
success: success,
failure: function (o) {
alert("error");
},
var postData = postData;
var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
@@ -65,99 +65,110 @@
<div class="merge">
${_('merge')}<img alt="merge" src="${h.url("/images/icons/arrow_join.png")}"/>
</div>
%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 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()}
<script type="text/javascript">
var deleteComment = function(comment_id){
var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
var postData = '_method=delete';
var success = function(o){
var n = YUD.get('comment-'+comment_id);
n.parentNode.removeChild(n);
ajaxPOST(url,postData,success);
</script>
</%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>
<span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-button-small">${_('Delete')}</span>
${h.rst(co.text)|n}
\ No newline at end of file
Status change: