@@ -35,7 +35,6 @@ def api_call(apikey, apihost, format, me
Builds API data with given random ID
:param random_id:
:type random_id:
"""
return {
"id": random_id,
@@ -80,7 +79,9 @@ class RcConf(object):
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)
@@ -106,7 +107,6 @@ class RcConf(object):
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):
@@ -66,7 +66,6 @@ def main(argv=None):
Main execution function for cli
:param argv:
:type argv:
if argv is None:
argv = 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
# 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 = 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('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')
args, other = parser.parse_known_args()
return parser, args, other
def _run(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']
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
else:
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
def main(argv=None):
try:
return _run(argv)
except Exception, e:
print e
return 1
if __name__ == '__main__':
sys.exit(main(sys.argv))
@@ -391,6 +391,9 @@ def make_map(config):
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
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)
@@ -42,9 +42,10 @@ from rhodecode.model.repo import RepoMod
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
@@ -888,6 +889,7 @@ class ApiController(JSONRPCController):
fork_name)
# perms handled inside
def delete_repo(self, apiuser, repoid, forks=Optional(None)):
Deletes a given repository
@@ -1064,3 +1066,44 @@ class ApiController(JSONRPCController):
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('')):
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)
return dict(
msg='created new gist',
gist_url=gist.gist_url(),
gist_id=gist.gist_access_id,
gist_type=gist.gist_type,
files=files.keys()
except Exception:
raise JSONRPCError('failed to create gist')
def update_gist(self, apiuser):
def delete_gist(self, apiuser):
@@ -57,6 +57,7 @@ from rhodecode.model.db import Repositor
from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
_context_url, get_line_ctx, get_ignore_ws
from rhodecode.lib.exceptions import NonRelativePathError
@@ -371,25 +372,32 @@ class FilesController(BaseRepoController
h.flash(_('No filename'), category='warning')
return redirect(url('changeset_home', repo_name=c.repo_name,
revision='tip'))
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')
@@ -165,7 +165,6 @@ class PullrequestsController(BaseRepoCon
Load context data needed for generating compare diff
:param pull_request:
:type pull_request:
org_repo = pull_request.org_repo
(org_ref_type,
@@ -558,7 +558,6 @@ class DbManage(object):
bad permissions, we must clean them up
:param username:
:type username:
default_user = User.get_by_username(username)
if not default_user:
@@ -39,6 +39,13 @@ def upgrade(migrate_engine):
tbl.create()
# 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
@@ -48,7 +55,7 @@ def upgrade(migrate_engine):
user_id.create(table=tbl)
# RepoGroup
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
tbl = RepoGroup.__table__
@@ -236,7 +236,6 @@ class DiffProcessor(object):
Escaper for diff escapes special chars and checks the diff limit
:param string:
:type string:
self.cur_diff_size += len(string)
@@ -331,7 +330,6 @@ class DiffProcessor(object):
a_blob_id, b_blob_id, b_mode, a_file, b_file
:param diff_chunk:
:type diff_chunk:
if self.vcs == 'git':
@@ -66,6 +66,10 @@ class RepoGroupAssignmentError(Exception
class NonRelativePathError(Exception):
class HTTPLockedRC(HTTPClientError):
Special Exception For locked Repos in RhodeCode, the return code can
@@ -306,11 +306,8 @@ def handle_git_receive(repo_path, revs,
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
@@ -59,7 +59,6 @@ class GitRepository(object):
Small fix for repo_path
:param path:
:type path:
return path.split(self.repo_name, 1)[-1].strip('/')
@@ -27,6 +27,7 @@ import os
import re
import uuid
import datetime
import webob
@@ -607,3 +608,39 @@ def _extract_extras(env=None):
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]
@@ -104,8 +104,7 @@ class BaseModel(object):
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,
@@ -115,8 +114,7 @@ class BaseModel(object):
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
from rhodecode.model.db import Repository
return self._get_instance(Repository, repository,
@@ -126,8 +124,7 @@ class BaseModel(object):
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,
@@ -1130,7 +1130,6 @@ class Repository(Base, BaseModel):
Returns statuses for this repository
:param revisions: list of revisions to get statuses for
:type revisions: list
statuses = ChangesetStatus.query()\
@@ -2122,6 +2121,44 @@ class UserNotification(Base, BaseModel):
Session().add(self)
class Gist(Base, BaseModel):
__tablename__ = 'gists'
__table_args__ = (
Index('g_gist_access_id_idx', 'gist_access_id'),
Index('g_created_on_idx', 'created_on'),
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8'}
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'
@@ -419,3 +419,16 @@ def PullRequestForm(repo_id):
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.model.repo import RepoModel
from rhodecode.model.scm import ScmModel
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
@@ -115,7 +115,6 @@ class RepoModel(BaseModel):
Get's all repositories that user have at least read access
:type user:
from rhodecode.lib.auth import AuthUser
user = self._get_user(user)
@@ -652,7 +651,13 @@ class RepoModel(BaseModel):
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
@@ -662,6 +667,7 @@ class RepoModel(BaseModel):
:param alias:
:param parent_id:
:param clone_uri:
from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
@@ -670,10 +676,12 @@ class RepoModel(BaseModel):
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):
@@ -690,13 +698,14 @@ class RepoModel(BaseModel):
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):
@@ -54,6 +54,7 @@ from rhodecode.model import BaseModel
from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
UserFollowing, UserLog, User, RepoGroup, PullRequest
from rhodecode.lib.hooks import log_push_action
@@ -531,44 +532,76 @@ class ScmModel(BaseModel):
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
IMC = self._get_IMC_module(repo.alias)
scm_instance = repo.scm_instance_no_cache()
# decoding here will force that we have proper encoded values
# in any other case this will throw exceptions and deny commit
if isinstance(content, (basestring,)):
content = safe_str(content)
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))
message = safe_unicode(message)
author = safe_unicode(author)
path = safe_str(f_path)
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))
self.mark_for_invalidation(repo_name)
self._handle_push(repo,
username=user.username,
action='push_local',
repo_name=repo_name,
tip = imc.commit(message=message,
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):
@@ -610,7 +643,6 @@ class ScmModel(BaseModel):
grouped by type
:param repo:
:type repo:
hist_l = []
@@ -11,7 +11,7 @@ from webhelpers.pylonslib.secure_form im
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
@@ -25,7 +25,7 @@ from rhodecode.lib.auth import HasReposG
# silence warnings and pylint
UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
@@ -4,6 +4,9 @@
#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"); }
@@ -25,6 +28,9 @@
#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"); }
@@ -14,12 +14,12 @@ div.codeblock {
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;
@@ -47,7 +47,7 @@ div.codeblock .code-header .stats .butto
div.codeblock .code-header .author {
margin-left: 25px;
margin-left: 15px;
font-weight: bold;
height: 25px;
@@ -55,18 +55,22 @@ 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;
@@ -97,19 +101,19 @@ div.annotatediv {
padding: 0px;
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 */
@@ -2306,6 +2306,11 @@ h3.files_location {
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;
@@ -3566,8 +3571,12 @@ div.gravatar img {
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 {
@@ -3598,12 +3607,14 @@ div.gravatar img {
outline: none;
.ui-btn:hover {
text-decoration: none;
color: #515151;
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;
color: #999;
@@ -3645,6 +3656,7 @@ div.gravatar img {
.ui-btn.green {
color: #fff;
background-color: #57a957;
background-repeat: repeat-x;
background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
@@ -3659,6 +3671,22 @@ div.gravatar img {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
.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;
## -*- 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
@@ -286,6 +286,18 @@
</a>
<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')}
@@ -26,7 +26,6 @@ def _build_data(apikey, method, **kw):
random_id = random.randrange(1, 9999)
return random_id, json.dumps({
from rhodecode.tests import *
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))
@@ -16,13 +16,17 @@ def _commit_change(repo, filename, conte
_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(
@@ -317,15 +321,9 @@ class TestCompareController(TestControll
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
@@ -339,32 +337,20 @@ class TestCompareController(TestControll
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'
@@ -383,14 +369,18 @@ class TestCompareController(TestControll
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)
@@ -151,6 +151,7 @@ setup(
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
Status change: