changeset 3840:dc4644865e8b beta

Implemented simple gist functionality ref #530. - creation of public/private gists with given lifetime - rhodecode-gist CLI for quick gist creation
author Marcin Kuzminski <marcin@python-works.com>
date Sat, 11 May 2013 20:24:02 +0200
parents 9dec870411e0
children 979edf6a2990
files rhodecode/bin/base.py rhodecode/bin/rhodecode_api.py rhodecode/bin/rhodecode_gist.py rhodecode/config/routing.py rhodecode/controllers/admin/gists.py rhodecode/controllers/api/api.py rhodecode/controllers/files.py rhodecode/controllers/pullrequests.py rhodecode/lib/db_manage.py rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py rhodecode/lib/diffs.py rhodecode/lib/exceptions.py rhodecode/lib/hooks.py rhodecode/lib/middleware/pygrack.py rhodecode/lib/utils2.py rhodecode/model/__init__.py rhodecode/model/db.py rhodecode/model/forms.py rhodecode/model/gist.py rhodecode/model/repo.py rhodecode/model/scm.py rhodecode/model/validators.py rhodecode/public/css/contextbar.css rhodecode/public/css/pygments.css rhodecode/public/css/style.css rhodecode/templates/admin/gists/index.html rhodecode/templates/admin/gists/new.html rhodecode/templates/admin/gists/show.html rhodecode/templates/base/base.html rhodecode/tests/api/api_base.py rhodecode/tests/functional/test_admin_gists.py rhodecode/tests/functional/test_compare.py setup.py
diffstat 33 files changed, 1199 insertions(+), 134 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/bin/base.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/bin/base.py	Sat May 11 20:24:02 2013 +0200
@@ -35,7 +35,6 @@
         Builds API data with given random ID
 
         :param random_id:
-        :type random_id:
         """
         return {
             "id": random_id,
@@ -80,7 +79,9 @@
 
     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 @@
         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):
--- a/rhodecode/bin/rhodecode_api.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/bin/rhodecode_api.py	Sat May 11 20:24:02 2013 +0200
@@ -66,7 +66,6 @@
     Main execution function for cli
 
     :param argv:
-    :type argv:
     """
     if argv is None:
         argv = sys.argv
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/bin/rhodecode_gist.py	Sat May 11 20:24:02 2013 +0200
@@ -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 <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import with_statement
+import os
+import sys
+import stat
+import argparse
+import fileinput
+
+from rhodecode.bin.base import api_call, RcConf
+
+
+def argparser(argv):
+    usage = (
+      "rhodecode-gist [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
+      "[--config=CONFIG] [--save-config] "
+      "[filename or stdin use - for terminal stdin ]\n"
+      "Create config file: rhodecode-gist --apikey=<key> --apihost=http://rhodecode.server --save-config"
+    )
+
+    parser = argparse.ArgumentParser(description='RhodeCode Gist cli',
+                                     usage=usage)
+
+    ## config
+    group = parser.add_argument_group('config')
+    group.add_argument('--apikey', help='api access key')
+    group.add_argument('--apihost', help='api host')
+    group.add_argument('--config', help='config file')
+    group.add_argument('--save-config', action='store_true',
+                       help='save the given config into a file')
+
+    group = parser.add_argument_group('GIST')
+    group.add_argument('-f', '--filename', help='set uploaded gist filename')
+    group.add_argument('-p', '--private', action='store_true',
+                       help='Create private Gist')
+    group.add_argument('-d', '--description', help='Gist description')
+    group.add_argument('-l', '--lifetime', metavar='MINUTES',
+                       help='Gist lifetime in minutes, -1 (Default) is forever')
+
+    args, other = parser.parse_known_args()
+    return parser, args, other
+
+
+def _run(argv):
+    conf = None
+    parser, args, other = argparser(argv)
+
+    api_credentials_given = (args.apikey and args.apihost)
+    if args.save_config:
+        if not api_credentials_given:
+            raise parser.error('--save-config requires --apikey and --apihost')
+        conf = RcConf(config_location=args.config,
+                      autocreate=True, config={'apikey': args.apikey,
+                                               'apihost': args.apihost})
+        sys.exit()
+
+    if not conf:
+        conf = RcConf(config_location=args.config, autoload=True)
+        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))
--- a/rhodecode/config/routing.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/config/routing.py	Sat May 11 20:24:02 2013 +0200
@@ -391,6 +391,9 @@
         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
     #==========================================================================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/admin/gists.py	Sat May 11 20:24:02 2013 +0200
@@ -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 <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/>.
+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:
+        #    <input type="hidden" name="_method" value="PUT" />
+        # 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:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # 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)
--- a/rhodecode/controllers/api/api.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/controllers/api/api.py	Sat May 11 20:24:02 2013 +0200
@@ -42,9 +42,10 @@
 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 @@
                                                             fork_name)
             )
 
+    # perms handled inside
     def delete_repo(self, apiuser, repoid, forks=Optional(None)):
         """
         Deletes a given repository
@@ -1064,3 +1066,44 @@
                     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
--- a/rhodecode/controllers/files.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/controllers/files.py	Sat May 11 20:24:02 2013 +0200
@@ -57,6 +57,7 @@
 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 @@
                 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:
--- a/rhodecode/controllers/pullrequests.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/controllers/pullrequests.py	Sat May 11 20:24:02 2013 +0200
@@ -165,7 +165,6 @@
         Load context data needed for generating compare diff
 
         :param pull_request:
-        :type pull_request:
         """
         org_repo = pull_request.org_repo
         (org_ref_type,
--- a/rhodecode/lib/db_manage.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/db_manage.py	Sat May 11 20:24:02 2013 +0200
@@ -558,7 +558,6 @@
         bad permissions, we must clean them up
 
         :param username:
-        :type username:
         """
         default_user = User.get_by_username(username)
         if not default_user:
--- a/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/dbmigrate/versions/012_version_1_7_0.py	Sat May 11 20:24:02 2013 +0200
@@ -39,6 +39,13 @@
     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 @@
     user_id.create(table=tbl)
 
     #==========================================================================
-    # UserGroup
+    # RepoGroup
     #==========================================================================
     from rhodecode.lib.dbmigrate.schema.db_1_7_0 import RepoGroup
     tbl = RepoGroup.__table__
--- a/rhodecode/lib/diffs.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/diffs.py	Sat May 11 20:24:02 2013 +0200
@@ -236,7 +236,6 @@
         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 @@
             a_blob_id, b_blob_id, b_mode, a_file, b_file
 
         :param diff_chunk:
-        :type diff_chunk:
         """
 
         if self.vcs == 'git':
--- a/rhodecode/lib/exceptions.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/exceptions.py	Sat May 11 20:24:02 2013 +0200
@@ -66,6 +66,10 @@
     pass
 
 
+class NonRelativePathError(Exception):
+    pass
+
+
 class HTTPLockedRC(HTTPClientError):
     """
     Special Exception For locked Repos in RhodeCode, the return code can
--- a/rhodecode/lib/hooks.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/hooks.py	Sat May 11 20:24:02 2013 +0200
@@ -306,11 +306,8 @@
     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
--- a/rhodecode/lib/middleware/pygrack.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/middleware/pygrack.py	Sat May 11 20:24:02 2013 +0200
@@ -59,7 +59,6 @@
         Small fix for repo_path
 
         :param path:
-        :type path:
         """
         return path.split(self.repo_name, 1)[-1].strip('/')
 
--- a/rhodecode/lib/utils2.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/lib/utils2.py	Sat May 11 20:24:02 2013 +0200
@@ -27,6 +27,7 @@
 import re
 import sys
 import time
+import uuid
 import datetime
 import traceback
 import webob
@@ -607,3 +608,39 @@
 
 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]
--- a/rhodecode/model/__init__.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/__init__.py	Sat May 11 20:24:02 2013 +0200
@@ -104,8 +104,7 @@
         """
         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 @@
         """
         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 @@
         """
         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,
--- a/rhodecode/model/db.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/db.py	Sat May 11 20:24:02 2013 +0200
@@ -1130,7 +1130,6 @@
         Returns statuses for this repository
 
         :param revisions: list of revisions to get statuses for
-        :type revisions: list
         """
 
         statuses = ChangesetStatus.query()\
@@ -2122,6 +2121,44 @@
         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__ = (
--- a/rhodecode/model/forms.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/forms.py	Sat May 11 20:24:02 2013 +0200
@@ -419,3 +419,16 @@
         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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/model/gist.py	Sat May 11 20:24:02 2013 +0200
@@ -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 <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/>.
+
+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
--- a/rhodecode/model/repo.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/repo.py	Sat May 11 20:24:02 2013 +0200
@@ -115,7 +115,6 @@
         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 @@
             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 @@
         :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 @@
             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 @@
         )
         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):
         """
--- a/rhodecode/model/scm.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/scm.py	Sat May 11 20:24:02 2013 +0200
@@ -54,6 +54,7 @@
 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 @@
                           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 @@
         grouped by type
 
         :param repo:
-        :type repo:
         """
 
         hist_l = []
--- a/rhodecode/model/validators.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/model/validators.py	Sat May 11 20:24:02 2013 +0200
@@ -11,7 +11,7 @@
 
 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 @@
 
 # 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__)
 
--- a/rhodecode/public/css/contextbar.css	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/public/css/contextbar.css	Sat May 11 20:24:02 2013 +0200
@@ -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"); }
--- a/rhodecode/public/css/pygments.css	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/public/css/pygments.css	Sat May 11 20:24:02 2013 +0200
@@ -14,12 +14,12 @@
 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 .author {
-    margin-left: 25px;
+    margin-left: 15px;
     font-weight: bold;
     height: 25px;
 }
@@ -55,18 +55,22 @@
     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 @@
     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 */
--- a/rhodecode/public/css/style.css	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/public/css/style.css	Sat May 11 20:24:02 2013 +0200
@@ -2306,6 +2306,11 @@
     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 @@
     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 @@
     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 @@
 }
 
 .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 @@
     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;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/gists/index.html	Sat May 11 20:24:02 2013 +0200
@@ -0,0 +1,68 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('Gists')} &middot; ${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>
+
+<%def name="page_nav()">
+    ${self.menu('gists')}
+</%def>
+
+<%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>
+        %endif
+    </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>
+            <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')}
+                  %else:
+                   ${_('Expires')}: ${h.age(h.time_to_datetime(gist.gist_expires))}
+                  %endif
+                </span>
+            </div>
+            <div>${_('Created')} ${h.age(gist.created_on)}
+            </div>
+
+            <div style="border:0px;padding:10px 0px 0px 35px;color:#AAA">${gist.gist_description}</div>
+          </div>
+        % endfor
+
+        <div class="notification-paginator">
+          <div class="pagination-wh pagination-left">
+          ${c.gists_pager.pager('$link_previous ~2~ $link_next')}
+          </div>
+        </div>
+    %else:
+        <div class="table">${_('There are no gists yet')}</div>
+    %endif
+</div>
+</%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/gists/new.html	Sat May 11 20:24:02 2013 +0200
@@ -0,0 +1,64 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('New gist')} &middot; ${c.rhodecode_name}
+</%def>
+
+<%def name="js_extra()">
+<script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
+</%def>
+<%def name="css_extra()">
+<link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${_('New gist')}
+</%def>
+
+<%def name="page_nav()">
+    ${self.menu('gists')}
+</%def>
+
+<%def name="main()">
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}
+    </div>
+
+    <div class="table">
+        <div id="files_data">
+          ${h.form(h.url('gists'), method='post',id='eform')}
+            <div>
+                <div class="gravatar">
+                   <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.rhodecode_user.full_contact),32)}"/>
+                </div>
+                <textarea style="resize:vertical; width:400px;border: 1px solid #ccc;border-radius: 3px;" id="description" name="description" placeholder="${_('Gist description ...')}"></textarea>
+            </div>
+            <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')}
+                    ##<input type="text" value="" size="30" name="filename" id="filename" placeholder="gistfile1.txt">
+                    ${h.select('lifetime', '', c.lifetime_options)}
+                </div>
+                <div id="editor_container">
+                    <pre id="editor_pre"></pre>
+                    <textarea id="editor" name="content" style="display:none"></textarea>
+                </div>
+            </div>
+            <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")}
+            </div>
+            ${h.end_form()}
+            <script type="text/javascript">
+            initCodeMirror('editor','');
+            </script>
+        </div>
+    </div>
+
+</div>
+</%def>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/gists/show.html	Sat May 11 20:24:02 2013 +0200
@@ -0,0 +1,87 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('gist')}:${c.gist.gist_access_id} &middot; ${c.rhodecode_name}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${_('Gist')} &middot; gist:${c.gist.gist_access_id}
+</%def>
+
+<%def name="page_nav()">
+    ${self.menu('gists')}
+</%def>
+
+<%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>
+        %endif
+    </div>
+    <div class="table">
+        <div id="files_data">
+            <div id="body" class="codeblock">
+                <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>
+                          %else:
+                            <div class="ui-btn yellow badge">${_('Private gist')}</div>
+                          %endif
+                        </div>
+                       <span style="color: #AAA">
+                         %if c.gist.gist_expires == -1:
+                          ${_('Expires')}: ${_('never')}
+                         %else:
+                          ${_('Expires')}: ${h.age(h.time_to_datetime(c.gist.gist_expires))}
+                         %endif
+                       </span>
+                        <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")}
+                          %endif
+                        </div>
+                    </div>
+
+                    <div class="author">
+                        <div class="gravatar">
+                            <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.file_changeset.author),16)}"/>
+                        </div>
+                        <div title="${c.file_changeset.author}" class="user">${h.person(c.file_changeset.author)} - ${_('created')} ${h.age(c.file_changeset.date)}</div>
+                    </div>
+                    <div class="commit">${h.urlify_commit(c.file_changeset.message,c.repo_name)}</div>
+                </div>
+            </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>
+                <div class="code-body">
+                    ${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
+                </div>
+              </div>
+               %endfor
+        </div>
+    </div>
+
+
+</div>
+</%def>
--- a/rhodecode/templates/base/base.html	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/templates/base/base.html	Sat May 11 20:24:02 2013 +0200
@@ -286,6 +286,18 @@
               </a>
             </li>
           %endif
+            <li ${is_current('gists')}>
+              <a class="menu_link gists childs" title="${_('Show public gists')}"  href="${h.url('gists')}">
+              ${_('Gists')}
+              </a>
+                <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>
+                  %if c.rhodecode_user.username != 'default':
+                    <li>${h.link_to(_('My private gists'),h.url('gists', private=1),class_='gists-private ')}</li>
+                  %endif
+                </ul>
+            </li>
           <li ${is_current('search')}>
               <a class="menu_link search" title="${_('Search in repositories')}"  href="${h.url('search')}">
               ${_('Search')}
--- a/rhodecode/tests/api/api_base.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/tests/api/api_base.py	Sat May 11 20:24:02 2013 +0200
@@ -26,7 +26,6 @@
     Builds API data with given random ID
 
     :param random_id:
-    :type random_id:
     """
     random_id = random.randrange(1, 9999)
     return random_id, json.dumps({
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/functional/test_admin_gists.py	Sat May 11 20:24:02 2013 +0200
@@ -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('<div class="ui-btn green badge">Public gist</div>')
+
+    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('<div class="ui-btn yellow badge">Private gist</div>')
+
+    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('<div class="ui-btn green badge">Public gist</div>')
+
+    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('<div class="ui-btn green badge">Public gist</div>')
+
+    def test_edit(self):
+        self.skipTest('not implemented')
+        response = self.app.get(url('edit_gist', id=1))
--- a/rhodecode/tests/functional/test_compare.py	Sat May 11 00:25:05 2013 +0200
+++ b/rhodecode/tests/functional/test_compare.py	Sat May 11 20:24:02 2013 +0200
@@ -16,13 +16,17 @@
         _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 @@
         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 @@
         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 @@
         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'
--- a/setup.py	Sat May 11 00:25:05 2013 +0200
+++ b/setup.py	Sat May 11 20:24:02 2013 +0200
@@ -151,6 +151,7 @@
     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