@@ -14,120 +14,120 @@ except ImportError:
import simplejson as json
except ImportError:
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 = {}
try:
with open(self._conf_name, 'rb') as conf:
config = json.load(conf)
except IOError, e:
@@ -45,49 +45,48 @@ def argparser(argv):
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']
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
# 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 os
import sys
import stat
import argparse
import fileinput
from rhodecode.bin.base import api_call, RcConf
def argparser(argv):
usage = (
"rhodecode-gist [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
"[--config=CONFIG] [--save-config] "
"[filename or stdin use - for terminal stdin ]\n"
"Create config file: rhodecode-gist --apikey=<key> --apihost=http://rhodecode.server --save-config"
parser = argparse.ArgumentParser(description='RhodeCode Gist cli',
usage=usage)
## config
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):
host = args.apihost or conf['apihost']
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"
pass
# 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 0
return _run(argv)
except Exception, e:
print e
return 1
if __name__ == '__main__':
sys.exit(main(sys.argv))
@@ -370,48 +370,51 @@ def make_map(config):
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}",
action="show", conditions=dict(method=["GET"]))
m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
#ADMIN MAIN PAGES
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/admin') as m:
m.connect('admin_home', '', action='index')
m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
action='add_repo')
#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,
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)
@@ -21,51 +21,52 @@
# 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::
@@ -867,48 +868,49 @@ class ApiController(JSONRPCController):
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
except Exception:
raise JSONRPCError(
'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:
raise JSONRPCError('repository `%s` does not exist' % (repoid))
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)
@@ -1043,24 +1045,65 @@ class ApiController(JSONRPCController):
: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
),
success=True
'failed to edit permission for user group: `%s` in '
'repo: `%s`' % (
def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
gist_type=Optional(Gist.GIST_PUBLIC),
gist_lifetime=Optional(-1),
gist_description=Optional('')):
if isinstance(owner, Optional):
owner = apiuser.user_id
owner = get_user_or_error(owner)
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):
@@ -36,48 +36,49 @@ 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:
@@ -350,67 +351,74 @@ class FilesController(BaseRepoController
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')
h.flash(_('Error occurred during commit'), category='error')
return redirect(url('changeset_home',
repo_name=c.repo_name, revision='tip'))
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]
@@ -144,49 +144,48 @@ class PullrequestsController(BaseRepoCon
# 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])
@@ -537,49 +537,48 @@ class DbManage(object):
self.sa.add(setting)
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')
@@ -18,45 +18,52 @@ log = logging.getLogger(__name__)
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
@@ -215,49 +215,48 @@ class DiffProcessor(object):
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)
@@ -310,49 +309,48 @@ class DiffProcessor(object):
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
@@ -45,40 +45,44 @@ 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 import CONFIG
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)
@@ -285,53 +285,50 @@ def log_delete_repository(repository_dic
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'
@@ -38,49 +38,48 @@ class FileWrapper(object):
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')
@@ -6,48 +6,49 @@
Some simple helper functions
:created_on: Jan 5, 2011
:copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
import re
import uuid
import datetime
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:
@@ -586,24 +587,60 @@ def _extract_extras(env=None):
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]
@@ -83,58 +83,55 @@ class BaseModel(object):
: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()
@@ -1109,49 +1109,48 @@ class Repository(Base, BaseModel):
@property
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,
@@ -2101,33 +2100,71 @@ class Notification(Base, BaseModel):
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
Session().add(self)
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')
@classmethod
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)
@@ -398,24 +398,37 @@ def UserExtraIpForm():
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>
import shutil
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
@@ -94,49 +94,48 @@ class RepoModel(BaseModel):
.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):
repo = self.sa.query(Repository)\
.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.write',
'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)\
@@ -631,93 +630,103 @@ class RepoModel(BaseModel):
obj = self.sa.query(UserGroupRepoToPerm)\
.filter(UserGroupRepoToPerm.repository == repo)\
.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
@@ -33,48 +33,49 @@ import pkg_resources
from os.path import dirname as dn, join as jn
from sqlalchemy import func
import rhodecode
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
@@ -510,128 +511,159 @@ class ScmModel(BaseModel):
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.mark_for_invalidation(repo_name)
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
repo = repo.scm_instance
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
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]
/**
* 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,
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 {
float: 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;
display: block;
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 */
@@ -2285,48 +2285,53 @@ a.metatag[tag="license"]:hover {
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;
margin: 0 !important;
#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;
@@ -3545,141 +3550,164 @@ div.gravatar img {
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;
display: inline-block;
top: -2px;
.ui-btn:focus {
.ui-btn:hover {
text-decoration: none;
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 {
vertical-align: bottom;
## -*- 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
@@ -265,48 +265,60 @@
<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(){
@@ -5,49 +5,48 @@ 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):
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)
@@ -296,116 +300,102 @@ class TestCompareController(TestControll
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))
## files
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):
repo1 = fixture.create_repo('one', repo_type='hg',
repo_description='diff-test',
cur_user=TEST_USER_ADMIN_LOGIN)
self.r1_id = repo1.repo_id
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])
self.r2_id = repo2.repo_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'
response = self.app.get(url(controller='compare', action='index',
repo_name=r2_name,
org_ref_type="branch",
org_ref=rev1,
other_ref_type="branch",
other_ref=rev2,
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""")
@@ -130,44 +130,45 @@ setup(
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: