changeset 1136:93b980ebee55

changes for release 1.1.5
author Marcin Kuzminski <marcin@python-works.com>
date Thu, 17 Mar 2011 01:13:48 +0100
parents 3cdacd152b24
children 49032f99b4ab
files README.rst docs/changelog.rst docs/contributing.rst docs/index.rst docs/installation.rst docs/setup.rst docs/upgrade.rst rhodecode/__init__.py rhodecode/config/deployment.ini_tmpl rhodecode/controllers/admin/settings.py rhodecode/controllers/files.py rhodecode/controllers/home.py rhodecode/controllers/journal.py rhodecode/lib/__init__.py rhodecode/lib/auth.py rhodecode/lib/auth_ldap.py rhodecode/lib/db_manage.py rhodecode/lib/helpers.py rhodecode/model/db.py rhodecode/model/meta.py rhodecode/model/scm.py setup.py
diffstat 22 files changed, 371 insertions(+), 235 deletions(-) [+]
line wrap: on
line diff
--- a/README.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/README.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -8,19 +8,19 @@
 It works on http/https and has a built in permission/authentication system with 
 the ability to authenticate via LDAP.
 
-RhodeCode is similar in some respects to github or bitbucket, 
-however RhodeCode can be run as standalone hosted application on your own server.  It is open source 
-and donation ware and focuses more on providing a customized, self administered 
-interface for Mercurial(and soon GIT) repositories. RhodeCode is powered by a vcs_ 
-library that Lukasz Balcerzak and I created to handle multiple different version 
-control systems.
+RhodeCode is similar in some respects to github or bitbucket_, 
+however RhodeCode can be run as standalone hosted application on your own server.  
+It is open source and donation ware and focuses more on providing a customized, 
+self administered interface for Mercurial(and soon GIT) repositories. 
+RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to 
+handle multiple different version control systems.
 
 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
 
 RhodeCode demo
 --------------
 
-http://hg.python-works.com
+http://demo.rhodecode.org
 
 The default access is anonymous but you can login to an administrative account
 using the following credentials:
@@ -31,8 +31,8 @@
 Source code
 -----------
 
-The latest source for RhodeCode can be obtained from my own RhodeCode instance
-https://rhodecode.org 
+The latest source for RhodeCode can be obtained from official RhodeCode instance
+https://hg.rhodecode.org 
 
 Rarely updated source code and issue tracker is available at bitbcuket
 http://bitbucket.org/marcinkuzminski/rhodecode
@@ -123,6 +123,7 @@
 .. _python: http://www.python.org/
 .. _django: http://www.djangoproject.com/
 .. _mercurial: http://mercurial.selenic.com/
+.. _bitbucket: http://bitbucket.org/
 .. _subversion: http://subversion.tigris.org/
 .. _git: http://git-scm.com/
 .. _celery: http://celeryproject.org/
--- a/docs/changelog.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/changelog.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -3,6 +3,28 @@
 Changelog
 =========
 
+
+1.1.5 (**2011-03-1X**)
+======================
+
+news
+----
+
+- basic windows support, by exchanging pybcrypt into sha256 for windows only
+  highly inspired by idea of mantis406
+
+fixes
+-----
+
+- fixed sorting by author in main page
+- fixed crashes with diffs on binary files
+- fixed #131 problem with boolean values for LDAP
+- fixed #122 mysql problems thanks to striker69 
+- fixed problem with errors on calling raw/raw_files/annotate functions 
+  with unknown revisions
+- fixed returned rawfiles attachment names with international character
+- cleaned out docs, big thanks to Jason Harris
+
 1.1.4 (**2011-02-19**)
 ======================
 
--- a/docs/contributing.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/contributing.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -7,13 +7,19 @@
 greatly appreciated!
 
 Could I request that you make your source contributions by first forking the
-RhodeCode repository on bitbucket
+RhodeCode repository on bitbucket_
 https://bitbucket.org/marcinkuzminski/rhodecode and then make your changes to
-your forked repository. Finally, when you are finished making a change, please
-send me a pull request.
+your forked repository. Please post all fixes into **BETA** branch since your 
+fix might be already fixed there and i try to merge all fixes from beta into
+stable, and not the other way. Finally, when you are finished making a change, 
+please send me a pull request.
 
 To run RhodeCode in a development version you always need to install the tip
 version of RhodeCode and the VCS library.
 
 | Thank you for any contributions!
-|  Marcin
\ No newline at end of file
+|  Marcin
+
+
+
+.. _bitbucket: http://bitbucket.org/
--- a/docs/index.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/index.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -48,6 +48,7 @@
 .. _python: http://www.python.org/
 .. _django: http://www.djangoproject.com/
 .. _mercurial: http://mercurial.selenic.com/
+.. _bitbucket: http://bitbucket.org/
 .. _subversion: http://subversion.tigris.org/
 .. _git: http://git-scm.com/
 .. _celery: http://celeryproject.org/
--- a/docs/installation.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/installation.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -9,12 +9,13 @@
 recommended one is rabbitmq_ to make the async tasks work.
 
 Of course RhodeCode works in sync mode also and then you do not have to install
-any third party applications. However, using Celery_ will give you a large speed improvement when using
-many big repositories. If you plan to use RhodeCode for say 7 to 10 small repositories, RhodeCode
-will perform perfectly well without celery running.
+any third party applications. However, using Celery_ will give you a large 
+speed improvement when using many big repositories. If you plan to use 
+RhodeCode for say 7 to 10 small repositories, RhodeCode will perform perfectly 
+well without celery running.
    
-If you make the decision to run RhodeCode with celery make sure you run celeryd using paster
-and message broker together with the application.   
+If you make the decision to run RhodeCode with celery make sure you run 
+celeryd using paster and message broker together with the application.   
 
 Installing RhodeCode from Cheese Shop
 -------------------------------------
--- a/docs/setup.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/setup.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -348,19 +348,19 @@
    double check the root path for your http setup. It should point to 
    for example:
    /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
-
-|
+   
+| 
 
 :Q: **Can't install celery/rabbitmq**
 :A: Don't worry RhodeCode works without them too. No extra setup is required.
 
 |
-
+ 
 :Q: **Long lasting push timeouts?**
 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
     are caused by https server and not RhodeCode.
-
-|
+    
+| 
 
 :Q: **Large pushes timeouts?**
 :A: Make sure you set a proper max_body_size for the http server.
--- a/docs/upgrade.rst	Sun Feb 27 00:35:11 2011 +0100
+++ b/docs/upgrade.rst	Thu Mar 17 01:13:48 2011 +0100
@@ -7,7 +7,8 @@
 --------------------------
 
 .. note::
-   Firstly, it is recommended that you **always** perform a database backup before doing an upgrade.
+   Firstly, it is recommended that you **always** perform a database backup 
+   before doing an upgrade.
 
 The easiest way to upgrade ``rhodecode`` is to run::
 
@@ -24,15 +25,16 @@
  
 This will display any changes made by the new version of RhodeCode to your
 current configuration. It will try to perform an automerge. It's always better
-to make a backup of your configuration file before hand and recheck the content after the automerge.
+to make a backup of your configuration file before hand and recheck the 
+content after the automerge.
 
 .. note::
    The next steps only apply to upgrading from non bugfix releases eg. from
    any minor or major releases. Bugfix releases (eg. 1.1.2->1.1.3) will 
    not have any database schema changes or whoosh library updates.
 
-It is also recommended that you rebuild the whoosh index after upgrading since the new whoosh 
-version could introduce some incompatible index changes.
+It is also recommended that you rebuild the whoosh index after upgrading since 
+the new whoosh version could introduce some incompatible index changes.
 
 
 The final step is to upgrade the database. To do this simply run::
@@ -40,8 +42,8 @@
     paster upgrade-db production.ini
  
 This will upgrade the schema and update some of the defaults in the database,
-and will always recheck the settings of the application, if there are no new options
-that need to be set.
+and will always recheck the settings of the application, if there are no new 
+options that need to be set.
 
 
 .. _virtualenv: http://pypi.python.org/pypi/virtualenv  
--- a/rhodecode/__init__.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/__init__.py	Thu Mar 17 01:13:48 2011 +0100
@@ -25,11 +25,12 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
-
+import platform
 
-VERSION = (1, 1, 4)
+VERSION = (1, 1, 5)
 __version__ = '.'.join((str(each) for each in VERSION[:4]))
 __dbversion__ = 2 #defines current db version for migrations
+__platform__ = platform.system()
 
 try:
     from rhodecode.lib.utils import get_current_revision
--- a/rhodecode/config/deployment.ini_tmpl	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/config/deployment.ini_tmpl	Thu Mar 17 01:13:48 2011 +0100
@@ -71,7 +71,7 @@
 celeryd.concurrency = 2
 #celeryd.log.file = celeryd.log
 celeryd.log.level = debug
-celeryd.max.tasks.per.child = 3
+celeryd.max.tasks.per.child = 1
 
 #tasks will never be sent to the queue, but executed locally instead.
 celery.always.eager = false
--- a/rhodecode/controllers/admin/settings.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/controllers/admin/settings.py	Thu Mar 17 01:13:48 2011 +0100
@@ -140,8 +140,8 @@
 
                 except:
                     log.error(traceback.format_exc())
-                    h.flash(_('error occurred during updating application settings'),
-                            category='error')
+                    h.flash(_('error occurred during updating'
+                              ' application settings'), category='error')
 
                     self.sa.rollback()
 
--- a/rhodecode/controllers/files.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/controllers/files.py	Thu Mar 17 01:13:48 2011 +0100
@@ -7,7 +7,7 @@
     
     :created_on: Apr 21, 2010
     :author: marcink
-    :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>    
+    :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>    
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software; you can redistribute it and/or
@@ -55,9 +55,30 @@
         super(FilesController, self).__before__()
         c.cut_off_limit = self.cut_off_limit
 
+    def __get_cs_or_redirect(self, rev, repo_name):
+        """
+        Safe way to get changeset if error occur it redirects to tip with
+        proper message
+        
+        :param rev: revision to fetch
+        :param repo_name: repo name to redirect after
+        """
+
+        _repo = ScmModel().get_repo(c.repo_name)
+        try:
+            return _repo.get_changeset(rev)
+        except EmptyRepositoryError, e:
+            h.flash(_('There are no files yet'), category='warning')
+            redirect(h.url('summary_home', repo_name=repo_name))
+
+        except RepositoryError, e:
+            h.flash(str(e), category='warning')
+            redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
+
     def index(self, repo_name, revision, f_path):
-        hg_model = ScmModel()
-        c.repo = hg_model.get_repo(c.repo_name)
+        cs = self.__get_cs_or_redirect(revision, repo_name)
+        c.repo = ScmModel().get_repo(c.repo_name)
+
         revision = request.POST.get('at_rev', None) or revision
 
         def get_next_rev(cur):
@@ -72,68 +93,64 @@
             return r
 
         c.f_path = f_path
+        c.changeset = cs
+        cur_rev = c.changeset.revision
+        prev_rev = c.repo.get_changeset(get_prev_rev(cur_rev)).raw_id
+        next_rev = c.repo.get_changeset(get_next_rev(cur_rev)).raw_id
 
+        c.url_prev = url('files_home', repo_name=c.repo_name,
+                         revision=prev_rev, f_path=f_path)
+        c.url_next = url('files_home', repo_name=c.repo_name,
+                     revision=next_rev, f_path=f_path)
 
         try:
-            c.changeset = c.repo.get_changeset(revision)
-            cur_rev = c.changeset.revision
-            prev_rev = c.repo.get_changeset(get_prev_rev(cur_rev)).raw_id
-            next_rev = c.repo.get_changeset(get_next_rev(cur_rev)).raw_id
-
-            c.url_prev = url('files_home', repo_name=c.repo_name,
-                             revision=prev_rev, f_path=f_path)
-            c.url_next = url('files_home', repo_name=c.repo_name,
-                         revision=next_rev, f_path=f_path)
-
-            try:
-                c.files_list = c.changeset.get_node(f_path)
-                c.file_history = self._get_history(c.repo, c.files_list, f_path)
-            except RepositoryError, e:
-                h.flash(str(e), category='warning')
-                redirect(h.url('files_home', repo_name=repo_name, revision=revision))
-
-        except EmptyRepositoryError, e:
-            h.flash(_('There are no files yet'), category='warning')
-            redirect(h.url('summary_home', repo_name=repo_name))
-
+            c.files_list = c.changeset.get_node(f_path)
+            c.file_history = self._get_history(c.repo, c.files_list, f_path)
         except RepositoryError, e:
             h.flash(str(e), category='warning')
-            redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
-
+            redirect(h.url('files_home', repo_name=repo_name,
+                           revision=revision))
 
 
         return render('files/files.html')
 
     def rawfile(self, repo_name, revision, f_path):
-        hg_model = ScmModel()
-        c.repo = hg_model.get_repo(c.repo_name)
-        file_node = c.repo.get_changeset(revision).get_node(f_path)
+        cs = self.__get_cs_or_redirect(revision, repo_name)
+        try:
+            file_node = cs.get_node(f_path)
+        except RepositoryError, e:
+            h.flash(str(e), category='warning')
+            redirect(h.url('files_home', repo_name=repo_name,
+                           revision=cs.raw_id))
+
+        fname = f_path.split('/')[-1].encode('utf8', 'replace')
+
+        response.content_disposition = 'attachment; filename=%s' % fname
         response.content_type = file_node.mimetype
-        response.content_disposition = 'attachment; filename=%s' \
-                                                    % f_path.split('/')[-1]
         return file_node.content
 
     def raw(self, repo_name, revision, f_path):
-        hg_model = ScmModel()
-        c.repo = hg_model.get_repo(c.repo_name)
-        file_node = c.repo.get_changeset(revision).get_node(f_path)
+        cs = self.__get_cs_or_redirect(revision, repo_name)
+        try:
+            file_node = cs.get_node(f_path)
+        except RepositoryError, e:
+            h.flash(str(e), category='warning')
+            redirect(h.url('files_home', repo_name=repo_name,
+                           revision=cs.raw_id))
+
         response.content_type = 'text/plain'
-
         return file_node.content
 
     def annotate(self, repo_name, revision, f_path):
-        hg_model = ScmModel()
-        c.repo = hg_model.get_repo(c.repo_name)
-
+        cs = self.__get_cs_or_redirect(revision, repo_name)
         try:
-            c.cs = c.repo.get_changeset(revision)
-            c.file = c.cs.get_node(f_path)
+            c.file = cs.get_node(f_path)
         except RepositoryError, e:
             h.flash(str(e), category='warning')
-            redirect(h.url('files_home', repo_name=repo_name, revision=revision))
+            redirect(h.url('files_home', repo_name=repo_name, revision=cs.raw_id))
 
-        c.file_history = self._get_history(c.repo, c.file, f_path)
-
+        c.file_history = self._get_history(ScmModel().get_repo(c.repo_name), c.file, f_path)
+        c.cs = cs
         c.f_path = f_path
 
         return render('files/files_annotate.html')
@@ -201,25 +218,34 @@
             response.content_type = 'text/plain'
             response.content_disposition = 'attachment; filename=%s' \
                                                     % diff_name
+            if node1.is_binary or node2.is_binary:
+                return _('binary file changed')
             return diff.raw_diff()
 
         elif c.action == 'raw':
             response.content_type = 'text/plain'
+            if node1.is_binary or node2.is_binary:
+                return _('binary file changed')
             return diff.raw_diff()
 
         elif c.action == 'diff':
             if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit:
                 c.cur_diff = _('Diff is to big to display')
+            elif node1.is_binary or node2.is_binary:
+                c.cur_diff = _('Binary file')
             else:
                 c.cur_diff = diff.as_html()
         else:
             #default option
             if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit:
                 c.cur_diff = _('Diff is to big to display')
+            elif node1.is_binary or node2.is_binary:
+                c.cur_diff = _('Binary file')
             else:
                 c.cur_diff = diff.as_html()
 
-        if not c.cur_diff: c.no_changes = True
+        if not c.cur_diff:
+            c.no_changes = True
         return render('files/file_diff.html')
 
     def _get_history(self, repo, node, f_path):
@@ -250,9 +276,3 @@
         hist_l.append(tags_group)
 
         return hist_l
-
-#                [
-#                 ([("u1", "User1"), ("u2", "User2")], "Users"),
-#                 ([("g1", "Group1"), ("g2", "Group2")], "Groups")
-#                 ]
-
--- a/rhodecode/controllers/home.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/controllers/home.py	Thu Mar 17 01:13:48 2011 +0100
@@ -43,7 +43,7 @@
         super(HomeController, self).__before__()
 
     def index(self):
-        sortables = ['name', 'description', 'last_change', 'tip', 'contact']
+        sortables = ['name', 'description', 'last_change', 'tip', 'owner']
         current_sort = request.GET.get('sort', 'name')
         current_sort_slug = current_sort.replace('-', '')
 
--- a/rhodecode/controllers/journal.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/controllers/journal.py	Thu Mar 17 01:13:48 2011 +0100
@@ -26,9 +26,12 @@
 # MA  02110-1301, USA.
 
 import logging
-from sqlalchemy import or_
+import traceback
 
 from pylons import request, response, session, tmpl_context as c, url
+from paste.httpexceptions import HTTPInternalServerError, HTTPBadRequest
+
+from sqlalchemy import or_
 
 from rhodecode.lib.auth import LoginRequired, NotAnonymous
 from rhodecode.lib.base import BaseController, render
@@ -36,8 +39,6 @@
 from rhodecode.model.db import UserLog, UserFollowing
 from rhodecode.model.scm import ScmModel
 
-from paste.httpexceptions import HTTPInternalServerError
-
 log = logging.getLogger(__name__)
 
 class JournalController(BaseController):
@@ -81,6 +82,7 @@
                                                     c.rhodecode_user.user_id)
                     return 'ok'
                 except:
+                    log.error(traceback.format_exc())
                     raise HTTPInternalServerError()
 
             repo_id = request.POST.get('follows_repo_id')
@@ -90,8 +92,9 @@
                                                     c.rhodecode_user.user_id)
                     return 'ok'
                 except:
+                    log.error(traceback.format_exc())
                     raise HTTPInternalServerError()
 
 
 
-        raise HTTPInternalServerError()
+        raise HTTPBadRequest()
--- a/rhodecode/lib/__init__.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/lib/__init__.py	Thu Mar 17 01:13:48 2011 +0100
@@ -26,4 +26,21 @@
 # MA  02110-1301, USA.
 
 def str2bool(v):
-    return v.lower() in ["yes", "true", "t", "1"] if v else None
+    if isinstance(v, (str, unicode)):
+        obj = v.strip().lower()
+        if obj in ['true', 'yes', 'on', 'y', 't', '1']:
+            return True
+        elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
+            return False
+        else:
+            raise ValueError("String is not true/false: %r" % obj)
+    return bool(obj)
+
+def generate_api_key(username, salt=None):
+    from tempfile import _RandomNameSequence
+    import hashlib
+
+    if salt is None:
+        salt = _RandomNameSequence().next()
+
+    return hashlib.sha1(username + salt).hexdigest()
--- a/rhodecode/lib/auth.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/lib/auth.py	Thu Mar 17 01:13:48 2011 +0100
@@ -1,8 +1,14 @@
-#!/usr/bin/env python
-# encoding: utf-8
-# authentication and permission libraries
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
-#
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.lib.auth
+    ~~~~~~~~~~~~~~~~~~
+    
+    authentication and permission libraries
+    
+    :created_on: Apr 4, 2010
+    :copyright: (c) 2010 by marcink.
+    :license: LICENSE_NAME, see LICENSE_FILE 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; version 2
@@ -17,26 +23,34 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
-"""
-Created on April 4, 2010
 
-@author: marcink
-"""
+import random
+import logging
+import traceback
+
+from decorator import decorator
+
 from pylons import config, session, url, request
 from pylons.controllers.util import abort, redirect
-from rhodecode.lib.exceptions import *
+from pylons.i18n.translation import _
+
+from rhodecode import __platform__
+
+if __platform__ == 'Windows':
+    from hashlib import sha256
+if __platform__ in ('Linux', 'Darwin'):
+    import bcrypt
+
+from rhodecode.lib import str2bool
+from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
 from rhodecode.lib.utils import get_repo_slug
 from rhodecode.lib.auth_ldap import AuthLdap
+
 from rhodecode.model import meta
 from rhodecode.model.user import UserModel
-from rhodecode.model.caching_query import FromCache
-from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \
-    UserToPerm
-import bcrypt
-from decorator import decorator
-import logging
-import random
-import traceback
+from rhodecode.model.db import Permission, RepoToPerm, Repository, \
+    User, UserToPerm
+
 
 log = logging.getLogger(__name__)
 
@@ -65,15 +79,46 @@
         self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
         return self.passwd
 
+class RhodeCodeCrypto(object):
+
+    @classmethod
+    def hash_string(cls, str_):
+        """
+        Cryptographic function used for password hashing based on pybcrypt
+        or pycrypto in windows
+        
+        :param password: password to hash
+        """
+        if __platform__ == 'Windows':
+            return sha256(str_).hexdigest()
+        elif __platform__ in ('Linux', 'Darwin'):
+            return bcrypt.hashpw(str_, bcrypt.gensalt(10))
+        else:
+            raise Exception('Unknown or unsupported platform %s' % __platform__)
+
+    @classmethod
+    def hash_check(cls, password, hashed):
+        """
+        Checks matching password with it's hashed value, runs different
+        implementation based on platform it runs on
+        
+        :param password: password
+        :param hashed: password in hashed form
+        """
+
+        if __platform__ == 'Windows':
+            return sha256(password).hexdigest() == hashed
+        elif __platform__ in ('Linux', 'Darwin'):
+            return bcrypt.hashpw(password, hashed) == hashed
+        else:
+            raise Exception('Unknown or unsupported platform %s' % __platform__)
+
 
 def get_crypt_password(password):
-    """Cryptographic function used for password hashing based on sha1
-    :param password: password to hash
-    """
-    return bcrypt.hashpw(password, bcrypt.gensalt(10))
+    return RhodeCodeCrypto.hash_string(password)
 
 def check_password(password, hashed):
-    return bcrypt.hashpw(password, hashed) == hashed
+    return RhodeCodeCrypto.hash_check(password, hashed)
 
 def authfunc(environ, username, password):
     """
@@ -126,7 +171,7 @@
         #======================================================================
         # FALLBACK TO LDAP AUTH IN ENABLE                
         #======================================================================
-        if ldap_settings.get('ldap_active', False):
+        if str2bool(ldap_settings.get('ldap_active')):
             log.debug("Authenticating user using ldap")
             kwargs = {
                   'server':ldap_settings.get('ldap_host', ''),
@@ -134,7 +179,7 @@
                   'port':ldap_settings.get('ldap_port'),
                   'bind_dn':ldap_settings.get('ldap_dn_user'),
                   'bind_pass':ldap_settings.get('ldap_dn_pass'),
-                  'use_ldaps':ldap_settings.get('ldap_ldaps'),
+                  'use_ldaps':str2bool(ldap_settings.get('ldap_ldaps')),
                   'ldap_version':3,
                   }
             log.debug('Checking for ldap authentication')
--- a/rhodecode/lib/auth_ldap.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/lib/auth_ldap.py	Thu Mar 17 01:13:48 2011 +0100
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # encoding: utf-8
 # ldap authentication lib
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+# Copyright (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
--- a/rhodecode/lib/db_manage.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/lib/db_manage.py	Thu Mar 17 01:13:48 2011 +0100
@@ -60,39 +60,19 @@
         init_model(engine)
         self.sa = meta.Session()
 
-    def check_for_db(self, override):
-        db_path = jn(self.root, self.dbname)
-        if self.dburi.startswith('sqlite'):
-            log.info('checking for existing db in %s', db_path)
-            if os.path.isfile(db_path):
-
-                self.db_exists = True
-                if not override:
-                    raise Exception('database already exists')
-            return 'sqlite'
-        if self.dburi.startswith('postgresql'):
-            self.db_exists = True
-            return 'postgresql'
-
-
     def create_tables(self, override=False):
         """Create a auth database
         """
 
-        db_type = self.check_for_db(override)
-        if self.db_exists:
-            log.info("database exist and it's going to be destroyed")
-            if self.tests:
-                destroy = True
-            else:
-                destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
-            if not destroy:
-                sys.exit()
-            if self.db_exists and destroy:
-                if db_type == 'sqlite':
-                    os.remove(jn(self.root, self.dbname))
-                if db_type == 'postgresql':
-                    meta.Base.metadata.drop_all()
+        log.info("Any existing database is going to be destroyed")
+        if self.tests:
+            destroy = True
+        else:
+            destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
+        if not destroy:
+            sys.exit()
+        if destroy:
+            meta.Base.metadata.drop_all()
 
         checkfirst = not override
         meta.Base.metadata.create_all(checkfirst=checkfirst)
@@ -322,10 +302,14 @@
         """Creates ldap settings"""
 
         try:
-            for k in ['ldap_active', 'ldap_host', 'ldap_port', 'ldap_ldaps',
-                      'ldap_dn_user', 'ldap_dn_pass', 'ldap_base_dn']:
+            for k, v in [('ldap_active', 'false'),
+                        ('ldap_host', ''),
+                        ('ldap_port', '389'),
+                        ('ldap_ldaps', 'false'),
+                        ('ldap_dn_user', ''), ('ldap_dn_pass', ''),
+                        ('ldap_base_dn', '')]:
 
-                setting = RhodeCodeSettings(k, '')
+                setting = RhodeCodeSettings(k, v)
                 self.sa.add(setting)
             self.sa.commit()
         except:
--- a/rhodecode/lib/helpers.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/lib/helpers.py	Thu Mar 17 01:13:48 2011 +0100
@@ -230,6 +230,8 @@
 class _FilesBreadCrumbs(object):
 
     def __call__(self, repo_name, rev, paths):
+        if isinstance(paths, str):
+            paths = paths.decode('utf-8', 'replace')
         url_l = [link_to(repo_name, url('files_home',
                                         repo_name=repo_name,
                                         revision=rev, f_path=''))]
@@ -483,7 +485,7 @@
     if len(x) > 1:
         action, action_params = x
 
-    tmpl = """<img src="%s/%s" alt="%s"/>"""
+    tmpl = """<img src="%s%s" alt="%s"/>"""
     map = {'user_deleted_repo':'database_delete.png',
            'user_created_repo':'database_add.png',
            'user_forked_repo':'arrow_divide.png',
@@ -550,6 +552,6 @@
         suf = ''
         if len(nodes) > 30:
             suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
-        return literal(pref + '<br/> '.join([x.path for x in nodes[:30]]) + suf)
+        return literal(pref + '<br/> '.join([x.path.decode('utf-8', 'replace') for x in nodes[:30]]) + suf)
     else:
         return ': ' + _('No Files')
--- a/rhodecode/model/db.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/model/db.py	Thu Mar 17 01:13:48 2011 +0100
@@ -30,51 +30,20 @@
 
 from sqlalchemy import *
 from sqlalchemy.exc import DatabaseError
-from sqlalchemy.orm import relationship, backref, class_mapper
-from sqlalchemy.orm.session import Session
+from sqlalchemy.orm import relationship, backref
+from sqlalchemy.orm.interfaces import MapperExtension
 
-from rhodecode.model.meta import Base
+from rhodecode.model.meta import Base, Session
 
 log = logging.getLogger(__name__)
 
-class BaseModel(object):
 
-    @classmethod
-    def _get_keys(cls):
-        """return column names for this model """
-        return class_mapper(cls).c.keys()
-
-    def get_dict(self):
-        """return dict with keys and values corresponding 
-        to this model data """
-
-        d = {}
-        for k in self._get_keys():
-            d[k] = getattr(self, k)
-        return d
-
-    def get_appstruct(self):
-        """return list with keys and values tupples corresponding 
-        to this model data """
-
-        l = []
-        for k in self._get_keys():
-            l.append((k, getattr(self, k),))
-        return l
-
-    def populate_obj(self, populate_dict):
-        """populate model with data from given populate_dict"""
-
-        for k in self._get_keys():
-            if k in populate_dict:
-                setattr(self, k, populate_dict[k])
-
-class RhodeCodeSettings(Base, BaseModel):
+class RhodeCodeSettings(Base):
     __tablename__ = 'rhodecode_settings'
     __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
     app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
     def __init__(self, k='', v=''):
         self.app_settings_name = k
@@ -84,27 +53,27 @@
         return "<%s('%s:%s')>" % (self.__class__.__name__,
                                   self.app_settings_name, self.app_settings_value)
 
-class RhodeCodeUi(Base, BaseModel):
+class RhodeCodeUi(Base):
     __tablename__ = 'rhodecode_ui'
     __table_args__ = {'useexisting':True}
     ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
 
 
-class User(Base, BaseModel):
+class User(Base):
     __tablename__ = 'users'
     __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
     user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     active = Column("active", Boolean(), nullable=True, unique=None, default=None)
     admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
-    name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
     is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
 
@@ -118,6 +87,10 @@
     def full_contact(self):
         return '%s %s <%s>' % (self.name, self.lastname, self.email)
 
+    @property
+    def short_contact(self):
+        return '%s %s' % (self.name, self.lastname)
+
 
     @property
     def is_admin(self):
@@ -127,6 +100,11 @@
         return "<%s('id:%s:%s')>" % (self.__class__.__name__,
                                      self.user_id, self.username)
 
+    @classmethod
+    def by_username(cls, username):
+        return Session.query(cls).filter(cls.username == username).one()
+
+
     def update_lastlogin(self):
         """Update user lastlogin"""
 
@@ -140,15 +118,15 @@
             session.rollback()
 
 
-class UserLog(Base, BaseModel):
+class UserLog(Base):
     __tablename__ = 'user_logs'
     __table_args__ = {'useexisting':True}
     user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
     user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
     repository_id = Column("repository_id", Integer(length=None, convert_unicode=False, assert_unicode=None), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
-    repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
 
     @property
@@ -158,16 +136,16 @@
     user = relationship('User')
     repository = relationship('Repository')
 
-class Repository(Base, BaseModel):
+class Repository(Base):
     __tablename__ = 'repositories'
     __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
     repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
-    repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
+    repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
+    repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
     user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
     private = Column("private", Boolean(), nullable=True, unique=None, default=None)
     enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
-    description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
 
     user = relationship('User')
@@ -178,23 +156,23 @@
     repo_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
 
     logs = relationship('UserLog', cascade='all')
-    
+
     def __repr__(self):
         return "<%s('%s:%s')>" % (self.__class__.__name__,
                                   self.repo_id, self.repo_name)
 
-class Permission(Base, BaseModel):
+class Permission(Base):
     __tablename__ = 'permissions'
     __table_args__ = {'useexisting':True}
     permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
     def __repr__(self):
         return "<%s('%s:%s')>" % (self.__class__.__name__,
                                   self.permission_id, self.permission_name)
 
-class RepoToPerm(Base, BaseModel):
+class RepoToPerm(Base):
     __tablename__ = 'repo_to_perm'
     __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
     repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
@@ -206,7 +184,7 @@
     permission = relationship('Permission')
     repository = relationship('Repository')
 
-class UserToPerm(Base, BaseModel):
+class UserToPerm(Base):
     __tablename__ = 'user_to_perm'
     __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
     user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
@@ -216,19 +194,19 @@
     user = relationship('User')
     permission = relationship('Permission')
 
-class Statistics(Base, BaseModel):
+class Statistics(Base):
     __tablename__ = 'statistics'
     __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
     stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
+    repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
     stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
-    commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data
+    commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
     commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
-    languages = Column("languages", LargeBinary(), nullable=False)#JSON data
+    languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
 
     repository = relationship('Repository', single_parent=True)
 
-class UserFollowing(Base, BaseModel):
+class UserFollowing(Base):
     __tablename__ = 'user_followings'
     __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
                       UniqueConstraint('user_id', 'follows_user_id')
@@ -244,12 +222,12 @@
     follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
     follows_repository = relationship('Repository', order_by='Repository.repo_name')
 
-class CacheInvalidation(Base, BaseModel):
+class CacheInvalidation(Base):
     __tablename__ = 'cache_invalidation'
     __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
     cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
-    cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
-    cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
 
 
@@ -262,10 +240,10 @@
         return "<%s('%s:%s')>" % (self.__class__.__name__,
                                   self.cache_id, self.cache_key)
 
-class DbMigrateVersion(Base, BaseModel):
+class DbMigrateVersion(Base):
     __tablename__ = 'db_migrate_version'
     __table_args__ = {'useexisting':True}
-    repository_id = Column('repository_id', String(250), primary_key=True)
+    repository_id = Column('repository_id', String(255), primary_key=True)
     repository_path = Column('repository_path', Text)
     version = Column('version', Integer)
 
--- a/rhodecode/model/meta.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/model/meta.py	Thu Mar 17 01:13:48 2011 +0100
@@ -1,8 +1,10 @@
 """SQLAlchemy Metadata and Session object"""
 from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.orm import scoped_session, sessionmaker, class_mapper
+from beaker import cache
+
 from rhodecode.model import caching_query
-from beaker import cache
+
 
 # Beaker CacheManager.  A home base for cache configurations.
 cache_manager = cache.CacheManager()
@@ -17,10 +19,52 @@
                 )
           )
 
+class BaseModel(object):
+    """Base Model for all classess
+    
+    """
+
+    @classmethod
+    def _get_keys(cls):
+        """return column names for this model """
+        return class_mapper(cls).c.keys()
+
+    def get_dict(self):
+        """return dict with keys and values corresponding 
+        to this model data """
+
+        d = {}
+        for k in self._get_keys():
+            d[k] = getattr(self, k)
+        return d
+
+    def get_appstruct(self):
+        """return list with keys and values tupples corresponding 
+        to this model data """
+
+        l = []
+        for k in self._get_keys():
+            l.append((k, getattr(self, k),))
+        return l
+
+    def populate_obj(self, populate_dict):
+        """populate model with data from given populate_dict"""
+
+        for k in self._get_keys():
+            if k in populate_dict:
+                setattr(self, k, populate_dict[k])
+
+    @classmethod
+    def query(cls):
+        return Session.query(cls)
+
+    @classmethod
+    def get(cls, id_):
+        return Session.query(cls).get(id_)
+
+
 # The declarative Base
-Base = declarative_base()
-#For another db...
-#Base2 = declarative_base()
+Base = declarative_base(cls=BaseModel)
 
 #to use cache use this in query
 #.options(FromCache("sqlalchemy_cache_type", "cachekey"))
--- a/rhodecode/model/scm.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/rhodecode/model/scm.py	Thu Mar 17 01:13:48 2011 +0100
@@ -146,6 +146,7 @@
                 tmp_d['rev'] = tip.revision
                 tmp_d['contact'] = repo.dbrepo.user.full_contact
                 tmp_d['contact_sort'] = tmp_d['contact']
+                tmp_d['owner_sort'] = tmp_d['contact']
                 tmp_d['repo_archives'] = list(repo._get_archives())
                 tmp_d['last_msg'] = tip.message
                 tmp_d['repo'] = repo
--- a/setup.py	Sun Feb 27 00:35:11 2011 +0100
+++ b/setup.py	Thu Mar 17 01:13:48 2011 +0100
@@ -1,19 +1,19 @@
 import sys
+from rhodecode import get_version
+from rhodecode import __platform__
+
 py_version = sys.version_info
 
-from rhodecode import get_version
-
 requirements = [
         "Pylons==1.0.0",
         "WebHelpers==1.2",
         "SQLAlchemy==0.6.6",
-        "Mako==0.3.6",
-        "vcs==0.1.10",
-        "pygments==1.3.1",
+        "Mako==0.4.0",
+        "vcs==0.1.11",
+        "pygments==1.4.0",
         "mercurial==1.7.5",
         "whoosh==1.3.4",
-        "celery==2.1.4",
-        "py-bcrypt",
+        "celery==2.2.4",
         "babel",
     ]
 
@@ -25,10 +25,14 @@
                'Operating System :: OS Independent',
                'Programming Language :: Python', ]
 
-if sys.version_info < (2, 6):
+if py_version < (2, 6):
     requirements.append("simplejson")
     requirements.append("pysqlite")
 
+if __platform__ in ('Linux', 'Darwin'):
+    requirements.append("py-bcrypt")
+
+
 #additional files from project that goes somewhere in the filesystem
 #relative to sys.prefix
 data_files = []
@@ -38,6 +42,10 @@
 
 description = ('Mercurial repository browser/management with '
                'build in push/pull server and full text search')
+keywords = ' '.join (['rhodecode', 'rhodiumcode', 'mercurial', 'git',
+                      'repository management', 'hgweb replacement'
+                      'hgwebdir', 'gitweb replacement', 'serving hgweb',
+                     ])
 #long description
 try:
     readme_file = 'README.rst'
@@ -66,7 +74,7 @@
     version=get_version(),
     description=description,
     long_description=long_description,
-    keywords='rhodiumcode mercurial web hgwebdir gitweb git replacement serving hgweb rhodecode',
+    keywords=keywords,
     license='BSD',
     author='Marcin Kuzminski',
     author_email='marcin@python-works.com',