# HG changeset patch # User Marcin Kuzminski # Date 2013-05-11 18:24:02 # Node ID dc4644865e8bc27ab1c22543a349491cedab7328 # Parent 9dec870411e08c6373383268884de0462691f0b8 Implemented simple gist functionality ref #530. - creation of public/private gists with given lifetime - rhodecode-gist CLI for quick gist creation diff --git a/rhodecode/bin/base.py b/rhodecode/bin/base.py --- a/rhodecode/bin/base.py +++ b/rhodecode/bin/base.py @@ -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): diff --git a/rhodecode/bin/rhodecode_api.py b/rhodecode/bin/rhodecode_api.py --- a/rhodecode/bin/rhodecode_api.py +++ b/rhodecode/bin/rhodecode_api.py @@ -66,7 +66,6 @@ def main(argv=None): Main execution function for cli :param argv: - :type argv: """ if argv is None: argv = sys.argv diff --git a/rhodecode/bin/rhodecode_gist.py b/rhodecode/bin/rhodecode_gist.py new file mode 100755 --- /dev/null +++ b/rhodecode/bin/rhodecode_gist.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.bin.gist + ~~~~~~~~~~~~~~~~~~ + + Gist CLI client for RhodeCode + + :created_on: May 9, 2013 + :author: marcink + :copyright: (C) 2010-2013 Marcin Kuzminski + :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 . + +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= --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) + if not conf: + if not api_credentials_given: + 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() + + else: + filename = DEFAULT_FILENAME + 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" + gist_content = sys.stdin.read() + else: + # "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): + """ + Main execution function for cli + + :param argv: + """ + if argv is None: + argv = sys.argv + + try: + return _run(argv) + except Exception, e: + print e + return 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -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 #========================================================================== diff --git a/rhodecode/controllers/admin/gists.py b/rhodecode/controllers/admin/gists.py new file mode 100644 --- /dev/null +++ b/rhodecode/controllers/admin/gists.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.gist + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + gist controller for RhodeCode + + :created_on: May 9, 2013 + :author: marcink + :copyright: (C) 2010-2013 Marcin Kuzminski + :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 . +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) + else: + 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') + + @LoginRequired() + @NotAnonymous() + def create(self): + """POST /admin/gists: Create a new item""" + # url('gists') + self.__load_defaults() + gist_form = GistForm([x[0] for x in c.lifetime_values])() + try: + 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" + ) + + except Exception, e: + 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)) + + @LoginRequired() + @NotAnonymous() + def new(self, format='html'): + """GET /admin/gists/new: Form to create a new item""" + # url('new_gist') + self.__load_defaults() + return render('admin/gists/new.html') + + @LoginRequired() + @NotAnonymous() + def update(self, id): + """PUT /admin/gists/id: Update an existing item""" + # Forms posted to this method should contain a hidden field: + # + # Or using helpers: + # h.form(url('gist', id=ID), + # method='put') + # url('gist', id=ID) + + @LoginRequired() + @NotAnonymous() + def delete(self, id): + """DELETE /admin/gists/id: Delete an existing item""" + # Forms posted to this method should contain a hidden field: + # + # Or using helpers: + # h.form(url('gist', id=ID), + # method='delete') + # url('gist', id=ID) + + @LoginRequired() + def show(self, id, format='html'): + """GET /admin/gists/id: Show a specific item""" + # url('gist', id=ID) + 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() + try: + c.file_changeset, c.files = GistModel().get_gist_files(gist_id) + except VCSError: + log.error(traceback.format_exc()) + raise HTTPNotFound() + + return render('admin/gists/show.html') + + @LoginRequired() + @NotAnonymous() + def edit(self, id, format='html'): + """GET /admin/gists/id/edit: Form to edit an existing item""" + # url('edit_gist', id=ID) diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -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 +from rhodecode.model.gist import GistModel log = logging.getLogger(__name__) @@ -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('')): + + try: + 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, + gist_type=gist_type, + lifetime=gist_lifetime) + Session().commit() + 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: + log.error(traceback.format_exc()) + raise JSONRPCError('failed to create gist') + + def update_gist(self, apiuser): + pass + + def delete_gist(self, apiuser): + pass diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -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 webob.exc import HTTPNotFound +from rhodecode.lib.exceptions import NonRelativePathError log = logging.getLogger(__name__) @@ -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') - return redirect(url('changeset_home', repo_name=c.repo_name, - revision='tip')) - 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 try: - 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) + nodes = { + 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: + h.flash(_('Location must be relative path and must not ' + 'contain .. in path'), category='warning') + return redirect(url('changeset_home', repo_name=c.repo_name, + revision='tip')) except (NodeError, NodeAlreadyExistsError), e: h.flash(_(e), category='error') except Exception: diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -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, diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -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: diff --git a/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py b/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py --- a/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py +++ b/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py @@ -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__ + tbl.create() + + #========================================================================== # 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) #========================================================================== - # UserGroup + # RepoGroup #========================================================================== from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup tbl = RepoGroup.__table__ diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -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': diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -66,6 +66,10 @@ class RepoGroupAssignmentError(Exception pass +class NonRelativePathError(Exception): + pass + + class HTTPLockedRC(HTTPClientError): """ Special Exception For locked Repos in RhodeCode, the return code can diff --git a/rhodecode/lib/hooks.py b/rhodecode/lib/hooks.py --- a/rhodecode/lib/hooks.py +++ b/rhodecode/lib/hooks.py @@ -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 diff --git a/rhodecode/lib/middleware/pygrack.py b/rhodecode/lib/middleware/pygrack.py --- a/rhodecode/lib/middleware/pygrack.py +++ b/rhodecode/lib/middleware/pygrack.py @@ -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('/') diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -27,6 +27,7 @@ import os import re import sys import time +import uuid import datetime import traceback 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 + else: + 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] diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -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, diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -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 + + @classmethod + 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' __table_args__ = ( diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -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 diff --git a/rhodecode/model/gist.py b/rhodecode/model/gist.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/gist.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.gist + ~~~~~~~~~~~~~~~~~~~~ + + gist model for RhodeCode + + :created_on: May 9, 2013 + :author: marcink + :copyright: (C) 2011-2013 Marcin Kuzminski + :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 . + +import os +import time +import logging +import traceback +import shutil + +from pylons.i18n.translation import _ +from rhodecode.lib.utils2 import safe_unicode, unique_id, safe_int, \ + time_to_datetime, safe_str, AttributeDict +from rhodecode.lib import helpers as h +from rhodecode.model import BaseModel +from rhodecode.model.db import Gist +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import ScmModel +from rhodecode.lib.vcs import get_repo + +log = logging.getLogger(__name__) + +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: + """ + root_path = RepoModel().repos_path + 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.gist_access_id = gist_id + self.sa.add(gist) + + 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, + message=message, + nodes=processed_mapping, + trigger_push_hook=False + ) + + return gist + + def delete(self, gist, fs_remove=True): + gist = self._get_gist(gist) + + try: + self.sa.delete(gist) + if fs_remove: + self.__delete_gist(gist) + else: + log.debug('skipping removal from filesystem') + + except Exception: + log.error(traceback.format_exc()) + raise diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -115,7 +115,6 @@ class RepoModel(BaseModel): Get's all repositories that user have at least read access :param user: - :type user: """ from rhodecode.lib.auth import AuthUser user = self._get_user(user) @@ -652,7 +651,13 @@ class RepoModel(BaseModel): log.error(traceback.format_exc()) raise - 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, + repo_store_location=None): """ 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: + :param repo_path: """ from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group from rhodecode.model.scm import ScmModel @@ -670,10 +676,12 @@ class RepoModel(BaseModel): new_parent_path = os.sep.join(parent.full_path_splitted) else: new_parent_path = '' - + if repo_store_location: + _paths = [repo_store_location] + else: + _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) else: raise Exception('Undefined alias %s' % alias) + return repo def __rename_repo(self, old, new): """ diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -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 +from rhodecode.lib.exceptions import NonRelativePathError log = logging.getLogger(__name__) @@ -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 + """ + user = self._get_user(user) - 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() - else: - 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'] + # 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() + else: + raise Exception('Content is of unrecognized type %s' % ( + type(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 else: - parents = [cs] - - m.add(FileNode(path, content=content)) - tip = m.commit(message=message, - author=author, - 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, - revisions=[tip.raw_id]) + tip = imc.commit(message=message, + author=author, + parents=parents, + branch=parent_cs.branch) + + self.mark_for_invalidation(repo.repo_name) + if trigger_push_hook: + self._handle_push(scm_instance, + username=user.username, + action='push_local', + repo_name=repo.repo_name, + revisions=[tip.raw_id]) return tip 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 = [] diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -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, \ - NotEmpty, IPAddress, CIDR + NotEmpty, IPAddress, CIDR, String, FancyValidator log = logging.getLogger(__name__) diff --git a/rhodecode/public/css/contextbar.css b/rhodecode/public/css/contextbar.css --- a/rhodecode/public/css/contextbar.css +++ b/rhodecode/public/css/contextbar.css @@ -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"); } diff --git a/rhodecode/public/css/pygments.css b/rhodecode/public/css/pygments.css --- a/rhodecode/public/css/pygments.css +++ b/rhodecode/public/css/pygments.css @@ -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 { - margin-left: 25px; + margin-left: 15px; font-weight: normal; white-space: pre; } +.code-highlighttable, div.codeblock .code-body table { width: 0 !important; border: 0px !important; } + +.code-highlighttable, div.codeblock .code-body table td { border: 0px !important; } + 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 */ diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -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 { - background-position: 0 -15px; + background-position: 0 -100px; 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 { + color: #fff; + background-color: #faa732; + background-repeat: repeat-x; + 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; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + .ui-btn.blue.hidden { display: none; } diff --git a/rhodecode/templates/admin/gists/index.html b/rhodecode/templates/admin/gists/index.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/gists/index.html @@ -0,0 +1,68 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('Gists')} · ${c.rhodecode_name} + + +<%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()"> +
+ +
+ ${self.breadcrumbs()} + %if c.rhodecode_user.username != 'default': + + %endif +
+ %if c.gists_pager.item_count>0: + % for gist in c.gists_pager: +
+ +
+ gravatar +
+
+ ${h.person(gist.owner.full_contact)} / + gist:${gist.gist_access_id} + + %if gist.gist_expires == -1: + ${_('Expires')}: ${_('never')} + %else: + ${_('Expires')}: ${h.age(h.time_to_datetime(gist.gist_expires))} + %endif + +
+
${_('Created')} ${h.age(gist.created_on)} +
+ +
${gist.gist_description}
+
+ % endfor + +
+
+ ${c.gists_pager.pager('$link_previous ~2~ $link_next')} +
+
+ %else: +
${_('There are no gists yet')}
+ %endif +
+ diff --git a/rhodecode/templates/admin/gists/new.html b/rhodecode/templates/admin/gists/new.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/gists/new.html @@ -0,0 +1,64 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('New gist')} · ${c.rhodecode_name} + + +<%def name="js_extra()"> + + +<%def name="css_extra()"> + + + +<%def name="breadcrumbs_links()"> + ${_('New gist')} + + +<%def name="page_nav()"> + ${self.menu('gists')} + + +<%def name="main()"> +
+ +
+ ${self.breadcrumbs()} +
+ +
+
+ ${h.form(h.url('gists'), method='post',id='eform')} +
+
+ gravatar +
+ +
+
+
+ ## + ${h.text('filename', size=30, placeholder='gistfile1.txt')} + ## + ${h.select('lifetime', '', c.lifetime_options)} +
+
+

+                    
+                
+
+
+ ${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()} + +
+
+ +
+ diff --git a/rhodecode/templates/admin/gists/show.html b/rhodecode/templates/admin/gists/show.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/gists/show.html @@ -0,0 +1,87 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('gist')}:${c.gist.gist_access_id} · ${c.rhodecode_name} + + +<%def name="breadcrumbs_links()"> + ${_('Gist')} · gist:${c.gist.gist_access_id} + + +<%def name="page_nav()"> + ${self.menu('gists')} + + +<%def name="main()"> +
+ +
+ ${self.breadcrumbs()} + %if c.rhodecode_user.username != 'default': + + %endif +
+
+
+
+
+
+
+ %if c.gist.gist_type == 'public': +
${_('Public gist')}
+ %else: +
${_('Private gist')}
+ %endif +
+ + %if c.gist.gist_expires == -1: + ${_('Expires')}: ${_('never')} + %else: + ${_('Expires')}: ${h.age(h.time_to_datetime(c.gist.gist_expires))} + %endif + +
${c.gist.gist_description}
+
+ ## 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")} + %endif +
+
+ +
+
+ gravatar +
+
${h.person(c.file_changeset.author)} - ${_('created')} ${h.age(c.file_changeset.date)}
+
+
${h.urlify_commit(c.file_changeset.message,c.repo_name)}
+
+
+ + ## iterate over the files + % for file in c.files: +
+
+ ${file.path} + ##
+ ## ${h.link_to(_('Show as raw'),h.url(''),class_="ui-btn")} + ##
+
+
+ ${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} +
+
+ %endfor +
+
+ + +
+ diff --git a/rhodecode/templates/base/base.html b/rhodecode/templates/base/base.html --- a/rhodecode/templates/base/base.html +++ b/rhodecode/templates/base/base.html @@ -286,6 +286,18 @@ %endif +
  • + + ${_('Gists')} + +
      +
    • ${h.link_to(_('Create new gist'),h.url('new_gist'),class_='gists-new ')}
    • +
    • ${h.link_to(_('Public gists'),h.url('gists'),class_='gists ')}
    • + %if c.rhodecode_user.username != 'default': +
    • ${h.link_to(_('My private gists'),h.url('gists', private=1),class_='gists-private ')}
    • + %endif +
    +
  • ${_('Search')} diff --git a/rhodecode/tests/api/api_base.py b/rhodecode/tests/api/api_base.py --- a/rhodecode/tests/api/api_base.py +++ b/rhodecode/tests/api/api_base.py @@ -26,7 +26,6 @@ def _build_data(apikey, method, **kw): Builds API data with given random ID :param random_id: - :type random_id: """ random_id = random.randrange(1, 9999) return random_id, json.dumps({ diff --git a/rhodecode/tests/functional/test_admin_gists.py b/rhodecode/tests/functional/test_admin_gists.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/functional/test_admin_gists.py @@ -0,0 +1,125 @@ +from rhodecode.tests import * +from rhodecode.model.gist import GistModel +from rhodecode.model.meta import Session +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) + Session().commit() + return gist + + +class TestGistsController(TestController): + + def tearDown(self): + for g in Gist.get_all(): + GistModel().delete(g) + Session().commit() + + 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 = self.app.get(url('gists')) + # Test response... + 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): + self.log_user() + gist = _create_gist('gist5', gist_type='private') + response = self.app.get(url('gists', private=1)) + # Test response... + + #and privates + response.mustcontain('gist:%s' % gist.gist_access_id) + + def test_create_missing_description(self): + self.log_user() + response = self.app.post(url('gists'), + params={'lifetime': -1}, status=200) + + response.mustcontain('Missing value') + + def test_create(self): + self.log_user() + response = self.app.post(url('gists'), + 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('
    Public gist
    ') + + def test_create_private(self): + self.log_user() + response = self.app.post(url('gists'), + params={'lifetime': -1, + 'content': 'private gist test', + 'filename': 'private-foo', + 'private': 'private'}, + status=302) + response = response.follow() + response.mustcontain('added file: private-foo<') + response.mustcontain('private gist test') + response.mustcontain('
    Private gist
    ') + + def test_create_with_description(self): + self.log_user() + response = self.app.post(url('gists'), + params={'lifetime': -1, + 'content': 'gist test', + 'filename': 'foo-desc', + 'description': 'gist-desc', + 'public': 'public'}, + status=302) + response = response.follow() + response.mustcontain('added file: foo-desc') + response.mustcontain('gist test') + response.mustcontain('gist-desc') + response.mustcontain('
    Public gist
    ') + + def test_new(self): + self.log_user() + 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): + self.skipTest('not implemented') + 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') + response.mustcontain('gist-desc') + response.mustcontain('
    Public gist
    ') + + def test_edit(self): + self.skipTest('not implemented') + response = self.app.get(url('edit_gist', id=1)) diff --git a/rhodecode/tests/functional/test_compare.py b/rhodecode/tests/functional/test_compare.py --- a/rhodecode/tests/functional/test_compare.py +++ b/rhodecode/tests/functional/test_compare.py @@ -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, + nodes = { + filename: { + 'content': content + } + } + cs = ScmModel().create_nodes( + user=TEST_USER_ADMIN_LOGIN, repo=repo, + message=message, + nodes=nodes, + parent_cs=_cs, author=TEST_USER_ADMIN_LOGIN, - message=message, - content=content, - f_path=filename ) else: 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, - author=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) Session().commit() 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, - author=TEST_USER_ADMIN_LOGIN, - message='commit1-fork', - content='file1-line1-from-fork', - f_path='file1-fork' - ) - cs2 = ScmModel().create_node( - repo=repo2.scm_instance, repo_name=r2_name, - cs=cs1, user=TEST_USER_ADMIN_LOGIN, - author=TEST_USER_ADMIN_LOGIN, - message='commit2-fork', - content='file2-line1-from-fork', - f_path='file2-fork' - ) - cs3 = ScmModel().create_node( - repo=repo2.scm_instance, repo_name=r2_name, - cs=cs2, user=TEST_USER_ADMIN_LOGIN, - author=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], + newfile=True) + cs2 = _commit_change(repo=r2_name, filename='file2-fork', + content='file2-line1-from-fork', message='commit2-fork', + vcs_type='hg', parent=cs1, + newfile=True) + + 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 ! - 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 = 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) + #compare ! rev1 = 'default' rev2 = 'default' diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -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