"""
Base utils for shell scripts
import os
import sys
import random
import urllib2
import pprint
try:
from rhodecode.lib.ext_json import json
except ImportError:
import simplejson as json
import json
CONFIG_NAME = '.rhodecode'
FORMAT_PRETTY = 'pretty'
FORMAT_JSON = 'json'
def api_call(apikey, apihost, format, method=None, **kw):
Api_call wrapper for RhodeCode
:param apikey:
:param apihost:
:param format: formatting, pretty means prints and pprint of json
json returns unparsed json
:param method:
def _build_data(random_id):
Builds API data with given random ID
:param random_id:
:type random_id:
return {
"id": random_id,
"api_key": apikey,
"method": method,
"args": kw
}
if not method:
raise Exception('please specify method name !')
id_ = random.randrange(1, 9999)
req = urllib2.Request('%s/_admin/api' % apihost,
data=json.dumps(_build_data(id_)),
headers={'content-type': 'text/plain'})
if format == FORMAT_PRETTY:
sys.stdout.write('calling %s to %s \n' % (req.get_data(), apihost))
ret = urllib2.urlopen(req)
raw_json = ret.read()
json_data = json.loads(raw_json)
id_ret = json_data['id']
_formatted_json = pprint.pformat(json_data)
if id_ret == id_:
if format == FORMAT_JSON:
sys.stdout.write(str(raw_json))
else:
sys.stdout.write('rhodecode returned:\n%s\n' % (_formatted_json))
raise Exception('something went wrong. '
'ID mismatch got %s, expected %s | %s' % (
id_ret, id_, _formatted_json))
class RcConf(object):
RhodeCode config for API
conf = RcConf()
conf['key']
def __init__(self, config_location=None, autoload=True, autocreate=False,
config=None):
self._conf_name = CONFIG_NAME if not config_location else config_location
HOME = os.getenv('HOME', os.getenv('USERPROFILE')) or ''
HOME_CONF = os.path.abspath(os.path.join(HOME, CONFIG_NAME))
self._conf_name = HOME_CONF if not config_location else config_location
self._conf = {}
if autocreate:
self.make_config(config)
if autoload:
self._conf = self.load_config()
def __getitem__(self, key):
return self._conf[key]
def __nonzero__(self):
if self._conf:
return True
return False
def __eq__(self):
return self._conf.__eq__()
def __repr__(self):
return 'RcConf<%s>' % self._conf.__repr__()
def make_config(self, config):
Saves given config as a JSON dump in the _conf_name location
:param config:
:type config:
update = False
if os.path.exists(self._conf_name):
update = True
with open(self._conf_name, 'wb') as f:
json.dump(config, f, indent=4)
if update:
sys.stdout.write('Updated config in %s\n' % self._conf_name)
sys.stdout.write('Created new config in %s\n' % self._conf_name)
def update_config(self, new_config):
Reads the JSON config updates it's values with new_config and
saves it back as JSON dump
:param new_config:
config = {}
with open(self._conf_name, 'rb') as conf:
config = json.load(conf)
except IOError, e:
sys.stderr.write(str(e) + '\n')
config.update(new_config)
def load_config(self):
Loads config from file and returns loaded JSON object
return json.load(conf)
#sys.stderr.write(str(e) + '\n')
pass
# -*- coding: utf-8 -*-
rhodecode.bin.api
~~~~~~~~~~~~~~~~~
Api CLI client for RhodeCode
:created_on: Jun 3, 2012
:author: marcink
:copyright: (C) 2010-2012 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/>.
from __future__ import with_statement
import argparse
from rhodecode.bin.base import api_call, RcConf, FORMAT_JSON, FORMAT_PRETTY
def argparser(argv):
usage = (
"rhodecode-api [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
"[--config=CONFIG] [--save-config] "
"METHOD <key:val> <key2:val> ...\n"
"Create config file: rhodecode-gist --apikey=<key> --apihost=http://rhodecode.server --save-config"
)
parser = argparse.ArgumentParser(description='RhodeCode API cli',
usage=usage)
## config
group = parser.add_argument_group('config')
group.add_argument('--apikey', help='api access key')
group.add_argument('--apihost', help='api host')
group.add_argument('--config', help='config file')
group.add_argument('--save-config', action='store_true', help='save the given config into a file')
group = parser.add_argument_group('API')
group.add_argument('method', metavar='METHOD', nargs='?', type=str, default=None,
help='API method name to call followed by key:value attributes',
group.add_argument('--format', dest='format', type=str,
help='output format default: `pretty` can '
'be also `%s`' % FORMAT_JSON,
default=FORMAT_PRETTY
args, other = parser.parse_known_args()
return parser, args, other
def main(argv=None):
Main execution function for cli
:param argv:
:type argv:
if argv is None:
argv = sys.argv
conf = None
parser, args, other = argparser(argv)
api_credentials_given = (args.apikey and args.apihost)
if args.save_config:
if not api_credentials_given:
raise parser.error('--save-config requires --apikey and --apihost')
conf = RcConf(config_location=args.config,
autocreate=True, config={'apikey': args.apikey,
'apihost': args.apihost})
sys.exit()
if not conf:
conf = RcConf(config_location=args.config, autoload=True)
parser.error('Could not find config file and missing '
'--apikey or --apihost in params')
apikey = args.apikey or conf['apikey']
host = args.apihost or conf['apihost']
method = args.method
margs = dict(map(lambda s: s.split(':', 1), other))
except Exception:
sys.stderr.write('Error parsing arguments \n')
api_call(apikey, host, args.format, method, **margs)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
new file 100755
rhodecode.bin.gist
~~~~~~~~~~~~~~~~~~
Gist CLI client for RhodeCode
:created_on: May 9, 2013
:copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
import stat
import fileinput
from rhodecode.bin.base import api_call, RcConf
"rhodecode-gist [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
"[filename or stdin use - for terminal stdin ]\n"
parser = argparse.ArgumentParser(description='RhodeCode Gist cli',
group.add_argument('--save-config', action='store_true',
help='save the given config into a file')
group = parser.add_argument_group('GIST')
group.add_argument('-f', '--filename', help='set uploaded gist filename')
group.add_argument('-p', '--private', action='store_true',
help='Create private Gist')
group.add_argument('-d', '--description', help='Gist description')
group.add_argument('-l', '--lifetime', metavar='MINUTES',
help='Gist lifetime in minutes, -1 (Default) is forever')
def _run(argv):
DEFAULT_FILENAME = 'gistfile1.txt'
if other:
# skip multifiles for now
filename = other[0]
if filename == '-':
filename = DEFAULT_FILENAME
gist_content = ''
for line in fileinput.input():
gist_content += line
with open(filename, 'rb') as f:
gist_content = f.read()
gist_content = None
# little bit hacky but cross platform check where the
# stdin comes from we skip the terminal case it can be handled by '-'
mode = os.fstat(0).st_mode
if stat.S_ISFIFO(mode):
# "stdin is piped"
gist_content = sys.stdin.read()
elif stat.S_ISREG(mode):
# "stdin is redirected"
# "stdin is terminal"
# make sure we don't upload binary stuff
if gist_content and '\0' in gist_content:
raise Exception('Error: binary files upload is not possible')
filename = args.filename or filename
if gist_content:
files = {
filename: {
'content': gist_content,
'lexer': None
margs = dict(
gist_lifetime=args.lifetime,
gist_description=args.description,
gist_type='private' if args.private else 'public',
files=files
api_call(apikey, host, 'json', 'create_gist', **margs)
return _run(argv)
except Exception, e:
print e
return 1
@@ -298,192 +298,195 @@ def make_map(config):
#ajax delete user group perm
m.connect('delete_user_group_perm_member', "/users_groups/{id}/revoke_perm",
action="delete_user_group_perm_member",
conditions=dict(method=["DELETE"]))
#ADMIN GROUP REST ROUTES
rmap.resource('group', 'groups',
controller='admin/groups', path_prefix=ADMIN_PREFIX)
#ADMIN PERMISSIONS REST ROUTES
rmap.resource('permission', 'permissions',
controller='admin/permissions', path_prefix=ADMIN_PREFIX)
#ADMIN DEFAULTS REST ROUTES
rmap.resource('default', 'defaults',
controller='admin/defaults', path_prefix=ADMIN_PREFIX)
##ADMIN LDAP SETTINGS
rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
controller='admin/ldap_settings', action='ldap_settings',
conditions=dict(method=["POST"]))
rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
controller='admin/ldap_settings')
#ADMIN SETTINGS REST ROUTES
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/settings') as m:
m.connect("admin_settings", "/settings",
action="create", conditions=dict(method=["POST"]))
action="index", conditions=dict(method=["GET"]))
m.connect("formatted_admin_settings", "/settings.{format}",
m.connect("admin_new_setting", "/settings/new",
action="new", conditions=dict(method=["GET"]))
m.connect("formatted_admin_new_setting", "/settings/new.{format}",
m.connect("/settings/{setting_id}",
action="update", conditions=dict(method=["PUT"]))
action="delete", conditions=dict(method=["DELETE"]))
m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
action="edit", conditions=dict(method=["GET"]))
m.connect("formatted_admin_edit_setting",
"/settings/{setting_id}.{format}/edit",
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_my_repos", "/my_account/repos",
action="my_account_my_repos", conditions=dict(method=["GET"]))
m.connect("admin_settings_my_pullrequests", "/my_account/pull_requests",
action="my_account_my_pullrequests", conditions=dict(method=["GET"]))
#NOTIFICATION REST ROUTES
controller='admin/notifications') as m:
m.connect("notifications", "/notifications",
m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
action="mark_all_read", conditions=dict(method=["GET"]))
m.connect("formatted_notifications", "/notifications.{format}",
m.connect("new_notification", "/notifications/new",
m.connect("formatted_new_notification", "/notifications/new.{format}",
m.connect("/notification/{notification_id}",
m.connect("edit_notification", "/notification/{notification_id}/edit",
m.connect("formatted_edit_notification",
"/notification/{notification_id}.{format}/edit",
m.connect("notification", "/notification/{notification_id}",
m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
#ADMIN MAIN PAGES
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')
#ADMIN GIST
rmap.resource('gist', 'gists', controller='admin/gists',
path_prefix=ADMIN_PREFIX)
#==========================================================================
# API V2
controller='api/api') as m:
m.connect('api', '/api')
#USER JOURNAL
rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
controller='journal', action='index')
rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
controller='journal', action='journal_rss')
rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
controller='journal', action='journal_atom')
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_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
rmap.connect('public_journal_atom',
'%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
action="public_journal_atom")
rmap.connect('public_journal_atom_old',
'%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
controller='journal', action='toggle_following',
#SEARCH
rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
controller='search',
conditions=dict(function=check_repo))
rmap.connect('search_repo', '/{repo_name:.*?}/search',
conditions=dict(function=check_repo),
#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',
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('repo_size', '/{repo_name:.*?}/repo_size',
controller='summary', action='repo_size',
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',
# no longer user, but kept for routes to work
rmap.connect("_edit_repo", "/{repo_name:.*?}/edit",
controller='admin/repos', action="edit",
conditions=dict(method=["GET"], function=check_repo)
rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
new file 100644
rhodecode.controllers.admin.gist
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gist controller for RhodeCode
import time
import logging
import traceback
import formencode
from formencode import htmlfill
from pylons import request, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from pylons.i18n.translation import _
from rhodecode.model.forms import GistForm
from rhodecode.model.gist import GistModel
from rhodecode.model.meta import Session
from rhodecode.model.db import Gist
from rhodecode.lib import helpers as h
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.auth import LoginRequired, NotAnonymous
from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
from rhodecode.lib.helpers import Page
from webob.exc import HTTPNotFound
from sqlalchemy.sql.expression import or_
from rhodecode.lib.vcs.exceptions import VCSError
log = logging.getLogger(__name__)
class GistsController(BaseController):
"""REST Controller styled on the Atom Publishing Protocol"""
def __load_defaults(self):
c.lifetime_values = [
(str(-1), _('forever')),
(str(5), _('5 minutes')),
(str(60), _('1 hour')),
(str(60 * 24), _('1 day')),
(str(60 * 24 * 30), _('1 month')),
]
c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
@LoginRequired()
def index(self, format='html'):
"""GET /admin/gists: All items in the collection"""
# url('gists')
c.show_private = request.GET.get('private') and c.rhodecode_user.username != 'default'
gists = Gist().query()\
.filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
.order_by(Gist.created_on.desc())
if c.show_private:
c.gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
.filter(Gist.gist_owner == c.rhodecode_user.user_id)
c.gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
p = safe_int(request.GET.get('page', 1), 1)
c.gists_pager = Page(c.gists, page=p, items_per_page=10)
return render('admin/gists/index.html')
@NotAnonymous()
def create(self):
"""POST /admin/gists: Create a new item"""
self.__load_defaults()
gist_form = GistForm([x[0] for x in c.lifetime_values])()
form_result = gist_form.to_python(dict(request.POST))
#TODO: multiple files support, from the form
nodes = {
form_result['filename'] or 'gistfile1.txt': {
'content': form_result['content'],
'lexer': None # autodetect
_public = form_result['public']
gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
gist = GistModel().create(
description=form_result['description'],
owner=c.rhodecode_user,
gist_mapping=nodes,
gist_type=gist_type,
lifetime=form_result['lifetime']
Session().commit()
new_gist_id = gist.gist_access_id
except formencode.Invalid, errors:
defaults = errors.value
return formencode.htmlfill.render(
render('admin/gists/new.html'),
defaults=defaults,
errors=errors.error_dict or {},
prefix_error=False,
encoding="UTF-8"
log.error(traceback.format_exc())
h.flash(_('Error occurred during gist creation'), category='error')
return redirect(url('new_gist'))
return redirect(url('gist', id=new_gist_id))
def new(self, format='html'):
"""GET /admin/gists/new: Form to create a new item"""
# url('new_gist')
return render('admin/gists/new.html')
def update(self, id):
"""PUT /admin/gists/id: Update an existing item"""
# Forms posted to this method should contain a hidden field:
# <input type="hidden" name="_method" value="PUT" />
# Or using helpers:
# h.form(url('gist', id=ID),
# method='put')
# url('gist', id=ID)
def delete(self, id):
"""DELETE /admin/gists/id: Delete an existing item"""
# <input type="hidden" name="_method" value="DELETE" />
# method='delete')
def show(self, id, format='html'):
"""GET /admin/gists/id: Show a specific item"""
gist_id = id
c.gist = Gist.get_or_404(gist_id)
#check if this gist is not expired
if c.gist.gist_expires != -1:
if time.time() > c.gist.gist_expires:
log.error('Gist expired at %s' %
(time_to_datetime(c.gist.gist_expires)))
raise HTTPNotFound()
c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
except VCSError:
return render('admin/gists/show.html')
def edit(self, id, format='html'):
"""GET /admin/gists/id/edit: Form to edit an existing item"""
# url('edit_gist', id=ID)
rhodecode.controllers.api
~~~~~~~~~~~~~~~~~~~~~~~~~
API controller for RhodeCode
:created_on: Aug 20, 2011
:copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
# 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; version 2
# of the License or (at your opinion) any later version of the license.
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
from rhodecode.controllers.api import JSONRPCController, JSONRPCError
from rhodecode.lib.auth import PasswordGenerator, AuthUser, \
HasPermissionAllDecorator, HasPermissionAnyDecorator, \
HasPermissionAnyApi, HasRepoPermissionAnyApi
from rhodecode.lib.utils import map_groups, repo2db_mapper
from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_int
from rhodecode.model.scm import ScmModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.user import UserModel
from rhodecode.model.users_group import UserGroupModel
from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap,\
Permission, User
Permission, User, Gist
from rhodecode.lib.compat import json
from rhodecode.lib.exceptions import DefaultUserException
class OptionalAttr(object):
Special Optional Option that defines other attribute
def __init__(self, attr_name):
self.attr_name = attr_name
return '<OptionalAttr:%s>' % self.attr_name
def __call__(self):
return self
#alias
OAttr = OptionalAttr
class Optional(object):
Defines an optional parameter::
param = param.getval() if isinstance(param, Optional) else param
param = param() if isinstance(param, Optional) else param
is equivalent of::
param = Optional.extract(param)
def __init__(self, type_):
self.type_ = type_
return '<Optional:%s>' % self.type_.__repr__()
return self.getval()
def getval(self):
returns value from this Optional instance
return self.type_
@classmethod
def extract(cls, val):
if isinstance(val, cls):
return val.getval()
return val
def get_user_or_error(userid):
Get user by id or name or return JsonRPCError if not found
:param userid:
user = UserModel().get_user(userid)
if user is None:
raise JSONRPCError("user `%s` does not exist" % userid)
return user
def get_repo_or_error(repoid):
Get repo by id or name or return JsonRPCError if not found
repo = RepoModel().get_repo(repoid)
if repo is None:
raise JSONRPCError('repository `%s` does not exist' % (repoid))
return repo
def get_users_group_or_error(usersgroupid):
Get user group by id or name or return JsonRPCError if not found
users_group = UserGroupModel().get_group(usersgroupid)
if users_group is None:
raise JSONRPCError('user group `%s` does not exist' % usersgroupid)
return users_group
def get_perm_or_error(permid):
Get permission by id or name or return JsonRPCError if not found
@@ -795,272 +796,314 @@ class ApiController(JSONRPCController):
enable_locking = defs.get('repo_enable_locking')
if isinstance(enable_downloads, Optional):
enable_downloads = defs.get('repo_enable_downloads')
clone_uri = Optional.extract(clone_uri)
description = Optional.extract(description)
landing_rev = Optional.extract(landing_rev)
# create structure of groups and return the last group
group = map_groups(repo_name)
repo = RepoModel().create_repo(
repo_name=repo_name,
repo_type=repo_type,
description=description,
owner=owner,
private=private,
clone_uri=clone_uri,
repos_group=group,
landing_rev=landing_rev,
enable_statistics=enable_statistics,
enable_downloads=enable_downloads,
enable_locking=enable_locking
return dict(
msg="Created new repository `%s`" % (repo.repo_name),
repo=repo.get_api_data()
raise JSONRPCError('failed to create repository `%s`' % repo_name)
@HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
def fork_repo(self, apiuser, repoid, fork_name, owner=Optional(OAttr('apiuser')),
description=Optional(''), copy_permissions=Optional(False),
private=Optional(False), landing_rev=Optional('tip')):
repo = get_repo_or_error(repoid)
repo_name = repo.repo_name
_repo = RepoModel().get_by_repo_name(fork_name)
if _repo:
type_ = 'fork' if _repo.fork else 'repo'
raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
if HasPermissionAnyApi('hg.admin')(user=apiuser):
elif HasRepoPermissionAnyApi('repository.admin',
'repository.write',
'repository.read')(user=apiuser,
repo_name=repo.repo_name):
if not isinstance(owner, Optional):
#forbid setting owner for non-admins
raise JSONRPCError(
'Only RhodeCode admin can specify `owner` param'
if isinstance(owner, Optional):
owner = apiuser.user_id
owner = get_user_or_error(owner)
group = map_groups(fork_name)
form_data = dict(
repo_name=fork_name,
repo_name_full=fork_name,
repo_group=group,
repo_type=repo.repo_type,
description=Optional.extract(description),
private=Optional.extract(private),
copy_permissions=Optional.extract(copy_permissions),
landing_rev=Optional.extract(landing_rev),
update_after_clone=False,
fork_parent_id=repo.repo_id,
RepoModel().create_fork(form_data, cur_user=owner)
msg='Created fork of `%s` as `%s`' % (repo.repo_name,
fork_name),
success=True # cannot return the repo data here since fork
# cann be done async
'failed to fork repository `%s` as `%s`' % (repo_name,
fork_name)
# perms handled inside
def delete_repo(self, apiuser, repoid, forks=Optional(None)):
Deletes a given repository
:param apiuser:
:param repoid:
:param forks: detach or delete, what do do with attached forks for repo
if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
# check if we have admin permission for this repo !
if HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
repo_name=repo.repo_name) is False:
handle_forks = Optional.extract(forks)
_forks_msg = ''
_forks = [f for f in repo.forks]
if handle_forks == 'detach':
_forks_msg = ' ' + _('Detached %s forks') % len(_forks)
elif handle_forks == 'delete':
_forks_msg = ' ' + _('Deleted %s forks') % len(_forks)
elif _forks:
'Cannot delete `%s` it still contains attached forks'
% repo.repo_name
RepoModel().delete(repo, forks=forks)
msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
success=True
'failed to delete repository `%s`' % repo.repo_name
@HasPermissionAllDecorator('hg.admin')
def grant_user_permission(self, apiuser, repoid, userid, perm):
Grant permission for user on given repository, or update existing one
if found
:param perm:
user = get_user_or_error(userid)
perm = get_perm_or_error(perm)
RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
perm.permission_name, user.username, repo.repo_name
),
'failed to edit permission for user: `%s` in repo: `%s`' % (
userid, repoid
def revoke_user_permission(self, apiuser, repoid, userid):
Revoke permission for user on given repository
RepoModel().revoke_user_permission(repo=repo, user=user)
msg='Revoked perm for user: `%s` in repo: `%s`' % (
user.username, repo.repo_name
def grant_users_group_permission(self, apiuser, repoid, usersgroupid,
perm):
Grant permission for user group on given repository, or update
existing one if found
:param usersgroupid:
users_group = get_users_group_or_error(usersgroupid)
RepoModel().grant_users_group_permission(repo=repo,
group_name=users_group,
perm=perm)
msg='Granted perm: `%s` for user group: `%s` in '
'repo: `%s`' % (
perm.permission_name, users_group.users_group_name,
repo.repo_name
'failed to edit permission for user group: `%s` in '
usersgroupid, repo.repo_name
def revoke_users_group_permission(self, apiuser, repoid, usersgroupid):
Revoke permission for user group on given repository
RepoModel().revoke_users_group_permission(repo=repo,
group_name=users_group)
msg='Revoked perm for user group: `%s` in repo: `%s`' % (
users_group.users_group_name, repo.repo_name
def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
gist_type=Optional(Gist.GIST_PUBLIC),
gist_lifetime=Optional(-1),
gist_description=Optional('')):
description = Optional.extract(gist_description)
gist_type = Optional.extract(gist_type)
gist_lifetime = Optional.extract(gist_lifetime)
# files: {
# 'filename': {'content':'...', 'lexer': null},
# 'filename2': {'content':'...', 'lexer': null}
#}
gist = GistModel().create(description=description,
gist_mapping=files,
lifetime=gist_lifetime)
msg='created new gist',
gist_url=gist.gist_url(),
gist_id=gist.gist_access_id,
gist_type=gist.gist_type,
files=files.keys()
raise JSONRPCError('failed to create gist')
def update_gist(self, apiuser):
def delete_gist(self, apiuser):
rhodecode.controllers.files
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Files controller for RhodeCode
:created_on: Apr 21, 2010
import tempfile
import shutil
from pylons import request, response, tmpl_context as c, url
from pylons.controllers.util import redirect
from rhodecode.lib.utils import jsonify
from rhodecode.lib import diffs
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
str2bool
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.vcs.backends.base import EmptyChangeset
from rhodecode.lib.vcs.conf import settings
from rhodecode.lib.vcs.exceptions import RepositoryError, \
ChangesetDoesNotExistError, EmptyRepositoryError, \
ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
NodeDoesNotExistError, ChangesetError, NodeError
from rhodecode.lib.vcs.nodes import FileNode
from rhodecode.model.db import Repository
from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
_context_url, get_line_ctx, get_ignore_ws
from rhodecode.lib.exceptions import NonRelativePathError
class FilesController(BaseRepoController):
def __before__(self):
super(FilesController, self).__before__()
c.cut_off_limit = self.cut_off_limit
def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
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
return c.rhodecode_repo.get_changeset(rev)
except EmptyRepositoryError, e:
if not redirect_after:
return None
url_ = url('files_add_home',
repo_name=c.repo_name,
revision=0, f_path='')
add_new = h.link_to(_('Click here to add new file'), url_)
h.flash(h.literal(_('There are no files yet %s') % add_new),
category='warning')
redirect(h.url('summary_home', repo_name=repo_name))
except RepositoryError, e: # including ChangesetDoesNotExistError
h.flash(str(e), category='error')
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')
except RepositoryError, e:
return file_node
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def index(self, repo_name, revision, f_path, annotate=False):
# redirect 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)
c.changeset = self.__get_cs_or_redirect(revision, repo_name)
c.branch = request.GET.get('branch', None)
c.f_path = f_path
c.annotate = annotate
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.file = c.changeset.get_node(f_path)
@@ -278,211 +279,218 @@ class FilesController(BaseRepoController
# create multiple heads via file editing
_branches = repo.scm_instance.branches
# check if revision is a branch name or branch hash
if revision not in _branches.keys() + _branches.values():
h.flash(_('You can only edit files with revision '
'being a valid branch '), category='warning')
return redirect(h.url('files_home',
repo_name=repo_name, revision='tip',
f_path=f_path))
r_post = request.POST
c.cs = self.__get_cs_or_redirect(revision, repo_name)
c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
if c.file.is_binary:
return redirect(url('files_home', repo_name=c.repo_name,
revision=c.cs.raw_id, f_path=f_path))
c.default_message = _('Edited file %s via RhodeCode') % (f_path)
if r_post:
old_content = c.file.content
sl = old_content.splitlines(1)
first_line = sl[0] if sl else ''
# modes: 0 - Unix, 1 - Mac, 2 - DOS
mode = detect_mode(first_line, 0)
content = convert_line_endings(r_post.get('content', ''), mode)
message = r_post.get('message') or c.default_message
author = self.rhodecode_user.full_contact
if content == old_content:
h.flash(_('No changes'), category='warning')
return redirect(url('changeset_home', repo_name=c.repo_name,
revision='tip'))
self.scm_model.commit_change(repo=c.rhodecode_repo,
repo_name=repo_name, cs=c.cs,
user=self.rhodecode_user.user_id,
author=author, message=message,
content=content, f_path=f_path)
h.flash(_('Successfully committed to %s') % f_path,
category='success')
h.flash(_('Error occurred during commit'), category='error')
return redirect(url('changeset_home',
repo_name=c.repo_name, revision='tip'))
return render('files/files_edit.html')
@HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
def add(self, repo_name, revision, f_path):
repo = Repository.get_by_repo_name(repo_name)
if repo.enable_locking and repo.locked[0]:
h.flash(_('This repository is has been locked by %s on %s')
% (h.person_by_id(repo.locked[0]),
h.fmt_date(h.time_to_datetime(repo.locked[1]))),
'warning')
repo_name=repo_name, revision='tip'))
c.cs = self.__get_cs_or_redirect(revision, repo_name,
redirect_after=False)
if c.cs is None:
c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
c.default_message = (_('Added file via RhodeCode'))
unix_mode = 0
content = convert_line_endings(r_post.get('content', ''), unix_mode)
filename = r_post.get('filename')
location = r_post.get('location', '')
file_obj = r_post.get('upload_file', None)
if file_obj is not None and hasattr(file_obj, 'filename'):
filename = file_obj.filename
content = file_obj.file
if not content:
h.flash(_('No content'), category='warning')
if not filename:
h.flash(_('No filename'), category='warning')
if location.startswith('/') or location.startswith('.') or '../' in location:
h.flash(_('Location must be relative path and must not '
'contain .. in path'), category='warning')
if location:
location = os.path.normpath(location)
#strip all crap out of file, just leave the basename
filename = os.path.basename(filename)
node_path = os.path.join(location, filename)
self.scm_model.create_node(repo=c.rhodecode_repo,
content=content, f_path=node_path)
node_path: {
'content': content
self.scm_model.create_nodes(
user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
message=message,
nodes=nodes,
parent_cs=c.cs,
author=author,
h.flash(_('Successfully committed to %s') % node_path,
except NonRelativePathError, e:
except (NodeError, NodeAlreadyExistsError), e:
h.flash(_(e), category='error')
return render('files/files_add.html')
def archivefile(self, repo_name, fname):
fileformat = None
revision = None
ext = None
subrepos = request.GET.get('subrepos') == 'true'
for a_type, ext_data in settings.ARCHIVE_SPECS.items():
archive_spec = fname.split(ext_data[1])
if len(archive_spec) == 2 and archive_spec[1] == '':
fileformat = a_type or ext_data[1]
revision = archive_spec[0]
ext = ext_data[1]
dbrepo = RepoModel().get_by_repo_name(repo_name)
if not dbrepo.enable_downloads:
return _('Downloads disabled')
if c.rhodecode_repo.alias == 'hg':
# patch and reset hooks section of UI config to not run any
# hooks on fetching archives with subrepos
for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
cs = c.rhodecode_repo.get_changeset(revision)
content_type = settings.ARCHIVE_SPECS[fileformat][0]
except ChangesetDoesNotExistError:
return _('Unknown revision %s') % revision
except EmptyRepositoryError:
return _('Empty repository')
except (ImproperArchiveTypeError, KeyError):
return _('Unknown archive type')
# archive cache
from rhodecode import CONFIG
rev_name = cs.raw_id[:12]
archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
safe_str(rev_name), ext)
use_cached_archive = False # defines if we use cached version of archive
archive_cache_enabled = CONFIG.get('archive_cache_dir')
if not subrepos and archive_cache_enabled:
#check if we it's ok to write
if not os.path.isdir(CONFIG['archive_cache_dir']):
os.makedirs(CONFIG['archive_cache_dir'])
cached_archive_path = os.path.join(CONFIG['archive_cache_dir'], archive_name)
if os.path.isfile(cached_archive_path):
log.debug('Found cached archive in %s' % cached_archive_path)
fd, archive = None, cached_archive_path
use_cached_archive = True
log.debug('Archive %s is not yet cached' % (archive_name))
if not use_cached_archive:
#generate new archive
fd, archive = tempfile.mkstemp()
t = open(archive, 'wb')
log.debug('Creating new temp archive in %s' % archive)
cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
if archive_cache_enabled:
#if we generated the archive and use cache rename that
log.debug('Storing new archive in %s' % cached_archive_path)
shutil.move(archive, cached_archive_path)
archive = cached_archive_path
finally:
t.close()
def get_chunked_archive(archive):
stream = open(archive, 'rb')
while True:
data = stream.read(16 * 1024)
if not data:
stream.close()
if fd: # fd means we used temporary file
os.close(fd)
if not archive_cache_enabled:
log.debug('Destroing temp archive %s' % archive)
os.remove(archive)
break
yield data
response.content_disposition = str('attachment; filename=%s' % (archive_name))
@@ -72,193 +72,192 @@ class PullrequestsController(BaseRepoCon
"""return a structure with repo's interesting changesets, suitable for
the selectors in pullrequest.html
rev: a revision that must be in the list somehow and selected by default
branch: a branch that must be in the list and selected by default - even if closed
branch_rev: a revision of which peers should be preferred and available."""
# list named branches that has been merged to this named branch - it should probably merge back
peers = []
if rev:
rev = safe_str(rev)
if branch:
branch = safe_str(branch)
if branch_rev:
branch_rev = safe_str(branch_rev)
# not restricting to merge() would also get branch point and be better
# (especially because it would get the branch point) ... but is currently too expensive
otherbranches = {}
for i in repo._repo.revs(
"sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
branch_rev, branch_rev):
cs = repo.get_changeset(i)
otherbranches[cs.branch] = cs.raw_id
for abranch, node in otherbranches.iteritems():
selected = 'branch:%s:%s' % (abranch, node)
peers.append((selected, abranch))
selected = None
branches = []
for abranch, branchrev in repo.branches.iteritems():
n = 'branch:%s:%s' % (abranch, branchrev)
branches.append((n, abranch))
if rev == branchrev:
selected = n
if branch == abranch:
branch = None
if branch: # branch not in list - it is probably closed
revs = repo._repo.revs('max(branch(%s))', branch)
if revs:
cs = repo.get_changeset(revs[0])
selected = 'branch:%s:%s' % (branch, cs.raw_id)
branches.append((selected, branch))
bookmarks = []
for bookmark, bookmarkrev in repo.bookmarks.iteritems():
n = 'book:%s:%s' % (bookmark, bookmarkrev)
bookmarks.append((n, bookmark))
if rev == bookmarkrev:
tags = []
for tag, tagrev in repo.tags.iteritems():
n = 'tag:%s:%s' % (tag, tagrev)
tags.append((n, tag))
if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
# prio 1: rev was selected as existing entry above
# prio 2: create special entry for rev; rev _must_ be used
specials = []
if rev and selected is None:
selected = 'rev:%s:%s' % (rev, rev)
specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
# prio 3: most recent peer branch
if peers and not selected:
selected = peers[0][0][0]
# prio 4: tip revision
if not selected:
selected = 'tag:tip:%s' % repo.tags['tip']
groups = [(specials, _("Special")),
(peers, _("Peer branches")),
(bookmarks, _("Bookmarks")),
(branches, _("Branches")),
(tags, _("Tags")),
return [g for g in groups if g[0]], selected
def _get_is_allowed_change_status(self, pull_request):
owner = self.rhodecode_user.user_id == pull_request.user_id
reviewer = self.rhodecode_user.user_id in [x.user_id for x in
pull_request.reviewers]
return (self.rhodecode_user.admin or owner or reviewer)
def _load_compare_data(self, pull_request, enable_comments=True):
Load context data needed for generating compare diff
:param pull_request:
:type pull_request:
org_repo = pull_request.org_repo
(org_ref_type,
org_ref_name,
org_ref_rev) = pull_request.org_ref.split(':')
other_repo = org_repo
(other_ref_type,
other_ref_name,
other_ref_rev) = pull_request.other_ref.split(':')
# despite opening revisions for bookmarks/branches/tags, we always
# convert this to rev to prevent changes after bookmark or branch change
org_ref = ('rev', org_ref_rev)
other_ref = ('rev', other_ref_rev)
c.org_repo = org_repo
c.other_repo = other_repo
c.fulldiff = fulldiff = request.GET.get('fulldiff')
c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
c.org_ref = org_ref[1]
c.org_ref_type = org_ref[0]
c.other_ref = other_ref[1]
c.other_ref_type = other_ref[0]
diff_limit = self.cut_off_limit if not fulldiff else None
# we swap org/other ref since we run a simple diff on one repo
log.debug('running diff between %s and %s in %s'
% (other_ref, org_ref, org_repo.scm_instance.path))
txtdiff = org_repo.scm_instance.get_diff(rev1=safe_str(other_ref[1]), rev2=safe_str(org_ref[1]))
diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
diff_limit=diff_limit)
_parsed = diff_processor.prepare()
c.limited_diff = False
if isinstance(_parsed, LimitedDiffContainer):
c.limited_diff = True
c.files = []
c.changes = {}
c.lines_added = 0
c.lines_deleted = 0
for f in _parsed:
st = f['stats']
c.lines_added += st['added']
c.lines_deleted += st['deleted']
fid = h.FID('', f['filename'])
c.files.append([fid, f['operation'], f['filename'], f['stats']])
htmldiff = diff_processor.as_html(enable_comments=enable_comments,
parsed_lines=[f])
c.changes[fid] = [f['operation'], f['filename'], htmldiff]
def show_all(self, repo_name):
c.pull_requests = PullRequestModel().get_all(repo_name)
c.repo_name = repo_name
c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
if request.environ.get('HTTP_X_PARTIAL_XHR'):
return c.pullrequest_data
return render('/pullrequests/pullrequest_show_all.html')
def index(self):
org_repo = c.rhodecode_db_repo
if org_repo.scm_instance.alias != 'hg':
log.error('Review not available for GIT REPOS')
raise HTTPNotFound
org_repo.scm_instance.get_changeset()
h.flash(h.literal(_('There are no changesets yet')),
redirect(url('summary_home', repo_name=org_repo.repo_name))
org_rev = request.GET.get('rev_end')
@@ -465,193 +465,192 @@ class DbManage(object):
hooks4 = RhodeCodeUi()
hooks4.ui_section = 'hooks'
hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
self.sa.add(hooks4)
hooks5 = RhodeCodeUi()
hooks5.ui_section = 'hooks'
hooks5.ui_key = RhodeCodeUi.HOOK_PULL
hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
self.sa.add(hooks5)
hooks6 = RhodeCodeUi()
hooks6.ui_section = 'hooks'
hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
self.sa.add(hooks6)
# enable largefiles
largefiles = RhodeCodeUi()
largefiles.ui_section = 'extensions'
largefiles.ui_key = 'largefiles'
largefiles.ui_value = ''
self.sa.add(largefiles)
# enable hgsubversion disabled by default
hgsubversion = RhodeCodeUi()
hgsubversion.ui_section = 'extensions'
hgsubversion.ui_key = 'hgsubversion'
hgsubversion.ui_value = ''
hgsubversion.ui_active = False
self.sa.add(hgsubversion)
# enable hggit disabled by default
hggit = RhodeCodeUi()
hggit.ui_section = 'extensions'
hggit.ui_key = 'hggit'
hggit.ui_value = ''
hggit.ui_active = False
self.sa.add(hggit)
def create_ldap_options(self, skip_existing=False):
"""Creates ldap settings"""
for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
('ldap_dn_pass', ''), ('ldap_base_dn', ''),
('ldap_filter', ''), ('ldap_search_scope', ''),
('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
log.debug('Skipping option %s' % k)
continue
setting = RhodeCodeSetting(k, v)
self.sa.add(setting)
def create_default_options(self, skip_existing=False):
"""Creates default settings"""
for k, v in [
('default_repo_enable_locking', False),
('default_repo_enable_downloads', False),
('default_repo_enable_statistics', False),
('default_repo_private', False),
('default_repo_type', 'hg')]:
def fixup_groups(self):
def_usr = User.get_default_user()
for g in RepoGroup.query().all():
g.group_name = g.get_new_name(g.name)
self.sa.add(g)
# get default perm
default = UserRepoGroupToPerm.query()\
.filter(UserRepoGroupToPerm.group == g)\
.filter(UserRepoGroupToPerm.user == def_usr)\
.scalar()
if default is None:
log.debug('missing default permission for group %s adding' % g)
perm_obj = ReposGroupModel()._create_default_perms(g)
self.sa.add(perm_obj)
def reset_permissions(self, username):
Resets permissions to default state, usefull when old systems had
bad permissions, we must clean them up
:param username:
:type username:
default_user = User.get_by_username(username)
if not default_user:
return
u2p = UserToPerm.query()\
.filter(UserToPerm.user == default_user).all()
fixed = False
if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
for p in u2p:
Session().delete(p)
fixed = True
self.populate_default_permissions()
return fixed
def update_repo_info(self):
RepoModel.update_repoinfo()
def config_prompt(self, test_repo_path='', retries=3):
defaults = self.cli_args
_path = defaults.get('repos_location')
if retries == 3:
log.info('Setting up repositories config')
if _path is not None:
path = _path
elif not self.tests and not test_repo_path:
path = raw_input(
'Enter a valid absolute path to store repositories. '
'All repositories in that path will be added automatically:'
path = test_repo_path
path_ok = True
# check proper dir
if not os.path.isdir(path):
path_ok = False
log.error('Given path %s is not a valid directory' % path)
elif not os.path.isabs(path):
log.error('Given path %s is not an absolute path' % path)
# check write access
elif not os.access(path, os.W_OK) and path_ok:
log.error('No write permission to given path %s' % path)
if retries == 0:
sys.exit('max retries reached')
if not path_ok:
retries -= 1
return self.config_prompt(test_repo_path, retries)
real_path = os.path.normpath(os.path.realpath(path))
if real_path != os.path.normpath(path):
if not ask_ok(('Path looks like a symlink, Rhodecode will store '
'given path as %s ? [y/n]') % (real_path)):
log.error('Canceled by user')
sys.exit(-1)
return real_path
def create_settings(self, path):
self.create_ui_settings()
#HG UI OPTIONS
web1 = RhodeCodeUi()
web1.ui_section = 'web'
web1.ui_key = 'push_ssl'
web1.ui_value = 'false'
web2 = RhodeCodeUi()
web2.ui_section = 'web'
web2.ui_key = 'allow_archive'
web2.ui_value = 'gz zip bz2'
web3 = RhodeCodeUi()
web3.ui_section = 'web'
web3.ui_key = 'allow_push'
web3.ui_value = '*'
web4 = RhodeCodeUi()
web4.ui_section = 'web'
web4.ui_key = 'baseurl'
web4.ui_value = '/'
paths = RhodeCodeUi()
paths.ui_section = 'paths'
paths.ui_key = '/'
paths.ui_value = path
phases = RhodeCodeUi()
import datetime
from sqlalchemy import *
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm import relation, backref, class_mapper, joinedload
from sqlalchemy.orm.session import Session
from sqlalchemy.ext.declarative import declarative_base
from rhodecode.lib.dbmigrate.migrate import *
from rhodecode.lib.dbmigrate.migrate.changeset import *
from rhodecode.model.meta import Base
from rhodecode.model import meta
from rhodecode.lib.dbmigrate.versions import _reset_base
def upgrade(migrate_engine):
Upgrade operations go here.
Don't create your own engine; bind migrate_engine to your metadata
_reset_base(migrate_engine)
# UserUserGroupToPerm
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserUserGroupToPerm
tbl = UserUserGroupToPerm.__table__
tbl.create()
# UserGroupUserGroupToPerm
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroupUserGroupToPerm
tbl = UserGroupUserGroupToPerm.__table__
# Gist
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import Gist
tbl = Gist.__table__
# UserGroup
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroup
tbl = UserGroup.__table__
user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=False, default=None)
# create username column
user_id.create(table=tbl)
# RepoGroup
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
tbl = RepoGroup.__table__
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
@@ -143,288 +143,286 @@ class LimitedDiffContainer(object):
self.cur_diff_size = cur_diff_size
def __iter__(self):
for l in self.diff:
yield l
class DiffProcessor(object):
Give it a unified or git diff and it returns a list of the files that were
mentioned in the diff together with a dict of meta information that
can be used to render it in a HTML template.
_chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
_newline_marker = re.compile(r'^\\ No newline at end of file')
_git_header_re = re.compile(r"""
#^diff[ ]--git
[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
^rename[ ]from[ ](?P<rename_from>\S+)\n
^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
(?:^old[ ]mode[ ](?P<old_mode>\d+)\n
^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
(?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
(?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
(?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
\.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
(?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
(?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
(?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
""", re.VERBOSE | re.MULTILINE)
_hg_header_re = re.compile(r"""
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
(?:^rename[ ]from[ ](?P<rename_from>\S+)\n
#used for inline highlighter word split
_token_re = re.compile(r'()(>|<|&|\W+?)')
def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
:param diff: a text in diff format
:param vcs: type of version controll hg or git
:param format: format of diff passed, `udiff` or `gitdiff`
:param diff_limit: define the size of diff that is considered "big"
based on that parameter cut off will be triggered, set to None
to show full diff
if not isinstance(diff, basestring):
raise Exception('Diff must be a basestring got %s instead' % type(diff))
self._diff = diff
self._format = format
self.adds = 0
self.removes = 0
# calculate diff size
self.diff_size = len(diff)
self.diff_limit = diff_limit
self.cur_diff_size = 0
self.parsed = False
self.parsed_diff = []
self.vcs = vcs
if format == 'gitdiff':
self.differ = self._highlight_line_difflib
self._parser = self._parse_gitdiff
self.differ = self._highlight_line_udiff
self._parser = self._parse_udiff
def _copy_iterator(self):
make a fresh copy of generator, we should not iterate thru
an original as it's needed for repeating operations on
this instance of DiffProcessor
self.__udiff, iterator_copy = tee(self.__udiff)
return iterator_copy
def _escaper(self, string):
Escaper for diff escapes special chars and checks the diff limit
:param string:
:type string:
self.cur_diff_size += len(string)
# escaper get's iterated on each .next() call and it checks if each
# parsed line doesn't exceed the diff limit
if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
raise DiffLimitExceeded('Diff Limit Exceeded')
return safe_unicode(string).replace('&', '&')\
.replace('<', '<')\
.replace('>', '>')
def _line_counter(self, l):
Checks each line and bumps total adds/removes for this diff
:param l:
if l.startswith('+') and not l.startswith('+++'):
self.adds += 1
elif l.startswith('-') and not l.startswith('---'):
self.removes += 1
return safe_unicode(l)
def _highlight_line_difflib(self, line, next_):
Highlight inline changes in both lines.
if line['action'] == 'del':
old, new = line, next_
old, new = next_, line
oldwords = self._token_re.split(old['line'])
newwords = self._token_re.split(new['line'])
sequence = difflib.SequenceMatcher(None, oldwords, newwords)
oldfragments, newfragments = [], []
for tag, i1, i2, j1, j2 in sequence.get_opcodes():
oldfrag = ''.join(oldwords[i1:i2])
newfrag = ''.join(newwords[j1:j2])
if tag != 'equal':
if oldfrag:
oldfrag = '<del>%s</del>' % oldfrag
if newfrag:
newfrag = '<ins>%s</ins>' % newfrag
oldfragments.append(oldfrag)
newfragments.append(newfrag)
old['line'] = "".join(oldfragments)
new['line'] = "".join(newfragments)
def _highlight_line_udiff(self, line, next_):
start = 0
limit = min(len(line['line']), len(next_['line']))
while start < limit and line['line'][start] == next_['line'][start]:
start += 1
end = -1
limit -= start
while -end <= limit and line['line'][end] == next_['line'][end]:
end -= 1
end += 1
if start or end:
def do(l):
last = end + len(l['line'])
if l['action'] == 'add':
tag = 'ins'
tag = 'del'
l['line'] = '%s<%s>%s</%s>%s' % (
l['line'][:start],
tag,
l['line'][start:last],
l['line'][last:]
do(line)
do(next_)
def _get_header(self, diff_chunk):
parses the diff header, and returns parts, and leftover diff
parts consists of 14 elements::
a_path, b_path, similarity_index, rename_from, rename_to,
old_mode, new_mode, new_file_mode, deleted_file_mode,
a_blob_id, b_blob_id, b_mode, a_file, b_file
:param diff_chunk:
:type diff_chunk:
if self.vcs == 'git':
match = self._git_header_re.match(diff_chunk)
diff = diff_chunk[match.end():]
return match.groupdict(), imap(self._escaper, diff.splitlines(1))
elif self.vcs == 'hg':
match = self._hg_header_re.match(diff_chunk)
raise Exception('VCS type %s is not supported' % self.vcs)
def _clean_line(self, line, command):
if command in ['+', '-', ' ']:
#only modify the line if it's actually a diff thing
line = line[1:]
return line
def _parse_gitdiff(self, inline_diff=True):
_files = []
diff_container = lambda arg: arg
##split the diff in chunks of separate --git a/file b/file chunks
for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
head, diff = self._get_header(raw_diff)
op = None
stats = {
'added': 0,
'deleted': 0,
'binary': False,
'ops': {},
if head['deleted_file_mode']:
op = 'D'
stats['binary'] = True
stats['ops'][DEL_FILENODE] = 'deleted file'
elif head['new_file_mode']:
op = 'A'
stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
else: # modify operation, can be cp, rename, chmod
# CHMOD
if head['new_mode'] and head['old_mode']:
op = 'M'
stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
% (head['old_mode'], head['new_mode']))
# RENAME
if (head['rename_from'] and head['rename_to']
and head['rename_from'] != head['rename_to']):
stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
% (head['rename_from'], head['rename_to']))
# FALL BACK: detect missed old style add or remove
if op is None:
if not head['a_file'] and head['b_file']:
stats['ops'][NEW_FILENODE] = 'new file'
elif head['a_file'] and not head['b_file']:
# it's not ADD not DELETE
stats['ops'][MOD_FILENODE] = 'modified file'
# a real non-binary diff
if head['a_file'] or head['b_file']:
chunks, _stats = self._parse_lines(diff)
stats['binary'] = False
stats['added'] = _stats[0]
stats['deleted'] = _stats[1]
# explicit mark that it's a modified file
if op == 'M':
except DiffLimitExceeded:
diff_container = lambda _diff: \
LimitedDiffContainer(self.diff_limit,
self.cur_diff_size, _diff)
else: # GIT binary patch (or empty diff)
# GIT Binary patch
if head['bin_patch']:
rhodecode.lib.exceptions
~~~~~~~~~~~~~~~~~~~~~~~~
Set of custom exceptions used in RhodeCode
:created_on: Nov 17, 2010
from webob.exc import HTTPClientError
class LdapUsernameError(Exception):
class LdapPasswordError(Exception):
class LdapConnectionError(Exception):
class LdapImportError(Exception):
class DefaultUserException(Exception):
class UserOwnsReposException(Exception):
class UserGroupsAssignedException(Exception):
class StatusChangeOnClosedPullRequestError(Exception):
class AttachedForksError(Exception):
class RepoGroupAssignmentError(Exception):
class NonRelativePathError(Exception):
class HTTPLockedRC(HTTPClientError):
Special Exception For locked Repos in RhodeCode, the return code can
be overwritten by _code keyword argument passed into constructors
code = 423
title = explanation = 'Repository Locked'
def __init__(self, reponame, username, *args, **kwargs):
from rhodecode.lib.utils2 import safe_int
_code = CONFIG.get('lock_ret_code')
self.code = safe_int(_code, self.code)
self.title = self.explanation = ('Repository `%s` locked by '
'user `%s`' % (reponame, username))
super(HTTPLockedRC, self).__init__(*args, **kwargs)
@@ -213,178 +213,175 @@ def log_push_action(ui, repo, **kwargs):
if str(_http_ret.code).startswith('2'):
#2xx Codes don't raise exceptions
sys.stdout.write(_http_ret.title)
def log_create_repository(repository_dict, created_by, **kwargs):
Post create repository Hook. This is a dummy function for admins to re-use
if needed. It's taken from rhodecode-extensions module and executed
if present
:param repository: dict dump of repository object
:param created_by: username who created repository
available keys of repository_dict:
'repo_type',
'description',
'private',
'created_on',
'enable_downloads',
'repo_id',
'user_id',
'enable_statistics',
'clone_uri',
'fork_id',
'group_id',
'repo_name'
from rhodecode import EXTENSIONS
callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
if isfunction(callback):
kw = {}
kw.update(repository_dict)
kw.update({'created_by': created_by})
kw.update(kwargs)
return callback(**kw)
def log_delete_repository(repository_dict, deleted_by, **kwargs):
Post delete repository Hook. This is a dummy function for admins to re-use
:param deleted_by: username who deleted the repository
callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
kw.update({'deleted_by': deleted_by,
'deleted_on': time.time()})
handle_git_pre_receive = (lambda repo_path, revs, env:
handle_git_receive(repo_path, revs, env, hook_type='pre'))
handle_git_post_receive = (lambda repo_path, revs, env:
handle_git_receive(repo_path, revs, env, hook_type='post'))
def handle_git_receive(repo_path, revs, env, hook_type='post'):
A really hacky method that is runned by git post-receive hook and logs
an push action together with pushed revisions. It's executed by subprocess
thus needs all info to be able to create a on the fly pylons enviroment,
connect to database and run the logging code. Hacky as sh*t but works.
:param repo_path:
:type repo_path:
:param revs:
:type revs:
:param env:
:type env:
from paste.deploy import appconfig
from sqlalchemy import engine_from_config
from rhodecode.config.environment import load_environment
from rhodecode.model import init_model
from rhodecode.model.db import RhodeCodeUi
from rhodecode.lib.utils import make_ui
extras = _extract_extras(env)
path, ini_name = os.path.split(extras['config'])
conf = appconfig('config:%s' % ini_name, relative_to=path)
load_environment(conf.global_conf, conf.local_conf)
engine = engine_from_config(conf, 'sqlalchemy.db1.')
init_model(engine)
baseui = make_ui('db')
# fix if it's not a bare repo
if repo_path.endswith(os.sep + '.git'):
repo_path = repo_path[:-5]
repo = Repository.get_by_full_path(repo_path)
if not repo:
raise OSError('Repository %s not found in database'
% (safe_str(repo_path)))
_hooks = dict(baseui.configitems('hooks')) or {}
if hook_type == 'pre':
repo = repo.scm_instance
#post push shouldn't use the cached instance never
repo = repo.scm_instance_no_cache()
pre_push(baseui, repo)
# if push hook is enabled via web interface
elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
rev_data = []
for l in revs:
old_rev, new_rev, ref = l.split(' ')
_ref_data = ref.split('/')
if _ref_data[1] in ['tags', 'heads']:
rev_data.append({'old_rev': old_rev,
'new_rev': new_rev,
'ref': ref,
'type': _ref_data[1],
'name': _ref_data[2].strip()})
git_revs = []
for push_ref in rev_data:
_type = push_ref['type']
if _type == 'heads':
if push_ref['old_rev'] == EmptyChangeset().raw_id:
cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
heads = repo.run_git_command(cmd)[0]
heads = heads.replace(push_ref['ref'], '')
heads = ' '.join(map(lambda c: c.strip('\n').strip(),
heads.splitlines()))
cmd = (('log %(new_rev)s' % push_ref) +
' --reverse --pretty=format:"%H" --not ' + heads)
git_revs += repo.run_git_command(cmd)[0].splitlines()
elif push_ref['new_rev'] == EmptyChangeset().raw_id:
#delete branch case
git_revs += ['delete_branch=>%s' % push_ref['name']]
cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
' --reverse --pretty=format:"%H"')
elif _type == 'tags':
git_revs += ['tag=>%s' % push_ref['name']]
log_push_action(baseui, repo, _git_revs=git_revs)
import socket
import subprocess
from webob import Request, Response, exc
import rhodecode
from rhodecode.lib.vcs import subprocessio
class FileWrapper(object):
def __init__(self, fd, content_length):
self.fd = fd
self.content_length = content_length
self.remain = content_length
def read(self, size):
if size <= self.remain:
data = self.fd.read(size)
except socket.error:
raise IOError(self)
self.remain -= size
elif self.remain:
data = self.fd.read(self.remain)
self.remain = 0
data = None
return data
return '<FileWrapper %s len: %s, read: %s>' % (
self.fd, self.content_length, self.content_length - self.remain
class GitRepository(object):
git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
commands = ['git-upload-pack', 'git-receive-pack']
def __init__(self, repo_name, content_path, extras):
files = set([f.lower() for f in os.listdir(content_path)])
if not (self.git_folder_signature.intersection(files)
== self.git_folder_signature):
raise OSError('%s missing git signature' % content_path)
self.content_path = content_path
self.valid_accepts = ['application/x-%s-result' %
c for c in self.commands]
self.repo_name = repo_name
self.extras = extras
def _get_fixedpath(self, path):
Small fix for repo_path
:param path:
:type path:
return path.split(self.repo_name, 1)[-1].strip('/')
def inforefs(self, request, environ):
WSGI Response producer for HTTP GET Git Smart
HTTP /info/refs request.
git_command = request.GET.get('service')
if git_command not in self.commands:
log.debug('command %s not allowed' % git_command)
return exc.HTTPMethodNotAllowed()
# note to self:
# please, resist the urge to add '\n' to git capture and increment
# line count by 1.
# The code in Git client not only does NOT need '\n', but actually
# blows up if you sprinkle "flush" (0000) as "0001\n".
# It reads binary, per number of bytes specified.
# if you do add '\n' as part of data, count it.
server_advert = '# service=%s' % git_command
packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
_git_path = rhodecode.CONFIG.get('git_path', 'git')
out = subprocessio.SubprocessIOChunker(
r'%s %s --stateless-rpc --advertise-refs "%s"' % (
_git_path, git_command[4:], self.content_path),
starting_values=[
packet_len + server_advert + '0000'
except EnvironmentError, e:
raise exc.HTTPExpectationFailed()
resp = Response()
resp.content_type = 'application/x-%s-advertisement' % str(git_command)
resp.charset = None
resp.app_iter = out
return resp
def backend(self, request, environ):
WSGI Response producer for HTTP POST Git Smart HTTP requests.
Reads commands and data from HTTP POST's body.
returns an iterator obj with contents of git command's
response to stdout
git_command = self._get_fixedpath(request.path_info)
if 'CONTENT_LENGTH' in environ:
inputstream = FileWrapper(environ['wsgi.input'],
request.content_length)
inputstream = environ['wsgi.input']
gitenv = os.environ
# forget all configs
gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
opts = dict(
env=gitenv,
cwd=os.getcwd()
cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
self.content_path),
log.debug('handling cmd %s' % cmd)
cmd,
inputstream=inputstream,
**opts
if git_command in [u'git-receive-pack']:
# updating refs manually after each push.
# Needed for pre-1.7.0.4 git clients using regular HTTP mode.
cmd = (u'%s --git-dir "%s" '
'update-server-info' % (_git_path, self.content_path))
subprocess.call(cmd, shell=True)
resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
def __call__(self, environ, start_response):
request = Request(environ)
rhodecode.lib.utils
~~~~~~~~~~~~~~~~~~~
Some simple helper functions
:created_on: Jan 5, 2011
import re
import uuid
import webob
from pylons.i18n.translation import _, ungettext
from rhodecode.lib.vcs.utils.lazy import LazyProperty
def __get_lem():
Get language extension map based on what's inside pygments lexers
from pygments import lexers
from string import lower
from collections import defaultdict
d = defaultdict(lambda: [])
def __clean(s):
s = s.lstrip('*')
s = s.lstrip('.')
if s.find('[') != -1:
exts = []
start, stop = s.find('['), s.find(']')
for suffix in s[start + 1:stop]:
exts.append(s[:s.find('[')] + suffix)
return map(lower, exts)
return map(lower, [s])
for lx, t in sorted(lexers.LEXERS.items()):
m = map(__clean, t[-2])
if m:
m = reduce(lambda x, y: x + y, m)
for ext in m:
desc = lx.replace('Lexer', '')
d[ext].append(desc)
return dict(d)
def str2bool(_str):
returs True/False value from given string, it tries to translate the
string into boolean
:param _str: string value to translate into boolean
:rtype: boolean
:returns: boolean from given string
if _str is None:
if _str in (True, False):
return _str
_str = str(_str).strip().lower()
return _str in ('t', 'true', 'y', 'yes', 'on', '1')
def aslist(obj, sep=None, strip=True):
Returns given string separated by sep as list
:param obj:
:param sep:
:param strip:
if isinstance(obj, (basestring)):
lst = obj.split(sep)
if strip:
lst = [v.strip() for v in lst]
return lst
elif isinstance(obj, (list, tuple)):
return obj
elif obj is None:
return []
return [obj]
def convert_line_endings(line, mode):
Converts a given line "line end" accordingly to given mode
Available modes are::
0 - Unix
1 - Mac
2 - DOS
:param line: given line to convert
:param mode: mode to convert to
:rtype: str
:return: converted line according to mode
@@ -514,96 +515,132 @@ def get_changeset_safe(repo, rev):
def datetime_to_time(dt):
if dt:
return time.mktime(dt.timetuple())
def time_to_datetime(tm):
if tm:
if isinstance(tm, basestring):
tm = float(tm)
except ValueError:
return datetime.datetime.fromtimestamp(tm)
MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
def extract_mentioned_users(s):
Returns unique usernames from given string s that have @mention
:param s: string to get mentions
usrs = set()
for username in re.findall(MENTIONS_REGEX, s):
usrs.add(username)
return sorted(list(usrs), key=lambda k: k.lower())
class AttributeDict(dict):
def __getattr__(self, attr):
return self.get(attr, None)
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
def fix_PATH(os_=None):
Get current active python path, and append it to PATH variable to fix issues
of subprocess calls and different python versions
if os_ is None:
os = os_
cur_path = os.path.split(sys.executable)[0]
if not os.environ['PATH'].startswith(cur_path):
os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
def obfuscate_url_pw(engine):
_url = engine or ''
from sqlalchemy.engine import url as sa_url
_url = sa_url.make_url(engine)
if _url.password:
_url.password = 'XXXXX'
return str(_url)
def get_server_url(environ):
req = webob.Request(environ)
return req.host_url + req.script_name
def _extract_extras(env=None):
Extracts the rc extras data from os.environ, and wraps it into named
AttributeDict object
if not env:
env = os.environ
rc_extras = json.loads(env['RC_SCM_DATA'])
print os.environ
print >> sys.stderr, traceback.format_exc()
rc_extras = {}
for k in ['username', 'repository', 'locked_by', 'scm', 'make_lock',
'action', 'ip']:
rc_extras[k]
except KeyError, e:
raise Exception('Missing key %s in os.environ %s' % (e, rc_extras))
return AttributeDict(rc_extras)
def _set_extras(extras):
os.environ['RC_SCM_DATA'] = json.dumps(extras)
def unique_id(hexlen=32):
alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
return suuid(truncate_to=hexlen, alphabet=alphabet)
def suuid(url=None, truncate_to=22, alphabet=None):
Generate and return a short URL safe UUID.
If the url parameter is provided, set the namespace to the provided
URL and generate a UUID.
:param url to get the uuid for
:truncate_to: truncate the basic 22 UUID to shorter version
The IDs won't be universally unique any longer, but the probability of
a collision will still be very low.
# Define our alphabet.
_ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
# If no URL is given, generate a random UUID.
if url is None:
unique_id = uuid.uuid4().int
unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
alphabet_length = len(_ALPHABET)
output = []
while unique_id > 0:
digit = unique_id % alphabet_length
output.append(_ALPHABET[digit])
unique_id = int(unique_id / alphabet_length)
return "".join(output)[:truncate_to]
@@ -11,130 +11,127 @@
:example:
.. code-block:: python
from pylons import config
conf = appconfig('config:development.ini', relative_to = './../../')
engine = engine_from_config(config, 'sqlalchemy.')
# RUN YOUR CODE HERE
from rhodecode.lib.utils2 import safe_str, obfuscate_url_pw
def init_model(engine):
Initializes db session, bind the engine with the metadata,
Call this before using any of the tables or classes in the model,
preferably once in application start
:param engine: engine to bind to
engine_str = obfuscate_url_pw(str(engine.url))
log.info("initializing db for %s" % engine_str)
meta.Base.metadata.bind = engine
class BaseModel(object):
Base Model for all RhodeCode models, it adds sql alchemy session
into instance of model
:param sa: If passed it reuses this session instead of creating a new one
cls = None # override in child class
def __init__(self, sa=None):
if sa is not None:
self.sa = sa
self.sa = meta.Session()
def _get_instance(self, cls, instance, callback=None):
Get's instance of given cls using some simple lookup mechanism.
:param cls: class to fetch
:param instance: int or Instance
:param callback: callback to call if all lookups failed
if isinstance(instance, cls):
return instance
elif isinstance(instance, (int, long)) or safe_str(instance).isdigit():
return cls.get(instance)
if instance:
if callback is None:
raise Exception(
'given object must be int, long or Instance of %s '
'got %s, no callback provided' % (cls, type(instance))
return callback(instance)
def _get_user(self, user):
Helper method to get user by ID, or username fallback
:param user:
:type user: UserID, username, or User instance
:param user: UserID, username, or User instance
from rhodecode.model.db import User
return self._get_instance(User, user,
callback=User.get_by_username)
def _get_repo(self, repository):
Helper method to get repository by ID, or repository name
:param repository:
:type repository: RepoID, repository name or Repository Instance
:param repository: RepoID, repository name or Repository Instance
return self._get_instance(Repository, repository,
callback=Repository.get_by_repo_name)
def _get_perm(self, permission):
Helper method to get permission by ID, or permission name
:param permission:
:type permission: PermissionID, permission_name or Permission instance
:param permission: PermissionID, permission_name or Permission instance
from rhodecode.model.db import Permission
return self._get_instance(Permission, permission,
callback=Permission.get_by_key)
def get_all(self):
Returns all instances of what is defined in `cls` class variable
return self.cls.getAll()
@@ -1037,193 +1037,192 @@ class Repository(Base, BaseModel):
decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
args = {
'user': '',
'pass': '',
'scheme': parsed_url.scheme,
'netloc': parsed_url.netloc,
'prefix': decoded_path,
'path': self.repo_name
args.update(override)
return default_clone_uri % args
# SCM PROPERTIES
def get_changeset(self, rev=None):
return get_changeset_safe(self.scm_instance, rev)
def get_landing_changeset(self):
Returns landing changeset, or if that doesn't exist returns the tip
cs = self.get_changeset(self.landing_rev) or self.get_changeset()
return cs
def update_changeset_cache(self, cs_cache=None):
Update cache of last changeset for repository, keys should be::
short_id
raw_id
revision
message
date
author
:param cs_cache:
from rhodecode.lib.vcs.backends.base import BaseChangeset
if cs_cache is None:
cs_cache = EmptyChangeset()
# use no-cache version here
scm_repo = self.scm_instance_no_cache()
if scm_repo:
cs_cache = scm_repo.get_changeset()
if isinstance(cs_cache, BaseChangeset):
cs_cache = cs_cache.__json__()
if (cs_cache != self.changeset_cache or not self.changeset_cache):
_default = datetime.datetime.fromtimestamp(0)
last_change = cs_cache.get('date') or _default
log.debug('updated repo %s with new cs cache %s'
% (self.repo_name, cs_cache))
self.updated_on = last_change
self.changeset_cache = cs_cache
Session().add(self)
log.debug('Skipping repo:%s already with latest changes'
% self.repo_name)
@property
def tip(self):
return self.get_changeset('tip')
def author(self):
return self.tip.author
def last_change(self):
return self.scm_instance.last_change
def get_comments(self, revisions=None):
Returns comments for this repository grouped by revisions
:param revisions: filter query by revisions only
cmts = ChangesetComment.query()\
.filter(ChangesetComment.repo == self)
if revisions:
cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
grouped = defaultdict(list)
for cmt in cmts.all():
grouped[cmt.revision].append(cmt)
return grouped
def statuses(self, revisions=None):
Returns statuses for this repository
:param revisions: list of revisions to get statuses for
:type revisions: list
statuses = ChangesetStatus.query()\
.filter(ChangesetStatus.repo == self)\
.filter(ChangesetStatus.version == 0)
statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
grouped = {}
#maybe we have open new pullrequest without a status ?
stat = ChangesetStatus.STATUS_UNDER_REVIEW
status_lbl = ChangesetStatus.get_status_lbl(stat)
for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
for rev in pr.revisions:
pr_id = pr.pull_request_id
pr_repo = pr.other_repo.repo_name
grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
for stat in statuses.all():
pr_id = pr_repo = None
if stat.pull_request:
pr_id = stat.pull_request.pull_request_id
pr_repo = stat.pull_request.other_repo.repo_name
grouped[stat.revision] = [str(stat.status), stat.status_lbl,
pr_id, pr_repo]
def _repo_size(self):
log.debug('calculating repository size...')
return h.format_byte_size(self.scm_instance.size)
# SCM CACHE INSTANCE
def set_invalidate(self):
Mark caches of this repo as invalid.
CacheInvalidation.set_invalidate(self.repo_name)
def scm_instance_no_cache(self):
return self.__get_instance()
def scm_instance(self):
full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
if full_cache:
return self.scm_instance_cached()
def scm_instance_cached(self, valid_cache_keys=None):
@cache_region('long_term')
def _c(repo_name):
rn = self.repo_name
valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
if not valid:
log.debug('Cache for %s invalidated, getting new object' % (rn))
region_invalidate(_c, None, rn)
log.debug('Getting obj for %s from cache' % (rn))
return _c(rn)
def __get_instance(self):
repo_full_path = self.repo_full_path
alias = get_scm(repo_full_path)[0]
log.debug('Creating instance of %s repository from %s'
% (alias, repo_full_path))
backend = get_backend(alias)
log.error('Perhaps this repository is in db and not in '
'filesystem run rescan repositories with '
'"destroy old data " option from admin panel')
if alias == 'hg':
repo = backend(safe_str(repo_full_path), create=False,
baseui=self._ui)
# skip hidden web repository
if repo._get_hidden():
repo = backend(repo_full_path, create=False)
class RepoGroup(Base, BaseModel):
__tablename__ = 'groups'
@@ -2029,105 +2028,143 @@ class PullRequestReviewers(Base, BaseMod
__tablename__ = 'pull_request_reviewers'
__table_args__ = (
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8'},
def __init__(self, user=None, pull_request=None):
self.user = user
self.pull_request = pull_request
pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
user = relationship('User')
pull_request = relationship('PullRequest')
class Notification(Base, BaseModel):
__tablename__ = 'notifications'
Index('notification_type_idx', 'type'),
TYPE_CHANGESET_COMMENT = u'cs_comment'
TYPE_MESSAGE = u'message'
TYPE_MENTION = u'mention'
TYPE_REGISTRATION = u'registration'
TYPE_PULL_REQUEST = u'pull_request'
TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
subject = Column('subject', Unicode(512), nullable=True)
body = Column('body', UnicodeText(50000), nullable=True)
created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
type_ = Column('type', Unicode(256))
created_by_user = relationship('User')
notifications_to_users = relationship('UserNotification', lazy='joined',
cascade="all, delete, delete-orphan")
def recipients(self):
return [x.user for x in UserNotification.query()\
.filter(UserNotification.notification == self)\
.order_by(UserNotification.user_id.asc()).all()]
def create(cls, created_by, subject, body, recipients, type_=None):
if type_ is None:
type_ = Notification.TYPE_MESSAGE
notification = cls()
notification.created_by_user = created_by
notification.subject = subject
notification.body = body
notification.type_ = type_
notification.created_on = datetime.datetime.now()
for u in recipients:
assoc = UserNotification()
assoc.notification = notification
u.notifications.append(assoc)
Session().add(notification)
return notification
def description(self):
from rhodecode.model.notification import NotificationModel
return NotificationModel().make_description(self)
class UserNotification(Base, BaseModel):
__tablename__ = 'user_to_notification'
UniqueConstraint('user_id', 'notification_id'),
'mysql_charset': 'utf8'}
user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
read = Column('read', Boolean, default=False)
sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
user = relationship('User', lazy="joined")
notification = relationship('Notification', lazy="joined",
order_by=lambda: Notification.created_on.desc(),)
def mark_as_read(self):
self.read = True
class Gist(Base, BaseModel):
__tablename__ = 'gists'
Index('g_gist_access_id_idx', 'gist_access_id'),
Index('g_created_on_idx', 'created_on'),
GIST_PUBLIC = u'public'
GIST_PRIVATE = u'private'
gist_id = Column('gist_id', Integer(), primary_key=True)
gist_access_id = Column('gist_access_id', UnicodeText(1024))
gist_description = Column('gist_description', UnicodeText(1024))
gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
gist_expires = Column('gist_expires', Float(), nullable=False)
gist_type = Column('gist_type', Unicode(128), nullable=False)
modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
owner = relationship('User')
def get_or_404(cls, id_):
res = cls.query().filter(cls.gist_access_id == id_).scalar()
if not res:
return res
def get_by_access_id(cls, gist_access_id):
return cls.query().filter(cls.gist_access_id==gist_access_id).scalar()
def gist_url(self):
from pylons import url
return url('gist', id=self.gist_access_id, qualified=True)
class DbMigrateVersion(Base, BaseModel):
__tablename__ = 'db_migrate_version'
repository_id = Column('repository_id', String(250), primary_key=True)
repository_path = Column('repository_path', Text)
version = Column('version', Integer)
@@ -326,96 +326,109 @@ def DefaultPermissionsForm(repo_perms_ch
default_repo_create = v.OneOf(create_choices)
default_user_group_create = v.OneOf(user_group_create_choices)
#default_repo_group_create = v.OneOf(repo_group_create_choices) #not impl. yet
default_fork = v.OneOf(fork_choices)
default_register = v.OneOf(register_choices)
default_extern_activate = v.OneOf(extern_activate_choices)
return _DefaultPermissionsForm
def CustomDefaultPermissionsForm():
class _CustomDefaultPermissionsForm(formencode.Schema):
filter_extra_fields = True
allow_extra_fields = True
inherit_default_permissions = v.StringBoolean(if_missing=False)
create_repo_perm = v.StringBoolean(if_missing=False)
create_user_group_perm = v.StringBoolean(if_missing=False)
#create_repo_group_perm Impl. later
fork_repo_perm = v.StringBoolean(if_missing=False)
return _CustomDefaultPermissionsForm
def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
class _DefaultsForm(formencode.Schema):
default_repo_type = v.OneOf(supported_backends)
default_repo_private = v.StringBoolean(if_missing=False)
default_repo_enable_statistics = v.StringBoolean(if_missing=False)
default_repo_enable_downloads = v.StringBoolean(if_missing=False)
default_repo_enable_locking = v.StringBoolean(if_missing=False)
return _DefaultsForm
def LdapSettingsForm(tls_reqcert_choices, search_scope_choices,
tls_kind_choices):
class _LdapSettingsForm(formencode.Schema):
#pre_validators = [LdapLibValidator]
ldap_active = v.StringBoolean(if_missing=False)
ldap_host = v.UnicodeString(strip=True,)
ldap_port = v.Number(strip=True,)
ldap_tls_kind = v.OneOf(tls_kind_choices)
ldap_tls_reqcert = v.OneOf(tls_reqcert_choices)
ldap_dn_user = v.UnicodeString(strip=True,)
ldap_dn_pass = v.UnicodeString(strip=True,)
ldap_base_dn = v.UnicodeString(strip=True,)
ldap_filter = v.UnicodeString(strip=True,)
ldap_search_scope = v.OneOf(search_scope_choices)
ldap_attr_login = v.AttrLoginValidator()(not_empty=True)
ldap_attr_firstname = v.UnicodeString(strip=True,)
ldap_attr_lastname = v.UnicodeString(strip=True,)
ldap_attr_email = v.UnicodeString(strip=True,)
return _LdapSettingsForm
def UserExtraEmailForm():
class _UserExtraEmailForm(formencode.Schema):
email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
return _UserExtraEmailForm
def UserExtraIpForm():
class _UserExtraIpForm(formencode.Schema):
ip = v.ValidIp()(not_empty=True)
return _UserExtraIpForm
def PullRequestForm(repo_id):
class _PullRequestForm(formencode.Schema):
user = v.UnicodeString(strip=True, required=True)
org_repo = v.UnicodeString(strip=True, required=True)
org_ref = v.UnicodeString(strip=True, required=True)
other_repo = v.UnicodeString(strip=True, required=True)
other_ref = v.UnicodeString(strip=True, required=True)
revisions = All(#v.NotReviewedRevisions(repo_id)(),
v.UniqueList(not_empty=True))
review_members = v.UniqueList(not_empty=True)
pullrequest_title = v.UnicodeString(strip=True, required=True, min=3)
pullrequest_desc = v.UnicodeString(strip=True, required=False)
ancestor_rev = v.UnicodeString(strip=True, required=True)
merge_rev = v.UnicodeString(strip=True, required=True)
return _PullRequestForm
def GistForm(lifetime_options):
class _GistForm(formencode.Schema):
filename = v.UnicodeString(strip=True, required=False)
description = v.UnicodeString(required=False, if_missing='')
lifetime = v.OneOf(lifetime_options)
content = v.UnicodeString(required=True, not_empty=True)
public = v.UnicodeString(required=False, if_missing='')
private = v.UnicodeString(required=False, if_missing='')
return _GistForm
rhodecode.model.gist
~~~~~~~~~~~~~~~~~~~~
gist model for RhodeCode
:copyright: (C) 2011-2013 Marcin Kuzminski <marcin@python-works.com>
from rhodecode.lib.utils2 import safe_unicode, unique_id, safe_int, \
time_to_datetime, safe_str, AttributeDict
from rhodecode.model import BaseModel
from rhodecode.lib.vcs import get_repo
GIST_STORE_LOC = '.gist_store'
class GistModel(BaseModel):
def _get_gist(self, gist):
Helper method to get gist by ID, or gist_access_id as a fallback
:param gist: GistID, gist_access_id, or Gist instance
return self._get_instance(Gist, gist,
callback=Gist.get_by_access_id)
def __delete_gist(self, gist):
removes gist from filesystem
:param gist: gist object
root_path = RepoModel().repos_path
rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
log.info("Removing %s" % (rm_path))
shutil.rmtree(rm_path)
def get_gist_files(self, gist_access_id):
Get files for given gist
:param gist_access_id:
r = get_repo(os.path.join(*map(safe_str,
[root_path, GIST_STORE_LOC, gist_access_id])))
cs = r.get_changeset()
return (
cs, [n for n in cs.get_node('/')]
def create(self, description, owner, gist_mapping,
gist_type=Gist.GIST_PUBLIC, lifetime=-1):
:param description: description of the gist
:param owner: user who created this gist
:param gist_mapping: mapping {filename:{'content':content},...}
:param gist_type: type of gist private/public
:param lifetime: in minutes, -1 == forever
gist_id = safe_unicode(unique_id(20))
lifetime = safe_int(lifetime, -1)
gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
log.debug('set GIST expiration date to: %s'
% (time_to_datetime(gist_expires)
if gist_expires != -1 else 'forever'))
#create the Database version
gist = Gist()
gist.gist_description = description
gist.gist_access_id = gist_id
gist.gist_owner = owner.user_id
gist.gist_expires = gist_expires
gist.gist_type = safe_unicode(gist_type)
self.sa.add(gist)
self.sa.flush()
if gist_type == Gist.GIST_PUBLIC:
# use DB ID for easy to use GIST ID
gist_id = safe_unicode(gist.gist_id)
gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
log.debug('Creating new %s GIST repo in %s' % (gist_type, gist_repo_path))
repo = RepoModel()._create_repo(repo_name=gist_repo_path, alias='hg',
parent=None)
processed_mapping = {}
for filename in gist_mapping:
content = gist_mapping[filename]['content']
#TODO: expand support for setting explicit lexers
# if lexer is None:
# try:
# lexer = pygments.lexers.guess_lexer_for_filename(filename,content)
# except pygments.util.ClassNotFound:
# lexer = 'text'
processed_mapping[filename] = {'content': content}
# now create single multifile commit
message = 'added file'
message += 's: ' if len(processed_mapping) > 1 else ': '
message += ', '.join([x for x in processed_mapping])
#fake RhodeCode Repository object
fake_repo = AttributeDict(dict(
repo_name=gist_repo_path,
scm_instance_no_cache=lambda: repo,
))
ScmModel().create_nodes(
user=owner.user_id, repo=fake_repo,
nodes=processed_mapping,
trigger_push_hook=False
return gist
def delete(self, gist, fs_remove=True):
gist = self._get_gist(gist)
self.sa.delete(gist)
if fs_remove:
self.__delete_gist(gist)
log.debug('skipping removal from filesystem')
raise
@@ -22,193 +22,192 @@
from datetime import datetime
from rhodecode.lib.vcs.backends import get_backend
from rhodecode.lib.utils2 import LazyProperty, safe_str, safe_unicode,\
remove_prefix, obfuscate_url_pw
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.hooks import log_create_repository, log_delete_repository
from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
Statistics, UserGroup, UserGroupRepoToPerm, RhodeCodeUi, RepoGroup,\
RhodeCodeSetting, RepositoryField
from rhodecode.lib.auth import HasRepoPermissionAny, HasUserGroupPermissionAny
from rhodecode.lib.exceptions import AttachedForksError
from rhodecode.model.scm import UserGroupList
class RepoModel(BaseModel):
cls = Repository
URL_SEPARATOR = Repository.url_sep()
def _get_user_group(self, users_group):
return self._get_instance(UserGroup, users_group,
callback=UserGroup.get_by_group_name)
def _get_repo_group(self, repos_group):
return self._get_instance(RepoGroup, repos_group,
callback=RepoGroup.get_by_group_name)
def _create_default_perms(self, repository, private):
# create default permission
default = 'repository.read'
def_user = User.get_default_user()
for p in def_user.user_perms:
if p.permission.permission_name.startswith('repository.'):
default = p.permission.permission_name
default_perm = 'repository.none' if private else default
repo_to_perm = UserRepoToPerm()
repo_to_perm.permission = Permission.get_by_key(default_perm)
repo_to_perm.repository = repository
repo_to_perm.user_id = def_user.user_id
return repo_to_perm
@LazyProperty
def repos_path(self):
Get's the repositories root path from database
q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
return q.ui_value
def get(self, repo_id, cache=False):
repo = self.sa.query(Repository)\
.filter(Repository.repo_id == repo_id)
if cache:
repo = repo.options(FromCache("sql_cache_short",
"get_repo_%s" % repo_id))
return repo.scalar()
def get_repo(self, repository):
return self._get_repo(repository)
def get_by_repo_name(self, repo_name, cache=False):
.filter(Repository.repo_name == repo_name)
"get_repo_%s" % repo_name))
def get_all_user_repos(self, user):
Get's all repositories that user have at least read access
:type user:
from rhodecode.lib.auth import AuthUser
user = self._get_user(user)
repos = AuthUser(user_id=user.user_id).permissions['repositories']
access_check = lambda r: r[1] in ['repository.read',
'repository.admin']
repos = [x[0] for x in filter(access_check, repos.items())]
return Repository.query().filter(Repository.repo_name.in_(repos))
def get_users_js(self):
users = self.sa.query(User).filter(User.active == True).all()
return json.dumps([
{
'id': u.user_id,
'fname': u.name,
'lname': u.lastname,
'nname': u.username,
'gravatar_lnk': h.gravatar_url(u.email, 14)
} for u in users]
def get_users_groups_js(self):
users_groups = self.sa.query(UserGroup)\
.filter(UserGroup.users_group_active == True).all()
users_groups = UserGroupList(users_groups, perm_set=['usergroup.read',
'usergroup.write',
'usergroup.admin'])
'id': gr.users_group_id,
'grname': gr.users_group_name,
'grmembers': len(gr.members),
} for gr in users_groups]
def _render_datatable(cls, tmpl, *args, **kwargs):
from pylons import tmpl_context as c
_tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
tmpl = template.get_def(tmpl)
kwargs.update(dict(_=_, h=h, c=c))
return tmpl.render(*args, **kwargs)
def update_repoinfo(cls, repositories=None):
if not repositories:
repositories = Repository.getAll()
for repo in repositories:
repo.update_changeset_cache()
def get_repos_as_dict(self, repos_list=None, admin=False, perm_check=True,
super_user_actions=False):
_render = self._render_datatable
def quick_menu(repo_name):
return _render('quick_menu', repo_name)
def repo_lnk(name, rtype, private, fork_of):
return _render('repo_name', name, rtype, private, fork_of,
short_name=not admin, admin=False)
def last_change(last_change):
return _render("last_change", last_change)
def rss_lnk(repo_name):
return _render("rss", repo_name)
def atom_lnk(repo_name):
return _render("atom", repo_name)
def last_rev(repo_name, cs_cache):
return _render('revision', repo_name, cs_cache.get('revision'),
cs_cache.get('raw_id'), cs_cache.get('author'),
cs_cache.get('message'))
def desc(desc):
if c.visual.stylify_metatags:
return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
return h.urlify_text(h.truncate(desc, 60))
def repo_actions(repo_name):
return _render('repo_actions', repo_name, super_user_actions)
def owner_actions(user_id, username):
return _render('user_name', user_id, username)
repos_data = []
for repo in repos_list:
@@ -559,188 +558,198 @@ class RepoModel(BaseModel):
.filter(UserRepoToPerm.user == user)\
.filter(UserRepoToPerm.repository == repo)\
if obj is None:
# create new !
obj = UserRepoToPerm()
obj.repository = repo
obj.user = user
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s' % (perm, user, repo))
def revoke_user_permission(self, repo, user):
:param repo: Instance of Repository, repository_id, or repository name
:param user: Instance of User, user_id or username
repo = self._get_repo(repo)
obj = self.sa.query(UserRepoToPerm)\
if obj:
self.sa.delete(obj)
log.debug('Revoked perm on %s on %s' % (repo, user))
def grant_users_group_permission(self, repo, group_name, perm):
:param group_name: Instance of UserGroup, users_group_id,
or user group name
:param perm: Instance of Permission, or permission_name
group_name = self._get_user_group(group_name)
permission = self._get_perm(perm)
# check if we have that permission already
obj = self.sa.query(UserGroupRepoToPerm)\
.filter(UserGroupRepoToPerm.users_group == group_name)\
.filter(UserGroupRepoToPerm.repository == repo)\
# create new
obj = UserGroupRepoToPerm()
obj.users_group = group_name
log.debug('Granted perm %s to %s on %s' % (perm, group_name, repo))
def revoke_users_group_permission(self, repo, group_name):
log.debug('Revoked perm to %s on %s' % (repo, group_name))
def delete_stats(self, repo_name):
removes stats for given repo
:param repo_name:
repo = self._get_repo(repo_name)
obj = self.sa.query(Statistics)\
.filter(Statistics.repository == repo).scalar()
def __create_repo(self, repo_name, alias, parent, clone_uri=False):
def _create_repo(self, repo_name, alias, parent, clone_uri=False,
repo_store_location=None):
return self.__create_repo(repo_name, alias, parent, clone_uri,
repo_store_location)
def __create_repo(self, repo_name, alias, parent, clone_uri=False,
makes repository on filesystem. It's group aware means it'll create
a repository within a group, and alter the paths accordingly of
group location
:param alias:
:param parent_id:
:param clone_uri:
from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
if parent:
new_parent_path = os.sep.join(parent.full_path_splitted)
new_parent_path = ''
if repo_store_location:
_paths = [repo_store_location]
_paths = [self.repos_path, new_parent_path, repo_name]
# we need to make it str for mercurial
repo_path = os.path.join(*map(lambda x: safe_str(x),
[self.repos_path, new_parent_path, repo_name]))
repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
# check if this path is not a repository
if is_valid_repo(repo_path, self.repos_path):
raise Exception('This path %s is a valid repository' % repo_path)
# check if this path is a group
if is_valid_repos_group(repo_path, self.repos_path):
raise Exception('This path %s is a valid group' % repo_path)
log.info('creating repo %s in %s @ %s' % (
repo_name, safe_unicode(repo_path),
obfuscate_url_pw(clone_uri)
backend(repo_path, create=True, src_url=clone_uri)
repo = backend(repo_path, create=True, src_url=clone_uri)
elif alias == 'git':
r = backend(repo_path, create=True, src_url=clone_uri, bare=True)
repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
# add rhodecode hook into this repo
ScmModel().install_git_hook(repo=r)
ScmModel().install_git_hook(repo=repo)
raise Exception('Undefined alias %s' % alias)
def __rename_repo(self, old, new):
renames repository on filesystem
:param old: old name
:param new: new name
log.info('renaming repo from %s to %s' % (old, new))
old_path = os.path.join(self.repos_path, old)
new_path = os.path.join(self.repos_path, new)
if os.path.isdir(new_path):
'Was trying to rename to already existing dir %s' % new_path
shutil.move(old_path, new_path)
def __delete_repo(self, repo):
removes repo from filesystem, the removal is acctually made by
added rm__ prefix into dir, and rename internat .hg/.git dirs so this
repository is no longer valid for rhodecode, can be undeleted later on
by reverting the renames on this repository
:param repo: repo object
rm_path = os.path.join(self.repos_path, repo.repo_name)
# disable hg/git internal that it doesn't get detected as repo
alias = repo.repo_type
bare = getattr(repo.scm_instance, 'bare', False)
if not bare:
# skip this for bare git repos
shutil.move(os.path.join(rm_path, '.%s' % alias),
os.path.join(rm_path, 'rm__.%s' % alias))
# disable repo
_now = datetime.now()
_ms = str(_now.microsecond).rjust(6, '0')
_d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
repo.just_name)
if repo.group:
args = repo.group.full_path_splitted + [_d]
_d = os.path.join(*args)
shutil.move(rm_path, os.path.join(self.repos_path, _d))
rhodecode.model.scm
Scm model for RhodeCode
:created_on: Apr 9, 2010
import cStringIO
import pkg_resources
from os.path import dirname as dn, join as jn
from sqlalchemy import func
from rhodecode.lib.vcs import get_backend
from rhodecode.lib.vcs.exceptions import RepositoryError
from rhodecode import BACKENDS
from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url,\
_set_extras
from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny,\
HasUserGroupPermissionAnyDecorator, HasUserGroupPermissionAny
from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
action_logger, REMOVED_REPO_PAT
from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
UserFollowing, UserLog, User, RepoGroup, PullRequest
from rhodecode.lib.hooks import log_push_action
class UserTemp(object):
def __init__(self, user_id):
self.user_id = user_id
return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
class RepoTemp(object):
def __init__(self, repo_id):
self.repo_id = repo_id
return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
class CachedRepoList(object):
Cached repo list, uses in-memory cache after initialization, that is
super fast
def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
self.db_repo_list = db_repo_list
self.repos_path = repos_path
self.order_by = order_by
self.reversed = (order_by or '').startswith('-')
if not perm_set:
perm_set = ['repository.read', 'repository.write',
self.perm_set = perm_set
def __len__(self):
return len(self.db_repo_list)
return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
# pre-propagated valid_cache_keys to save executing select statements
# for each repo
valid_cache_keys = CacheInvalidation.get_valid_cache_keys()
for dbr in self.db_repo_list:
scmr = dbr.scm_instance_cached(valid_cache_keys)
# check permission at this level
if not HasRepoPermissionAny(
*self.perm_set)(dbr.repo_name, 'get repo check'):
last_change = scmr.last_change
tip = h.get_changeset_safe(scmr, 'tip')
log.error(
'%s this repository is present in database but it '
'cannot be created as an scm instance, org_exc:%s'
% (dbr.repo_name, traceback.format_exc())
tmp_d = {}
tmp_d['name'] = dbr.repo_name
tmp_d['name_sort'] = tmp_d['name'].lower()
tmp_d['raw_name'] = tmp_d['name'].lower()
tmp_d['description'] = dbr.description
tmp_d['description_sort'] = tmp_d['description'].lower()
tmp_d['last_change'] = last_change
tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
tmp_d['tip'] = tip.raw_id
tmp_d['tip_sort'] = tip.revision
tmp_d['rev'] = tip.revision
tmp_d['contact'] = dbr.user.full_contact
tmp_d['contact_sort'] = tmp_d['contact']
tmp_d['owner_sort'] = tmp_d['contact']
tmp_d['repo_archives'] = list(scmr._get_archives())
tmp_d['last_msg'] = tip.message
tmp_d['author'] = tip.author
tmp_d['dbrepo'] = dbr.get_dict()
tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
yield tmp_d
class SimpleCachedRepoList(CachedRepoList):
Lighter version of CachedRepoList without the scm initialisation
@@ -438,258 +439,289 @@ class ScmModel(BaseModel):
extras = {
'ip': _get_ip_addr(environ),
'username': username,
'action': 'push_local',
'repository': repo_name,
'scm': repo_alias,
'config': CONFIG['__file__'],
'server_url': get_server_url(environ),
'make_lock': None,
'locked_by': [None, None]
_set_extras(extras)
def _handle_push(self, repo, username, action, repo_name, revisions):
Triggers push action hooks
:param repo: SCM repo
:param username: username who pushes
:param action: push/push_loca/push_remote
:param repo_name: name of repo
:param revisions: list of revisions that we pushed
self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
_scm_repo = repo._repo
# trigger push hook
if repo.alias == 'hg':
log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
elif repo.alias == 'git':
log_push_action(None, _scm_repo, _git_revs=revisions)
def _get_IMC_module(self, scm_type):
Returns InMemoryCommit class based on scm_type
:param scm_type:
if scm_type == 'hg':
from rhodecode.lib.vcs.backends.hg import \
MercurialInMemoryChangeset as IMC
elif scm_type == 'git':
from rhodecode.lib.vcs.backends.git import \
GitInMemoryChangeset as IMC
return IMC
def pull_changes(self, repo, username):
dbrepo = self.__get_repo(repo)
clone_uri = dbrepo.clone_uri
if not clone_uri:
raise Exception("This repository doesn't have a clone uri")
repo = dbrepo.scm_instance
repo_name = dbrepo.repo_name
if repo.alias == 'git':
repo.fetch(clone_uri)
repo.pull(clone_uri)
self.mark_for_invalidation(repo_name)
def commit_change(self, repo, repo_name, cs, user, author, message,
content, f_path):
Commits changes
:param repo: SCM instance
IMC = self._get_IMC_module(repo.alias)
# decoding here will force that we have proper encoded values
# in any other case this will throw exceptions and deny commit
content = safe_str(content)
path = safe_str(f_path)
# message and author needs to be unicode
# proper backend should then translate that into required type
message = safe_unicode(message)
author = safe_unicode(author)
imc = IMC(repo)
imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
tip = imc.commit(message=message,
parents=[cs], branch=cs.branch)
self._handle_push(repo,
username=user.username,
action='push_local',
revisions=[tip.raw_id])
return tip
def create_node(self, repo, repo_name, cs, user, author, message, content,
f_path):
def create_nodes(self, user, repo, message, nodes, parent_cs=None,
author=None, trigger_push_hook=True):
Commits given multiple nodes into repo
:param user: RhodeCode User object or user_id, the commiter
:param repo: RhodeCode Repository object
:param message: commit message
:param nodes: mapping {filename:{'content':content},...}
:param parent_cs: parent changeset, can be empty than it's initial commit
:param author: author of commit, cna be different that commiter only for git
:param trigger_push_hook: trigger push hooks
:returns: new commited changeset
scm_instance = repo.scm_instance_no_cache()
if isinstance(content, (basestring,)):
elif isinstance(content, (file, cStringIO.OutputType,)):
content = content.read()
raise Exception('Content is of unrecognized type %s' % (
type(content)
processed_nodes = []
for f_path in nodes:
if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
raise NonRelativePathError('%s is not an relative path' % f_path)
if f_path:
f_path = os.path.normpath(f_path)
f_path = safe_str(f_path)
content = nodes[f_path]['content']
processed_nodes.append((f_path, content))
m = IMC(repo)
commiter = user.full_contact
author = safe_unicode(author) if author else commiter
if isinstance(cs, EmptyChangeset):
IMC = self._get_IMC_module(scm_instance.alias)
imc = IMC(scm_instance)
if not parent_cs:
parent_cs = EmptyChangeset(alias=scm_instance.alias)
if isinstance(parent_cs, EmptyChangeset):
# EmptyChangeset means we we're editing empty repository
parents = None
parents = [cs]
m.add(FileNode(path, content=content))
tip = m.commit(message=message,
parents=parents, branch=cs.branch)
parents = [parent_cs]
# add multiple nodes
for path, content in processed_nodes:
imc.add(FileNode(path, content=content))
parents=parents,
branch=parent_cs.branch)
self.mark_for_invalidation(repo.repo_name)
if trigger_push_hook:
self._handle_push(scm_instance,
repo_name=repo.repo_name,
def get_nodes(self, repo_name, revision, root_path='/', flat=True):
recursive walk in root dir and return a set of all path in that dir
based on repository walk function
:param repo_name: name of repository
:param revision: revision for which to list nodes
:param root_path: root path to list
:param flat: return as a list, if False returns a dict with decription
_files = list()
_dirs = list()
_repo = self.__get_repo(repo_name)
changeset = _repo.scm_instance.get_changeset(revision)
root_path = root_path.lstrip('/')
for topnode, dirs, files in changeset.walk(root_path):
for f in files:
_files.append(f.path if flat else {"name": f.path,
"type": "file"})
for d in dirs:
_dirs.append(d.path if flat else {"name": d.path,
"type": "dir"})
except RepositoryError:
log.debug(traceback.format_exc())
return _dirs, _files
def get_unread_journal(self):
return self.sa.query(UserLog).count()
def get_repo_landing_revs(self, repo=None):
Generates select option with tags branches and bookmarks (for hg only)
grouped by type
:param repo:
:type repo:
hist_l = []
choices = []
repo = self.__get_repo(repo)
hist_l.append(['tip', _('latest tip')])
choices.append('tip')
return choices, hist_l
branches_group = ([(k, k) for k, v in
repo.branches.iteritems()], _("Branches"))
hist_l.append(branches_group)
choices.extend([x[0] for x in branches_group[0]])
bookmarks_group = ([(k, k) for k, v in
repo.bookmarks.iteritems()], _("Bookmarks"))
hist_l.append(bookmarks_group)
choices.extend([x[0] for x in bookmarks_group[0]])
tags_group = ([(k, k) for k, v in
repo.tags.iteritems()], _("Tags"))
hist_l.append(tags_group)
choices.extend([x[0] for x in tags_group[0]])
def install_git_hook(self, repo, force_create=False):
Creates a rhodecode hook inside a git repository
:param repo: Instance of VCS repo
:param force_create: Create even if same name hook exists
loc = jn(repo.path, 'hooks')
if not repo.bare:
loc = jn(repo.path, '.git', 'hooks')
if not os.path.isdir(loc):
os.makedirs(loc)
tmpl_post = pkg_resources.resource_string(
'rhodecode', jn('config', 'post_receive_tmpl.py')
tmpl_pre = pkg_resources.resource_string(
'rhodecode', jn('config', 'pre_receive_tmpl.py')
for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
_hook_file = jn(loc, '%s-receive' % h_type)
_rhodecode_hook = False
log.debug('Installing git hook in repo %s' % repo)
if os.path.exists(_hook_file):
# let's take a look at this hook, maybe it's rhodecode ?
log.debug('hook exists, checking if it is from rhodecode')
_HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
with open(_hook_file, 'rb') as f:
data = f.read()
matches = re.compile(r'(?:%s)\s*=\s*(.*)'
% 'RC_HOOK_VER').search(data)
if matches:
ver = matches.groups()[0]
log.debug('got %s it is rhodecode' % (ver))
_rhodecode_hook = True
# there is no hook in this dir, so we want to create one
if _rhodecode_hook or force_create:
log.debug('writing %s hook file !' % h_type)
with open(_hook_file, 'wb') as f:
tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
f.write(tmpl)
os.chmod(_hook_file, 0755)
log.debug('skipping writing hook file')
Set of generic validators
from webhelpers.pylonslib.secure_form import authentication_token
from formencode.validators import (
UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
NotEmpty, IPAddress, CIDR
NotEmpty, IPAddress, CIDR, String, FancyValidator
from rhodecode.lib.compat import OrderedSet
from rhodecode.lib import ipaddr
from rhodecode.lib.utils import repo_name_slug
from rhodecode.lib.utils2 import safe_int, str2bool
from rhodecode.model.db import RepoGroup, Repository, UserGroup, User,\
ChangesetStatus
from rhodecode.lib.exceptions import LdapImportError
from rhodecode.config.routing import ADMIN_PREFIX
from rhodecode.lib.auth import HasReposGroupPermissionAny, HasPermissionAny
# silence warnings and pylint
UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
class UniqueList(formencode.FancyValidator):
Unique List !
messages = dict(
empty=_('Value cannot be an empty list'),
missing_value=_('Value cannot be an empty list'),
def _to_python(self, value, state):
if isinstance(value, list):
return value
elif isinstance(value, set):
return list(value)
elif isinstance(value, tuple):
elif value is None:
return [value]
def empty_value(self, value):
class StateObj(object):
this is needed to translate the messages using _() in validators
_ = staticmethod(_)
def M(self, key, state=None, **kwargs):
returns string from self.message based on given key,
passed kw params are used to substitute %(named)s params inside
translated strings
:param msg:
:param state:
if state is None:
state = StateObj()
state._ = staticmethod(_)
#inject validator into state object
return self.message(key, state, **kwargs)
def ValidUsername(edit=False, old_data={}):
class _validator(formencode.validators.FancyValidator):
messages = {
'username_exists': _(u'Username "%(username)s" already exists'),
'system_invalid_username':
_(u'Username "%(username)s" is forbidden'),
'invalid_username':
_(u'Username may only contain alphanumeric characters '
'underscores, periods or dashes and must begin with '
'alphanumeric character')
def validate_python(self, value, state):
if value in ['default', 'new_user']:
msg = M(self, 'system_invalid_username', state, username=value)
raise formencode.Invalid(msg, value, state)
#check if user is unique
old_un = None
if edit:
old_un = User.get(old_data.get('user_id')).username
if old_un != value or not edit:
if User.get_by_username(value, case_insensitive=True):
msg = M(self, 'username_exists', state, username=value)
if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
msg = M(self, 'invalid_username', state)
return _validator
def ValidRepoUser():
'invalid_username': _(u'Username %(username)s is not valid')
User.query().filter(User.active == True)\
.filter(User.username == value).one()
/**
* Stylesheets for the context bar
*/
#quick .repo_switcher { background-image: url("../images/icons/database.png"); }
#quick .journal { background-image: url("../images/icons/book.png"); }
#quick .gists { background-image: url("../images/icons/note.png"); }
#quick .gists-private { background-image: url("../images/icons/note_error.png"); }
#quick .gists-new { background-image: url("../images/icons/note_add.png"); }
#quick .search { background-image: url("../images/icons/search_16.png"); }
#quick .admin { background-image: url("../images/icons/cog_edit.png"); }
#context-bar a.follow { background-image: url("../images/icons/heart.png"); }
#context-bar a.following { background-image: url("../images/icons/heart_delete.png"); }
#context-bar a.fork { background-image: url("../images/icons/arrow_divide.png"); }
#context-bar a.summary { background-image: url("../images/icons/clipboard_16.png"); }
#context-bar a.changelogs { background-image: url("../images/icons/time.png"); }
#context-bar a.files { background-image: url("../images/icons/file.png"); }
#context-bar a.switch-to { background-image: url("../images/icons/arrow_switch.png"); }
#context-bar a.options { background-image: url("../images/icons/table_gear.png"); }
#context-bar a.forks { background-image: url("../images/icons/arrow_divide.png"); }
#context-bar a.pull-request { background-image: url("../images/icons/arrow_join.png"); }
#context-bar a.branches { background-image: url("../images/icons/arrow_branch.png"); }
#context-bar a.tags { background-image: url("../images/icons/tag_blue.png"); }
#context-bar a.bookmarks { background-image: url("../images/icons/tag_green.png"); }
#context-bar a.settings { background-image: url("../images/icons/cog.png"); }
#context-bar a.search { background-image: url("../images/icons/search_16.png"); }
#context-bar a.admin { background-image: url("../images/icons/cog_edit.png"); }
#context-bar a.journal { background-image: url("../images/icons/book.png"); }
#context-bar a.gists { background-image: url("../images/icons/note.png"); }
#context-bar a.gists-private { background-image: url("../images/icons/note_error.png"); }
#context-bar a.gists-new { background-image: url("../images/icons/note_add.png"); }
#context-bar a.repos { background-image: url("../images/icons/database_edit.png"); }
#context-bar a.repos_groups { background-image: url("../images/icons/database_link.png"); }
#context-bar a.users { background-image: url("../images/icons/user_edit.png"); }
#context-bar a.groups { background-image: url("../images/icons/group_edit.png"); }
#context-bar a.permissions { background-image: url("../images/icons/key.png"); }
#context-bar a.ldap { background-image: url("../images/icons/server_key.png"); }
#context-bar a.defaults { background-image: url("../images/icons/wrench.png"); }
#context-bar a.settings { background-image: url("../images/icons/cog_edit.png"); }
#context-bar a.compare_request { background-image: url('../images/icons/arrow_inout.png')}
#context-bar a.locking_del { background-image: url('../images/icons/lock_delete.png')}
#context-bar a.locking_add { background-image: url('../images/icons/lock_add.png')}
#content #context-bar {
position: relative;
overflow: visible;
background-color: #336699;
border-top: 1px solid #517da8;
border-bottom: 1px solid #003162;
padding: 0 5px;
min-height: 36px;
#header #header-inner #quick a,
#content #context-bar,
#content #context-bar a {
color: #FFFFFF;
#header #header-inner #quick a:hover,
#content #context-bar a:hover {
text-decoration: none;
#content #context-bar .icon {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: text-bottom;
ul.horizontal-list {
display: block;
ul.horizontal-list > li {
float: left;
#header #header-inner #quick ul,
ul.horizontal-list > li ul {
position: absolute;
display: none;
right: 0;
z-index: 999;
#header #header-inner #quick li:hover > ul,
ul.horizontal-list li:hover > ul {
#header #header-inner #quick li ul li,
ul.horizontal-list ul li {
border-bottom: 1px solid rgba(0,0,0,0.1);
border-top: 1px solid rgba(255,255,255,0.1);
ul.horizontal-list > li ul ul {
right: 100%;
top: -1px;
min-width: 200px;
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
#header #header-inner #quick ul a,
ul.horizontal-list li a {
white-space: nowrap;
#breadcrumbs {
padding: 6px 0 5px 0;
padding-left: 5px;
font-weight: bold;
font-size: 14px;
#breadcrumbs span {
font-size: 1.4em;
div.codeblock {
overflow: auto;
padding: 0px;
border: 1px solid #ccc;
background: #f8f8f8;
font-size: 100%;
line-height: 100%;
/* new */
line-height: 125%;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
div.codeblock .code-header {
border-bottom: 1px solid #CCCCCC;
background: #EEEEEE;
padding: 10px 0 10px 0;
padding: 10px 0 5px 0;
div.codeblock .code-header .stats {
clear: both;
padding: 6px 8px 6px 10px;
padding: 2px 8px 2px 14px;
border-bottom: 1px solid rgb(204, 204, 204);
height: 23px;
margin-bottom: 6px;
div.codeblock .code-header .stats .left {
div.codeblock .code-header .stats .left.img {
margin-top: -2px;
div.codeblock .code-header .stats .left.item {
padding: 0 9px 0 9px;
border-right: 1px solid #ccc;
div.codeblock .code-header .stats .left.item pre {
div.codeblock .code-header .stats .left.item.last {
border-right: none;
div.codeblock .code-header .stats .buttons {
float: right;
padding-right: 4px;
div.codeblock .code-header .author {
margin-left: 25px;
margin-left: 15px;
height: 25px;
div.codeblock .code-header .author .user {
padding-top: 3px;
div.codeblock .code-header .commit {
font-weight: normal;
white-space: pre;
.code-highlighttable,
div.codeblock .code-body table {
width: 0 !important;
border: 0px !important;
div.codeblock .code-body table td {
div.code-body {
background-color: #FFFFFF;
div.codeblock .code-header .search-path {
padding: 0px 0px 0px 10px;
div.search-code-body {
padding: 5px 0px 5px 10px;
div.search-code-body pre .match {
background-color: #FAFFA6;
div.search-code-body pre .break {
background-color: #DDE7EF;
width: 100%;
color: #747474;
div.annotatediv {
margin-left: 2px;
margin-right: 4px;
.code-highlight {
margin-top: 5px;
margin-bottom: 5px;
border-left: 2px solid #ccc;
border-left: 1px solid #ccc;
.code-highlight pre, .linenodiv pre {
padding: 5px;
padding: 5px 2px 0px 5px;
margin: 0;
.code-highlight pre div:target {
background-color: #FFFFBE !important;
.linenos { padding: 0px !important; border:0px !important;}
.linenos a { text-decoration: none; }
.code { display: block; }
.code { display: block; border:0px !important; }
.code-highlight .hll, .codehilite .hll { background-color: #ffffcc }
.code-highlight .c, .codehilite .c { color: #408080; font-style: italic } /* Comment */
.code-highlight .err, .codehilite .err { border: 1px solid #FF0000 } /* Error */
.code-highlight .k, .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.code-highlight .o, .codehilite .o { color: #666666 } /* Operator */
.code-highlight .cm, .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
.code-highlight .cp, .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
.code-highlight .c1, .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
.code-highlight .cs, .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
.code-highlight .gd, .codehilite .gd { color: #A00000 } /* Generic.Deleted */
.code-highlight .ge, .codehilite .ge { font-style: italic } /* Generic.Emph */
.code-highlight .gr, .codehilite .gr { color: #FF0000 } /* Generic.Error */
.code-highlight .gh, .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.code-highlight .gi, .codehilite .gi { color: #00A000 } /* Generic.Inserted */
.code-highlight .go, .codehilite .go { color: #808080 } /* Generic.Output */
.code-highlight .gp, .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.code-highlight .gs, .codehilite .gs { font-weight: bold } /* Generic.Strong */
.code-highlight .gu, .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.code-highlight .gt, .codehilite .gt { color: #0040D0 } /* Generic.Traceback */
.code-highlight .kc, .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.code-highlight .kd, .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.code-highlight .kn, .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.code-highlight .kp, .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.code-highlight .kr, .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.code-highlight .kt, .codehilite .kt { color: #B00040 } /* Keyword.Type */
.code-highlight .m, .codehilite .m { color: #666666 } /* Literal.Number */
.code-highlight .s, .codehilite .s { color: #BA2121 } /* Literal.String */
.code-highlight .na, .codehilite .na { color: #7D9029 } /* Name.Attribute */
.code-highlight .nb, .codehilite .nb { color: #008000 } /* Name.Builtin */
.code-highlight .nc, .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.code-highlight .no, .codehilite .no { color: #880000 } /* Name.Constant */
.code-highlight .nd, .codehilite .nd { color: #AA22FF } /* Name.Decorator */
.code-highlight .ni, .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
.code-highlight .ne, .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.code-highlight .nf, .codehilite .nf { color: #0000FF } /* Name.Function */
.code-highlight .nl, .codehilite .nl { color: #A0A000 } /* Name.Label */
.code-highlight .nn, .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.code-highlight .nt, .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.code-highlight .nv, .codehilite .nv { color: #19177C } /* Name.Variable */
.code-highlight .ow, .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.code-highlight .w, .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.code-highlight .mf, .codehilite .mf { color: #666666 } /* Literal.Number.Float */
.code-highlight .mh, .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
.code-highlight .mi, .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
.code-highlight .mo, .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
.code-highlight .sb, .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.code-highlight .sc, .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.code-highlight .sd, .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.code-highlight .s2, .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.code-highlight .se, .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
.code-highlight .sh, .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.code-highlight .si, .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
.code-highlight .sx, .codehilite .sx { color: #008000 } /* Literal.String.Other */
.code-highlight .sr, .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
.code-highlight .s1, .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.code-highlight .ss, .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.code-highlight .bp, .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.code-highlight .vc, .codehilite .vc { color: #19177C } /* Name.Variable.Class */
.code-highlight .vg, .codehilite .vg { color: #19177C } /* Name.Variable.Global */
.code-highlight .vi, .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.code-highlight .il, .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
@@ -2213,192 +2213,197 @@ a.metatag[tag="license"]:hover {
height: 30px;
font-style: italic;
#journal .journal_icon {
#journal .journal_action {
padding-top: 4px;
min-height: 2px;
float: left
#journal .journal_action_params {
clear: left;
padding-left: 22px;
#journal .journal_repo {
margin-left: 6px;
#journal .date {
color: #777777;
font-size: 11px;
#journal .journal_repo .journal_repo_name {
font-size: 1.1em;
#journal .compare_view {
padding: 5px 0px 5px 0px;
width: 95px;
.journal_highlight {
padding: 0 2px;
vertical-align: bottom;
.trending_language_tbl, .trending_language_tbl td {
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
.trending_language_tbl, .trending_language_tbl tr {
border-spacing: 1px;
.trending_language {
background-color: #003367;
color: #FFF;
min-width: 20px;
height: 12px;
margin-bottom: 0px;
margin-left: 5px;
padding: 3px;
h3.files_location {
font-size: 1.8em;
font-weight: 700;
border-bottom: none !important;
margin: 10px 0 !important;
#files_data dl dt {
width: 60px;
#files_data dl dd {
padding: 5px !important;
#files_data .codeblock #editor_container .error-message {
color: red;
padding: 10px 10px 10px 26px
.file_history {
padding-top: 10px;
font-size: 16px;
.file_author {
.file_author .item {
color: #888;
.tablerow0 {
background-color: #F8F8F8;
.tablerow1 {
.changeset_id {
color: #666666;
margin-right: -3px;
.changeset_hash {
color: #000000;
#changeset_content {
border-left: 1px solid #CCC;
border-right: 1px solid #CCC;
border-bottom: 1px solid #CCC;
#changeset_compare_view_content {
border: 1px solid #CCC;
#changeset_content .container {
min-height: 100px;
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 {
width: 20%;
text-align: right;
#changeset_content .container .message {
white-space: pre-wrap;
#changeset_content .container .message a:hover {
.cs_files .cur_cs {
margin: 10px 2px;
.cs_files .node {
.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 {
background-color: #FF8888;
@@ -3473,285 +3478,308 @@ table.code-browser .submodule-dir {
right: 5px;
top: 5px;
div#legend_data {
padding-left: 10px;
div#legend_container table {
border: none !important;
div#legend_container table, div#legend_choices table {
table#permissions_manage {
table#permissions_manage span.private_repo_msg {
font-size: 0.8em;
opacity: 0.6;
table#permissions_manage td.private_repo_msg {
table#permissions_manage tr#add_perm_input td {
vertical-align: middle;
div.gravatar {
background-color: #FFF;
margin-right: 0.7em;
padding: 1px 1px 1px 1px;
line-height: 0;
-webkit-border-radius: 3px;
-khtml-border-radius: 3px;
border-radius: 3px;
div.gravatar img {
-webkit-border-radius: 2px;
-khtml-border-radius: 2px;
border-radius: 2px;
#header, #content, #footer {
min-width: 978px;
#content {
padding: 10px 10px 14px 10px;
#content.hover {
padding: 55px 10px 14px 10px !important;
#content div.box div.title div.search {
border-left: 1px solid #316293;
#content div.box div.title div.search div.input input {
border: 1px solid #316293;
.ui-btn {
color: #515151;
background-color: #DADADA;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
background-image: linear-gradient(to bottom, #F4F4F4, #DADADA);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
border-top: 1px solid #DDD;
border-left: 1px solid #c6c6c6;
border-right: 1px solid #DDD;
border-bottom: 1px solid #c6c6c6;
outline: none;
margin: 0px 3px 3px 0px;
-webkit-border-radius: 4px 4px 4px 4px !important;
-khtml-border-radius: 4px 4px 4px 4px !important;
border-radius: 4px 4px 4px 4px !important;
cursor: pointer !important;
padding: 3px 3px 3px 3px;
background-position: 0 -15px;
background-position: 0 -100px;
.ui-btn.badge {
cursor: default !important;
.ui-btn.disabled {
color: #999;
.ui-btn.xsmall {
padding: 1px 2px 1px 1px;
.ui-btn.large {
padding: 6px 12px;
.ui-btn.clone {
padding: 5px 2px 6px 1px;
margin: 0px 0px 3px -4px;
-webkit-border-radius: 0px 4px 4px 0px !important;
-khtml-border-radius: 0px 4px 4px 0px !important;
border-radius: 0px 4px 4px 0px !important;
width: 100px;
top: -2px;
.ui-btn:focus {
.ui-btn:hover {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
.ui-btn.badge:hover {
box-shadow: none !important;
.ui-btn.disabled:hover {
background-position: 0;
.ui-btn.red {
color: #fff;
background-color: #c43c35;
background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
border-color: #c43c35 #c43c35 #882a25;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
.ui-btn.blue {
background-color: #339bb9;
background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
border-color: #339bb9 #339bb9 #22697d;
.ui-btn.green {
background-color: #57a957;
background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
background-image: -moz-linear-gradient(top, #62c462, #57a957);
background-image: -ms-linear-gradient(top, #62c462, #57a957);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
background-image: -webkit-linear-gradient(top, #62c462, #57a957);
background-image: -o-linear-gradient(top, #62c462, #57a957);
background-image: linear-gradient(to bottom, #62c462, #57a957);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
border-color: #57a957 #57a957 #3d773d;
.ui-btn.yellow {
background-color: #faa732;
background-image: -khtml-gradient(linear, left top, left bottom, from(#fbb450), to(#f89406));
background-image: -moz-linear-gradient(top, #fbb450, #f89406);
background-image: -ms-linear-gradient(top, #fbb450, #f89406);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fbb450), color-stop(100%, #f89406));
background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
background-image: -o-linear-gradient(top, #fbb450, #f89406);
background-image: linear-gradient(to bottom, #fbb450, #f89406);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);
border-color: #f89406 #f89406 #ad6704;
.ui-btn.blue.hidden {
.ui-btn.active {
ins, div.options a:hover {
img,
#header #header-inner #quick li a:hover span.normal,
#content div.box div.form div.fields div.field div.textarea table td table td a,
#clone_url,
#clone_url_id
border: none;
img.icon, .right .merge img {
#header ul#logged-user, #content div.box div.title ul.links,
#content div.box div.message div.dismiss,
#content div.box div.traffic div.legend ul {
padding: 0;
#header #header-inner #home, #header #header-inner #logo,
#content div.box ul.left, #content div.box ol.left,
div#commit_history,
div#legend_data, div#legend_container, div#legend_choices {
#header #header-inner #quick li #quick_login,
#header #header-inner #quick li:hover ul ul,
#header #header-inner #quick li:hover ul ul ul,
#header #header-inner #quick li:hover ul ul ul ul,
#content #left #menu ul.closed, #content #left #menu li ul.collapsed, .yui-tt-shadow {
#header #header-inner #quick li:hover #quick_login,
#header #header-inner #quick li:hover ul, #header #header-inner #quick li li:hover ul, #header #header-inner #quick li li li:hover ul, #header #header-inner #quick li li li li:hover ul, #content #left #menu ul.opened, #content #left #menu li ul.expanded {
#content div.graph {
padding: 0 10px 10px;
#content div.box div.title ul.links li a:hover,
#content div.box div.title ul.links li.ui-tabs-selected a {
background: #6388ad; /* Old browsers */
background: -moz-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.1)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* W3C */
/*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#88bfe8', endColorstr='#70b0e0',GradientType=0 ); /* IE6-9 */*/
#content div.box ol.lower-roman, #content div.box ol.upper-roman, #content div.box ol.lower-alpha, #content div.box ol.upper-alpha, #content div.box ol.decimal {
margin: 10px 24px 10px 44px;
#content div.box div.form, #content div.box div.table, #content div.box div.traffic {
padding: 0 20px 10px;
#content div.box div.form div.fields, #login div.form, #login div.form div.fields, #register div.form, #register div.form div.fields {
#content div.box div.form div.fields div.field div.label span, #login div.form div.fields div.field div.label span, #register div.form div.fields div.field div.label span {
height: 1%;
color: #363636;
padding: 2px 0 0;
## -*- coding: utf-8 -*-
<%inherit file="/base/base.html"/>
<%def name="title()">
${_('Gists')} · ${c.rhodecode_name}
</%def>
<%def name="breadcrumbs_links()">
%if c.show_private:
${_('Private Gists for user %s') % c.rhodecode_user.username}
%else:
${_('Public Gists')}
%endif
- ${c.gists_pager.item_count}
<%def name="page_nav()">
${self.menu('gists')}
<%def name="main()">
<div class="box">
<!-- box / title -->
<div class="title">
${self.breadcrumbs()}
%if c.rhodecode_user.username != 'default':
<ul class="links">
<li>
<span>${h.link_to(_(u'Create new gist'), h.url('new_gist'))}</span>
</li>
</ul>
</div>
%if c.gists_pager.item_count>0:
% for gist in c.gists_pager:
<div class="gist-item" style="padding:10px 20px 10px 15px">
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(h.email_or_none(gist.owner.full_contact),24)}"/>
<div title="${gist.owner.full_contact}" class="user">
<b>${h.person(gist.owner.full_contact)}</b> /
<b><a href="${h.url('gist',id=gist.gist_access_id)}">gist:${gist.gist_access_id}</a></b>
<span style="color: #AAA">
%if gist.gist_expires == -1:
${_('Expires')}: ${_('never')}
${_('Expires')}: ${h.age(h.time_to_datetime(gist.gist_expires))}
</span>
<div>${_('Created')} ${h.age(gist.created_on)}
<div style="border:0px;padding:10px 0px 0px 35px;color:#AAA">${gist.gist_description}</div>
% endfor
<div class="notification-paginator">
<div class="pagination-wh pagination-left">
${c.gists_pager.pager('$link_previous ~2~ $link_next')}
<div class="table">${_('There are no gists yet')}</div>
${_('New gist')} · ${c.rhodecode_name}
<%def name="js_extra()">
<script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
<%def name="css_extra()">
<link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
${_('New gist')}
<div class="table">
<div id="files_data">
${h.form(h.url('gists'), method='post',id='eform')}
<div>
<img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.rhodecode_user.full_contact),32)}"/>
<textarea style="resize:vertical; width:400px;border: 1px solid #ccc;border-radius: 3px;" id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
<div id="body" class="codeblock">
<div style="padding: 10px 10px 10px 22px;color:#666666">
##<input type="text" value="" size="30" name="filename" id="filename" placeholder="gistfile1.txt">
${h.text('filename', size=30, placeholder='gistfile1.txt')}
${h.select('lifetime', '', c.lifetime_options)}
<div id="editor_container">
<pre id="editor_pre"></pre>
<textarea id="editor" name="content" style="display:none"></textarea>
<div style="padding-top: 5px">
${h.submit('private',_('Create private gist'),class_="ui-btn yellow")}
${h.submit('public',_('Create public gist'),class_="ui-btn")}
${h.reset('reset',_('Reset'),class_="ui-btn")}
${h.end_form()}
<script type="text/javascript">
initCodeMirror('editor','');
</script>
${_('gist')}:${c.gist.gist_access_id} · ${c.rhodecode_name}
${_('Gist')} · gist:${c.gist.gist_access_id}
<div class="code-header">
<div class="stats">
<div class="left" style="margin: -4px 0px 0px 0px">
%if c.gist.gist_type == 'public':
<div class="ui-btn green badge">${_('Public gist')}</div>
<div class="ui-btn yellow badge">${_('Private gist')}</div>
%if c.gist.gist_expires == -1:
${_('Expires')}: ${h.age(h.time_to_datetime(c.gist.gist_expires))}
<div class="left item last">${c.gist.gist_description}</div>
<div class="buttons">
## only owner should see that
%if c.gist.owner.username == c.rhodecode_user.username:
##${h.link_to(_('Edit'),h.url(''),class_="ui-btn")}
##${h.link_to(_('Delete'),h.url(''),class_="ui-btn red")}
<div class="author">
<img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.file_changeset.author),16)}"/>
<div title="${c.file_changeset.author}" class="user">${h.person(c.file_changeset.author)} - ${_('created')} ${h.age(c.file_changeset.date)}</div>
<div class="commit">${h.urlify_commit(c.file_changeset.message,c.repo_name)}</div>
## iterate over the files
% for file in c.files:
<div style="border: 1px solid #EEE;margin-top:20px">
<div id="${h.FID('G', file.path)}" class="stats" style="border-bottom: 1px solid #DDD;padding: 8px 14px;">
<b>${file.path}</b>
##<div class="buttons">
## ${h.link_to(_('Show as raw'),h.url(''),class_="ui-btn")}
##</div>
<div class="code-body">
${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
%endfor
@@ -193,149 +193,161 @@
<span>${_('Not logged in')}</span>
</a>
<div class="user-menu">
<div id="quick_login">
%if c.rhodecode_user.username == 'default':
<h4>${_('Login to your account')}</h4>
${h.form(h.url('login_home',came_from=h.url.current()))}
<div class="form">
<div class="fields">
<div class="field">
<div class="label">
<label for="username">${_('Username')}:</label>
<div class="input">
${h.text('username',class_='focus')}
<label for="password">${_('Password')}:</label>
${h.password('password',class_='focus')}
<div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
<div class="register">
%if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
${h.link_to(_("Don't have an account ?"),h.url('register'))}
<div class="submit">
${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
<div class="links_left">
<div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
<div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
<div class="email">${c.rhodecode_user.email}</div>
<div class="links_right">
<ol class="links">
<li><a href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a></li>
<li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
<li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
</ol>
<%def name="menu(current=None)">
<%
def is_current(selected):
if selected == current:
return h.literal('class="current"')
%>
<ul id="quick" class="horizontal-list">
<!-- repo switcher -->
<li ${is_current('repositories')}>
<a class="menu_link repo_switcher childs" id="repo_switcher" title="${_('Switch repository')}" href="${h.url('home')}">
${_('Repositories')}
<ul id="repo_switcher_list" class="repo_switcher">
<a href="#">${_('loading...')}</a>
##ROOT MENU
<li ${is_current('journal')}>
<a class="menu_link journal" title="${_('Show recent activity')}" href="${h.url('journal')}">
${_('Journal')}
<a class="menu_link journal" title="${_('Public journal')}" href="${h.url('public_journal')}">
${_('Public journal')}
<li ${is_current('gists')}>
<a class="menu_link gists childs" title="${_('Show public gists')}" href="${h.url('gists')}">
${_('Gists')}
<ul class="admin_menu">
<li>${h.link_to(_('Create new gist'),h.url('new_gist'),class_='gists-new ')}</li>
<li>${h.link_to(_('Public gists'),h.url('gists'),class_='gists ')}</li>
<li>${h.link_to(_('My private gists'),h.url('gists', private=1),class_='gists-private ')}</li>
<li ${is_current('search')}>
<a class="menu_link search" title="${_('Search in repositories')}" href="${h.url('search')}">
${_('Search')}
% if h.HasPermissionAll('hg.admin')('access admin main page'):
<li ${is_current('admin')}>
<a class="menu_link admin childs" title="${_('Admin')}" href="${h.url('admin_home')}">
${_('Admin')}
${admin_menu()}
% elif c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
<a class="menu_link admin childs" title="${_('Admin')}">
${admin_menu_simple(c.rhodecode_user.repository_groups_admin,
c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
% endif
${usermenu()}
YUE.on('repo_switcher','mouseover',function(){
var target = 'q_filter_rs';
var qfilter_activate = function(){
var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
var func = function(node){
return node.parentNode;
q_filter(target,nodes,func);
var loaded = YUD.hasClass('repo_switcher','loaded');
if(!loaded){
YUD.addClass('repo_switcher','loaded');
ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
function(o){qfilter_activate();YUD.get(target).focus()},
function(o){YUD.removeClass('repo_switcher','loaded');}
,null);
}else{
YUD.get(target).focus();
return false;
});
YUE.on('header-dd', 'click',function(e){
YUD.addClass('header-inner', 'hover');
YUD.addClass('content', 'hover');
import mock
from rhodecode.tests import *
from rhodecode.tests.fixture import Fixture
from rhodecode.model.db import Repository, User
from rhodecode.lib.utils2 import time_to_datetime
API_URL = '/_admin/api'
TEST_USER_GROUP = 'test_users_group'
fixture = Fixture()
def _build_data(apikey, method, **kw):
random_id = random.randrange(1, 9999)
return random_id, json.dumps({
})
jsonify = lambda obj: json.loads(json.dumps(obj))
def crash(*args, **kwargs):
raise Exception('Total Crash !')
def api_call(test_obj, params):
response = test_obj.app.post(API_URL, content_type='application/json',
params=params)
return response
## helpers
def make_users_group(name=TEST_USER_GROUP):
gr = fixture.create_user_group(name, cur_user=TEST_USER_ADMIN_LOGIN)
UserGroupModel().add_user_to_group(users_group=gr,
user=TEST_USER_ADMIN_LOGIN)
return gr
def destroy_users_group(name=TEST_USER_GROUP):
UserGroupModel().delete(users_group=name, force=True)
class BaseTestApi(object):
REPO = None
REPO_TYPE = None
def setUpClass(self):
self.usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
self.apikey = self.usr.api_key
self.test_user = UserModel().create_or_update(
username='test-api',
password='test',
email='test@api.rhodecode.org',
firstname='first',
lastname='last'
self.TEST_USER_LOGIN = self.test_user.username
self.apikey_regular = self.test_user.api_key
def teardownClass(self):
def setUp(self):
self.maxDiff = None
make_users_group()
def tearDown(self):
destroy_users_group()
def _compare_ok(self, id_, expected, given):
expected = jsonify({
'id': id_,
'error': None,
'result': expected
given = json.loads(given)
self.assertEqual(expected, given)
def _compare_error(self, id_, expected, given):
'error': expected,
'result': None
# def test_Optional(self):
# from rhodecode.controllers.api.api import Optional
# option1 = Optional(None)
# self.assertEqual('<Optional:%s>' % None, repr(option1))
# self.assertEqual(1, Optional.extract(Optional(1)))
# self.assertEqual('trololo', Optional.extract('trololo'))
def test_api_wrong_key(self):
id_, params = _build_data('trololo', 'get_user')
response = api_call(self, params)
from rhodecode.model.db import User, Gist
def _create_gist(f_name, content='some gist', lifetime=-1,
description='gist-desc', gist_type='public'):
gist_mapping = {
f_name: {'content': content}
user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
gist = GistModel().create(description, owner=user,
gist_mapping=gist_mapping, gist_type=gist_type,
lifetime=lifetime)
class TestGistsController(TestController):
for g in Gist.get_all():
GistModel().delete(g)
def test_index(self):
self.log_user()
response = self.app.get(url('gists'))
# Test response...
response.mustcontain('There are no gists yet')
_create_gist('gist1')
_create_gist('gist2', lifetime=1400)
_create_gist('gist3', description='gist3-desc')
_create_gist('gist4', gist_type='private')
response.mustcontain('gist:1')
response.mustcontain('gist:2')
response.mustcontain('Expires: in 23 hours') # we don't care about the end
response.mustcontain('gist:3')
response.mustcontain('gist3-desc')
response.mustcontain(no=['gist:4'])
def test_index_private_gists(self):
gist = _create_gist('gist5', gist_type='private')
response = self.app.get(url('gists', private=1))
#and privates
response.mustcontain('gist:%s' % gist.gist_access_id)
def test_create_missing_description(self):
response = self.app.post(url('gists'),
params={'lifetime': -1}, status=200)
response.mustcontain('Missing value')
def test_create(self):
params={'lifetime': -1,
'content': 'gist test',
'filename': 'foo',
'public': 'public'},
status=302)
response = response.follow()
response.mustcontain('added file: foo')
response.mustcontain('gist test')
response.mustcontain('<div class="ui-btn green badge">Public gist</div>')
def test_create_private(self):
'content': 'private gist test',
'filename': 'private-foo',
'private': 'private'},
response.mustcontain('added file: private-foo<')
response.mustcontain('private gist test')
response.mustcontain('<div class="ui-btn yellow badge">Private gist</div>')
def test_create_with_description(self):
'filename': 'foo-desc',
'description': 'gist-desc',
response.mustcontain('added file: foo-desc')
response.mustcontain('gist-desc')
def test_new(self):
response = self.app.get(url('new_gist'))
def test_update(self):
self.skipTest('not implemented')
response = self.app.put(url('gist', id=1))
def test_delete(self):
response = self.app.delete(url('gist', id=1))
def test_show(self):
gist = _create_gist('gist-show-me')
response = self.app.get(url('gist', id=gist.gist_access_id))
response.mustcontain('added file: gist-show-me<')
response.mustcontain('test_admin (RhodeCode Admin) - created just now')
def test_edit(self):
response = self.app.get(url('edit_gist', id=1))
def _commit_change(repo, filename, content, message, vcs_type, parent=None, newfile=False):
repo = Repository.get_by_repo_name(repo)
_cs = parent
if not parent:
_cs = EmptyChangeset(alias=vcs_type)
if newfile:
cs = ScmModel().create_node(
repo=repo.scm_instance, repo_name=repo.repo_name,
cs=_cs, user=TEST_USER_ADMIN_LOGIN,
cs = ScmModel().create_nodes(
user=TEST_USER_ADMIN_LOGIN, repo=repo,
parent_cs=_cs,
author=TEST_USER_ADMIN_LOGIN,
content=content,
f_path=filename
cs = ScmModel().commit_change(
cs=parent, user=TEST_USER_ADMIN_LOGIN,
class TestCompareController(TestController):
self.r1_id = None
self.r2_id = None
if self.r2_id:
RepoModel().delete(self.r2_id)
if self.r1_id:
RepoModel().delete(self.r1_id)
Session.remove()
def test_compare_forks_on_branch_extra_commits_hg(self):
repo1 = fixture.create_repo('one', repo_type='hg',
repo_description='diff-test',
cur_user=TEST_USER_ADMIN_LOGIN)
self.r1_id = repo1.repo_id
#commit something !
cs0 = _commit_change(repo1.repo_name, filename='file1', content='line1\n',
message='commit1', vcs_type='hg', parent=None, newfile=True)
#fork this repo
repo2 = fixture.create_fork('one', 'one-fork')
self.r2_id = repo2.repo_id
#add two extra commit into fork
cs1 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\n',
message='commit2', vcs_type='hg', parent=cs0)
cs2 = _commit_change(repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
message='commit3', vcs_type='hg', parent=cs1)
rev1 = 'default'
rev2 = 'default'
response = self.app.get(url(controller='compare', action='index',
repo_name=repo1.repo_name,
org_ref_type="branch",
org_ref=rev2,
other_repo=repo2.repo_name,
other_ref_type="branch",
other_ref=rev1,
merge='1',
response.mustcontain('%s@%s -> %s@%s' % (repo1.repo_name, rev2, repo2.repo_name, rev1))
response.mustcontain("""Showing 2 commits""")
response.mustcontain("""1 file changed with 2 insertions and 0 deletions""")
response.mustcontain("""<div class="message tooltip" title="commit2" style="white-space:normal">commit2</div>""")
response.mustcontain("""<div class="message tooltip" title="commit3" style="white-space:normal">commit3</div>""")
response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (repo2.repo_name, cs1.raw_id, cs1.short_id))
response.mustcontain("""<a href="/%s/changeset/%s">r2:%s</a>""" % (repo2.repo_name, cs2.raw_id, cs2.short_id))
## files
response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&merge=1#C--826e8142e6ba">file1</a>""" % (repo1.repo_name, rev2, rev1, repo2.repo_name))
#swap
response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s?other_repo=%s&merge=True">[swap]</a>""" % (repo2.repo_name, rev1, rev2, repo1.repo_name))
def test_compare_forks_on_branch_extra_commits_origin_has_incomming_hg(self):
#now commit something to origin repo
cs1_prim = _commit_change(repo1.repo_name, filename='file2', content='line1file2\n',
message='commit2', vcs_type='hg', parent=cs0, newfile=True)
@@ -224,188 +228,174 @@ class TestCompareController(TestControll
# cs2:
# cs3: x
# cs4: x
# cs5: x
#make repo1, and cs1+cs2
repo1 = fixture.create_repo('repo1', repo_type='hg',
message='commit1', vcs_type='hg', parent=None,
newfile=True)
cs1 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\n',
repo2 = fixture.create_fork('repo1', 'repo1-fork')
#now make cs3-6
cs2 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
cs3 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\n',
message='commit4', vcs_type='hg', parent=cs2)
cs4 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\n',
message='commit5', vcs_type='hg', parent=cs3)
cs5 = _commit_change(repo1.repo_name, filename='file1', content='line1\nline2\nline3\nline4\nline5\nline6\n',
message='commit6', vcs_type='hg', parent=cs4)
org_ref_type="rev",
org_ref=cs2.short_id, # parent of cs3, not in repo2
other_ref_type="rev",
other_ref=cs5.short_id,
response.mustcontain('%s@%s -> %s@%s' % (repo1.repo_name, cs2.short_id, repo1.repo_name, cs5.short_id))
response.mustcontain("""Showing 3 commits""")
response.mustcontain("""1 file changed with 3 insertions and 0 deletions""")
response.mustcontain("""<div class="message tooltip" title="commit4" style="white-space:normal">commit4</div>""")
response.mustcontain("""<div class="message tooltip" title="commit5" style="white-space:normal">commit5</div>""")
response.mustcontain("""<div class="message tooltip" title="commit6" style="white-space:normal">commit6</div>""")
response.mustcontain("""<a href="/%s/changeset/%s">r3:%s</a>""" % (repo1.repo_name, cs3.raw_id, cs3.short_id))
response.mustcontain("""<a href="/%s/changeset/%s">r4:%s</a>""" % (repo1.repo_name, cs4.raw_id, cs4.short_id))
response.mustcontain("""<a href="/%s/changeset/%s">r5:%s</a>""" % (repo1.repo_name, cs5.raw_id, cs5.short_id))
response.mustcontain("""#C--826e8142e6ba">file1</a>""")
def test_compare_cherry_pick_changeset_mixed_branches(self):
#TODO write this tastecase
def test_compare_remote_branches_hg(self):
repo2 = fixture.create_fork(HG_REPO, HG_FORK)
rev1 = '56349e29c2af'
rev2 = '7d4bc8ec6be5'
repo_name=HG_REPO,
org_ref=rev1,
other_ref=rev2,
other_repo=HG_FORK,
response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
## outgoing changesets between those revisions
response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_FORK))
response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_FORK))
response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_FORK, rev2))
response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&merge=1#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&merge=1#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s?other_repo=%s&merge=1#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2, HG_FORK))
def test_org_repo_new_commits_after_forking_simple_diff(self):
r1_name = repo1.repo_name
#commit something initially !
cs0 = ScmModel().create_node(
repo=repo1.scm_instance, repo_name=r1_name,
cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
message='commit1',
content='line1',
f_path='file1'
cs0 = _commit_change(repo=r1_name, filename='file1',
content='line1', message='commit1', vcs_type='hg',
self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
#fork the repo1
repo2 = fixture.create_repo('one-fork', repo_type='hg',
cur_user=TEST_USER_ADMIN_LOGIN,
clone_uri=repo1.repo_full_path,
fork_of='one')
self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
r2_name = repo2.repo_name
#make 3 new commits in fork
cs1 = ScmModel().create_node(
repo=repo2.scm_instance, repo_name=r2_name,
cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
message='commit1-fork',
content='file1-line1-from-fork',
f_path='file1-fork'
cs2 = ScmModel().create_node(
cs=cs1, user=TEST_USER_ADMIN_LOGIN,
message='commit2-fork',
content='file2-line1-from-fork',
f_path='file2-fork'
cs3 = ScmModel().create_node(
cs=cs2, user=TEST_USER_ADMIN_LOGIN,
message='commit3-fork',
content='file3-line1-from-fork',
f_path='file3-fork'
cs1 = _commit_change(repo=r2_name, filename='file1-fork',
content='file1-line1-from-fork', message='commit1-fork',
vcs_type='hg', parent=repo2.scm_instance[-1],
cs2 = _commit_change(repo=r2_name, filename='file2-fork',
content='file2-line1-from-fork', message='commit2-fork',
vcs_type='hg', parent=cs1,
cs3 = _commit_change(repo=r2_name, filename='file3-fork',
content='file3-line1-from-fork', message='commit3-fork',
vcs_type='hg', parent=cs2, newfile=True)
#compare !
repo_name=r2_name,
other_repo=r1_name,
response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
response.mustcontain('No files')
response.mustcontain('No changesets')
#add new commit into parent !
message='commit2-parent',
content='line1-added-after-fork',
f_path='file2'
# cs0 = ScmModel().create_node(
# repo=repo1.scm_instance, repo_name=r1_name,
# cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
# author=TEST_USER_ADMIN_LOGIN,
# message='commit2-parent',
# content='line1-added-after-fork',
# f_path='file2'
# )
cs0 = _commit_change(repo=r1_name, filename='file2',
content='line1-added-after-fork', message='commit2-parent',
vcs_type='hg', parent=None, newfile=True)
response.mustcontain("""commit2-parent""")
response.mustcontain("""1 file changed with 1 insertions and 0 deletions""")
response.mustcontain("""line1-added-after-fork""")
@@ -58,116 +58,117 @@ if sys.version_info < (2, 6):
requirements.append("pysqlite")
if sys.version_info < (2, 7):
requirements.append("unittest2")
requirements.append("argparse")
if is_windows:
requirements.append("mercurial==2.6.0")
requirements.append("py-bcrypt")
dependency_links = [
classifiers = [
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Pylons',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
# additional files from project that goes somewhere in the filesystem
# relative to sys.prefix
data_files = []
# additional files that goes into package itself
package_data = {'rhodecode': ['i18n/*/LC_MESSAGES/*.mo', ], }
description = ('RhodeCode is a fast and powerful management tool '
'for Mercurial and GIT with a built in push/pull server, '
'full text search and code-review.')
keywords = ' '.join(['rhodecode', 'rhodiumcode', 'mercurial', 'git',
'code review', 'repo groups', 'ldap'
'repository management', 'hgweb replacement'
'hgwebdir', 'gitweb replacement', 'serving hgweb', ])
# long description
readme_file = 'README.rst'
changelog_file = 'docs/changelog.rst'
long_description = open(readme_file).read() + '\n\n' + \
open(changelog_file).read()
except IOError, err:
sys.stderr.write("[WARNING] Cannot find file specified as "
"long_description (%s)\n or changelog (%s) skipping that file" \
% (readme_file, changelog_file))
long_description = description
from setuptools import setup, find_packages
from ez_setup import use_setuptools
use_setuptools()
# packages
packages = find_packages(exclude=['ez_setup'])
setup(
name='RhodeCode',
version=__version__,
long_description=long_description,
keywords=keywords,
license=__license__,
author=__author__,
author_email='marcin@python-works.com',
dependency_links=dependency_links,
url=__url__,
install_requires=requirements,
classifiers=classifiers,
setup_requires=["PasteScript>=1.6.3"],
data_files=data_files,
packages=packages,
include_package_data=True,
test_suite='nose.collector',
package_data=package_data,
message_extractors={'rhodecode': [
('**.py', 'python', None),
('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
('public/**', 'ignore', None)]},
zip_safe=False,
paster_plugins=['PasteScript', 'Pylons'],
entry_points="""
[console_scripts]
rhodecode-api = rhodecode.bin.rhodecode_api:main
rhodecode-gist = rhodecode.bin.rhodecode_gist:main
[paste.app_factory]
main = rhodecode.config.middleware:make_app
[paste.app_install]
main = pylons.util:PylonsInstaller
[paste.global_paster_command]
setup-rhodecode=rhodecode.lib.paster_commands.setup_rhodecode:Command
cleanup-repos=rhodecode.lib.paster_commands.cleanup:Command
update-repoinfo=rhodecode.lib.paster_commands.update_repoinfo:Command
make-rcext=rhodecode.lib.paster_commands.make_rcextensions:Command
repo-scan=rhodecode.lib.paster_commands.repo_scan:Command
cache-keys=rhodecode.lib.paster_commands.cache_keys:Command
ishell=rhodecode.lib.paster_commands.ishell:Command
make-index=rhodecode.lib.indexers:MakeIndex
upgrade-db=rhodecode.lib.dbmigrate:UpgradeDb
celeryd=rhodecode.lib.celerypylons.commands:CeleryDaemonCommand
""",
Status change: