@@ -32,13 +32,12 @@ def api_call(apikey, apihost, format, me
"""
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
@@ -77,13 +76,15 @@ class RcConf(object):
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()
@@ -103,13 +104,12 @@ class RcConf(object):
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)
@@ -63,13 +63,12 @@ def argparser(argv):
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)
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):
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
try:
return _run(argv)
except Exception, e:
print e
return 1
if __name__ == '__main__':
sys.exit(main(sys.argv))
@@ -388,12 +388,15 @@ def make_map(config):
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')
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)
@@ -39,15 +39,16 @@ from rhodecode.lib import helpers as h
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):
@@ -885,12 +886,13 @@ class ApiController(JSONRPCController):
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:
@@ -1061,6 +1063,47 @@ class ApiController(JSONRPCController):
'failed to edit permission for user group: `%s` in '
'repo: `%s`' % (
users_group.users_group_name, repo.repo_name
def create_gist(self, apiuser, files, owner=Optional(OAttr('apiuser')),
gist_type=Optional(Gist.GIST_PUBLIC),
gist_lifetime=Optional(-1),
gist_description=Optional('')):
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):
@@ -54,12 +54,13 @@ from rhodecode.model.repo import RepoMod
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):
@@ -368,31 +369,38 @@ class FilesController(BaseRepoController
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',
@@ -162,13 +162,12 @@ class PullrequestsController(BaseRepoCon
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(':')
@@ -555,13 +555,12 @@ class DbManage(object):
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()\
@@ -36,22 +36,29 @@ def upgrade(migrate_engine):
from rhodecode.lib.dbmigrate.schema.db_1_7_0 import UserGroupUserGroupToPerm
tbl = UserGroupUserGroupToPerm.__table__
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
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__
@@ -233,13 +233,12 @@ class DiffProcessor(object):
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
@@ -328,13 +327,12 @@ class DiffProcessor(object):
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))
@@ -63,12 +63,16 @@ 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
@@ -303,17 +303,14 @@ def handle_git_receive(repo_path, revs,
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
@@ -56,13 +56,12 @@ class GitRepository(object):
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
@@ -24,12 +24,13 @@
import re
import uuid
import datetime
import webob
from pylons.i18n.translation import _, ungettext
from rhodecode.lib.vcs.utils.lazy import LazyProperty
@@ -604,6 +605,42 @@ def _extract_extras(env=None):
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]
@@ -101,36 +101,33 @@ class BaseModel(object):
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):
@@ -1127,13 +1127,12 @@ class Repository(Base, BaseModel):
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)
if revisions:
@@ -2119,12 +2118,50 @@ class UserNotification(Base, BaseModel):
def mark_as_read(self):
self.read = True
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'
'mysql_charset': 'utf8'},
@@ -416,6 +416,19 @@ def PullRequestForm(repo_id):
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
@@ -112,13 +112,12 @@ class RepoModel(BaseModel):
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',
@@ -649,34 +648,43 @@ class RepoModel(BaseModel):
if obj:
self.sa.delete(obj)
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 repo_name:
: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
@@ -687,19 +695,20 @@ class RepoModel(BaseModel):
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
@@ -51,12 +51,13 @@ from rhodecode.lib.auth import HasRepoPe
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):
@@ -528,50 +529,82 @@ class ScmModel(BaseModel):
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
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,
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):
recursive walk in root dir and return a set of all path in that dir
based on repository walk function
@@ -607,13 +640,12 @@ class ScmModel(BaseModel):
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')])
@@ -8,13 +8,13 @@ import logging
from collections import defaultdict
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,\
@@ -22,13 +22,13 @@ from rhodecode.model.db import RepoGroup
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):
/**
* 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"); }
@@ -22,12 +25,15 @@
#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"); }
@@ -11,18 +11,18 @@ div.codeblock {
-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 {
@@ -44,32 +44,36 @@ div.codeblock .code-header .stats .left.
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;
@@ -94,25 +98,25 @@ div.annotatediv {
margin-right: 4px;
.code-highlight {
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 */
.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 */
@@ -2303,12 +2303,17 @@ h3.files_location {
#files_data dl dd {
margin: 0 !important;
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 {
float: left;
@@ -3563,14 +3568,18 @@ div.gravatar img {
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;
@@ -3595,18 +3604,20 @@ div.gravatar img {
top: -2px;
.ui-btn:focus {
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;
@@ -3642,12 +3653,13 @@ div.gravatar img {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
border-color: #339bb9 #339bb9 #22697d;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
.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));
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));
@@ -3656,12 +3668,28 @@ div.gravatar img {
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 {
## -*- 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
@@ -283,12 +283,24 @@
<li ${is_current('journal')}>
<a class="menu_link journal" title="${_('Public journal')}" href="${h.url('public_journal')}">
${_('Public journal')}
</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')}
% if h.HasPermissionAll('hg.admin')('access admin main page'):
@@ -23,13 +23,12 @@ fixture = Fixture()
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))
@@ -13,19 +13,23 @@ def _commit_change(repo, filename, conte
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,
@@ -314,21 +318,15 @@ class TestCompareController(TestControll
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,
@@ -336,38 +334,26 @@ class TestCompareController(TestControll
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,
@@ -380,20 +366,24 @@ class TestCompareController(TestControll
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)
org_ref_type="branch",
@@ -148,12 +148,13 @@ setup(
('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
Status change: