@@ -337,77 +337,82 @@ def make_map(config):
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',
conditions=dict(function=check_repo))
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_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.files
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Files 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 os
import logging
import mimetypes
import traceback
from os.path import join as jn
from pylons import request, response, session, tmpl_context as c, url
from pylons.i18n.translation import _
from pylons.controllers.util import redirect
from pylons.decorators import jsonify
from vcs.backends import ARCHIVE_SPECS
from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
EmptyRepositoryError, ImproperArchiveTypeError, VCSError
from vcs.nodes import FileNode, NodeKind
from vcs.utils import diffs as differ
from rhodecode.lib import convert_line_endings, detect_mode, safe_str
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
import rhodecode.lib.helpers as h
from rhodecode.model.repo import RepoModel
log = logging.getLogger(__name__)
class FilesController(BaseRepoController):
@LoginRequired()
def __before__(self):
super(FilesController, self).__before__()
c.cut_off_limit = self.cut_off_limit
def __get_cs_or_redirect(self, rev, repo_name):
Safe way to get changeset if error occur it redirects to tip with
proper message
:param rev: revision to fetch
:param repo_name: repo name to redirect after
try:
return c.rhodecode_repo.get_changeset(rev)
except EmptyRepositoryError, e:
h.flash(_('There are no files yet'), category='warning')
redirect(h.url('summary_home', repo_name=repo_name))
except RepositoryError, e:
h.flash(str(e), category='warning')
redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
def __get_filenode_or_redirect(self, repo_name, cs, path):
Returns file_node, if error occurs or given path is directory,
it'll redirect to top level path
:param repo_name: repo_name
:param cs: given changeset
:param path: path to lookup
file_node = cs.get_node(path)
if file_node.is_dir():
raise RepositoryError('given path is a directory')
redirect(h.url('files_home', repo_name=repo_name,
revision=cs.raw_id))
return file_node
def __get_paths(self, changeset, starting_path):
"""recursive walk in root dir and return a set of all path in that dir
based on repository walk function
_files = list()
_dirs = list()
tip = changeset
for topnode, dirs, files in tip.walk(starting_path):
for f in files:
_files.append(f.path)
for d in dirs:
_dirs.append(d.path)
log.debug(traceback.format_exc())
pass
return _dirs, _files
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def index(self, repo_name, revision, f_path):
#reditect to given revision from form if given
post_revision = request.POST.get('at_rev', None)
if post_revision:
cs = self.__get_cs_or_redirect(post_revision, repo_name)
redirect(url('files_home', repo_name=c.repo_name,
revision=cs.raw_id, f_path=f_path))
c.changeset = self.__get_cs_or_redirect(revision, repo_name)
c.branch = request.GET.get('branch', None)
c.f_path = f_path
cur_rev = c.changeset.revision
#prev link
prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
c.url_prev = url('files_home', repo_name=c.repo_name,
revision=prev_rev.raw_id, f_path=f_path)
if c.branch:
c.url_prev += '?branch=%s' % c.branch
except (ChangesetDoesNotExistError, VCSError):
c.url_prev = '#'
#next link
next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
c.url_next = url('files_home', repo_name=c.repo_name,
revision=next_rev.raw_id, f_path=f_path)
c.url_next += '?branch=%s' % c.branch
c.url_next = '#'
#files or dirs
c.files_list = c.changeset.get_node(f_path)
if c.files_list.is_file():
c.file_history = self._get_node_history(c.changeset, f_path)
else:
c.file_history = []
revision=revision))
@@ -368,48 +390,58 @@ class FilesController(BaseRepoController
diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
format='gitdiff')
c.cur_diff = diff.as_html()
#default option
if node1.is_binary or node2.is_binary:
c.cur_diff = _('Binary file')
elif node1.size > self.cut_off_limit or \
node2.size > self.cut_off_limit:
c.cur_diff = ''
c.big_diff = True
if not c.cur_diff and not c.big_diff:
c.no_changes = True
return render('files/file_diff.html')
def _get_node_history(self, cs, f_path):
changesets = cs.get_file_history(f_path)
hist_l = []
changesets_group = ([], _("Changesets"))
branches_group = ([], _("Branches"))
tags_group = ([], _("Tags"))
for chs in changesets:
n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
changesets_group[0].append((chs.raw_id, n_desc,))
hist_l.append(changesets_group)
for name, chs in c.rhodecode_repo.branches.items():
#chs = chs.split(':')[-1]
branches_group[0].append((chs, name),)
hist_l.append(branches_group)
for name, chs in c.rhodecode_repo.tags.items():
tags_group[0].append((chs, name),)
hist_l.append(tags_group)
return hist_l
@jsonify
def nodelist(self, repo_name, revision, f_path):
if request.environ.get('HTTP_X_PARTIAL_XHR'):
cs = self.__get_cs_or_redirect(revision, repo_name)
_d, _f = self.__get_paths(cs, f_path)
return _d + _f
@@ -7,97 +7,96 @@ background:transparent;
margin:0;
padding:0;
}
body {
line-height:1;
height:100%;
background:url("../images/background.png") repeat scroll 0 0 #B0B0B0;
font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif;
font-size:12px;
color:#000;
ol,ul {
list-style:none;
blockquote,q {
quotes:none;
blockquote:before,blockquote:after,q:before,q:after {
content:none;
:focus {
outline:0;
del {
text-decoration:line-through;
table {
border-collapse:collapse;
border-spacing:0;
html {
a {
color:#003367;
text-decoration:none;
cursor:pointer;
font-weight:700;
a:hover {
color:#316293;
text-decoration:underline;
h1,h2,h3,h4,h5,h6 {
color:#292929;
h1 {
font-size:22px;
h2 {
font-size:20px;
h3 {
font-size:18px;
h4 {
font-size:16px;
h5 {
font-size:14px;
h6 {
font-size:11px;
ul.circle {
list-style-type:circle;
ul.disc {
list-style-type:disc;
ul.square {
list-style-type:square;
@@ -1818,117 +1817,136 @@ 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;
div.browserblock {
overflow:hidden;
border:1px solid #ccc;
background:#f8f8f8;
font-size:100%;
line-height:125%;
div.browserblock .browser-header {
background:#FFF;
padding:10px 0px 25px 0px;
padding:10px 0px 15px 0px;
width: 100%;
div.browserblock .browser-nav {
float:left
div.browserblock .browser-branch {
float:left;
div.browserblock .browser-branch label {
color:#4A4A4A;
vertical-align:text-top;
div.browserblock .browser-header span {
margin-left:5px;
div.browserblock .browser-search{
clear:both;
padding:8px 8px 0px 5px;
div.browserblock .search_activate #filter_activate{
vertical-align: sub;
border: 1px solid;
padding:2px;
border-radius: 4px 4px 4px 4px;
background: url("../images/button.png") repeat-x scroll 0 0 #E5E3E3;
border-color: #DDDDDD #DDDDDD #C6C6C6 #C6C6C6;
color: #515151;
div.browserblock .search_activate a:hover{
text-decoration: none !important;
div.browserblock .browser-body {
background:#EEE;
border-top:1px solid #CCC;
table.code-browser {
width:100%;
table.code-browser tr {
margin:3px;
table.code-browser thead th {
background-color:#EEE;
height:20px;
font-size:1.1em;
text-align:left;
padding-left:10px;
table.code-browser tbody td {
table.code-browser .browser-file {
background:url("../images/icons/document_16.png") no-repeat scroll 3px;
height:16px;
padding-left:20px;
.diffblock .changeset_file{
background:url("../images/icons/file.png") no-repeat scroll 3px;
padding-left:22px;
font-size: 14px;
.diffblock .changeset_header{
margin-left: 6px !important;
table.code-browser .browser-dir {
background:url("../images/icons/folder_16.png") no-repeat scroll 3px;
@@ -2674,48 +2692,53 @@ padding:0;
#login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox {
margin:0 0 0 184px;
#login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label {
color:#565656;
#login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input {
font-size:1em;
font-family:Verdana, Helvetica, Sans-Serif;
#changeset_content .container .wrapper,#graph_content .container .wrapper {
width:600px;
#changeset_content .container .left,#graph_content .container .left {
width:70%;
padding-left:5px;
#changeset_content .container .left .date,.ac .match {
padding-top: 5px;
padding-bottom:5px;
div#legend_container table td,div#legend_choices table td {
border:none !important;
height:20px !important;
padding:0 !important;
#q_filter{
border:0 none;
color:#AAAAAA;
margin-bottom:-4px;
margin-top:-4px;
padding-left:3px;
#node_filter{
border:0px solid #545454;
<%def name="file_class(node)">
%if node.is_file():
<%return "browser-file" %>
%else:
<%return "browser-dir"%>
%endif
</%def>
<div id="body" class="browserblock">
<div class="browser-header">
<div class="browser-nav">
${h.form(h.url.current())}
<div class="info_box">
<span class="rev">${_('view')}@rev</span>
<a class="rev" href="${c.url_prev}" title="${_('previous revision')}">«</a>
${h.text('at_rev',value=c.changeset.revision,size=3)}
${h.text('at_rev',value=c.changeset.revision,size=5)}
<a class="rev" href="${c.url_next}" title="${_('next revision')}">»</a>
## ${h.submit('view',_('view'),class_="ui-button-small")}
</div>
${h.end_form()}
<div class="browser-branch">
${h.checkbox('stay_at_branch',c.changeset.branch,c.changeset.branch==c.branch)}
<label>${_('follow current branch')}</label>
<script type="text/javascript">
YUE.on('stay_at_branch','click',function(e){
if(e.target.checked){
var uri = "${h.url.current(branch='__BRANCH__')}"
uri = uri.replace('__BRANCH__',e.target.value);
window.location = uri;
else{
window.location = "${h.url.current()}";
})
</script>
<div class="browser-search">
<div class="search_activate">
<a id="filter_activate" href="#">${_('search file list')}</a>
<div>
<div id="node_filter_box_loading" style="display:none">${_('Loading file list...')}</div>
<div id="node_filter_box" style="display:none">
${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.files_list.path)}/<input type="text" value="type to search..." name="filter" size="25" id="node_filter" autocomplete="off">
var n_filter = YUD.get('node_filter');
var F = YAHOO.namespace('node_filter');
var url = '${h.url("files_nodelist_home",repo_name="__REPO__",revision="__REVISION__",f_path="__FPATH__")}';
var node_url = '${h.url("files_home",repo_name="__REPO__",revision="__REVISION__",f_path="__FPATH__")}';
url = url.replace('__REPO__','${c.repo_name}');
url = url.replace('__REVISION__','${c.changeset.raw_id}');
url = url.replace('__FPATH__','${c.files_list.path}');
node_url = node_url.replace('__REPO__','${c.repo_name}');
node_url = node_url.replace('__REVISION__','${c.changeset.raw_id}');
F.filterTimeout = null;
var nodes = null;
F.initFilter = function(){
YUD.setStyle('node_filter_box_loading','display','');
YUD.setStyle('filter_activate','display','none');
YUC.initHeader('X-PARTIAL-XHR',true);
YUC.asyncRequest('GET',url,{
success:function(o){
nodes = JSON.parse(o.responseText);
YUD.setStyle('node_filter_box_loading','display','none');
YUD.setStyle('node_filter_box','display','');
},
failure:function(o){
console.log('failed to load');
},null);
F.updateFilter = function(e) {
return function(){
// Reset timeout
var query = e.target.value;
var match = [];
var matches = 0;
var matches_max = 20;
if (query != ""){
for(var i=0;i<nodes.length;i++){
var pos = nodes[i].toLowerCase().indexOf(query)
if(query && pos != -1){
matches++
//show only certain amount to not kill browser
if (matches > matches_max){
break;
var n = nodes[i];
var n_hl = n.substring(0,pos)
+"<b>{0}</b>".format(n.substring(pos,pos+query.length))
+n.substring(pos+query.length)
match.push('<tr><td><a class="browser-file" href="{0}">{1}</a></td><td colspan="5"></td></tr>'.format(node_url.replace('__FPATH__',n),n_hl));
if(match.length >= matches_max){
match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format("${_('search truncated')}"));
if(query != ""){
YUD.setStyle('tbody','display','none');
YUD.setStyle('tbody_filtered','display','');
if (match.length==0){
match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format("${_('no matching files')}"));
YUD.get('tbody_filtered').innerHTML = match.join("");
YUD.setStyle('tbody','display','');
YUD.setStyle('tbody_filtered','display','none');
YUE.on(YUD.get('filter_activate'),'click',function(){
F.initFilter();
YUE.on(n_filter,'click',function(){
n_filter.value = '';
});
YUE.on(n_filter,'keyup',function(e){
clearTimeout(F.filterTimeout);
F.filterTimeout = setTimeout(F.updateFilter(e),600);
<div class="browser-body">
<table class="code-browser">
<thead>
<tr>
<th>${_('Name')}</th>
<th>${_('Size')}</th>
<th>${_('Mimetype')}</th>
<th>${_('Revision')}</th>
<th>${_('Last modified')}</th>
<th>${_('Last commiter')}</th>
</tr>
</thead>
<tbody id="tbody">
%if c.files_list.parent:
<tr class="parity0">
<td>
${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.files_list.parent.path),class_="browser-dir")}
</td>
<td></td>
%for cnt,node in enumerate(c.files_list):
<tr class="parity${cnt%2}">
${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node))}
${h.format_byte_size(node.size,binary=True)}
${node.mimetype}
<span class="tooltip" title="${node.last_changeset.raw_id}">
${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}</span>
<span class="tooltip" title="${node.last_changeset.date}">
${h.age(node.last_changeset.date)}</span>
${node.last_changeset.author}
%endfor
</tbody>
<tbody id="tbody_filtered" style="display:none">
</table>
\ No newline at end of file
Status change: