"""
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
@@ -21,87 +21,86 @@
# 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
# -*- coding: utf-8 -*-
rhodecode.bin.gist
~~~~~~~~~~~~~~~~~~
Gist CLI client for RhodeCode
:created_on: May 9, 2013
:author: marcink
:copyright: (C) 2010-2013 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
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
@@ -346,96 +346,99 @@ def make_map(config):
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
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/notifications') as m:
m.connect("notifications", "/notifications",
action="create", conditions=dict(method=["POST"]))
action="index", conditions=dict(method=["GET"]))
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",
action="new", conditions=dict(method=["GET"]))
m.connect("formatted_new_notification", "/notifications/new.{format}",
m.connect("/notification/{notification_id}",
action="update", conditions=dict(method=["PUT"]))
action="delete", conditions=dict(method=["DELETE"]))
m.connect("edit_notification", "/notification/{notification_id}/edit",
action="edit", conditions=dict(method=["GET"]))
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',
conditions=dict(method=["POST"]))
#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')
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
@@ -843,96 +844,97 @@ class ApiController(JSONRPCController):
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'
raise JSONRPCError('repository `%s` does not exist' % (repoid))
if isinstance(owner, Optional):
owner = apiuser.user_id
owner = get_user_or_error(owner)
# create structure of groups and return the last group
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)
return dict(
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
repo = get_repo_or_error(repoid)
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
@@ -1019,48 +1021,89 @@ class ApiController(JSONRPCController):
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
:param usersgroupid:
users_group = get_users_group_or_error(usersgroupid)
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,
owner=owner,
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):
@@ -12,96 +12,97 @@
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
@@ -326,115 +327,122 @@ class FilesController(BaseRepoController
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')
return redirect(h.url('files_home',
repo_name=repo_name, revision='tip'))
r_post = request.POST
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'))
c.f_path = f_path
if r_post:
unix_mode = 0
content = convert_line_endings(r_post.get('content', ''), unix_mode)
message = r_post.get('message') or c.default_message
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')
return redirect(url('changeset_home', repo_name=c.repo_name,
revision='tip'))
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)
author = self.rhodecode_user.full_contact
self.scm_model.create_node(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=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,
category='success')
except NonRelativePathError, e:
except (NodeError, NodeAlreadyExistsError), e:
h.flash(_(e), category='error')
return render('files/files_add.html')
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
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
@@ -120,97 +120,96 @@ class PullrequestsController(BaseRepoCon
for bookmark, bookmarkrev in repo.bookmarks.iteritems():
n = 'book:%s:%s' % (bookmark, bookmarkrev)
bookmarks.append((n, bookmark))
if rev == bookmarkrev:
selected = n
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
@@ -513,97 +513,96 @@ class DbManage(object):
('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)
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
@@ -191,192 +191,190 @@ class DiffProcessor(object):
#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'
@@ -21,64 +21,68 @@
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)
@@ -261,101 +261,98 @@ def log_delete_repository(repository_dic
if present
:param repository: dict dump of repository object
:param deleted_by: username who deleted the 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, 'DELETE_REPO_HOOK', None)
if isfunction(callback):
kw = {}
kw.update(repository_dict)
kw.update({'deleted_by': deleted_by,
'deleted_on': time.time()})
kw.update(kwargs)
return callback(**kw)
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,
@@ -14,97 +14,96 @@ log = logging.getLogger(__name__)
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
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
@@ -562,48 +563,84 @@ def fix_PATH(os_=None):
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]
@@ -59,82 +59,79 @@ def init_model(engine):
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()
@@ -1085,97 +1085,96 @@ class Repository(Base, BaseModel):
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):
import rhodecode
@@ -2077,57 +2076,95 @@ class Notification(Base, BaseModel):
.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'
__table_args__ = (
UniqueConstraint('user_id', 'notification_id'),
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'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)
created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
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:
raise HTTPNotFound
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'
'mysql_charset': 'utf8'},
repository_id = Column('repository_id', String(250), primary_key=True)
repository_path = Column('repository_path', Text)
version = Column('version', Integer)
@@ -374,48 +374,61 @@ def LdapSettingsForm(tls_reqcert_choices
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):
allow_extra_fields = True
filter_extra_fields = True
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
@@ -70,97 +70,96 @@ class RepoModel(BaseModel):
default = p.permission.permission_name
break
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)
@@ -607,140 +606,150 @@ class RepoModel(BaseModel):
.filter(UserGroupRepoToPerm.repository == repo)\
if obj is None:
# create new
obj = UserGroupRepoToPerm()
obj.repository = repo
obj.users_group = group_name
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s' % (perm, group_name, repo))
def revoke_users_group_permission(self, repo, group_name):
:param repo: Instance of Repository, repository_id, or repository name
:param group_name: Instance of UserGroup, users_group_id,
or user group name
repo = self._get_repo(repo)
group_name = self._get_user_group(group_name)
obj = self.sa.query(UserGroupRepoToPerm)\
.filter(UserGroupRepoToPerm.users_group == group_name)\
if obj:
self.sa.delete(obj)
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 = get_backend(alias)
if alias == 'hg':
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)
return repo
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))
@@ -9,96 +9,97 @@
:copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
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__())
def __iter__(self):
# 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:
@@ -486,176 +487,207 @@ class ScmModel(BaseModel):
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',
repo_name=repo_name,
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]])
if repo.alias == 'hg':
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(
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 []
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()
/**
* 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;
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;
font-weight: bold;
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 */
@@ -2261,96 +2261,101 @@ a.metatag[tag="license"]:hover {
.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;
@@ -3521,189 +3526,212 @@ div.gravatar img {
#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;
text-align: center;
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 {
display: none;
.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 {
## -*- 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
@@ -241,96 +241,108 @@
<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')}
</a>
<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');
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',
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):
def tearDown(self):
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,
return cs
class TestCompareController(TestController):
def setUp(self):
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)
@@ -272,140 +276,126 @@ class TestCompareController(TestControll
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))
## files
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'
response = self.app.get(url(controller='compare', action='index',
repo_name=HG_REPO,
org_ref_type="rev",
org_ref=rev1,
other_ref_type="rev",
other_ref=rev2,
other_repo=HG_FORK,
merge='1',
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',
newfile=True)
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 !
rev1 = 'default'
rev2 = 'default'
repo_name=r2_name,
org_ref_type="branch",
other_ref_type="branch",
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""")
@@ -106,68 +106,69 @@ try:
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__,
description=description,
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: